fix(vendors): align Vendors list UI with design specs (SH-219) (#92)
Some checks are pending
CI / ci (push) Waiting to run
CI / governance (push) Waiting to run
Deploy / deploy (push) Waiting to run

* 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:
Arthur Bassi 2026-08-07 11:12:45 -03:00 • committed by GitHub
parent ca7829036f
commit 88216520d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 96 additions and 50 deletions

View 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) |

View file

@ -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",

View file

@ -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} />}
</>
}
/>
);
}

View file

@ -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}