mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 18:22:05 +00:00
fix(vendors): align Vendors list UI with design specs (SH-219) (#92)
* docs(work-orders): add pending BE contract checklist for design parity gaps * fix(vendors): align list header, Total Jobs, and Filters with design (SH-219) * test(vendors): align e2e subtitle with SH-219 technicians copy Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ca7829036f
commit
88216520d8
4 changed files with 96 additions and 50 deletions
58
docs/work-orders/pending-be-contract.md
Normal file
58
docs/work-orders/pending-be-contract.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Pending BE contract (WO design parity gaps)
|
||||
|
||||
Local checklist for FE branches off `dev`. Confirm with backend before shipping PR3/PR5 to production. Until confirmed, FE may use typed clients + mocks.
|
||||
|
||||
## Board row / PATCH
|
||||
|
||||
| Field | Needed by | Notes |
|
||||
| -------------------------------------------------------- | ------------- | ---------------------------------------------------------------------- |
|
||||
| `completedDate` on GET `/board` and `/board/search` rows | PR1, PR4 | PATCH `field: "completedDate"` expected; confirm GET includes ISO date |
|
||||
| `severity` (`1`–`5` \| `null`) on row, create, PATCH | PR2, PR5 | Required for Emergency/Reactive; PATCH `field: "severity"` |
|
||||
| `upliftSummary` on row | PR3, PR4, PR5 | `{ hasUplift, pendingCount, primaryStatus? }` |
|
||||
| `hasPendingUplift` on row/detail | PR4 | Prefer server 422 on Completed / completion-doc when pending |
|
||||
| `additionalContacts[]` | PR6 | `{ name, phone, notes? }[]` on create/detail/PATCH |
|
||||
| `isAddOn` | PR5 (filter) | Indicator, not a WO type (SH-126) |
|
||||
|
||||
## Proposed WO-scoped uplift endpoints (confirm vs NTE `uplifts` queue)
|
||||
|
||||
| Method | Path | Body |
|
||||
| ------ | ------------------------------------------- | --------------------------------- |
|
||||
| GET | `workorders/{id}/uplifts` | — |
|
||||
| POST | `workorders/{id}/uplifts` | `{ amount, notes, attachments? }` |
|
||||
| POST | `workorders/{id}/uplifts/{upliftId}/cancel` | — |
|
||||
| POST | `workorders/{id}/uplifts/{upliftId}/revoke` | `{ reason? }` |
|
||||
|
||||
Statuses (design): `pending` \| `approved` \| `auto_approved` \| `rejected` \| `cancelled` \| `revoked`.
|
||||
|
||||
## Search query params (`GET workorders/board/search`)
|
||||
|
||||
| Param | Meaning |
|
||||
| ------------------------------ | ------------------------- |
|
||||
| `severities` | multi `1`–`5` |
|
||||
| `rescheduled` / `carriedOver` | boolean (≥2 counters) |
|
||||
| `addOn` | boolean (`isAddOn`) |
|
||||
| `flagColors` | multi `#RRGGBB` |
|
||||
| `internalOnly` | WO# starts with `SH` |
|
||||
| `avetaOnly` | if `avetaRequired` exists |
|
||||
| `hasUplift` + `upliftStatuses` | uplift filters |
|
||||
|
||||
Already documented in `board-search-api.md`: datePreset, sites, types, overdue, dispatchers, statuses, pmTypes, vendorIds, docStatuses.
|
||||
|
||||
## Comments
|
||||
|
||||
| Field | Needed by |
|
||||
| ---------------------------------------- | --------- |
|
||||
| POST `{ text, mentions?: string[] }` | PR9 |
|
||||
| Response includes mentions for highlight | PR9 |
|
||||
|
||||
## Vendor
|
||||
|
||||
| Item | Needed by |
|
||||
| -------------------------------------------------------------- | --------- |
|
||||
| Create technician under company (confirm existing vendors API) | PR7 |
|
||||
|
||||
## Optional
|
||||
|
||||
| Item | Needed by |
|
||||
| ----------------------------------- | -------------------------------- |
|
||||
| 422 on duplicate WO number (global) | PR8 (FE dialog works without it) |
|
||||
2
e2e/vendors/vendors.spec.ts
vendored
2
e2e/vendors/vendors.spec.ts
vendored
|
|
@ -310,7 +310,7 @@ test.describe("Vendor directory prototype parity", () => {
|
|||
await page.goto("/vendors");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Vendors / Technicians" })).toBeVisible();
|
||||
await expect(page.getByText("3 vendor entries · 2 active")).toBeVisible();
|
||||
await expect(page.getByText("3 technicians · 2 active")).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "Active (2)" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
|
|
@ -8,6 +7,9 @@ import {
|
|||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { SlidersHorizontal } from "lucide-react";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import { ToolbarButton } from "@/components/ui/toolbar-button";
|
||||
|
||||
interface VendorsListToolbarProps {
|
||||
globalCount: number;
|
||||
|
|
@ -33,52 +35,34 @@ export function VendorsListToolbar({
|
|||
onAddVendor,
|
||||
}: VendorsListToolbarProps) {
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: { xs: "stretch", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: "baseline" }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Vendors / Technicians
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
{globalCount} vendor entries · {activeCount} active
|
||||
</Typography>
|
||||
{isFetching && !isLoading && <CircularProgress size={16} />}
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={2}
|
||||
sx={{ alignItems: { xs: "stretch", sm: "center" } }}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Search name, company, trade..."
|
||||
className="min-w-[260px]"
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onOpenFilters}
|
||||
startIcon={<FilterListIcon />}
|
||||
aria-label="Open vendor filters"
|
||||
>
|
||||
Filters
|
||||
{appliedFilterCount > 0 && (
|
||||
<Chip size="small" color="primary" label={appliedFilterCount} sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={onAddVendor}>
|
||||
+ Add Vendor
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<PageHeader
|
||||
title="Vendors / Technicians"
|
||||
subtitle={`${globalCount} technicians · ${activeCount} active`}
|
||||
actions={
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Search name, company, trade..."
|
||||
className="min-w-[260px]"
|
||||
/>
|
||||
<ToolbarButton
|
||||
variant="outline"
|
||||
icon={<SlidersHorizontal size={13} />}
|
||||
onClick={onOpenFilters}
|
||||
aria-label="Open vendor filters"
|
||||
badge={appliedFilterCount}
|
||||
>
|
||||
Filters
|
||||
</ToolbarButton>
|
||||
<Button variant="contained" onClick={onAddVendor}>
|
||||
+ Add Vendor
|
||||
</Button>
|
||||
{Boolean(isFetching && !isLoading) && <CircularProgress size={16} />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ function VendorTableRow({ row, onOpenDetail, onOpenDeactivation }: VendorTableRo
|
|||
"—"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{row.totalJobs ?? 0}</TableCell>
|
||||
<TableCell align="center">{row.totalJobs ?? 0}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
size="small"
|
||||
|
|
@ -223,7 +223,11 @@ export function VendorsTable({
|
|||
{TABLE_COLUMNS.map((column) => (
|
||||
<TableCell
|
||||
key={column}
|
||||
align={column === "View" || column === "Edit" ? "center" : "left"}
|
||||
align={
|
||||
column === "View" || column === "Edit" || column === "Total Jobs"
|
||||
? "center"
|
||||
: "left"
|
||||
}
|
||||
className="font-semibold"
|
||||
>
|
||||
{column}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue