feat(CC-088): add Meeting Prep page (brief builder, urgent tasks context, action items, export)

This commit is contained in:
admin-valentin 2026-08-02 17:45:01 +00:00
parent e225a6b756
commit 6c0996ccfd

View file

@ -0,0 +1,133 @@
'use client';
import { useMemo, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
interface Task { id: string; title: string; status: string; tags: string[]; dueDate: string | null; }
interface Decision { id: string; title: string; impact: string | null; status: string; createdAt: string; }
interface Contact { id: string; firstName: string; lastName: string | null; role: string | null; organization: string | null; }
export default function MeetingPrepPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const [meetingTitle, setMeetingTitle] = useState('');
const [participants, setParticipants] = useState('');
const [objective, setObjective] = useState('');
const [agenda, setAgenda] = useState('');
const [generated, setGenerated] = useState(false);
const { data: tasks = [] } = useQuery({ queryKey: ['mp-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const { data: decisions = [] } = useQuery({ queryKey: ['mp-decisions', tenantId], queryFn: () => apiFetch<Decision[]>('/v1/decisions?limit=50', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const { data: contacts = [] } = useQuery({ queryKey: ['mp-contacts', tenantId], queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const urgentTasks = useMemo(() =>
tasks.filter((t) => (t.tags.includes('meeting') || t.priority === 'urgent') && t.status !== 'completed').slice(0, 5),
[tasks]);
const recentDecisions = useMemo(() =>
decisions.filter((d) => d.status !== 'cancelled').slice(0, 5),
[decisions]);
const createMeetingTaskMut = useMutation({
mutationFn: (title: string) => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: {
title, tags: ['meeting', 'action-item'], status: 'todo', priority: 'normal',
}}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['mp-tasks', tenantId] }),
});
function exportBrief() {
const lines: string[] = [];
lines.push(`MEETING BRIEF: ${meetingTitle || 'Întâlnire'}`);
lines.push(`Data: ${new Date().toLocaleDateString('ro-RO', { dateStyle: 'full' })}`);
lines.push('');
if (participants) { lines.push(`PARTICIPANȚI: ${participants}`); lines.push(''); }
if (objective) { lines.push(`OBIECTIV: ${objective}`); lines.push(''); }
if (agenda) { lines.push(`AGENDĂ:\n${agenda}`); lines.push(''); }
if (urgentTasks.length > 0) {
lines.push('TASKURI URGENTE DE DISCUTAT:');
urgentTasks.forEach((t) => lines.push(`${t.title}`));
lines.push('');
}
if (recentDecisions.length > 0) {
lines.push('DECIZII RECENTE RELEVANTE:');
recentDecisions.slice(0, 3).forEach((d) => lines.push(`${d.title}`));
lines.push('');
}
lines.push('---');
lines.push('Generat de CEO OS');
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `meeting-brief-${new Date().toISOString().slice(0,10)}.txt`; a.click();
setGenerated(true);
}
const [newActionItem, setNewActionItem] = useState('');
return (
<div className="max-w-3xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Pregătire Întâlnire</h1>
<p className="text-sm text-ink-faint mt-1">Brieful tău pentru orice meeting contextualizat din datele CEO OS.</p>
</div>
{/* Meeting info */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Detalii întâlnire</p>
<input value={meetingTitle} onChange={(e) => setMeetingTitle(e.target.value)}
placeholder="Titlu meeting (ex: QBR Q3 cu investitorii)"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<input value={participants} onChange={(e) => setParticipants(e.target.value)}
placeholder="Participanți (ex: Maria, Alexandru, echipa produs)"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<input value={objective} onChange={(e) => setObjective(e.target.value)}
placeholder="Obiectivul principal al meetingului"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<textarea value={agenda} onChange={(e) => setAgenda(e.target.value)}
placeholder="Agendă (un punct pe linie)" rows={4}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring resize-none" />
</div>
{/* Context from CEO OS */}
{urgentTasks.length > 0 && (
<div className="card p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Taskuri urgente din CEO OS</p>
{urgentTasks.map((t) => (
<p key={t.id} className="text-xs text-ink flex items-center gap-2"><span className="text-warn"></span>{t.title}</p>
))}
</div>
)}
{recentDecisions.length > 0 && (
<div className="card p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Decizii recente relevante</p>
{recentDecisions.map((d) => (
<p key={d.id} className="text-xs text-ink flex items-center gap-2"><span className="text-primary"></span>{d.title}</p>
))}
</div>
)}
{/* Add action item */}
<div className="card p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Adaugă action item după meeting</p>
<div className="flex gap-2">
<input value={newActionItem} onChange={(e) => setNewActionItem(e.target.value)}
placeholder="ex: Trimite follow-up propunere până joi"
className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring"
onKeyDown={(e) => { if (e.key === 'Enter' && newActionItem.trim()) { createMeetingTaskMut.mutate(newActionItem.trim()); setNewActionItem(''); }}} />
<button onClick={() => { if (newActionItem.trim()) { createMeetingTaskMut.mutate(newActionItem.trim()); setNewActionItem(''); }}}
disabled={!newActionItem.trim()}
className="rounded-lg bg-primary px-3 py-2 text-sm text-white disabled:opacity-50">+</button>
</div>
</div>
<button onClick={exportBrief}
className="w-full rounded-lg bg-primary py-3 text-sm font-semibold text-white hover:bg-primary/90">
Exportă Brief Meeting (.txt)
</button>
{generated && <p className="text-center text-sm text-signal-ok">Brief exportat.</p>}
</div>
);
}