|
|
|
@ -1,7 +1,7 @@
|
|
|
|
import { useState } from 'react'
|
|
|
|
import { useState } from 'react'
|
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
import { formatINR } from '@sims/domain'
|
|
|
|
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 { getDuesAging, getModuleRevenue, getProfitability, getAwsRanking } from '../api'
|
|
|
|
import { useData } from './Clients'
|
|
|
|
import { useData } from './Clients'
|
|
|
|
|
|
|
|
|
|
|
|
@ -15,6 +15,7 @@ const prevMonth = (): string => {
|
|
|
|
export function Reports() {
|
|
|
|
export function Reports() {
|
|
|
|
const nav = useNavigate()
|
|
|
|
const nav = useNavigate()
|
|
|
|
const [month, setMonth] = useState(prevMonth())
|
|
|
|
const [month, setMonth] = useState(prevMonth())
|
|
|
|
|
|
|
|
const [report, setReport] = useState<'dues' | 'revenue' | 'profit' | 'aws'>('dues')
|
|
|
|
const dues = useData(getDuesAging, [])
|
|
|
|
const dues = useData(getDuesAging, [])
|
|
|
|
const revenue = useData(() => getModuleRevenue(), [])
|
|
|
|
const revenue = useData(() => getModuleRevenue(), [])
|
|
|
|
const profit = useData(() => getProfitability(), [])
|
|
|
|
const profit = useData(() => getProfitability(), [])
|
|
|
|
@ -24,10 +25,21 @@ export function Reports() {
|
|
|
|
<div className="wf-page">
|
|
|
|
<div className="wf-page">
|
|
|
|
<PageHeader title="Reports" desc="Dues aging, module revenue, client profitability, and the AWS cost chart." />
|
|
|
|
<PageHeader title="Reports" desc="Dues aging, module revenue, client profitability, and the AWS cost chart." />
|
|
|
|
|
|
|
|
|
|
|
|
<h3>Dues aging</h3>
|
|
|
|
<FilterChips
|
|
|
|
{dues.error !== undefined && <Notice tone="err">{dues.error}</Notice>}
|
|
|
|
active={report}
|
|
|
|
{dues.data === undefined || dues.data.length === 0
|
|
|
|
onChange={(k) => setReport(k as typeof report)}
|
|
|
|
? <EmptyState>No outstanding dues.</EmptyState>
|
|
|
|
chips={[
|
|
|
|
|
|
|
|
{ key: 'dues', label: 'Dues aging' },
|
|
|
|
|
|
|
|
{ key: 'revenue', label: 'Module revenue' },
|
|
|
|
|
|
|
|
{ key: 'profit', label: 'Profitability' },
|
|
|
|
|
|
|
|
{ key: 'aws', label: 'AWS costs' },
|
|
|
|
|
|
|
|
]}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{report === 'dues' && (
|
|
|
|
|
|
|
|
dues.error !== undefined ? <ErrorState message={dues.error} onRetry={dues.reload} />
|
|
|
|
|
|
|
|
: dues.data === undefined ? <Skeleton rows={5} />
|
|
|
|
|
|
|
|
: dues.data.length === 0 ? <EmptyState>No outstanding dues.</EmptyState>
|
|
|
|
: (
|
|
|
|
: (
|
|
|
|
<DataTable
|
|
|
|
<DataTable
|
|
|
|
columns={[
|
|
|
|
columns={[
|
|
|
|
@ -36,16 +48,18 @@ export function Reports() {
|
|
|
|
{ key: 'b4', label: '90+', numeric: true }, { key: 'total', label: 'Total', numeric: true },
|
|
|
|
{ key: 'b4', label: '90+', numeric: true }, { key: 'total', label: 'Total', numeric: true },
|
|
|
|
]}
|
|
|
|
]}
|
|
|
|
onRowClick={(_r, i) => nav(`/clients/${dues.data![i]!.clientId}`)}
|
|
|
|
onRowClick={(_r, i) => 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) => ({
|
|
|
|
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: 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),
|
|
|
|
}))}
|
|
|
|
}))}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<h3 style={{ marginTop: 20 }}>Module-wise revenue</h3>
|
|
|
|
{report === 'revenue' && (
|
|
|
|
{revenue.error !== undefined && <Notice tone="err">{revenue.error}</Notice>}
|
|
|
|
revenue.error !== undefined ? <ErrorState message={revenue.error} onRetry={revenue.reload} />
|
|
|
|
{revenue.data === undefined || revenue.data.length === 0
|
|
|
|
: revenue.data === undefined ? <Skeleton rows={5} />
|
|
|
|
? <EmptyState>No invoiced revenue yet.</EmptyState>
|
|
|
|
: revenue.data.length === 0 ? <EmptyState>No invoiced revenue yet.</EmptyState>
|
|
|
|
: (
|
|
|
|
: (
|
|
|
|
<DataTable
|
|
|
|
<DataTable
|
|
|
|
columns={[
|
|
|
|
columns={[
|
|
|
|
@ -54,12 +68,13 @@ export function Reports() {
|
|
|
|
]}
|
|
|
|
]}
|
|
|
|
rows={revenue.data.map((m) => ({ module: `${m.moduleName} (${m.moduleCode})`, billed: inr(m.billedPaise), settled: inr(m.settledPaise) }))}
|
|
|
|
rows={revenue.data.map((m) => ({ module: `${m.moduleName} (${m.moduleCode})`, billed: inr(m.billedPaise), settled: inr(m.settledPaise) }))}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<h3 style={{ marginTop: 20 }}>Client profitability</h3>
|
|
|
|
{report === 'profit' && (
|
|
|
|
{profit.error !== undefined && <Notice tone="err">{profit.error}</Notice>}
|
|
|
|
profit.error !== undefined ? <ErrorState message={profit.error} onRetry={profit.reload} />
|
|
|
|
{profit.data === undefined || profit.data.length === 0
|
|
|
|
: profit.data === undefined ? <Skeleton rows={5} />
|
|
|
|
? <EmptyState>No client activity yet.</EmptyState>
|
|
|
|
: profit.data.length === 0 ? <EmptyState>No client activity yet.</EmptyState>
|
|
|
|
: (
|
|
|
|
: (
|
|
|
|
<DataTable
|
|
|
|
<DataTable
|
|
|
|
columns={[
|
|
|
|
columns={[
|
|
|
|
@ -73,13 +88,15 @@ export function Reports() {
|
|
|
|
aws: inr(p.awsCostPaise), margin: inr(p.marginPaise),
|
|
|
|
aws: inr(p.awsCostPaise), margin: inr(p.marginPaise),
|
|
|
|
}))}
|
|
|
|
}))}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<h3 style={{ marginTop: 20 }}>AWS cost chart</h3>
|
|
|
|
{report === 'aws' && (
|
|
|
|
|
|
|
|
<>
|
|
|
|
<Field label="Month"><input type="month" className="wf" value={month} onChange={(e) => setMonth(e.target.value)} /></Field>
|
|
|
|
<Field label="Month"><input type="month" className="wf" value={month} onChange={(e) => setMonth(e.target.value)} /></Field>
|
|
|
|
{ranking.error !== undefined && <Notice tone="err">{ranking.error}</Notice>}
|
|
|
|
{ranking.error !== undefined ? <ErrorState message={ranking.error} onRetry={ranking.reload} />
|
|
|
|
{ranking.data === undefined || ranking.data.rows.length === 0
|
|
|
|
: ranking.data === undefined ? <Skeleton rows={5} />
|
|
|
|
? <EmptyState>No AWS cost recorded for {month}.</EmptyState>
|
|
|
|
: ranking.data.rows.length === 0 ? <EmptyState>No AWS cost recorded for {month}.</EmptyState>
|
|
|
|
: (
|
|
|
|
: (
|
|
|
|
<div style={{ margin: '10px 0' }}>
|
|
|
|
<div style={{ margin: '10px 0' }}>
|
|
|
|
{ranking.data.rows.map((row) => (
|
|
|
|
{ranking.data.rows.map((row) => (
|
|
|
|
@ -94,6 +111,8 @@ export function Reports() {
|
|
|
|
))}
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|