401 lines
13 KiB
TypeScript
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;
|