286 lines
10 KiB
TypeScript
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;
|