feat(d30): Home as landing page + more sidebar counts

- Make the Home hub the landing: `/` now renders Home and the Dashboard
  moves to `/dashboard` (nav item, routes, and the hub's Dashboard card
  all updated). Unknown routes still fall back to `/` (Home).
- Add sidebar count badges for Clients (total book size), Documents
  (drafts still to finish) and Modules (count) — neutral, best-effort,
  alongside the existing Reminders/Pipeline/Tickets/Renewals/SMS counts.

Typecheck clean; web build green. Verified live: / → Home, /dashboard →
Dashboard, and the Modules count badge renders from real data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 3 days ago
parent b8e2be628f
commit 8ed56a97d5

@ -5,8 +5,8 @@ import {
avatarColors, Badge, CommandPalette, initials, ModeToggle, type PaletteItem, avatarColors, Badge, CommandPalette, initials, ModeToggle, type PaletteItem,
} from '@sims/ui' } from '@sims/ui'
import { import {
clearSession, displayName, getEmailStatus, getPipeline, getReminders, getRenewals, clearSession, displayName, getClients, getDocuments, getEmailStatus, getModules, getPipeline,
getSearch, getSmsBalances, getTickets, hasSession, role, getReminders, getRenewals, getSearch, getSmsBalances, getTickets, hasSession, role,
} from './api' } from './api'
import { NAV_GROUPS } from './nav' import { NAV_GROUPS } from './nav'
@ -24,8 +24,10 @@ export function Layout() {
return !v return !v
}) })
const [queueCounts, setQueueCounts] = useState({ queued: 0, failed: 0 }) const [queueCounts, setQueueCounts] = useState({ queued: 0, failed: 0 })
// Section-badge counts (Pipeline / Tickets / Renewals / SMS) — fetched once on mount. // Section-badge counts — fetched once on mount (best-effort, stay 0 on failure).
const [sectionCounts, setSectionCounts] = useState({ pipeline: 0, tickets: 0, renewals: 0, smsLow: 0 }) const [sectionCounts, setSectionCounts] = useState({
pipeline: 0, tickets: 0, renewals: 0, smsLow: 0, clients: 0, docDrafts: 0, modules: 0,
})
const [healthy, setHealthy] = useState<boolean | undefined>() const [healthy, setHealthy] = useState<boolean | undefined>()
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const [userOpen, setUserOpen] = useState(false) const [userOpen, setUserOpen] = useState(false)
@ -60,6 +62,9 @@ export function Layout() {
getTickets({ status: 'open' }).then((r) => setSectionCounts((c) => ({ ...c, tickets: r.total }))).catch(() => { /* best-effort */ }) getTickets({ status: 'open' }).then((r) => setSectionCounts((c) => ({ ...c, tickets: r.total }))).catch(() => { /* best-effort */ })
getRenewals().then((rows) => setSectionCounts((c) => ({ ...c, renewals: rows.length }))).catch(() => { /* best-effort */ }) getRenewals().then((rows) => setSectionCounts((c) => ({ ...c, renewals: rows.length }))).catch(() => { /* best-effort */ })
getSmsBalances().then((r) => setSectionCounts((c) => ({ ...c, smsLow: r.lowCount }))).catch(() => { /* best-effort */ }) getSmsBalances().then((r) => setSectionCounts((c) => ({ ...c, smsLow: r.lowCount }))).catch(() => { /* best-effort */ })
getClients().then((cs) => setSectionCounts((c) => ({ ...c, clients: cs.length }))).catch(() => { /* best-effort */ })
getDocuments({ status: 'draft', pageSize: 1 }).then((r) => setSectionCounts((c) => ({ ...c, docDrafts: r.total }))).catch(() => { /* best-effort */ })
getModules().then((ms) => setSectionCounts((c) => ({ ...c, modules: ms.length }))).catch(() => { /* best-effort */ })
}, []) }, [])
// Close the mobile drawer and the user menu when the route changes. // Close the mobile drawer and the user menu when the route changes.
@ -165,6 +170,9 @@ export function Layout() {
if (sectionCounts.tickets > 0) counts['/tickets'] = { n: sectionCounts.tickets, title: `${sectionCounts.tickets} open` } if (sectionCounts.tickets > 0) counts['/tickets'] = { n: sectionCounts.tickets, title: `${sectionCounts.tickets} open` }
if (sectionCounts.renewals > 0) counts['/renewals'] = { n: sectionCounts.renewals, title: `${sectionCounts.renewals} upcoming` } if (sectionCounts.renewals > 0) counts['/renewals'] = { n: sectionCounts.renewals, title: `${sectionCounts.renewals} upcoming` }
if (sectionCounts.smsLow > 0) counts['/sms-balances'] = { n: sectionCounts.smsLow, tone: 'warn', title: `${sectionCounts.smsLow} low on balance` } if (sectionCounts.smsLow > 0) counts['/sms-balances'] = { n: sectionCounts.smsLow, tone: 'warn', title: `${sectionCounts.smsLow} low on balance` }
if (sectionCounts.clients > 0) counts['/clients'] = { n: sectionCounts.clients, title: `${sectionCounts.clients} client${sectionCounts.clients === 1 ? '' : 's'}` }
if (sectionCounts.docDrafts > 0) counts['/documents'] = { n: sectionCounts.docDrafts, title: `${sectionCounts.docDrafts} draft${sectionCounts.docDrafts === 1 ? '' : 's'} to finish` }
if (sectionCounts.modules > 0) counts['/modules'] = { n: sectionCounts.modules, title: `${sectionCounts.modules} module${sectionCounts.modules === 1 ? '' : 's'}` }
return ( return (
<div className={`hq-shell${collapsed ? ' side-collapsed' : ''}`}> <div className={`hq-shell${collapsed ? ' side-collapsed' : ''}`}>

@ -38,8 +38,8 @@ function App() {
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route element={<Layout />}> <Route element={<Layout />}>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Home />} />
<Route path="/home" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} />
<Route path="/pipeline" element={<Pipeline />} /> <Route path="/pipeline" element={<Pipeline />} />
<Route path="/reminders" element={<Reminders />} /> <Route path="/reminders" element={<Reminders />} />
<Route path="/projects" element={<Projects />} /> <Route path="/projects" element={<Projects />} />

@ -20,8 +20,8 @@ export const NAV_GROUPS: NavGroup[] = [
{ {
label: 'Overview', label: 'Overview',
items: [ items: [
{ to: '/home', label: 'Home', icon: LayoutGrid }, { to: '/', label: 'Home', icon: LayoutGrid },
{ to: '/', label: 'Dashboard', icon: LayoutDashboard }, { to: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
], ],
}, },
{ {

@ -37,7 +37,7 @@ export function Home() {
// Main, look-at-it-all-the-time sections only — the rest live in the sidebar. // Main, look-at-it-all-the-time sections only — the rest live in the sidebar.
const cards: HubCard[] = [ const cards: HubCard[] = [
{ to: '/', label: 'Dashboard', desc: 'Todays money — dues, renewals, follow-ups, the send queue.', icon: LayoutDashboard, stat: { text: 'My Day' } }, { to: '/dashboard', label: 'Dashboard', desc: 'Todays money — dues, renewals, follow-ups, the send queue.', icon: LayoutDashboard, stat: { text: 'My Day' } },
{ to: '/clients', label: 'Clients', desc: 'The client book — profiles, modules, ledger, support.', icon: Users }, { to: '/clients', label: 'Clients', desc: 'The client book — profiles, modules, ledger, support.', icon: Users },
{ to: '/documents', label: 'Documents', desc: 'Quotations, proformas, invoices, credit notes.', icon: Files }, { to: '/documents', label: 'Documents', desc: 'Quotations, proformas, invoices, credit notes.', icon: Files },
{ to: '/reminders', label: 'Reminders', desc: 'The email send queue — preview, send, dismiss.', icon: BellRing, ...(remStat !== undefined ? { stat: remStat } : {}) }, { to: '/reminders', label: 'Reminders', desc: 'The email send queue — preview, send, dismiss.', icon: BellRing, ...(remStat !== undefined ? { stat: remStat } : {}) },

Loading…
Cancel
Save