mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 03:23:12 +00:00
* chore: add eslint, prettier, husky and commitlint tooling * ci: add GitHub Actions workflow for lint and build * build: migrate from CRA to Vite with TypeScript config * docs: add architecture plan and design system documentation * fix: scope ESLint to new components and hooks directories * feat: add HTTP client, query cache and shared utilities * feat: add theme system and global application styles * feat: add shared UI, layout and domain badge components * feat: add auth domain, provider and login page * feat: add app shell, file-based routing and bootstrap * feat: add protected layout and dashboard module * feat: add accounts CRUD module * feat: add assets CRUD module * feat: add contacts CRUD module * feat: add employees CRUD module * feat: add locations CRUD module * feat: add calendar events module * feat: add follow-ups CRUD module * feat: add PM schedules CRUD module * feat: add work orders module with dispatch modals * feat: add vendors CRUD and portal token panel * feat: add vendor purchase orders module * feat: add uplifts queue module * feat: add settings for dropdowns and task templates * feat: add vendor portal routes and signature capture * test: add Vitest setup and Playwright login e2e spec * chore: remove legacy CRA pages, Redux store and JS hooks * chore: update gitignore and env example for Vite * docs: translate pt-BR docs and Cursor rules to English * fix(ci): fix login e2e session mock and prettier formatting * fix(build): use mjs router script for Node 20 CI compatibility * chore: remove migration scripts and unused generouted router * fix(auth): restore JWT and align CRUD with backend routes * fix(api): add no-content helpers and align paths with backend * fix(accounts): align detail and mutation payloads with backend * fix(contacts): resolve detail via GetContacts and map address DTOs * fix(work-orders): align routes, delete body, and create payload * fix(vendors): delete vendors via REST route * fix(pm-schedules): handle empty save and delete responses * fix(employees): add fallbacks for detail and dropdown calls * chore(calendar): disable routes until backend exists * chore(env): switch tracked env vars to Vite prefixes * fix(api): align frontend contracts with backend review findings Correct Work Order getById query param, asset site options via Location API, Employee JobTitleId payload, and remove stale API paths. * fix(employees,pm-schedules): align forms with backend API contracts Align PM Schedule form and save payload with PMSchedule_DTO fields. Bind employee Job Title select to jobTitleId for create/update payloads. Add regression tests for both flows. * fix(pm-schedules): gate edit/delete for Dev API contract Production Dev API exposes only GetList and Save. Hide unsupported edit/delete UI and block the edit route. Add regression tests for disabled actions. * docs(env): document VITE_API_URL must include /api suffix * refactor(api): extract shared API prefix URL resolution * feat(build): fail build on misconfigured absolute VITE_API_URL * test(api): cover API URL contract and prefix resolution * feat(auth): disable login submit until email and password are valid * test(auth): align login tests with disabled submit behavior * Feat/ab/menu-and-header (#17) * chore(deps): add lucide-react for layout icon migration * feat(auth): add getPrimaryUserRole helper for header display * style(theme): add sidebar active tokens and nav group typography * refactor(menu): migrate nav icons to lucide and trim menu groups * feat(layout): redesign sidebar, topbar, and admin shell viewport * chore(menu): hide Reports and Documents from sidebar --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> * ci: add frontend PR quality baseline (#18) * chore(deps): add lucide-react for layout icon migration * feat(auth): add getPrimaryUserRole helper for header display * style(theme): add sidebar active tokens and nav group typography * refactor(menu): migrate nav icons to lucide and trim menu groups * feat(layout): redesign sidebar, topbar, and admin shell viewport * chore(menu): hide Reports and Documents from sidebar * ci: add PR quality baseline checks * ci: avoid self-matching standards guard * ci: split frontend quality gates --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
221 lines
7.1 KiB
TypeScript
221 lines
7.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import AddIcon from "@mui/icons-material/Add";
|
|
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
|
import FolderOpenOutlinedIcon from "@mui/icons-material/FolderOpenOutlined";
|
|
import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined";
|
|
import {
|
|
Alert,
|
|
Box,
|
|
CircularProgress,
|
|
Paper,
|
|
Stack,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import { useDashboardStats } from "@/domain/dashboard/use-cases/use-dashboard-stats";
|
|
|
|
const INVOICE_STATUSES = ["Draft", "Challenged", "Pending Approval", "Approved", "Paid"] as const;
|
|
|
|
const PRIORITY_LEVELS = [5, 4, 3, 2, 1] as const;
|
|
|
|
interface KpiCardProps {
|
|
title: string;
|
|
description?: string;
|
|
value?: string | number;
|
|
icon: ReactNode;
|
|
color: "success" | "primary" | "info" | "warning";
|
|
onClick: () => void;
|
|
loading?: boolean;
|
|
}
|
|
|
|
function KpiCard({ title, description, value, icon, color, onClick, loading }: KpiCardProps) {
|
|
const colorMap = {
|
|
success: "success.main",
|
|
primary: "primary.main",
|
|
info: "info.main",
|
|
warning: "warning.main",
|
|
} as const;
|
|
|
|
return (
|
|
<Paper
|
|
variant="outlined"
|
|
onClick={onClick}
|
|
className="flex min-h-[140px] flex-1 cursor-pointer transition-shadow hover:shadow-md"
|
|
sx={{
|
|
borderLeftWidth: 4,
|
|
borderLeftStyle: "solid",
|
|
borderLeftColor: colorMap[color],
|
|
}}
|
|
>
|
|
<Box className="flex w-full gap-4 p-4">
|
|
<Box
|
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full"
|
|
sx={{ bgcolor: `${colorMap[color]}22`, color: colorMap[color] }}
|
|
>
|
|
{icon}
|
|
</Box>
|
|
|
|
<Box className="flex min-w-0 flex-col justify-center gap-1">
|
|
<Typography variant="subtitle1" className="font-semibold">
|
|
{title}
|
|
</Typography>
|
|
{value !== undefined && (
|
|
<Typography variant="h4" component="p" className="font-bold leading-tight">
|
|
{loading ? "..." : typeof value === "number" ? value.toLocaleString() : value}
|
|
</Typography>
|
|
)}
|
|
{description && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
{description}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const navigate = useNavigate();
|
|
const { data: stats, isLoading, isFetching, error } = useDashboardStats();
|
|
|
|
const formatStat = (value: number | undefined) => (isLoading ? undefined : (value ?? 0));
|
|
|
|
return (
|
|
<Box className="flex flex-col gap-4 p-4">
|
|
<Stack direction="row" spacing={2} alignItems="baseline">
|
|
<Typography variant="h5" component="h1">
|
|
Dashboard
|
|
</Typography>
|
|
{isFetching && !isLoading && <CircularProgress size={16} />}
|
|
</Stack>
|
|
|
|
{error && (
|
|
<Alert severity="error">
|
|
{error instanceof Error ? error.message : "Failed to load dashboard stats"}
|
|
</Alert>
|
|
)}
|
|
|
|
<Stack direction={{ xs: "column", lg: "row" }} spacing={2} useFlexGap flexWrap="wrap">
|
|
<KpiCard
|
|
title="Create a Work Order"
|
|
description="Report a problem you need fixed"
|
|
icon={<AddIcon />}
|
|
color="success"
|
|
onClick={() => navigate("/workorders/new")}
|
|
/>
|
|
<KpiCard
|
|
title="Open Work Orders"
|
|
value={formatStat(stats?.open)}
|
|
icon={<FolderOpenOutlinedIcon />}
|
|
color="primary"
|
|
loading={isLoading}
|
|
onClick={() => navigate("/workorders")}
|
|
/>
|
|
<KpiCard
|
|
title="Not Dispatched"
|
|
value={formatStat(stats?.notDispatched)}
|
|
icon={<ScheduleOutlinedIcon />}
|
|
color="info"
|
|
loading={isLoading}
|
|
onClick={() => navigate("/workorders")}
|
|
/>
|
|
<KpiCard
|
|
title="Completed Orders"
|
|
value={formatStat(stats?.completed)}
|
|
icon={<CheckCircleOutlineIcon />}
|
|
color="warning"
|
|
loading={isLoading}
|
|
onClick={() => navigate("/workorders")}
|
|
/>
|
|
</Stack>
|
|
|
|
<Paper variant="outlined" className="overflow-hidden">
|
|
<Box className="border-b border-border px-4 py-3">
|
|
<Typography variant="h6" component="h2">
|
|
Priority Analysis
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Age of open Work Orders by Priority
|
|
</Typography>
|
|
</Box>
|
|
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell className="font-semibold">Priority</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
Total
|
|
</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
0-14 Days
|
|
</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
15-29 Days
|
|
</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
30-59 Days
|
|
</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
60+ Days
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{PRIORITY_LEVELS.map((priority) => (
|
|
<TableRow key={priority}>
|
|
<TableCell>{priority}</TableCell>
|
|
<TableCell align="right">—</TableCell>
|
|
<TableCell align="right">—</TableCell>
|
|
<TableCell align="right">—</TableCell>
|
|
<TableCell align="right">—</TableCell>
|
|
<TableCell align="right">—</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Paper>
|
|
|
|
<Paper variant="outlined" className="overflow-hidden">
|
|
<Box className="border-b border-border px-4 py-3">
|
|
<Typography variant="h6" component="h2">
|
|
Invoice Analysis
|
|
</Typography>
|
|
</Box>
|
|
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell className="font-semibold">Status</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
Count
|
|
</TableCell>
|
|
<TableCell align="right" className="font-semibold">
|
|
Amount
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{INVOICE_STATUSES.map((status) => (
|
|
<TableRow key={status}>
|
|
<TableCell>{status}</TableCell>
|
|
<TableCell align="right">0</TableCell>
|
|
<TableCell align="right">$0.00</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Paper>
|
|
</Box>
|
|
);
|
|
}
|