feat(CC-088): add Meeting Prep page (brief builder, urgent tasks context, action items, export)
This commit is contained in:
parent
e225a6b756
commit
6c0996ccfd
1 changed files with 133 additions and 0 deletions
133
src/app/dashboard/meeting-prep/page.tsx
Normal file
133
src/app/dashboard/meeting-prep/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue