40234-vm/frontend/src/pages/client-portal.tsx
2026-06-09 13:31:08 +00:00

286 lines
10 KiB
TypeScript

import {
mdiAccountCircle,
mdiBookOpenVariant,
mdiCheckCircleOutline,
mdiChevronRight,
mdiMessageReplyTextOutline,
} from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
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';
import { useAppSelector } from '../stores/hooks';
type PortalClient = {
id: string;
name: string;
goals?: string;
sessions?: Array<{ id: string; title: string; shared_client_notes?: string }>;
action_items?: Array<{ id: string; title: string; status: string }>;
resources?: Array<{
id: string;
title: string;
description?: string;
url?: string;
}>;
};
function Panel({
children,
className = '',
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<section
className={`rounded-lg border border-[#19192d]/10 bg-white ${className}`}
>
{children}
</section>
);
}
const ClientPortal = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [clients, setClients] = React.useState<
Array<{ id: string; name: string; email?: string }>
>([]);
const [clientId, setClientId] = React.useState('');
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
null,
);
const isClientUser = currentUser?.app_role?.name === 'Client';
React.useEffect(() => {
async function loadClients() {
const response = await axios.get('/coaching/clients');
setClients(response.data);
if (response.data.length > 0) {
const currentClient = response.data.find((client) => {
return client.email === currentUser?.email;
});
if (isClientUser && currentClient) {
setClientId(currentClient.id);
return;
}
setClientId(response.data[0].id);
}
}
loadClients();
}, [currentUser?.email, isClientUser]);
React.useEffect(() => {
async function loadPortal() {
if (!clientId) {
return;
}
const response = await axios.get(`/coaching/client-portal/${clientId}`);
setPortalClient(response.data);
}
loadPortal();
}, [clientId]);
return (
<>
<Head>
<title>{getPageTitle('Client Portal')}</title>
</Head>
<SectionMain>
<div className='mx-auto max-w-7xl'>
<div
className={`mb-4 grid gap-4 ${
isClientUser ? '' : 'xl:grid-cols-[0.95fr_1.05fr]'
}`}
>
<div className='rounded-lg bg-[#19192d] p-5 text-white'>
<div className='flex items-center gap-3 text-[#b17a1e]'>
<BaseIcon path={mdiAccountCircle} size={18} />
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
Client portal
</span>
</div>
<h1 className='mt-3 text-xl font-semibold'>
{isClientUser ? 'Your client portal' : 'Client portal preview'}
</h1>
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
{isClientUser
? 'Review shared notes, commitments, resources, and reflections for your coaching work.'
: 'Preview the client-facing workspace with shared notes, commitments, resources, and a pre-session reflection prompt.'}
</p>
</div>
{!isClientUser && (
<Panel className='p-4'>
<label className='block text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
Preview as client
</label>
<select
value={clientId}
onChange={(event) => setClientId(event.target.value)}
className='mt-4 w-full rounded-lg border border-[#19192d]/10 bg-white px-3 py-2 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
>
{clients.map((client) => (
<option key={client.id} value={client.id}>
{client.name}
</option>
))}
</select>
<p className='mt-4 text-sm leading-6 text-[#72798a]'>
Coaches can preview what each client sees before sharing
notes, commitments, or resources.
</p>
</Panel>
)}
</div>
{portalClient && (
<div className='grid gap-4 xl:grid-cols-[1.05fr_0.95fr]'>
<Panel className='overflow-hidden'>
<div className='bg-[#fffdf9] p-5'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
Your coaching workspace
</p>
<h2 className='mt-2 text-xl font-semibold text-[#19192d]'>
{portalClient.name}
</h2>
<p className='mt-4 max-w-2xl leading-6 text-[#72798a]'>
{portalClient.goals}
</p>
</div>
<div className='p-5'>
<div className='flex items-center gap-3'>
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#f3fbf8] text-[#35b7a5]'>
<BaseIcon path={mdiMessageReplyTextOutline} size={18} />
</span>
<div>
<h3 className='text-lg font-semibold text-[#19192d]'>
Shared session notes
</h3>
<p className='text-sm text-[#72798a]'>
Only coach-approved notes appear here.
</p>
</div>
</div>
<div className='mt-5 space-y-4'>
{(portalClient.sessions || []).map((session) => (
<div
key={session.id}
className='rounded-lg border border-[#19192d]/10 bg-white p-5'
>
<p className='font-semibold text-[#19192d]'>
{session.title}
</p>
<p className='mt-3 leading-6 text-[#72798a]'>
{session.shared_client_notes}
</p>
</div>
))}
</div>
</div>
</Panel>
<div className='space-y-6'>
<Panel>
<div className='border-b border-[#19192d]/10 p-5'>
<h3 className='text-lg font-semibold text-[#19192d]'>
Commitments
</h3>
</div>
<div className='space-y-3 p-5'>
{(portalClient.action_items || []).map((item) => (
<div
key={item.id}
className='flex gap-3 rounded-lg border border-[#19192d]/10 bg-[#fffdf9] p-4'
>
<span className='mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-full bg-[#35b7a5] text-white'>
<BaseIcon path={mdiCheckCircleOutline} size={18} />
</span>
<div>
<p className='font-semibold text-[#19192d]'>
{item.title}
</p>
<p className='mt-1 text-sm text-[#72798a]'>
{item.status.replace('_', ' ')}
</p>
</div>
</div>
))}
</div>
</Panel>
<Panel>
<div className='border-b border-[#19192d]/10 p-5'>
<h3 className='text-lg font-semibold text-[#19192d]'>
Resources
</h3>
</div>
<div className='space-y-3 p-5'>
{(portalClient.resources || []).map((resource) => (
<a
key={resource.id}
href={resource.url}
className='flex items-center justify-between gap-4 rounded-lg border border-[#19192d]/10 bg-white p-4 transition hover:bg-[#fffdf9]'
>
<div className='flex gap-3'>
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#fbf8f1] text-[#b17a1e]'>
<BaseIcon path={mdiBookOpenVariant} size={18} />
</span>
<div>
<p className='font-semibold text-[#19192d]'>
{resource.title}
</p>
<p className='mt-1 text-sm leading-6 text-[#72798a]'>
{resource.description}
</p>
</div>
</div>
<BaseIcon
path={mdiChevronRight}
size={18}
className='text-[#35b7a5]'
/>
</a>
))}
</div>
</Panel>
<Panel className='bg-[#f3fbf8] p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
Pre-session reflection
</p>
<h3 className='mt-2 text-lg font-semibold text-[#19192d]'>
What changed since last time?
</h3>
<p className='mt-3 leading-6 text-[#72798a]'>
Final MVP should let the client answer this before a
session, then feed the response into the coach prep brief.
</p>
</Panel>
</div>
</div>
)}
</div>
</SectionMain>
</>
);
};
ClientPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default ClientPortal;