refactor(web): dashboards on domain layer

This commit is contained in:
Adam Moussa 2026-07-13 18:03:33 -04:00
parent b7c6386aca
commit d1c7e812f6
No known key found for this signature in database
2 changed files with 49 additions and 34 deletions

View file

@ -1,6 +1,5 @@
import { useMemo, useState, Fragment } from 'react'; import { useEffect, useMemo, useState, Fragment } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { import {
Box, Box,
Typography, Typography,
@ -37,11 +36,10 @@ import ReportIcon from '@mui/icons-material/Report';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { useProposals } from '../../../domain/proposals/use-cases';
import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; import type { ProposalListItem } from '../../../domain/proposals/types';
import { adminApi, type DashboardStats } from '../../../lib/api/admin'; import { useAdminDashboard } from '../../../domain/admin/use-cases';
import { QUERY_KEYS } from '../../../constants/queryKeys'; import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_CHIP_STYLES, STATUS_LABELS, PRIORITY_LABELS, PRIORITY_CHIP_STYLES, PRIORITY_ROW_SX } from '../../../constants';
import { SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_CHIP_STYLES, STATUS_LABELS, PRIORITY_LABELS, PRIORITY_CHIP_STYLES, PRIORITY_ROW_SX } from '../../../constants';
import { formatBidAmount, formatDate, formatDateFull, formatAge, getAgeColor, groupRevisions } from '../../../lib/format'; import { formatBidAmount, formatDate, formatDateFull, formatAge, getAgeColor, groupRevisions } from '../../../lib/format';
import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
@ -75,33 +73,48 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
const showTabs = !defaultStatus; const showTabs = !defaultStatus;
// Fix: WEB-M8 — capture error state for admin stats to show error instead of misleading zeros // Fix: WEB-M8 — capture error state for admin stats to show error instead of misleading zeros
const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useQuery<DashboardStats>({ const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useAdminDashboard();
queryKey: [QUERY_KEYS.dashboard],
queryFn: adminApi.getDashboard,
});
const [statusFilter, setStatusFilter] = useState(defaultStatus ?? ''); const [statusFilter, setStatusFilter] = useState(defaultStatus ?? '');
const [categoryFilter, setCategoryFilter] = useState(''); const [categoryFilter, setCategoryFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState(''); const [priorityFilter, setPriorityFilter] = useState('');
const [expanded, setExpanded] = useState<Set<string>>(new Set()); const [expanded, setExpanded] = useState<Set<string>>(new Set());
const [search, setSearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [page, setPage] = useState(DEFAULT_PAGE);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
// Debounced search resets to page 1 (mirrors the old usePaginatedList behavior).
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedSearch(search);
setPage(1);
}, DEBOUNCE_SEARCH);
return () => clearTimeout(handle);
}, [search]);
const { const {
rows, data: listData,
search, isPending: listPending,
setSearch, isPlaceholderData: listIsPlaceholder,
error: listError,
} = useProposals({
search: debouncedSearch,
page, page,
setPage,
pageSize, pageSize,
setPageSize,
totalCount,
loading,
err,
} = usePaginatedList<ProposalListItem>(proposalsApi.getAll, {
...(statusFilter && { status: statusFilter }), ...(statusFilter && { status: statusFilter }),
...(categoryFilter && { serviceCategory: categoryFilter }), ...(categoryFilter && { serviceCategory: categoryFilter }),
...(priorityFilter && { priority: priorityFilter }), ...(priorityFilter && { priority: priorityFilter }),
}); });
const rows = useMemo(() => listData?.items ?? [], [listData]);
const totalCount = listData?.totalCount ?? 0;
// Show skeletons on initial load and while previous-page placeholder data is
// displayed for a new filter/page/search key (matches the old loading flag).
const loading = listPending || listIsPlaceholder;
const err = listError ? listError.message || 'Failed to load' : '';
const grouped = useMemo(() => groupRevisions(rows), [rows]); const grouped = useMemo(() => groupRevisions(rows), [rows]);
const toggleExpand = (baseNumber: string) => { const toggleExpand = (baseNumber: string) => {
@ -127,6 +140,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
setCategoryFilter(''); setCategoryFilter('');
setPriorityFilter(''); setPriorityFilter('');
setSearch(''); setSearch('');
setPage(1);
}; };
const renderRow = (row: ProposalListItem, indent: boolean, showExpandIcon: boolean, isExpanded: boolean, baseNumber: string) => ( const renderRow = (row: ProposalListItem, indent: boolean, showExpandIcon: boolean, isExpanded: boolean, baseNumber: string) => (
@ -297,7 +311,10 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Category" label="Category"
value={categoryFilter} value={categoryFilter}
sx={{ width: 160 }} sx={{ width: 160 }}
onChange={(e) => setCategoryFilter(e.target.value)} onChange={(e) => {
setCategoryFilter(e.target.value);
setPage(1);
}}
> >
<MenuItem value="">All</MenuItem> <MenuItem value="">All</MenuItem>
{SERVICE_CATEGORIES.map((c) => ( {SERVICE_CATEGORIES.map((c) => (
@ -310,7 +327,10 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Priority" label="Priority"
value={priorityFilter} value={priorityFilter}
sx={{ width: 200 }} sx={{ width: 200 }}
onChange={(e) => setPriorityFilter(e.target.value)} onChange={(e) => {
setPriorityFilter(e.target.value);
setPage(1);
}}
> >
<MenuItem value="">All</MenuItem> <MenuItem value="">All</MenuItem>
{PRIORITIES.map((p) => ( {PRIORITIES.map((p) => (
@ -324,7 +344,10 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Status" label="Status"
value={statusFilter} value={statusFilter}
sx={{ width: 140 }} sx={{ width: 140 }}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => {
setStatusFilter(e.target.value);
setPage(1);
}}
> >
<MenuItem value="">All</MenuItem> <MenuItem value="">All</MenuItem>
{PROPOSAL_STATUSES.map((s) => ( {PROPOSAL_STATUSES.map((s) => (

View file

@ -1,5 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { import {
Box, Box,
Typography, Typography,
@ -24,8 +23,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { proposalsApi, type ProposalListItem, type ProposalStats } from '../../lib/api/proposals'; import { useProposals, useProposalStats } from '../../domain/proposals/use-cases';
import { QUERY_KEYS } from '../../constants/queryKeys';
import { STATUS_CHIP_STYLES } from '../../constants'; import { STATUS_CHIP_STYLES } from '../../constants';
import { formatCurrency, formatDate, formatDateFull } from '../../lib/format'; import { formatCurrency, formatDate, formatDateFull } from '../../lib/format';
import { useDocumentTitle } from '../../hooks/useDocumentTitle'; import { useDocumentTitle } from '../../hooks/useDocumentTitle';
@ -55,16 +53,10 @@ export default function Dashboard() {
useDocumentTitle('Dashboard'); useDocumentTitle('Dashboard');
const navigate = useNavigate(); const navigate = useNavigate();
const { data: recentData, isLoading: recentLoading, error: recentError } = useQuery<{ items: ProposalListItem[]; totalCount: number }>({ const { data: recentData, isLoading: recentLoading, isError: recentError } = useProposals({ page: 1, pageSize: 5, mine: true });
queryKey: [QUERY_KEYS.proposals, 'dashboard-recent'],
queryFn: () => proposalsApi.getAll({ page: 1, pageSize: 5, mine: true }),
});
// Fix: WEB-M8 — capture error state for stats to show error instead of misleading zeros // Fix: WEB-M8 — capture error state for stats to show error instead of misleading zeros
const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useQuery<ProposalStats>({ const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useProposalStats();
queryKey: [QUERY_KEYS.proposals, 'stats'],
queryFn: () => proposalsApi.getStats(),
});
const proposals = recentData?.items ?? []; const proposals = recentData?.items ?? [];
const total = stats?.totalCount ?? 0; const total = stats?.totalCount ?? 0;