shoc-frontend-new/src/app/(protected)/vendors/_components/vendors-table.tsx
Codex Review Integration 2bff7176be feat(vendors): add Service Area assignment, filter and column
Replace the free-text vendor State with a closed 50-state list, add an
admin-only Service Area select below Notes, a Service Area advanced filter
(stable ids plus Unassigned) and a Service Area directory column.
2026-09-16 11:55:40 -03:00

153 lines
4.4 KiB
TypeScript

import SearchOffOutlinedIcon from "@mui/icons-material/SearchOffOutlined";
import {
CircularProgress,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import { VendorTableRow } from "./vendor-table-row";
import { Text } from "@/components/ui/text";
import type { VendorListItem } from "@/domain/vendors/types/vendor";
const TABLE_COLUMNS = [
"Company",
"Company Phone",
"Company Email",
"Street Address",
"City",
"State",
"Service Area",
"Trade",
"Technician",
"Technician Phone",
"Total Jobs",
] as const;
interface VendorsTableProps {
rows: VendorListItem[];
isActive: boolean;
isFiltered: boolean;
isLoading: boolean;
showError: boolean;
onOpenDetail: (row: VendorListItem) => void;
onOpenEdit: (row: VendorListItem) => void;
}
export function VendorsTable({
rows,
isActive,
isFiltered,
isLoading,
showError,
onOpenDetail,
onOpenEdit,
}: VendorsTableProps) {
const showEmptyRow = !isLoading && !showError && rows.length === 0;
const columnCount = TABLE_COLUMNS.length + 2;
return (
<TableContainer
component={Paper}
variant="outlined"
sx={{
mt: 2,
mb: 2,
flex: "1 1 auto",
overflow: "auto",
borderRadius: 1.5,
fontSize: 13,
containerType: "inline-size",
}}
>
<Table
size="small"
stickyHeader
sx={{
minWidth: 2200,
tableLayout: "fixed",
"& .MuiTableCell-head": {
backgroundColor: "var(--color-bg-muted)",
color: "var(--color-foreground)",
fontSize: 10.5,
fontWeight: 700,
letterSpacing: "0.06em",
lineHeight: "24px",
px: 1.75,
py: 1.375,
textTransform: "uppercase",
whiteSpace: "nowrap",
},
"& .MuiTableCell-body": {
fontSize: 13,
px: 1.5,
py: 1,
height: 52,
lineHeight: 1.43,
whiteSpace: "nowrap",
},
"& .vendor-row-actions": { opacity: { xs: 1, md: 0 }, transition: "opacity 120ms ease" },
"& .MuiTableRow-root:hover .vendor-row-actions, & .MuiTableRow-root:focus-within .vendor-row-actions":
{ opacity: 1 },
}}
>
<colgroup>
{[190, 160, 220, 220, 140, 100, 150, 190, 220, 170, 110, 120, 130].map((width, index) => (
<col key={`${width}-${index}`} style={{ width }} />
))}
</colgroup>
<TableHead>
<TableRow>
{TABLE_COLUMNS.map((column) => (
<TableCell key={column} align={column === "Total Jobs" ? "center" : "left"}>
{column}
</TableCell>
))}
<TableCell>Status</TableCell>
<TableCell align="right" aria-label="Vendor actions" />
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columnCount} className="py-10">
<Stack sx={{ width: "calc(100cqw - 24px)", alignItems: "center" }}>
<CircularProgress size={28} />
</Stack>
</TableCell>
</TableRow>
) : showEmptyRow ? (
<TableRow>
<TableCell colSpan={columnCount} className="py-10">
<Stack spacing={1} sx={{ width: "calc(100cqw - 24px)", alignItems: "center" }}>
<SearchOffOutlinedIcon sx={{ color: "text.secondary", fontSize: 32 }} />
<Text
variant="description"
className="font-semibold"
>{`No ${isActive ? "active" : "inactive"} companies found`}</Text>
<Text variant="caption" tone="muted">
{isFiltered
? "Try a different search, or add a new vendor."
: "Add a vendor to get started."}
</Text>
</Stack>
</TableCell>
</TableRow>
) : (
rows.map((row) => (
<VendorTableRow
key={String(row.id ?? `company-${row.companyId ?? ""}`)}
row={row}
onOpenDetail={onOpenDetail}
onOpenEdit={onOpenEdit}
/>
))
)}
</TableBody>
</Table>
</TableContainer>
);
}