feat(hq-web): owner company profile editor and client 360 aws usage trend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 2 weeks ago
parent 2acca18fc9
commit dbccb8f954

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
import { Navigate, NavLink, Outlet, useNavigate } from 'react-router-dom'
import { Badge, ThemeSwitcher } from '@sims/ui'
import { clearSession, displayName, getEmailStatus, hasSession } from './api'
import { clearSession, displayName, getEmailStatus, hasSession, role } from './api'
const NAV = [
const BASE_NAV = [
{ to: '/', label: 'Dashboard' },
{ to: '/clients', label: 'Clients' },
{ to: '/modules', label: 'Modules' },
@ -25,6 +25,9 @@ export function Layout() {
if (!hasSession()) return <Navigate to="/login" replace />
// Company (the company.* settings editor) is owner-only — staff never see the item.
const nav_items = role() === 'owner' ? [...BASE_NAV, { to: '/settings/company', label: 'Company' }] : BASE_NAV
return (
<div className="hq-shell">
<nav className="hq-side">
@ -32,7 +35,7 @@ export function Layout() {
SiMS HQ
<small>Ops console</small>
</div>
{NAV.map((n) => (
{nav_items.map((n) => (
<NavLink key={n.to} to={n.to} className={({ isActive }) => (isActive ? 'active' : '')} end={n.to === '/'}>
{n.label}
</NavLink>

@ -12,6 +12,7 @@ import { Clients } from './pages/Clients'
import { ClientDetail } from './pages/ClientDetail'
import { Modules } from './pages/Modules'
import { Reports } from './pages/Reports'
import { CompanyProfile } from './pages/CompanyProfile'
import { NewDocument } from './pages/NewDocument'
import { DocumentView } from './pages/DocumentView'
@ -26,6 +27,7 @@ function App() {
<Route path="/clients/:id" element={<ClientDetail />} />
<Route path="/modules" element={<Modules />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings/company" element={<CompanyProfile />} />
<Route path="/documents/new" element={<NewDocument />} />
<Route path="/documents/:id" element={<DocumentView />} />
<Route path="*" element={<Navigate to="/" replace />} />

@ -8,6 +8,7 @@ import {
role, getRecurringPlans, createRecurringPlan, deactivateRecurringPlan,
getAmc, createAmc, deactivateAmc, generateAmcRenewalInvoice,
getInteractions, getInteractionTypes, createInteraction, updateInteraction,
getClientAwsUsage,
CLIENT_MODULE_STATUSES, CLIENT_STATUSES, KIND_LABEL, PAYMENT_MODES,
type AmcPaidStatus, type ClientModule, type ClientModuleStatus, type ClientStatus,
type InteractionType, type Kind, type Module, type Outcome, type PaymentMode,
@ -32,6 +33,7 @@ export function ClientDetail() {
const amc = useData(() => getAmc(id), [id])
const interactions = useData(() => getInteractions(id), [id])
const types = useData(getInteractionTypes, [])
const awsUsage = useData(() => getClientAwsUsage(id), [id])
const isOwner = role() === 'owner'
const [actionErr, setActionErr] = useState<string | undefined>()
@ -278,6 +280,23 @@ export function ClientDetail() {
}))}
/>
)}
<h3 style={{ marginTop: 20 }}>AWS usage</h3>
{awsUsage.error !== undefined && <Notice tone="err">{awsUsage.error}</Notice>}
{awsUsage.data === undefined || awsUsage.data.length === 0
? <EmptyState>No AWS usage recorded for this client.</EmptyState>
: (
<DataTable
columns={[
{ key: 'month', label: 'Month' }, { key: 'storage', label: 'Storage GB', numeric: true },
{ key: 'transfer', label: 'Transfer GB', numeric: true }, { key: 'cost', label: 'Cost', numeric: true },
{ key: 'source', label: 'Source' },
]}
rows={awsUsage.data.map((u) => ({
month: u.month, storage: u.storageGb, transfer: u.transferGb, cost: inr(u.costPaise), source: u.source,
}))}
/>
)}
</div>
)
}

@ -0,0 +1,54 @@
import { useEffect, useState } from 'react'
import { Navigate } from 'react-router-dom'
import { Button, Field, Notice, PageHeader } from '@sims/ui'
import { getCompanyProfile, putCompanyProfile, role } from '../api'
const FIELDS: { key: string; setting: string; label: string }[] = [
{ key: 'name', setting: 'company.name', label: 'Company name' },
{ key: 'address', setting: 'company.address', label: 'Address' },
{ key: 'gstin', setting: 'company.gstin', label: 'GSTIN' },
{ key: 'stateCode', setting: 'company.state_code', label: 'State code' },
{ key: 'phone', setting: 'company.phone', label: 'Phone' },
{ key: 'email', setting: 'company.email', label: 'Email' },
{ key: 'bank', setting: 'company.bank', label: 'Bank details' },
]
/** Owner-only editor for the company.* settings the letterhead PDFs read. */
export function CompanyProfile() {
const [f, setF] = useState<Record<string, string>>({})
const [msg, setMsg] = useState<{ tone: 'ok' | 'err'; text: string } | undefined>()
const [saving, setSaving] = useState(false)
useEffect(() => {
getCompanyProfile()
.then((c) => setF(Object.fromEntries(FIELDS.map((x) => [x.key, c[x.setting] ?? '']))))
.catch((e: Error) => setMsg({ tone: 'err', text: e.message }))
}, [])
if (role() !== 'owner') return <Navigate to="/" replace />
const save = () => {
setSaving(true); setMsg(undefined)
putCompanyProfile(f)
.then((c) => { setF(Object.fromEntries(FIELDS.map((x) => [x.key, c[x.setting] ?? '']))); setMsg({ tone: 'ok', text: 'Saved. PDFs use the new details immediately.' }) })
.catch((e: Error) => setMsg({ tone: 'err', text: e.message }))
.finally(() => setSaving(false))
}
return (
<div className="wf-page">
<PageHeader title="Company profile" desc="These details appear on every quotation, invoice, and receipt." />
{msg !== undefined && <Notice tone={msg.tone}>{msg.text}</Notice>}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 520, marginTop: 10 }}>
{FIELDS.map((x) => (
<Field key={x.key} label={x.label}>
{x.key === 'address' || x.key === 'bank'
? <textarea className="wf" rows={2} style={{ width: '100%' }} value={f[x.key] ?? ''} onChange={(e) => setF((p) => ({ ...p, [x.key]: e.target.value }))} />
: <input className="wf" style={{ width: '100%' }} value={f[x.key] ?? ''} onChange={(e) => setF((p) => ({ ...p, [x.key]: e.target.value }))} />}
</Field>
))}
<div><Button tone="primary" onClick={save}>{saving ? 'Saving…' : 'Save company profile'}</Button></div>
</div>
</div>
)
}
Loading…
Cancel
Save