mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 02:22:06 +00:00
263 lines
7.5 KiB
TypeScript
263 lines
7.5 KiB
TypeScript
import { type KeyboardEvent, type MouseEvent } from "react";
|
|
import { useNavigate } from "react-router";
|
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined";
|
|
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
|
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
|
import {
|
|
Avatar,
|
|
Chip,
|
|
CircularProgress,
|
|
IconButton,
|
|
Link,
|
|
Paper,
|
|
Stack,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Tooltip,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import type { VendorListItem } from "@/domain/vendors/types/vendor";
|
|
|
|
const TABLE_COLUMNS = [
|
|
"Company",
|
|
"Technician",
|
|
"Location",
|
|
"Trade",
|
|
"Phone",
|
|
"Email",
|
|
"Total Jobs",
|
|
"Status",
|
|
"View",
|
|
"Edit",
|
|
] as const;
|
|
|
|
function getInitials(name: string): string {
|
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) return "?";
|
|
const first = parts[0]?.[0] ?? "";
|
|
const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? "") : "";
|
|
return (first + last).toUpperCase();
|
|
}
|
|
|
|
function splitTrades(value: string | undefined): string[] {
|
|
return (value ?? "")
|
|
.split(",")
|
|
.map((trade) => trade.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function stopPropagation(event: MouseEvent): void {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
interface VendorTableRowProps {
|
|
row: VendorListItem;
|
|
onOpenDetail: (row: VendorListItem) => void;
|
|
onOpenDeactivation: (row: VendorListItem) => void;
|
|
}
|
|
|
|
function VendorTableRow({ row, onOpenDetail, onOpenDeactivation }: VendorTableRowProps) {
|
|
const navigate = useNavigate();
|
|
const trades = splitTrades(row.tradeSpecialties);
|
|
const primaryTrade = trades[0] ?? "—";
|
|
const extraTrades = trades.length > 1 ? trades.length - 1 : 0;
|
|
const locationText = [row.city, row.state].filter(Boolean).join(", ") || "—";
|
|
const hasVendorId = row.id != null;
|
|
const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`);
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onOpenDetail(row);
|
|
};
|
|
|
|
return (
|
|
<TableRow
|
|
key={rowKey}
|
|
hover
|
|
tabIndex={0}
|
|
role="button"
|
|
aria-label={`Open vendor details for ${row.companyName}`}
|
|
sx={{ cursor: "pointer" }}
|
|
onClick={() => onOpenDetail(row)}
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
<TableCell>
|
|
<Typography className="font-semibold">{row.companyName || "—"}</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Stack direction="row" spacing={1.5} sx={{ alignItems: "center" }}>
|
|
<Avatar
|
|
sx={{
|
|
width: 28,
|
|
height: 28,
|
|
fontSize: 12,
|
|
backgroundColor: "var(--accent, #1976d2)",
|
|
}}
|
|
>
|
|
{getInitials(row.contactName)}
|
|
</Avatar>
|
|
<Typography variant="body2">{row.contactName || "—"}</Typography>
|
|
</Stack>
|
|
</TableCell>
|
|
<TableCell>{locationText}</TableCell>
|
|
<TableCell>
|
|
{primaryTrade === "—" ? (
|
|
"—"
|
|
) : (
|
|
<Stack direction="row" spacing={0.5} sx={{ alignItems: "center" }}>
|
|
<span>{primaryTrade}</span>
|
|
{extraTrades > 0 && <Chip size="small" label={`+${extraTrades}`} />}
|
|
</Stack>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{row.phone ? (
|
|
<Link href={`tel:${row.phone}`} underline="hover" onClick={stopPropagation}>
|
|
{row.phone}
|
|
</Link>
|
|
) : (
|
|
"—"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{row.email ? (
|
|
<Link href={`mailto:${row.email}`} underline="hover" onClick={stopPropagation}>
|
|
{row.email}
|
|
</Link>
|
|
) : (
|
|
"—"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{row.totalJobs ?? 0}</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
size="small"
|
|
label={row.isActive ? "Active" : "Inactive"}
|
|
color={row.isActive ? "success" : "default"}
|
|
/>
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
<Tooltip title="View details">
|
|
<IconButton
|
|
size="small"
|
|
aria-label={`View vendor ${row.companyName}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onOpenDetail(row);
|
|
}}
|
|
>
|
|
<VisibilityOutlinedIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
<Stack
|
|
direction="row"
|
|
spacing={0.5}
|
|
sx={{ alignItems: "center", justifyContent: "center" }}
|
|
>
|
|
<Tooltip title="Edit">
|
|
<IconButton
|
|
size="small"
|
|
aria-label={`Edit vendor ${row.companyName}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
navigate(
|
|
row.id != null
|
|
? `/vendors/edit/${row.id}`
|
|
: `/vendors/company/${row.companyId ?? ""}`,
|
|
);
|
|
}}
|
|
>
|
|
<EditOutlinedIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
{hasVendorId && row.isActive && (
|
|
<Tooltip title="Deactivate">
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Deactivate vendor"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onOpenDeactivation(row);
|
|
}}
|
|
>
|
|
<DeleteOutlineIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
interface VendorsTableProps {
|
|
rows: VendorListItem[];
|
|
isLoading: boolean;
|
|
showError: boolean;
|
|
onOpenDetail: (row: VendorListItem) => void;
|
|
onOpenDeactivation: (row: VendorListItem) => void;
|
|
}
|
|
|
|
export function VendorsTable({
|
|
rows,
|
|
isLoading,
|
|
showError,
|
|
onOpenDetail,
|
|
onOpenDeactivation,
|
|
}: VendorsTableProps) {
|
|
const showEmptyRow = !isLoading && !showError && rows.length === 0;
|
|
|
|
return (
|
|
<TableContainer component={Paper} variant="outlined" sx={{ overflowX: "auto" }}>
|
|
<Table size="small" sx={{ minWidth: 960 }}>
|
|
<TableHead>
|
|
<TableRow>
|
|
{TABLE_COLUMNS.map((column) => (
|
|
<TableCell
|
|
key={column}
|
|
align={column === "View" || column === "Edit" ? "center" : "left"}
|
|
className="font-semibold"
|
|
>
|
|
{column}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={TABLE_COLUMNS.length} align="center" className="py-10">
|
|
<CircularProgress size={28} />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : showEmptyRow ? (
|
|
<TableRow>
|
|
<TableCell colSpan={TABLE_COLUMNS.length} align="center" className="py-10">
|
|
<Typography sx={{ color: "text.secondary" }}>
|
|
No vendors match the current filters.
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
rows.map((row) => (
|
|
<VendorTableRow
|
|
key={String(row.id ?? `company-${row.companyId ?? ""}`)}
|
|
row={row}
|
|
onOpenDetail={onOpenDetail}
|
|
onOpenDeactivation={onOpenDeactivation}
|
|
/>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
}
|