Merge pull request #121 from Sea-Haven-Industries/feat/sh-253-vendor-detail-parity

fix(vendors): vendor detail parity with the design prototype (SH-253)
This commit is contained in:
Alexandre Brandizzi 2026-08-20 11:23:29 -03:00 • committed by GitHub
commit 12411dc295
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 137 additions and 44 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -17,6 +17,7 @@ import {
Switch,
} from "@mui/material";
import { VendorRosterConflictAlert } from "./vendor-roster-conflict-alert";
import { VendorStatusBadge } from "./vendor-status-badge";
import { VendorRosterFormFields } from "./vendor-roster-form-fields";
import { VendorRosterLoadErrorAlert } from "./vendor-roster-load-error";
import { useVendorRosterForm } from "./use-vendor-roster-form";
@ -81,6 +82,13 @@ function DrawerHeader({
technician?: VendorRosterTechnician;
onClose: () => void;
}) {
const title = technician?.contactName || vendor?.contactName || roster?.name || "Vendor company";
const company = roster?.name || vendor?.companyName || "Vendor details";
// The company is a subtitle under the technician's name. When there is no
// technician the title already falls back to the company, and repeating it
// would render the same line twice.
const subtitle = company === title ? null : company;
return (
<Stack
direction="row"
@ -94,11 +102,13 @@ function DrawerHeader({
>
<Box>
<Text id="vendor-detail-drawer-title" variant="title" as="h2" className="font-semibold">
{technician?.contactName || vendor?.contactName || roster?.name || "Vendor company"}
</Text>
<Text variant="description" tone="muted">
{roster?.name || vendor?.companyName || "Vendor details"}
{title}
</Text>
{subtitle !== null && (
<Text variant="description" tone="muted">
{subtitle}
</Text>
)}
</Box>
<IconButton aria-label="Close drawer" onClick={onClose} size="small">
<CloseIcon fontSize="small" />
@ -118,17 +128,24 @@ function CompanySection({ roster }: { roster: VendorCompanyRoster }) {
<Field label="Company Phone" value={roster.companyPhone} />
<Field label="Company Email" value={roster.email} />
<Field label="Address" value={address} />
{Boolean(roster.googleMapsUrl) && (
<Link
href={roster.googleMapsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1"
>
<LaunchIcon fontSize="small" />
Open in Google Maps
</Link>
)}
<Box>
<Text variant="caption" tone="muted" sx={{ display: "block" }}>
Google Maps
</Text>
{roster.googleMapsUrl ? (
<Link
href={roster.googleMapsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1"
>
<LaunchIcon fontSize="small" />
Open in Google Maps
</Link>
) : (
<Text variant="description">—</Text>
)}
</Box>
</Stack>
);
}
@ -154,23 +171,14 @@ function TechnicianSection({ technician }: { technician: VendorRosterTechnician
)}
</Stack>
</Box>
<Stack direction="row" spacing={4} sx={{ alignItems: "flex-end" }}>
<Stack direction="row" sx={{ alignItems: "flex-end", justifyContent: "space-between" }}>
<Box>
<Text variant="title">{technician.totalJobs ?? 0}</Text>
<Text variant="caption" tone="muted" sx={{ display: "block" }}>
Total Jobs
</Text>
</Box>
<Box>
<Text variant="caption" tone="muted" sx={{ display: "block", mb: 0.5 }}>
Status
</Text>
<Chip
size="small"
label={technician.isActive ? "Active" : "Inactive"}
color={technician.isActive ? "success" : "default"}
/>
</Box>
<VendorStatusBadge isActive={technician.isActive} />
</Stack>
</Stack>
</Box>

View file

@ -0,0 +1,20 @@
import { Box, Stack } from "@mui/material";
import { Text } from "@/components/ui/text";
/** Dot plus text, so the status never reads by colour alone. */
export function VendorStatusBadge({ isActive }: { isActive: boolean }) {
return (
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
<Box
component="span"
sx={{
width: 6,
height: 6,
borderRadius: "9999px",
backgroundColor: isActive ? "success.main" : "text.secondary",
}}
/>
<Text variant="description">{isActive ? "Active" : "Inactive"}</Text>
</Stack>
);
}

View file

@ -18,6 +18,7 @@ import {
TableRow,
Tooltip,
} from "@mui/material";
import { VendorStatusBadge } from "./vendor-status-badge";
import { Text } from "@/components/ui/text";
import type { VendorListItem } from "@/domain/vendors/types/vendor";
@ -50,23 +51,6 @@ function stopPropagation(event: MouseEvent): void {
event.stopPropagation();
}
function VendorStatus({ isActive }: { isActive: boolean }) {
return (
<Stack direction="row" spacing={1} sx={{ alignItems: "center", pl: 1 }}>
<Box
component="span"
sx={{
width: 6,
height: 6,
borderRadius: "9999px",
backgroundColor: isActive ? "success.main" : "text.secondary",
}}
/>
<Text variant="description">{isActive ? "Active" : "Inactive"}</Text>
</Stack>
);
}
interface VendorTableRowProps {
row: VendorListItem;
onOpenDetail: (row: VendorListItem) => void;
@ -177,7 +161,9 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
</TableCell>
<TableCell align="center">{row.totalJobs ?? 0}</TableCell>
<TableCell>
<VendorStatus isActive={row.isActive} />
<Box sx={{ pl: 1 }}>
<VendorStatusBadge isActive={row.isActive} />
</Box>
</TableCell>
<TableCell align="right">
<Stack

View file

@ -136,6 +136,85 @@ describe("VendorDetailDrawer selected-technician display", () => {
});
});
describe("VendorDetailDrawer design parity", () => {
it("renders the company once when there is no technician name to head the panel", () => {
useVendorCompanyRoster.mockReturnValue(rosterWith([]));
renderWithProviders(
<VendorDetailDrawer open={true} vendor={{ ...vendor, contactName: "" }} onClose={vi.fn()} />,
{ route: "/vendors", withAuth: false },
);
const heading = screen.getByRole("heading", { level: 2, name: "Gateway Plumbing" });
expect(heading).toBeInTheDocument();
// The header block holds the title alone — no subtitle repeating the company.
expect(heading.parentElement?.children).toHaveLength(1);
});
it("keeps the company subtitle under a technician name", () => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]),
);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
expect(screen.getByRole("heading", { level: 2, name: "Adam" })).toBeInTheDocument();
expect(screen.getAllByText("Gateway Plumbing").length).toBeGreaterThan(0);
});
it("labels the Google Maps field below Address even when no URL is stored", () => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]),
);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
expect(screen.getByText("Google Maps")).toBeInTheDocument();
expect(screen.queryByRole("link", { name: /Open in Google Maps/ })).not.toBeInTheDocument();
});
it("links out to Google Maps when a URL is stored", () => {
const roster = rosterWith([
{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 },
]);
roster.data.googleMapsUrl = "https://maps.google.com/?q=1+Industrial+Pkwy";
useVendorCompanyRoster.mockReturnValue(roster);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
expect(screen.getByRole("link", { name: /Open in Google Maps/ })).toHaveAttribute(
"href",
"https://maps.google.com/?q=1+Industrial+Pkwy",
);
});
it("puts the status opposite Total Jobs and drops the redundant Status label", () => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]),
);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
const row = screen.getByText("Total Jobs").closest("div")?.parentElement;
expect(row).not.toBeNull();
expect(getComputedStyle(row as Element).justifyContent).toBe("space-between");
expect(row).toHaveTextContent("Active");
expect(screen.queryByText("Status")).not.toBeInTheDocument();
});
});
describe("VendorDetailDrawer deactivation prompt", () => {
const inactiveVendor: VendorListItem = { ...vendor, isActive: false };