feat(cc-053): Daily Briefing page

This commit is contained in:
admin-valentin 2026-08-01 10:43:43 +00:00
parent efe49a23c9
commit d12bc87481

View file

@ -1,75 +1,193 @@
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type Briefing } from '../../../lib/api';
import { apiFetch } from '../../../lib/api';
import type { BriefingData } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
export const dynamic = 'force-dynamic';
const B = '/dashboard';
export default function DailyBriefingPage() {
function ScoreMeter({ score }: { score: number }) {
const color = score >= 80 ? 'text-signal-ok' : score >= 60 ? 'text-signal-warn' : 'text-signal-danger';
return (
<div className="text-center">
<div className={`font-display text-6xl font-bold ${color}`}>{score}</div>
<div className="mt-1 text-xs font-medium uppercase tracking-wider text-ink-faint">Executive Health Score</div>
</div>
);
}
export default function BriefingPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const today = new Date().toLocaleDateString('ro-RO', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const { data: briefing, isLoading } = useQuery({
const { data, isLoading, error } = useQuery({
queryKey: ['briefing', tenantId],
queryFn: () => apiFetch<Briefing>('/v1/briefing/today', { tenantId }),
queryFn: () => apiFetch<BriefingData>('/v1/briefing', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 300_000,
});
return (
<div className="max-w-3xl">
<h1 className="mb-1 font-display text-2xl font-semibold text-ink">Daily Briefing</h1>
<p className="mb-8 text-sm text-ink-faint">
Prioritizarea e deterministă; AI-ul doar explică rezultatul, nu decide ordinea.
</p>
<div className="max-w-4xl space-y-8">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Daily Briefing</h1>
<p className="mt-1 text-sm text-ink-faint capitalize">{today}</p>
</div>
{isLoading && <p className="text-sm text-ink-faint">Se încarcă</p>}
{briefing && (
<div className="space-y-6">
{briefing.aiExplanation && (
<section className="card border-l-4 border-l-bronze p-5">
<p className="label mb-1.5">Sinteza zilei</p>
<p className="whitespace-pre-wrap text-sm text-ink-soft">{briefing.aiExplanation}</p>
</section>
)}
<section>
<h2 className="label mb-2">Sarcini restante</h2>
{briefing.overdueTasks.length === 0 ? (
<p className="card p-5 text-sm text-ink-faint">Nicio sarcină restantă.</p>
) : (
<ul className="space-y-2">
{briefing.overdueTasks.map((task) => (
<li key={task.id} className="card flex items-center justify-between p-4">
<span className="text-sm font-medium text-ink">{task.title}</span>
<span className="text-xs text-signal-danger">
{task.dueAt && new Date(task.dueAt).toLocaleDateString('ro-RO')}
</span>
</li>
))}
</ul>
)}
</section>
<section>
<h2 className="label mb-2">Următoarele 7 zile</h2>
{briefing.upcomingTasks.length === 0 ? (
<p className="card p-5 text-sm text-ink-faint">Nimic programat în următoarea săptămână.</p>
) : (
<ul className="space-y-2">
{briefing.upcomingTasks.map((task) => (
<li key={task.id} className="card flex items-center justify-between p-4">
<span className="text-sm font-medium text-ink">{task.title}</span>
<span className="text-xs text-ink-faint">
{task.dueAt && new Date(task.dueAt).toLocaleDateString('ro-RO')}
</span>
</li>
))}
</ul>
)}
</section>
{isLoading ? (
<div className="card p-12 text-center">
<p className="text-sm text-ink-faint">Se pregătește briefing-ul</p>
</div>
) : error || !data ? (
<div className="card p-12 text-center">
<p className="text-sm text-signal-danger">Nu s-a putut încărca briefing-ul.</p>
</div>
) : (
<>
{/* Score */}
<div className="card p-8 text-center">
<ScoreMeter score={data.score} />
<p className="mt-3 text-xs text-ink-faint">
Generat la {new Date(data.generatedAt).toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' })}
</p>
{data.score < 70 && (
<p className="mt-2 text-sm text-signal-warn">
{data.atRiskGoals.length > 0 && `${data.atRiskGoals.length} obiective la risc. `}
{data.overdueTasks.length > 0 && `${data.overdueTasks.length} taskuri expirate. `}
{data.pendingDecisions.length > 0 && `${data.pendingDecisions.length} decizii necesită review.`}
</p>
)}
</div>
{/* Alerts grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* Tasks due today */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Taskuri azi</h2>
<Link href={`${B}/tasks`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.todayTasks.length === 0 ? (
<p className="text-sm text-signal-ok">Nicio urgență pentru azi </p>
) : (
<ul className="space-y-2">
{data.todayTasks.slice(0, 4).map((t) => (
<li key={t.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-signal-warn" />
<p className="text-xs text-ink leading-snug">{t.title}</p>
</li>
))}
{data.todayTasks.length > 4 && <li className="text-xs text-ink-faint">+{data.todayTasks.length - 4} mai multe</li>}
</ul>
)}
</div>
{/* Overdue tasks */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Taskuri expirate</h2>
<Link href={`${B}/tasks`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.overdueTasks.length === 0 ? (
<p className="text-sm text-signal-ok">Nicio întârziere </p>
) : (
<ul className="space-y-2">
{data.overdueTasks.slice(0, 4).map((t) => (
<li key={t.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-signal-danger" />
<p className="text-xs text-ink leading-snug">{t.title}</p>
</li>
))}
{data.overdueTasks.length > 4 && <li className="text-xs text-ink-faint">+{data.overdueTasks.length - 4} mai multe</li>}
</ul>
)}
</div>
{/* At-risk goals */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Obiective la risc</h2>
<Link href={`${B}/goals`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.atRiskGoals.length === 0 ? (
<p className="text-sm text-signal-ok">Toate obiectivele pe plan </p>
) : (
<ul className="space-y-2">
{data.atRiskGoals.slice(0, 4).map((g) => (
<li key={g.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-signal-warn" />
<p className="text-xs text-ink leading-snug">{g.metric} {g.target} <span className="text-ink-line">({g.horizon})</span></p>
</li>
))}
</ul>
)}
</div>
{/* Pending decision reviews */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Review decizii</h2>
<Link href={`${B}/decisions`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.pendingDecisions.length === 0 ? (
<p className="text-sm text-signal-ok">Niciun review restant </p>
) : (
<ul className="space-y-2">
{data.pendingDecisions.slice(0, 3).map((d) => (
<li key={d.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-bronze-deep" />
<p className="text-xs text-ink leading-snug line-clamp-2">{d.context}</p>
</li>
))}
</ul>
)}
</div>
{/* Critical market signals */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Semnale critice</h2>
<Link href={`${B}/intelligence`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.criticalSignals.length === 0 ? (
<p className="text-sm text-signal-ok">Niciun semnal critic </p>
) : (
<ul className="space-y-2">
{data.criticalSignals.slice(0, 3).map((s) => (
<li key={s.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-signal-danger" />
<p className="text-xs text-ink leading-snug">{s.title}</p>
</li>
))}
</ul>
)}
</div>
{/* Expiring opportunities */}
<div className="card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Oportunități care expiră</h2>
<Link href={`${B}/opportunities`} className="text-[11px] text-bronze-deep hover:underline"></Link>
</div>
{data.expiringOpportunities.length === 0 ? (
<p className="text-sm text-signal-ok">Niciuna în 7 zile </p>
) : (
<ul className="space-y-2">
{data.expiringOpportunities.slice(0, 3).map((o) => (
<li key={o.id} className="flex items-start gap-2">
<span className="mt-0.5 size-1.5 shrink-0 rounded-full bg-signal-warn" />
<p className="text-xs text-ink leading-snug">{o.source}: {o.nextAction ?? 'acțiune necesară'}</p>
</li>
))}
</ul>
)}
</div>
</div>
</>
)}
</div>
);