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}
-
+
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)}%
+
+ ))}
- ))}
-
- )}
+ )}
+ >
+ )}
)
}