Add coach client management flow
This commit is contained in:
parent
be3bbea7aa
commit
cbebf46653
@ -29,6 +29,32 @@ function splitActionItems(value) {
|
|||||||
.slice(0, 8);
|
.slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clientPayload(data, userId) {
|
||||||
|
return {
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
status: data.status || "active",
|
||||||
|
goals: data.goals,
|
||||||
|
notes: data.notes,
|
||||||
|
company: data.company,
|
||||||
|
role_title: data.role_title,
|
||||||
|
tags: data.tags,
|
||||||
|
next_session_at: data.next_session_at || null,
|
||||||
|
last_session_at: data.last_session_at || null,
|
||||||
|
updatedById: userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientIncludes() {
|
||||||
|
return [
|
||||||
|
{ model: db.packages, as: "package" },
|
||||||
|
{ model: db.sessions, as: "sessions", order: [["session_at", "DESC"]] },
|
||||||
|
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
||||||
|
{ model: db.resources, as: "resources", order: [["createdAt", "DESC"]] },
|
||||||
|
{ model: db.prep_briefs, as: "prep_briefs", order: [["next_session_at", "DESC"]] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function getFirstUploadedFile(files, fieldName) {
|
function getFirstUploadedFile(files, fieldName) {
|
||||||
const file = files[fieldName];
|
const file = files[fieldName];
|
||||||
|
|
||||||
@ -270,17 +296,35 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/clients",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
|
||||||
|
if (!String(data.name || "").trim()) {
|
||||||
|
res.status(400).send({ error: "client_name_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await db.clients.create({
|
||||||
|
...clientPayload(data, req.currentUser.id),
|
||||||
|
ownerId: req.currentUser.id,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedClient = await db.clients.findByPk(client.id, {
|
||||||
|
include: clientIncludes(),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(savedClient);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/clients/:id",
|
"/clients/:id",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const client = await db.clients.findByPk(req.params.id, {
|
const client = await db.clients.findByPk(req.params.id, {
|
||||||
include: [
|
include: clientIncludes(),
|
||||||
{ model: db.packages, as: "package" },
|
|
||||||
{ model: db.sessions, as: "sessions", order: [["session_at", "DESC"]] },
|
|
||||||
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
|
||||||
{ model: db.resources, as: "resources" },
|
|
||||||
{ model: db.prep_briefs, as: "prep_briefs", order: [["next_session_at", "DESC"]] },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -292,6 +336,93 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/clients/:id",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
const client = await db.clients.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(data.name || "").trim()) {
|
||||||
|
res.status(400).send({ error: "client_name_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.update(clientPayload(data, req.currentUser.id));
|
||||||
|
|
||||||
|
const savedClient = await db.clients.findByPk(client.id, {
|
||||||
|
include: clientIncludes(),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(savedClient);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/clients/:id/action-items",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
const client = await db.clients.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(data.title || "").trim()) {
|
||||||
|
res.status(400).send({ error: "action_title_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionItem = await db.action_items.create({
|
||||||
|
clientId: client.id,
|
||||||
|
title: data.title,
|
||||||
|
due_at: data.due_at || null,
|
||||||
|
status: data.status || "not_started",
|
||||||
|
notes: data.notes,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(actionItem);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/clients/:id/resources",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
const client = await db.clients.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(data.title || "").trim()) {
|
||||||
|
res.status(400).send({ error: "resource_title_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = await db.resources.create({
|
||||||
|
clientId: client.id,
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
url: data.url,
|
||||||
|
resource_type: data.resource_type || "link",
|
||||||
|
is_shared: data.is_shared !== false,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(resource);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/session-memory",
|
"/session-memory",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import {
|
|||||||
mdiAccountGroup,
|
mdiAccountGroup,
|
||||||
mdiCalendarClock,
|
mdiCalendarClock,
|
||||||
mdiCheckCircleOutline,
|
mdiCheckCircleOutline,
|
||||||
|
mdiContentSaveOutline,
|
||||||
mdiFileDocumentOutline,
|
mdiFileDocumentOutline,
|
||||||
|
mdiLinkVariant,
|
||||||
|
mdiPlus,
|
||||||
mdiTarget,
|
mdiTarget,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@ -19,6 +22,8 @@ type ActionItem = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
due_at?: string;
|
||||||
|
notes?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PrepBrief = {
|
type PrepBrief = {
|
||||||
@ -31,10 +36,21 @@ type PrepBrief = {
|
|||||||
client_reflection_at?: string;
|
client_reflection_at?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Resource = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
resource_type?: string;
|
||||||
|
is_shared?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
ai_summary?: string;
|
ai_summary?: string;
|
||||||
|
session_at?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Client = {
|
type Client = {
|
||||||
@ -48,8 +64,10 @@ type Client = {
|
|||||||
role_title?: string;
|
role_title?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
next_session_at?: string;
|
next_session_at?: string;
|
||||||
|
last_session_at?: string;
|
||||||
sessions?: Session[];
|
sessions?: Session[];
|
||||||
action_items?: ActionItem[];
|
action_items?: ActionItem[];
|
||||||
|
resources?: Resource[];
|
||||||
prep_briefs?: PrepBrief[];
|
prep_briefs?: PrepBrief[];
|
||||||
package?: {
|
package?: {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -57,6 +75,77 @@ type Client = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ClientForm = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
status: string;
|
||||||
|
company: string;
|
||||||
|
role_title: string;
|
||||||
|
tags: string;
|
||||||
|
goals: string;
|
||||||
|
notes: string;
|
||||||
|
next_session_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function emptyClientForm(): ClientForm {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
status: 'active',
|
||||||
|
company: '',
|
||||||
|
role_title: '',
|
||||||
|
tags: '',
|
||||||
|
goals: '',
|
||||||
|
notes: '',
|
||||||
|
next_session_at: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function Panel({
|
function Panel({
|
||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
@ -81,6 +170,27 @@ function EmptyState({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className='block'>
|
||||||
|
<span className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div className='mt-2'>{children}</div>
|
||||||
|
</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 PrepText({ value }: { value?: string }) {
|
function PrepText({ value }: { value?: string }) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return <EmptyState label='No prep details saved yet.' />;
|
return <EmptyState label='No prep details saved yet.' />;
|
||||||
@ -105,6 +215,17 @@ const Clients = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [clients, setClients] = React.useState<Client[]>([]);
|
const [clients, setClients] = React.useState<Client[]>([]);
|
||||||
const [selectedClientId, setSelectedClientId] = React.useState<string>('');
|
const [selectedClientId, setSelectedClientId] = React.useState<string>('');
|
||||||
|
const [selectedClient, setSelectedClient] = React.useState<Client | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [clientForm, setClientForm] =
|
||||||
|
React.useState<ClientForm>(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 [notice, setNotice] = React.useState('');
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function loadClients() {
|
async function loadClients() {
|
||||||
@ -113,12 +234,14 @@ const Clients = () => {
|
|||||||
|
|
||||||
const queryClientId = router.query.clientId;
|
const queryClientId = router.query.clientId;
|
||||||
if (typeof queryClientId === 'string') {
|
if (typeof queryClientId === 'string') {
|
||||||
setSelectedClientId(queryClientId);
|
await selectClient(queryClientId, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
if (response.data.length > 0) {
|
||||||
setSelectedClientId(response.data[0].id);
|
await selectClient(response.data[0].id, false);
|
||||||
|
} else {
|
||||||
|
startNewClient();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,20 +250,134 @@ const Clients = () => {
|
|||||||
}
|
}
|
||||||
}, [router.isReady]);
|
}, [router.isReady]);
|
||||||
|
|
||||||
const selectedClient =
|
|
||||||
clients.find((client) => client.id === selectedClientId) || clients[0];
|
|
||||||
const latestReflection = selectedClient?.prep_briefs?.find((brief) => {
|
const latestReflection = selectedClient?.prep_briefs?.find((brief) => {
|
||||||
return Boolean(brief.client_reflection);
|
return Boolean(brief.client_reflection);
|
||||||
});
|
});
|
||||||
const latestPrepBrief = selectedClient?.prep_briefs?.[0];
|
const latestPrepBrief = selectedClient?.prep_briefs?.[0];
|
||||||
|
|
||||||
function selectClient(clientId: string) {
|
async function loadClient(clientId: string) {
|
||||||
setSelectedClientId(clientId);
|
const response = await axios.get(`/coaching/clients/${clientId}`);
|
||||||
router.replace(`/clients?clientId=${clientId}`, undefined, {
|
const client = response.data;
|
||||||
shallow: true,
|
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClientForm(field: keyof ClientForm, value: string) {
|
||||||
|
setClientForm((current) => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveClient() {
|
||||||
|
setIsSavingClient(true);
|
||||||
|
setNotice('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...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.');
|
||||||
|
} finally {
|
||||||
|
setIsSavingClient(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.');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -158,21 +395,35 @@ const Clients = () => {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className='mt-3 text-xl font-semibold'>Client records</h1>
|
<h1 className='mt-3 text-xl font-semibold'>Client records</h1>
|
||||||
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
|
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
|
||||||
Open a client record to review goals, coach notes, recent
|
Manage coaching relationships, prep context, commitments, and
|
||||||
sessions, and commitments.
|
shared resources in one working record.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-none bg-white/10 px-4 py-2 text-sm font-semibold text-[#fffdf9]'>
|
<button
|
||||||
{clients.length} clients
|
type='button'
|
||||||
</div>
|
className='inline-flex items-center justify-center gap-2 rounded-none bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white'
|
||||||
|
onClick={startNewClient}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiPlus} size={18} />
|
||||||
|
New client
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-6 xl:grid-cols-[0.85fr_1.15fr]'>
|
{notice && (
|
||||||
|
<div className='mb-4 rounded-none border border-[#35b7a5]/20 bg-[#fffdf9] px-4 py-3 text-sm font-semibold text-[#35b7a5]'>
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='grid gap-6 xl:grid-cols-[0.75fr_1.25fr]'>
|
||||||
<Panel className='overflow-hidden'>
|
<Panel className='overflow-hidden'>
|
||||||
<div className='border-b border-[#19192d]/10 p-7'>
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
||||||
Coaching relationships
|
Coaching relationships
|
||||||
</p>
|
</p>
|
||||||
|
<p className='mt-2 text-sm text-[#72798a]'>
|
||||||
|
{clients.length} clients in this workspace
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='divide-y divide-[#19192d]/10'>
|
<div className='divide-y divide-[#19192d]/10'>
|
||||||
{clients.map((client) => (
|
{clients.map((client) => (
|
||||||
@ -181,7 +432,7 @@ const Clients = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
onClick={() => selectClient(client.id)}
|
onClick={() => selectClient(client.id)}
|
||||||
className={`block w-full p-7 text-left transition ${
|
className={`block w-full p-7 text-left transition ${
|
||||||
selectedClient?.id === client.id
|
selectedClientId === client.id
|
||||||
? 'bg-[#fffdf9]'
|
? 'bg-[#fffdf9]'
|
||||||
: 'bg-white hover:bg-[#fffdf9]'
|
: 'bg-white hover:bg-[#fffdf9]'
|
||||||
}`}
|
}`}
|
||||||
@ -192,236 +443,384 @@ const Clients = () => {
|
|||||||
{client.name}
|
{client.name}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-sm text-[#72798a]'>
|
<p className='mt-1 text-sm text-[#72798a]'>
|
||||||
{client.role_title} · {client.company}
|
{[client.role_title, client.company]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · ') || client.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className='rounded-none bg-[#fffdf9] px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
|
<span className='rounded-none bg-white px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
|
||||||
{client.status}
|
{client.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4 flex flex-wrap gap-2'>
|
<p className='mt-4 text-sm text-[#72798a]'>
|
||||||
{(client.tags || '')
|
Next: {displayDateTime(client.next_session_at)}
|
||||||
.split(',')
|
</p>
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className='rounded-none bg-white px-3 py-1 text-xs text-[#72798a]'
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{selectedClient && (
|
<div className='space-y-6'>
|
||||||
<div className='space-y-6'>
|
<Panel className='p-7'>
|
||||||
<Panel className='p-4'>
|
<div className='flex flex-col justify-between gap-6 md:flex-row md:items-start'>
|
||||||
<div className='flex flex-col justify-between gap-6 md:flex-row md:items-start'>
|
<div>
|
||||||
<div>
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
{isCreatingClient ? 'New coaching client' : 'Client file'}
|
||||||
{selectedClient.package?.title || 'Coaching client'}
|
</p>
|
||||||
</p>
|
<h2 className='mt-2 text-xl font-semibold text-[#19192d]'>
|
||||||
<h2 className='mt-2 text-xl font-semibold text-[#19192d]'>
|
{isCreatingClient
|
||||||
{selectedClient.name}
|
? 'Create client record'
|
||||||
</h2>
|
: selectedClient?.name || 'Client record'}
|
||||||
<p className='mt-2 text-[#72798a]'>
|
</h2>
|
||||||
{selectedClient.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='rounded-none border border-[#19192d]/10 bg-[#fffdf9] p-6 md:min-w-56'>
|
|
||||||
<div className='flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'>
|
|
||||||
<BaseIcon path={mdiCalendarClock} size={18} />
|
|
||||||
Next session
|
|
||||||
</div>
|
|
||||||
<p className='mt-2 text-sm text-[#72798a]'>
|
|
||||||
{selectedClient.next_session_at ||
|
|
||||||
'No session scheduled'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='inline-flex items-center justify-center gap-2 rounded-none bg-[#19192d] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
disabled={isSavingClient}
|
||||||
|
onClick={saveClient}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiContentSaveOutline} size={18} />
|
||||||
|
{isSavingClient ? 'Saving...' : 'Save client'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-6 lg:grid-cols-2'>
|
<div className='mt-6 grid gap-6 lg:grid-cols-2'>
|
||||||
<div className='rounded-none bg-[#fffdf9] p-7'>
|
<Field label='Name'>
|
||||||
<div className='flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'>
|
<input
|
||||||
<BaseIcon path={mdiTarget} size={18} />
|
value={clientForm.name}
|
||||||
Goals
|
onChange={(event) =>
|
||||||
</div>
|
updateClientForm('name', event.target.value)
|
||||||
<p className='mt-3 leading-6 text-[#72798a]'>
|
}
|
||||||
{selectedClient.goals}
|
className={inputClass()}
|
||||||
</p>
|
/>
|
||||||
</div>
|
</Field>
|
||||||
<div className='rounded-none bg-[#fffdf9] p-7'>
|
<Field label='Email'>
|
||||||
<div className='flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'>
|
<input
|
||||||
<BaseIcon path={mdiFileDocumentOutline} size={18} />
|
value={clientForm.email}
|
||||||
Private coach notes
|
type='email'
|
||||||
</div>
|
onChange={(event) =>
|
||||||
<p className='mt-3 leading-6 text-[#72798a]'>
|
updateClientForm('email', event.target.value)
|
||||||
{selectedClient.notes}
|
}
|
||||||
</p>
|
className={inputClass()}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</Field>
|
||||||
</Panel>
|
<Field label='Status'>
|
||||||
|
<select
|
||||||
|
value={clientForm.status}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('status', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
>
|
||||||
|
<option value='lead'>Lead</option>
|
||||||
|
<option value='active'>Active</option>
|
||||||
|
<option value='paused'>Paused</option>
|
||||||
|
<option value='completed'>Completed</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<Field label='Next session'>
|
||||||
|
<input
|
||||||
|
value={clientForm.next_session_at}
|
||||||
|
type='datetime-local'
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('next_session_at', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label='Company'>
|
||||||
|
<input
|
||||||
|
value={clientForm.company}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('company', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label='Role title'>
|
||||||
|
<input
|
||||||
|
value={clientForm.role_title}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('role_title', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label='Tags'>
|
||||||
|
<input
|
||||||
|
value={clientForm.tags}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('tags', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
placeholder='executive, founder, delegation'
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Panel>
|
<div className='mt-6 grid gap-6 lg:grid-cols-2'>
|
||||||
<div className='border-b border-[#19192d]/10 p-7'>
|
<Field label='Goals'>
|
||||||
<div className='flex items-center justify-between gap-6'>
|
<textarea
|
||||||
<div>
|
value={clientForm.goals}
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
rows={5}
|
||||||
Next-session prep
|
onChange={(event) =>
|
||||||
</p>
|
updateClientForm('goals', event.target.value)
|
||||||
<h3 className='mt-2 text-lg font-semibold text-[#19192d]'>
|
}
|
||||||
Coach prep brief
|
className={inputClass()}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label='Private coach notes'>
|
||||||
|
<textarea
|
||||||
|
value={clientForm.notes}
|
||||||
|
rows={5}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateClientForm('notes', event.target.value)
|
||||||
|
}
|
||||||
|
className={inputClass()}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
{selectedClient && !isCreatingClient && (
|
||||||
|
<>
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
|
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-center'>
|
||||||
|
<div>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
||||||
|
Next-session prep
|
||||||
|
</p>
|
||||||
|
<h3 className='mt-2 text-lg font-semibold text-[#19192d]'>
|
||||||
|
Coach prep brief
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'>
|
||||||
|
<BaseIcon path={mdiCalendarClock} size={18} />
|
||||||
|
{displayDateTime(selectedClient.next_session_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{latestPrepBrief ? (
|
||||||
|
<div className='grid gap-6 p-7 lg:grid-cols-2'>
|
||||||
|
<div className='rounded-none bg-[#fffdf9] p-6'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
||||||
|
Client reflection
|
||||||
|
</p>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<PrepText
|
||||||
|
value={latestPrepBrief.client_reflection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-none bg-[#fffdf9] p-6'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
||||||
|
Open commitments
|
||||||
|
</p>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<PrepText
|
||||||
|
value={latestPrepBrief.open_commitments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-none bg-[#fffdf9] p-6'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
||||||
|
Suggested questions
|
||||||
|
</p>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<PrepText
|
||||||
|
value={latestPrepBrief.suggested_questions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-none bg-[#fffdf9] p-6'>
|
||||||
|
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
||||||
|
Watch points
|
||||||
|
</p>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<PrepText
|
||||||
|
value={latestPrepBrief.sensitive_topics}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='p-7'>
|
||||||
|
<EmptyState label='No next-session prep brief yet. Generate session memory or ask the client for a reflection.' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<div className='grid gap-6 lg:grid-cols-2'>
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
Open commitments
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<span className='rounded-none bg-[#fffdf9] px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
Ready
|
<div className='flex flex-col gap-3 md:flex-row'>
|
||||||
</span>
|
<input
|
||||||
</div>
|
value={newActionTitle}
|
||||||
</div>
|
onChange={(event) =>
|
||||||
{latestPrepBrief ? (
|
setNewActionTitle(event.target.value)
|
||||||
<div className='grid gap-6 p-7 lg:grid-cols-2'>
|
}
|
||||||
<div className='rounded-none bg-[#fffdf9] p-6'>
|
className={inputClass()}
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
placeholder='Add a commitment before the next session'
|
||||||
Client reflection
|
|
||||||
</p>
|
|
||||||
<div className='mt-3'>
|
|
||||||
<PrepText value={latestPrepBrief.client_reflection} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='rounded-none bg-[#fffdf9] p-6'>
|
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
|
||||||
Open commitments
|
|
||||||
</p>
|
|
||||||
<div className='mt-3'>
|
|
||||||
<PrepText value={latestPrepBrief.open_commitments} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='rounded-none bg-[#fffdf9] p-6'>
|
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
|
||||||
Suggested questions
|
|
||||||
</p>
|
|
||||||
<div className='mt-3'>
|
|
||||||
<PrepText
|
|
||||||
value={latestPrepBrief.suggested_questions}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<button
|
||||||
</div>
|
type='button'
|
||||||
<div className='rounded-none bg-[#fffdf9] p-6'>
|
className='inline-flex items-center justify-center gap-2 rounded-none bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
disabled={!newActionTitle.trim()}
|
||||||
Watch points
|
onClick={addActionItem}
|
||||||
</p>
|
|
||||||
<div className='mt-3'>
|
|
||||||
<PrepText value={latestPrepBrief.sensitive_topics} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='rounded-none bg-[#fffdf9] p-6 lg:col-span-2'>
|
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-[#35b7a5]'>
|
|
||||||
Previous summary
|
|
||||||
</p>
|
|
||||||
<div className='mt-3'>
|
|
||||||
<PrepText value={latestPrepBrief.previous_summary} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='p-5'>
|
|
||||||
<EmptyState label='No next-session prep brief yet.' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<div className='grid gap-6 lg:grid-cols-2'>
|
|
||||||
<Panel>
|
|
||||||
<div className='border-b border-[#19192d]/10 p-7'>
|
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
|
||||||
Session timeline
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-4 p-7'>
|
|
||||||
{(selectedClient.sessions || []).length > 0 ? (
|
|
||||||
(selectedClient.sessions || []).map((session) => (
|
|
||||||
<div
|
|
||||||
key={session.id}
|
|
||||||
className='rounded-none border border-[#19192d]/10 bg-[#fffdf9] p-6'
|
|
||||||
>
|
>
|
||||||
<p className='font-semibold text-[#19192d]'>
|
<BaseIcon path={mdiPlus} size={18} />
|
||||||
{session.title}
|
Add
|
||||||
</p>
|
</button>
|
||||||
<p className='mt-2 text-sm leading-6 text-[#72798a]'>
|
|
||||||
{session.ai_summary}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<EmptyState label='No saved session memories yet.' />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel>
|
|
||||||
<div className='border-b border-[#19192d]/10 p-7'>
|
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
|
||||||
Pre-session reflection
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className='p-5'>
|
|
||||||
{latestReflection ? (
|
|
||||||
<div className='rounded-none border border-[#19192d]/10 bg-[#fffdf9] p-6'>
|
|
||||||
<p className='text-sm leading-6 text-[#72798a]'>
|
|
||||||
{latestReflection.client_reflection}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<EmptyState label='No client reflection submitted yet.' />
|
<div className='space-y-4 p-7'>
|
||||||
)}
|
{(selectedClient.action_items || []).length > 0 ? (
|
||||||
</div>
|
(selectedClient.action_items || []).map((item) => (
|
||||||
</Panel>
|
<div
|
||||||
</div>
|
key={item.id}
|
||||||
|
className='flex gap-3 rounded-none border border-[#19192d]/10 bg-white p-6'
|
||||||
|
>
|
||||||
|
<span className='mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-none bg-[#fffdf9] text-[#35b7a5]'>
|
||||||
|
<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>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState label='No open commitments for this client.' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<div className='grid gap-6 lg:grid-cols-2'>
|
<Panel>
|
||||||
<Panel>
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
<div className='border-b border-[#19192d]/10 p-7'>
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
Shared resources
|
||||||
Open commitments
|
</h3>
|
||||||
</h3>
|
</div>
|
||||||
</div>
|
<div className='space-y-3 border-b border-[#19192d]/10 p-7'>
|
||||||
<div className='space-y-4 p-7'>
|
<input
|
||||||
{(selectedClient.action_items || []).length > 0 ? (
|
value={newResourceTitle}
|
||||||
(selectedClient.action_items || []).map((item) => (
|
onChange={(event) =>
|
||||||
<div
|
setNewResourceTitle(event.target.value)
|
||||||
key={item.id}
|
}
|
||||||
className='flex gap-3 rounded-none border border-[#19192d]/10 bg-white p-6'
|
className={inputClass()}
|
||||||
>
|
placeholder='Resource title'
|
||||||
<span className='mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-none bg-[#fffdf9] text-[#35b7a5]'>
|
/>
|
||||||
<BaseIcon
|
<input
|
||||||
path={mdiCheckCircleOutline}
|
value={newResourceUrl}
|
||||||
size={18}
|
onChange={(event) =>
|
||||||
/>
|
setNewResourceUrl(event.target.value)
|
||||||
</span>
|
}
|
||||||
<div>
|
className={inputClass()}
|
||||||
|
placeholder='https://...'
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='inline-flex items-center justify-center gap-2 rounded-none bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
disabled={!newResourceTitle.trim()}
|
||||||
|
onClick={addResource}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiPlus} size={18} />
|
||||||
|
Add resource
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-4 p-7'>
|
||||||
|
{(selectedClient.resources || []).length > 0 ? (
|
||||||
|
(selectedClient.resources || []).map((resource) => (
|
||||||
|
<a
|
||||||
|
key={resource.id}
|
||||||
|
href={resource.url || '#'}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className='flex items-start gap-3 rounded-none border border-[#19192d]/10 bg-white p-6 transition hover:bg-[#fffdf9]'
|
||||||
|
>
|
||||||
|
<span className='mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-none bg-[#fffdf9] text-[#35b7a5]'>
|
||||||
|
<BaseIcon path={mdiLinkVariant} size={18} />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className='block font-semibold text-[#19192d]'>
|
||||||
|
{resource.title}
|
||||||
|
</span>
|
||||||
|
<span className='mt-1 block text-sm text-[#72798a]'>
|
||||||
|
{resource.description || resource.url}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState label='No shared resources for this client yet.' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-6 lg:grid-cols-2'>
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
Session timeline
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-4 p-7'>
|
||||||
|
{(selectedClient.sessions || []).length > 0 ? (
|
||||||
|
(selectedClient.sessions || []).map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className='rounded-none border border-[#19192d]/10 bg-[#fffdf9] p-6'
|
||||||
|
>
|
||||||
<p className='font-semibold text-[#19192d]'>
|
<p className='font-semibold text-[#19192d]'>
|
||||||
{item.title}
|
{session.title}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-sm text-[#72798a]'>
|
<p className='mt-2 text-sm leading-6 text-[#72798a]'>
|
||||||
{item.status.replace('_', ' ')}
|
{session.ai_summary}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState label='No saved session memories yet.' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-7'>
|
||||||
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
Pre-session reflection
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className='p-7'>
|
||||||
|
{latestReflection ? (
|
||||||
|
<div className='rounded-none border border-[#19192d]/10 bg-[#fffdf9] p-6'>
|
||||||
|
<p className='text-sm leading-6 text-[#72798a]'>
|
||||||
|
{latestReflection.client_reflection}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
) : (
|
||||||
) : (
|
<EmptyState label='No client reflection submitted yet.' />
|
||||||
<EmptyState label='No open commitments for this client.' />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</Panel>
|
||||||
</Panel>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user