40234-vm/frontend/src/pages/dashboard.tsx
2026-06-11 11:17:17 +00:00

401 lines
13 KiB
TypeScript

import {
mdiAccountGroup,
mdiArrowRight,
mdiCheckCircleOutline,
mdiClockOutline,
mdiFileDocumentEditOutline,
mdiMicrophoneOutline,
mdiViewDashboardOutline,
} from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import type { ReactElement } from 'react';
import BaseIcon from '../components/BaseIcon';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
type Client = {
id: string;
name: string;
company?: string;
role_title?: string;
next_session_at?: string;
package?: {
title?: string;
};
};
type Session = {
id: string;
title?: string;
ai_summary?: string;
session_at?: string;
client?: Client;
};
type PrepBrief = {
id: string;
next_session_at?: string;
previous_summary?: string;
open_commitments?: string;
suggested_questions?: string;
sensitive_topics?: string;
client_reflection?: string;
client?: Client;
};
type Summary = {
counts: {
clients: number;
sessions: number;
actionItems: number;
resources: number;
prepBriefs: number;
};
activeClients: Client[];
nextSessions: Session[];
upcomingPrepBriefs: PrepBrief[];
};
const emptySummary: Summary = {
counts: {
clients: 0,
sessions: 0,
actionItems: 0,
resources: 0,
prepBriefs: 0,
},
activeClients: [],
nextSessions: [],
upcomingPrepBriefs: [],
};
function ShellCard({
children,
className = '',
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<section
className={`rounded-none border border-[#19192d]/10 bg-white ${className}`}
>
{children}
</section>
);
}
const Dashboard = () => {
const router = useRouter();
const [summary, setSummary] = React.useState<Summary>(emptySummary);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
async function loadSummary() {
const token = localStorage.getItem('token');
if (!token) {
setLoading(false);
router.push('/login');
return;
}
try {
const response = await axios.get('/coaching/summary');
setSummary(response.data);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
router.push('/login');
return;
}
throw error;
} finally {
setLoading(false);
}
}
loadSummary();
}, [router]);
const nextPrepBrief = summary.upcomingPrepBriefs[0];
const stats = [
{
href: '/clients',
icon: mdiAccountGroup,
label: 'Active clients',
value: summary.counts.clients,
},
{
href: '/session-memory',
icon: mdiFileDocumentEditOutline,
label: 'Session memories',
value: summary.counts.sessions,
},
{
href: '/clients',
icon: mdiCheckCircleOutline,
label: 'Open commitments',
value: summary.counts.actionItems,
},
{
href: '/session-memory',
icon: mdiClockOutline,
label: 'Prep briefs',
value: summary.counts.prepBriefs,
},
];
return (
<>
<Head>
<title>{getPageTitle('Dashboard')}</title>
</Head>
<SectionMain>
<div className='mx-auto max-w-7xl'>
<div className='grid gap-6 xl:grid-cols-[1.1fr_0.9fr]'>
<div className='rounded-none bg-[#19192d] p-7 text-white'>
<div className='flex items-center gap-3 text-[#35b7a5]'>
<BaseIcon path={mdiViewDashboardOutline} size={18} />
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
Workspace overview
</span>
</div>
<h1 className='mt-3 max-w-2xl text-xl font-semibold leading-tight'>
Manage upcoming sessions, client notes, commitments, and
follow-up drafts.
</h1>
<p className='mt-3 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
Review scheduled sessions, open tasks, recent notes, and drafts
that need approval.
</p>
<div className='mt-5 flex flex-wrap gap-3'>
<Link
href='/start-session'
className='inline-flex items-center gap-2 rounded-none bg-[#35b7a5] px-5 py-3 text-base font-semibold text-[#19192d]'
>
<BaseIcon path={mdiMicrophoneOutline} size={20} />
Start client session
</Link>
<Link
href='/clients'
className='rounded-none border border-white/25 px-4 py-2 text-sm font-semibold text-white'
>
Open client records
</Link>
</div>
</div>
<ShellCard className='p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Next session prep
</p>
{nextPrepBrief ? (
<div>
<h2 className='mt-4 text-lg font-semibold text-[#19192d]'>
{nextPrepBrief.client?.name || 'Client session'}
</h2>
<p className='mt-1 text-sm text-[#72798a]'>
{nextPrepBrief.client?.role_title} ·{' '}
{nextPrepBrief.client?.company}
</p>
<p className='mt-5 leading-6 text-[#72798a]'>
{nextPrepBrief.suggested_questions ||
nextPrepBrief.previous_summary}
</p>
<Link
href={`/clients/view?clientId=${nextPrepBrief.client?.id}`}
className='mt-4 inline-flex items-center gap-2 rounded-none bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white'
>
Open prep
<BaseIcon path={mdiArrowRight} size={18} />
</Link>
</div>
) : (
<p className='mt-4 leading-6 text-[#72798a]'>
{loading
? 'Loading your coaching workspace...'
: 'No upcoming session memory yet.'}
</p>
)}
</ShellCard>
</div>
<div className='mt-4 grid gap-6 md:grid-cols-2 xl:grid-cols-4'>
{stats.map((stat) => (
<Link key={stat.label} href={stat.href}>
<ShellCard className='h-full p-7 transition'>
<div className='flex items-start justify-between gap-6'>
<div>
<p className='text-sm font-semibold text-[#72798a]'>
{stat.label}
</p>
<p className='mt-2 text-2xl font-semibold text-[#19192d]'>
{loading ? '...' : stat.value}
</p>
</div>
<span className='grid h-9 w-9 place-items-center rounded-none bg-[#fffdf9] text-[#35b7a5]'>
<BaseIcon path={stat.icon} size={19} />
</span>
</div>
</ShellCard>
</Link>
))}
</div>
<div className='mt-4 grid gap-6 xl:grid-cols-[0.95fr_1.05fr]'>
<ShellCard>
<div className='border-b border-[#19192d]/10 p-6'>
<div className='flex items-center justify-between gap-6'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Client records
</p>
<h2 className='mt-2 text-lg font-semibold'>
Active clients
</h2>
</div>
<Link
className='text-sm font-semibold text-[#35b7a5]'
href='/clients'
>
View all
</Link>
</div>
</div>
<div className='divide-y divide-[#19192d]/10'>
{summary.activeClients.map((client) => (
<Link
key={client.id}
href={`/clients/view?clientId=${client.id}`}
className='block p-7 transition hover:bg-[#fffdf9]'
>
<div className='flex items-start justify-between gap-6'>
<div>
<p className='font-semibold text-[#19192d]'>
{client.name}
</p>
<p className='mt-1 text-sm text-[#72798a]'>
{client.role_title} · {client.company}
</p>
</div>
<span className='rounded-none bg-[#fffdf9] px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
{client.package?.title || 'Coaching'}
</span>
</div>
</Link>
))}
</div>
</ShellCard>
<ShellCard>
<div className='border-b border-[#19192d]/10 p-6'>
<div className='flex items-center justify-between gap-6'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Recent intelligence
</p>
<h2 className='mt-2 text-lg font-semibold'>
Session memory
</h2>
</div>
<Link
className='text-sm font-semibold text-[#35b7a5]'
href='/session-memory'
>
Open
</Link>
</div>
</div>
<div className='divide-y divide-[#19192d]/10'>
{summary.nextSessions.map((session) => (
<div key={session.id} className='p-5'>
<p className='font-semibold text-[#19192d]'>
{session.title}
</p>
<p className='mt-1 text-sm text-[#72798a]'>
{session.client?.name}
</p>
<p className='mt-3 leading-6 text-[#72798a]'>
{session.ai_summary}
</p>
</div>
))}
</div>
</ShellCard>
</div>
<ShellCard className='mt-4'>
<div className='border-b border-[#19192d]/10 p-6'>
<div className='flex items-center justify-between gap-6'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Next-session prep
</p>
<h2 className='mt-2 text-lg font-semibold'>
Ready prep briefs
</h2>
</div>
<Link
className='text-sm font-semibold text-[#35b7a5]'
href='/clients'
>
Open clients
</Link>
</div>
</div>
<div className='grid gap-0 divide-y divide-[#19192d]/10 xl:grid-cols-2 xl:divide-x xl:divide-y-0'>
{summary.upcomingPrepBriefs.map((brief) => (
<Link
key={brief.id}
href={`/clients/view?clientId=${brief.client?.id}`}
className='block p-7 transition hover:bg-[#fffdf9]'
>
<div className='flex items-start justify-between gap-6'>
<div>
<p className='font-semibold text-[#19192d]'>
{brief.client?.name}
</p>
<p className='mt-1 text-sm text-[#72798a]'>
{brief.client?.role_title} · {brief.client?.company}
</p>
</div>
<span className='rounded-none bg-[#fffdf9] px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
Ready
</span>
</div>
<p className='mt-4 line-clamp-2 text-sm leading-6 text-[#72798a]'>
{brief.client_reflection ||
brief.suggested_questions ||
brief.previous_summary}
</p>
</Link>
))}
{summary.upcomingPrepBriefs.length === 0 && (
<p className='p-5 text-sm text-[#72798a]'>
{loading
? 'Loading prep briefs...'
: 'No prep briefs ready yet.'}
</p>
)}
</div>
</ShellCard>
</div>
</SectionMain>
</>
);
};
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default Dashboard;