diff --git a/frontend/src/pages/clients.tsx b/frontend/src/pages/clients.tsx index 6220f89..81e4d0c 100644 --- a/frontend/src/pages/clients.tsx +++ b/frontend/src/pages/clients.tsx @@ -1,15 +1,12 @@ import { mdiAccountGroup, - mdiCalendarClock, - mdiCheckCircleOutline, + mdiArrowRight, mdiContentSaveOutline, - mdiFileDocumentOutline, - mdiLinkVariant, mdiPlus, - mdiTarget, } 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'; @@ -20,37 +17,7 @@ import LayoutAuthenticated from '../layouts/Authenticated'; type ActionItem = { id: string; - title: string; status: string; - due_at?: string; - notes?: string; -}; - -type PrepBrief = { - id: string; - previous_summary?: string; - open_commitments?: string; - suggested_questions?: string; - sensitive_topics?: string; - client_reflection?: string; - client_reflection_at?: string; -}; - -type Resource = { - id: string; - title: string; - description?: string; - url?: string; - resource_type?: string; - is_shared?: boolean; -}; - -type Session = { - id: string; - title: string; - ai_summary?: string; - session_at?: string; - status?: string; }; type Client = { @@ -58,21 +25,11 @@ type Client = { name: string; email: string; status: string; - goals?: string; - notes?: string; company?: string; role_title?: string; tags?: string; next_session_at?: string; - last_session_at?: string; - sessions?: Session[]; action_items?: ActionItem[]; - resources?: Resource[]; - prep_briefs?: PrepBrief[]; - package?: { - title?: string; - duration?: string; - }; }; type ClientForm = { @@ -101,36 +58,9 @@ function emptyClientForm(): ClientForm { }; } -function formFromClient(client: Client): ClientForm { - return { - name: client.name || '', - email: client.email || '', - status: client.status || 'active', - company: client.company || '', - role_title: client.role_title || '', - tags: client.tags || '', - goals: client.goals || '', - notes: client.notes || '', - next_session_at: toDateTimeInput(client.next_session_at), - }; -} - -function toDateTimeInput(value?: string) { - if (!value) { - return ''; - } - - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return ''; - } - - return date.toISOString().slice(0, 16); -} - function displayDateTime(value?: string) { if (!value) { - return 'No session scheduled'; + return 'Not scheduled'; } const date = new Date(value); @@ -162,12 +92,8 @@ function Panel({ ); } -function EmptyState({ label }: { label: string }) { - return ( -

- {label} -

