mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 06:13:13 +00:00
fix(vendors): vendor detail parity with the design prototype (SH-253)
Three divergences in the vendor detail drawer: - The header rendered the company as a subtitle under a title that had already fallen back to the company, printing the same name twice when the row carried no technician. The subtitle is now dropped when it would repeat the title. - The Google Maps link appeared only when a URL was stored, unlabelled, so an empty value left no trace of the field. It is now a labelled field below Address that reads em-dash when unset, as every other field in the panel does. - The status sat immediately beside Total Jobs. It now sits opposite it at the right edge, and reuses the vendors table's dot-plus-text badge so the state never reads by colour alone.
This commit is contained in:
parent
772d7f94df
commit
038ce263da
4 changed files with 137 additions and 44 deletions
|
|
@ -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>
|
||||
|
|
|
|||
20
src/app/(protected)/vendors/_components/vendor-status-badge.tsx
vendored
Normal file
20
src/app/(protected)/vendors/_components/vendor-status-badge.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -134,3 +134,82 @@ describe("VendorDetailDrawer selected-technician display", () => {
|
|||
expect(screen.queryByRole("switch", { name: "Active status" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue