diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index ed01b30..79aa3c2 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -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 + // 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 (
- {NAV.map((n) => ( + {nav_items.map((n) => ( (isActive ? 'active' : '')} end={n.to === '/'}> {n.label} diff --git a/apps/hq-web/src/main.tsx b/apps/hq-web/src/main.tsx index 17fbf78..ac196ee 100644 --- a/apps/hq-web/src/main.tsx +++ b/apps/hq-web/src/main.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/hq-web/src/pages/ClientDetail.tsx b/apps/hq-web/src/pages/ClientDetail.tsx index fbd133c..6af08f6 100644 --- a/apps/hq-web/src/pages/ClientDetail.tsx +++ b/apps/hq-web/src/pages/ClientDetail.tsx @@ -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() @@ -278,6 +280,23 @@ export function ClientDetail() { }))} /> )} + +

AWS usage

+ {awsUsage.error !== undefined && {awsUsage.error}} + {awsUsage.data === undefined || awsUsage.data.length === 0 + ? No AWS usage recorded for this client. + : ( + ({ + month: u.month, storage: u.storageGb, transfer: u.transferGb, cost: inr(u.costPaise), source: u.source, + }))} + /> + )} ) } diff --git a/apps/hq-web/src/pages/CompanyProfile.tsx b/apps/hq-web/src/pages/CompanyProfile.tsx new file mode 100644 index 0000000..fc6f333 --- /dev/null +++ b/apps/hq-web/src/pages/CompanyProfile.tsx @@ -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>({}) + 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 + + 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 ( +
+ + {msg !== undefined && {msg.text}} +
+ {FIELDS.map((x) => ( + + {x.key === 'address' || x.key === 'bank' + ?