- ); +function inputClass() { + return 'w-full rounded-none border border-[#19192d]/10 bg-[#fffdf9] px-4 py-3 text-sm text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'; } function Field({ @@ -187,115 +113,22 @@ function Field({ ); } -function inputClass() { - return 'w-full rounded-none border border-[#19192d]/10 bg-[#fffdf9] px-4 py-3 text-sm text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'; -} - -function PrepText({ value }: { value?: string }) { - if (!value) { - return ; - } - - return ( -
- {value - .split(/\n|;/) - .map((item) => item.trim()) - .filter(Boolean) - .map((item) => ( -

- {item} -

- ))} -
- ); -} - const Clients = () => { const router = useRouter(); const [clients, setClients] = React.useState([]); - const [selectedClientId, setSelectedClientId] = React.useState(''); - const [selectedClient, setSelectedClient] = React.useState( - null, - ); const [clientForm, setClientForm] = React.useState(emptyClientForm()); - const [isCreatingClient, setIsCreatingClient] = React.useState(false); - const [isSavingClient, setIsSavingClient] = React.useState(false); - const [newActionTitle, setNewActionTitle] = React.useState(''); - const [newResourceTitle, setNewResourceTitle] = React.useState(''); - const [newResourceUrl, setNewResourceUrl] = React.useState(''); + const [isCreating, setIsCreating] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); const [notice, setNotice] = React.useState(''); React.useEffect(() => { - async function loadClients() { - const response = await axios.get('/coaching/clients'); - setClients(response.data); + loadClients(); + }, []); - const queryClientId = router.query.clientId; - if (typeof queryClientId === 'string') { - await selectClient(queryClientId, false); - return; - } - - if (response.data.length > 0) { - await selectClient(response.data[0].id, false); - } else { - startNewClient(); - } - } - - if (router.isReady) { - loadClients(); - } - }, [router.isReady]); - - const latestReflection = selectedClient?.prep_briefs?.find((brief) => { - return Boolean(brief.client_reflection); - }); - const latestPrepBrief = selectedClient?.prep_briefs?.[0]; - - async function loadClient(clientId: string) { - const response = await axios.get(`/coaching/clients/${clientId}`); - const client = response.data; - - setSelectedClient(client); - setClientForm(formFromClient(client)); - setClients((current) => { - const exists = current.some((item) => item.id === client.id); - if (!exists) { - return [client, ...current]; - } - - return current.map((item) => { - if (item.id === client.id) { - return client; - } - - return item; - }); - }); - } - - async function selectClient(clientId: string, updateUrl = true) { - setSelectedClientId(clientId); - setIsCreatingClient(false); - setNotice(''); - await loadClient(clientId); - - if (updateUrl) { - router.replace(`/clients?clientId=${clientId}`, undefined, { - shallow: true, - }); - } - } - - function startNewClient() { - setIsCreatingClient(true); - setSelectedClientId(''); - setSelectedClient(null); - setClientForm(emptyClientForm()); - setNotice(''); + async function loadClients() { + const response = await axios.get('/coaching/clients'); + setClients(response.data); } function updateClientForm(field: keyof ClientForm, value: string) { @@ -307,75 +140,27 @@ const Clients = () => { }); } - async function saveClient() { - setIsSavingClient(true); + async function createClient() { + setIsSaving(true); setNotice(''); try { - const payload = { + const response = await axios.post('/coaching/clients', { ...clientForm, next_session_at: clientForm.next_session_at || null, - }; - const response = isCreatingClient - ? await axios.post('/coaching/clients', payload) - : await axios.patch(`/coaching/clients/${selectedClientId}`, payload); - - const client = response.data; - setSelectedClientId(client.id); - setSelectedClient(client); - setClientForm(formFromClient(client)); - setIsCreatingClient(false); - setClients((current) => { - const exists = current.some((item) => item.id === client.id); - if (!exists) { - return [client, ...current]; - } - - return current.map((item) => { - if (item.id === client.id) { - return client; - } - - return item; - }); }); - router.replace(`/clients?clientId=${client.id}`, undefined, { - shallow: true, - }); - setNotice('Client record saved.'); + await loadClients(); + setClientForm(emptyClientForm()); + setIsCreating(false); + await router.push(`/clients/${response.data.id}`); } finally { - setIsSavingClient(false); + setIsSaving(false); } } - async function addActionItem() { - if (!selectedClientId || !newActionTitle.trim()) { - return; - } - - await axios.post(`/coaching/clients/${selectedClientId}/action-items`, { - title: newActionTitle, - }); - setNewActionTitle(''); - await loadClient(selectedClientId); - setNotice('Commitment added.'); - } - - async function addResource() { - if (!selectedClientId || !newResourceTitle.trim()) { - return; - } - - await axios.post(`/coaching/clients/${selectedClientId}/resources`, { - title: newResourceTitle, - url: newResourceUrl, - resource_type: 'link', - is_shared: true, - }); - setNewResourceTitle(''); - setNewResourceUrl(''); - await loadClient(selectedClientId); - setNotice('Resource added and shared with client.'); + function openCreateForm() { + setIsCreating(true); + setNotice(''); } return ( @@ -385,7 +170,7 @@ const Clients = () => {
-
+
@@ -393,16 +178,16 @@ const Clients = () => { Client CRM
-

Client records

+

Clients

- Manage coaching relationships, prep context, commitments, and - shared resources in one working record. + Scan the practice, add a client, and open a dedicated client + file when you need the full context.

)} -
- -
-

- Coaching relationships -

-

- {clients.length} clients in this workspace -

+ {isCreating && ( + +
+
+

+ New client +

+

+ Create a client record +

+
+
-
- {clients.map((client) => ( - -
- -
- - - updateClientForm('name', event.target.value) - } - className={inputClass()} - /> - - - - updateClientForm('email', event.target.value) - } - className={inputClass()} - /> - - - - - - - updateClientForm('next_session_at', event.target.value) - } - className={inputClass()} - /> - - - - updateClientForm('company', event.target.value) - } - className={inputClass()} - /> - - - - updateClientForm('role_title', event.target.value) - } - className={inputClass()} - /> - - - - updateClientForm('tags', event.target.value) - } - className={inputClass()} - placeholder='executive, founder, delegation' - /> - -
- -
- -