feat(cc-053): Daily Briefing page
This commit is contained in:
parent
efe49a23c9
commit
d12bc87481
1 changed files with 174 additions and 56 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue