mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 11:33:12 +00:00
Merge branch 'dev' into feat/sh-241-wo-day-pagination
This commit is contained in:
commit
e6567c2c0a
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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue