From a244ddb2b01f96983e56314c6e6d415844780505 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 17 Jul 2026 03:02:19 +0530 Subject: [PATCH] feat(hq-web): reports chips + skeleton/error states across catalog pages Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/hq-web/src/pages/DocumentTemplate.tsx | 2 +- apps/hq-web/src/pages/Employees.tsx | 17 ++- apps/hq-web/src/pages/Modules.tsx | 16 +-- apps/hq-web/src/pages/NewDocument.tsx | 4 +- apps/hq-web/src/pages/Reports.tsx | 153 ++++++++++++--------- 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/apps/hq-web/src/pages/DocumentTemplate.tsx b/apps/hq-web/src/pages/DocumentTemplate.tsx index d1bc82f..1d3e0b9 100644 --- a/apps/hq-web/src/pages/DocumentTemplate.tsx +++ b/apps/hq-web/src/pages/DocumentTemplate.tsx @@ -110,7 +110,7 @@ export function DocumentTemplate() {
-
+
)} {error !== undefined && {error}} - {list.error !== undefined && {list.error}} {/* Branch on the fetch error first — `employees` stays undefined on failure, - so falling through would show "Loading…" forever under the error notice. */} - {list.error !== undefined ? null - : employees === undefined ? Loading… + so falling through would show the skeleton forever under the error state. */} + {list.error !== undefined ? + : employees === undefined ? : employees.length === 0 ? No employees yet. : ( <> void; onCancel: () => void }) { } return ( -
+
@@ -147,7 +146,7 @@ function EditEmployeeForm(props: { employee: Employee; onDone: () => void; onCan } return ( -
+
{/* Email is the login id — immutable after creation. */} @@ -180,7 +179,7 @@ function ResetPasswordForm(props: { employee: Employee; onDone: () => void; onCa } return ( -
+
setPassword(e.target.value)} /> diff --git a/apps/hq-web/src/pages/Modules.tsx b/apps/hq-web/src/pages/Modules.tsx index 7e6e4f4..b8a7c20 100644 --- a/apps/hq-web/src/pages/Modules.tsx +++ b/apps/hq-web/src/pages/Modules.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { formatINR, fromRupees } from '@sims/domain' -import { Badge, Button, DataTable, EmptyState, Field, Notice, PageHeader, Toolbar } from '@sims/ui' +import { Badge, Button, DataTable, EmptyState, ErrorState, Field, Notice, PageHeader, Skeleton, Toolbar } from '@sims/ui' import { addPrice, createModule, getModules, getPrices, patchModule, previewSample, role, KIND_LABEL, type Kind, type Module, @@ -33,12 +33,12 @@ export function Modules() { {creating && isOwner && ( { setCreating(false); modules.reload() }} /> )} - {modules.error !== undefined && {modules.error}} - {modules.data === undefined ? Loading… + {modules.error !== undefined ? + : modules.data === undefined ? : modules.data.length === 0 ? No modules yet. : ( {error !== undefined && {error}}
-
+
void }) { } return ( -
+
@@ -198,8 +198,8 @@ function PriceBook(props: { module: Module; isOwner: boolean }) { )} {error !== undefined && {error}} - {prices.error !== undefined && {prices.error}} - {prices.data === undefined ? Loading… + {prices.error !== undefined ? + : prices.data === undefined ? : prices.data.length === 0 ? No prices yet{props.isOwner ? ' — add the first row above' : ''}. : ( setSheetOpen(true)}>Preview
{sheetOpen && ( -
+
{paper}
{totalsStrip} @@ -296,7 +296,7 @@ export function NewDocument() { return (
{form}
-
+
{paper}
{totalsStrip}
diff --git a/apps/hq-web/src/pages/Reports.tsx b/apps/hq-web/src/pages/Reports.tsx index bb16c5e..e88915d 100644 --- a/apps/hq-web/src/pages/Reports.tsx +++ b/apps/hq-web/src/pages/Reports.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { formatINR } from '@sims/domain' -import { DataTable, EmptyState, Field, Notice, PageHeader } from '@sims/ui' +import { DataTable, EmptyState, ErrorState, Field, FilterChips, Skeleton, PageHeader } from '@sims/ui' import { getDuesAging, getModuleRevenue, getProfitability, getAwsRanking } from '../api' import { useData } from './Clients' @@ -15,6 +15,7 @@ const prevMonth = (): string => { export function Reports() { const nav = useNavigate() const [month, setMonth] = useState(prevMonth()) + const [report, setReport] = useState<'dues' | 'revenue' | 'profit' | 'aws'>('dues') const dues = useData(getDuesAging, []) const revenue = useData(() => getModuleRevenue(), []) const profit = useData(() => getProfitability(), []) @@ -24,76 +25,94 @@ export function Reports() {
-

Dues aging

- {dues.error !== undefined && {dues.error}} - {dues.data === undefined || dues.data.length === 0 - ? No outstanding dues. - : ( - nav(`/clients/${dues.data![i]!.clientId}`)} - rows={dues.data.map((d) => ({ - client: d.clientName, b1: inr(d.b0_30), b2: inr(d.b31_60), b3: inr(d.b61_90), b4: inr(d.b90p), total: inr(d.totalPaise), - }))} - /> - )} + setReport(k as typeof report)} + chips={[ + { key: 'dues', label: 'Dues aging' }, + { key: 'revenue', label: 'Module revenue' }, + { key: 'profit', label: 'Profitability' }, + { key: 'aws', label: 'AWS costs' }, + ]} + /> -

Module-wise revenue

- {revenue.error !== undefined && {revenue.error}} - {revenue.data === undefined || revenue.data.length === 0 - ? No invoiced revenue yet. - : ( - ({ module: `${m.moduleName} (${m.moduleCode})`, billed: inr(m.billedPaise), settled: inr(m.settledPaise) }))} - /> - )} + {report === 'dues' && ( + dues.error !== undefined ? + : dues.data === undefined ? + : dues.data.length === 0 ? No outstanding dues. + : ( + nav(`/clients/${dues.data![i]!.clientId}`)} + rowTone={(i) => ((dues.data![i]!.b90p > 0) ? 'err' : dues.data![i]!.b61_90 > 0 ? 'warn' : undefined)} + rows={dues.data.map((d) => ({ + client: d.clientName, b1: inr(d.b0_30), b2: inr(d.b31_60), b3: inr(d.b61_90), b4: inr(d.b90p), total: inr(d.totalPaise), + }))} + /> + ) + )} -

Client profitability

- {profit.error !== undefined && {profit.error}} - {profit.data === undefined || profit.data.length === 0 - ? No client activity yet. - : ( - nav(`/clients/${profit.data![i]!.clientId}`)} - rows={profit.data.map((p) => ({ - client: p.clientName, billed: inr(p.billedPaise), settled: inr(p.settledPaise), - aws: inr(p.awsCostPaise), margin: inr(p.marginPaise), - }))} - /> - )} + {report === 'revenue' && ( + revenue.error !== undefined ? + : revenue.data === undefined ? + : revenue.data.length === 0 ? No invoiced revenue yet. + : ( + ({ module: `${m.moduleName} (${m.moduleCode})`, billed: inr(m.billedPaise), settled: inr(m.settledPaise) }))} + /> + ) + )} -

AWS cost chart

- setMonth(e.target.value)} /> - {ranking.error !== undefined && {ranking.error}} - {ranking.data === undefined || ranking.data.rows.length === 0 - ? No AWS cost recorded for {month}. - : ( -
- {ranking.data.rows.map((row) => ( -
-
nav(`/clients/${row.clientId}`)}>{row.clientName}
-
-
-
-
{inr(row.costPaise)}
-
{(row.sharePctBp / 100).toFixed(1)}%
+ {report === 'profit' && ( + profit.error !== undefined ? + : profit.data === undefined ? + : profit.data.length === 0 ? No client activity yet. + : ( + nav(`/clients/${profit.data![i]!.clientId}`)} + rows={profit.data.map((p) => ({ + client: p.clientName, billed: inr(p.billedPaise), settled: inr(p.settledPaise), + aws: inr(p.awsCostPaise), margin: inr(p.marginPaise), + }))} + /> + ) + )} + + {report === 'aws' && ( + <> + setMonth(e.target.value)} /> + {ranking.error !== undefined ? + : ranking.data === undefined ? + : ranking.data.rows.length === 0 ? No AWS cost recorded for {month}. + : ( +
+ {ranking.data.rows.map((row) => ( +
+
nav(`/clients/${row.clientId}`)}>{row.clientName}
+
+
+
+
{inr(row.costPaise)}
+
{(row.sharePctBp / 100).toFixed(1)}%
+
+ ))}
- ))} -
- )} + )} + + )}
) }