shoc-frontend-new/src/app/(protected)/index.tsx
Arthur Bassi a0cf7b9ef0
Feat/vite typescript migration (#16)
* 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>
2026-06-18 14:41:17 -03:00

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>
);
}