shoc-frontend-new/src/app/(protected)/followups/index.tsx

255 lines
8.3 KiB
TypeScript

import { useMemo, useState } from "react";
import { useNavigate } from "react-router";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import {
Alert,
Box,
Button,
Chip,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
IconButton,
InputLabel,
MenuItem,
Paper,
Select,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import type { FollowUpListItem } from "@/domain/follow-ups/types/follow-up";
import { useDeleteFollowUp } from "@/domain/follow-ups/use-cases/use-delete-follow-up";
import { useFollowUpsList } from "@/domain/follow-ups/use-cases/use-follow-ups-list";
import { useDebounce } from "@/hooks/use-debounce";
import { toDateInputValue } from "@/lib/time-utils";
const STATUS_OPTIONS = ["Open", "Completed", "Cancelled", ""];
export default function FollowUpsListPage() {
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(12);
const [status, setStatus] = useState("Open");
const [deleteTarget, setDeleteTarget] = useState<FollowUpListItem | null>(null);
const debouncedSearch = useDebounce(search, 300);
const deleteFollowUp = useDeleteFollowUp();
const listParams = useMemo(
() => ({ search: debouncedSearch, page: page + 1, pageSize, status }),
[debouncedSearch, page, pageSize, status],
);
const { data, isLoading, isFetching, error } = useFollowUpsList(listParams);
const rows = data?.items ?? [];
const totalCount = data?.totalCount ?? 0;
return (
<Box className="flex flex-col gap-4 p-4">
<Stack
direction={{ xs: "column", md: "row" }}
spacing={2}
sx={{
alignItems: { xs: "stretch", md: "center" },
justifyContent: "space-between",
}}
>
<Stack
direction="row"
spacing={2}
sx={{
alignItems: "baseline",
}}
>
<Typography variant="h5" component="h1">
Follow-ups
</Typography>
<Typography
variant="body2"
sx={{
color: "text.secondary",
}}
>
{totalCount} total
</Typography>
{isFetching && !isLoading && <CircularProgress size={16} />}
</Stack>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
sx={{
alignItems: "center",
}}
>
<TextField
size="small"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(0);
}}
placeholder="Search follow-ups..."
className="min-w-sidebar"
/>
<FormControl size="small" className="min-w-[140px]">
<InputLabel>Status</InputLabel>
<Select
label="Status"
value={status}
onChange={(e) => {
setStatus(e.target.value);
setPage(0);
}}
>
{STATUS_OPTIONS.map((s) => (
<MenuItem key={s || "all"} value={s}>
{s || "All"}
</MenuItem>
))}
</Select>
</FormControl>
<Button variant="contained" onClick={() => navigate("/followups/new")}>
+ New Follow-up
</Button>
</Stack>
</Stack>
{Boolean(error) && (
<Alert severity="error">
{error instanceof Error ? error.message : "Failed to load follow-ups"}
</Alert>
)}
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell className="font-semibold">Reason</TableCell>
<TableCell className="font-semibold">Schedule</TableCell>
<TableCell className="font-semibold">Assigned To</TableCell>
<TableCell className="font-semibold">Account</TableCell>
<TableCell className="font-semibold">Location</TableCell>
<TableCell className="font-semibold">WO #</TableCell>
<TableCell className="font-semibold">Status</TableCell>
<TableCell align="right" className="font-semibold">
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={8} align="center" className="py-10">
<CircularProgress size={28} />
</TableCell>
</TableRow>
) : rows.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center" className="py-10">
<Typography
sx={{
color: "text.secondary",
}}
>
No follow-ups found
</Typography>
</TableCell>
</TableRow>
) : (
rows.map((row) => (
<TableRow key={String(row.id)} hover>
<TableCell>
<Button
variant="text"
size="small"
className="font-semibold"
onClick={() => navigate(`/followups/edit/${row.id}`)}
>
{row.reason || "—"}
</Button>
</TableCell>
<TableCell>
{toDateInputValue(row.scheduleDate) || "—"}
{row.scheduleStartTime ? ` ${row.scheduleStartTime.slice(0, 5)}` : ""}
</TableCell>
<TableCell>{row.assignedTo || "—"}</TableCell>
<TableCell>{row.accountName || "—"}</TableCell>
<TableCell>{row.locationName || "—"}</TableCell>
<TableCell>{row.internalWONumber || row.workOrderNumber || "—"}</TableCell>
<TableCell>
<Chip size="small" label={row.status || "—"} />
</TableCell>
<TableCell align="right">
<Tooltip title="Edit">
<IconButton
size="small"
onClick={() => navigate(`/followups/edit/${row.id}`)}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<IconButton size="small" onClick={() => setDeleteTarget(row)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={totalCount}
page={page}
onPageChange={(_e, nextPage) => setPage(nextPage)}
rowsPerPage={pageSize}
onRowsPerPageChange={(e) => {
setPageSize(Number(e.target.value));
setPage(0);
}}
rowsPerPageOptions={[12, 24, 48]}
/>
<Dialog open={Boolean(deleteTarget)} onClose={() => setDeleteTarget(null)}>
<DialogTitle>Delete Follow-up</DialogTitle>
<DialogContent>
<DialogContentText>Are you sure you want to delete this follow-up?</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteTarget(null)} disabled={deleteFollowUp.isPending}>
Cancel
</Button>
<Button
color="error"
variant="contained"
disabled={deleteFollowUp.isPending}
onClick={() => {
if (!deleteTarget) return;
deleteFollowUp.mutate(deleteTarget.id, {
onSuccess: () => setDeleteTarget(null),
});
}}
>
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
);
}