feat: navigatie completa pe cele 11 sectiuni din arhitectura de informatie

- lib/navigation.ts devine sursa unica de adevar pentru IA: 11 sectiuni,
  ~45 de pagini, fiecare cu status ready|planned si o descriere concreta
  a ce lipseste ca sa devina activa
- sidebar-ul randeaza sectiunile grupate; itemii neconstruiti sunt vizibil
  estompati si marcati cu un punct -- forma produsului e vizibila, dar nu
  pretindem ca exista
- dashboard/[...slug] este o singura ruta catch-all care explica exact ce
  lipseste pentru sectiunea ceruta, in loc de ~35 de fisiere-stub identice
  sau, mai rau, UI fals care pare functional
- Home se desparte corect: /dashboard = Executive Dashboard (ansamblu +
  sectiuni active), /dashboard/briefing = Daily Briefing (restante,
  urmatoarele 7 zile, sinteza AI)
This commit is contained in:
valentinbvro 2026-07-28 23:03:17 +02:00
parent b2260fe839
commit fc5c34fd61
5 changed files with 580 additions and 109 deletions

View file

@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { findNavItem } from '../../../lib/navigation';
export const dynamic = 'force-dynamic';
/**
* Ruta unica pentru sectiunile din arhitectura de informatie care nu sunt inca
* construite. Alternativa ar fi fost ~35 de fisiere-stub identice; asta tine
* lista intr-un singur loc (lib/navigation.ts) si spune exact ce lipseste,
* in loc sa arate un dashboard fals care pare functional.
*/
export default function PlannedSectionPage() {
const pathname = usePathname();
const entry = findNavItem(pathname);
if (!entry) {
return (
<div className="max-w-xl">
<h1 className="mb-2 font-display text-2xl font-semibold text-ink">Pagină inexistentă</h1>
<p className="mb-6 text-sm text-ink-faint">
Ruta <code className="rounded bg-paper-sunken px-1.5 py-0.5 text-xs">{pathname}</code> nu
face parte din structura platformei.
</p>
<Link href="/dashboard" className="btn-primary">
Înapoi la dashboard
</Link>
</div>
);
}
const { section, item } = entry;
return (
<div className="max-w-xl">
<p className="label mb-1">{section.label}</p>
<h1 className="mb-2 font-display text-2xl font-semibold text-ink">{item.label}</h1>
<p className="mb-6 text-sm text-ink-faint">
Secțiunea face parte din structura platformei, dar nu e construită încă.
</p>
{item.missing && (
<div className="card border-l-4 border-l-ink-line p-5">
<p className="label mb-1.5">Ce lipsește</p>
<p className="text-sm text-ink-soft">{item.missing}</p>
</div>
)}
<Link href="/dashboard" className="btn-ghost mt-6 inline-flex">
Înapoi la dashboard
</Link>
</div>
);
}

View file

@ -0,0 +1,76 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type Briefing } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
export const dynamic = 'force-dynamic';
export default function DailyBriefingPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: briefing, isLoading } = useQuery({
queryKey: ['briefing', tenantId],
queryFn: () => apiFetch<Briefing>('/v1/briefing/today', { tenantId }),
enabled: Boolean(tenantId),
});
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>
{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>
</div>
)}
</div>
);
}

View file

@ -1,110 +1,99 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type Briefing } from '../../lib/api';
import { useSession } from '../../components/session-provider';
import { NAV_SECTIONS, countByStatus } from '../../lib/navigation';
export default function OverviewPage() {
export const dynamic = 'force-dynamic';
export default function ExecutiveDashboardPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const progress = countByStatus();
const { data: briefing, isLoading } = useQuery({
const { data: briefing } = useQuery({
queryKey: ['briefing', tenantId],
queryFn: () => apiFetch<Briefing>('/v1/briefing/today', { tenantId }),
enabled: Boolean(tenantId),
});
const readyItems = NAV_SECTIONS.flatMap((section) =>
section.items
.filter((item) => item.status === 'ready' && item.href !== '/dashboard')
.map((item) => ({ ...item, section: section.label })),
);
const overdueCount = briefing?.overdueTasks.length ?? 0;
return (
<div className="max-w-4xl">
<h1 className="mb-1 font-display text-2xl font-semibold text-ink">
Bine ai revenit{activeTenant ? `, ${activeTenant.tenantName}` : ''}
</h1>
<p className="mb-8 text-sm text-ink-faint">Briefingul zilei prioritizare deterministă.</p>
<p className="mb-8 text-sm text-ink-faint">Executive Dashboard</p>
{isLoading && <p className="text-sm text-ink-faint">Se încarcă</p>}
{briefing && (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
<div className="card p-5">
<p className="label">Companii noi (7 zile)</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing.weekInReview.newOrganizations}
</p>
</div>
<div className="card p-5">
<p className="label">Segmente noi</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing.weekInReview.newSegments}
</p>
</div>
<div className="card p-5">
<p className="label">Research briefs noi</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing.weekInReview.newResearchBriefs}
</p>
</div>
</div>
{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>
)}
{briefing.overdueTasks.length > 0 && (
<section>
<h2 className="label mb-2">Sarcini restante</h2>
<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-6 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>
<p className="text-xs text-ink-faint">
Prioritizarea e deterministă; AI-ul doar explică rezultatul, nu decide ordinea.
<div className="mb-8 grid grid-cols-4 gap-4">
<Link href="/dashboard/briefing" className="card block p-5 transition-shadow hover:shadow-overlay">
<p className="label">Restante</p>
<p
className={`font-display text-3xl font-semibold ${
overdueCount > 0 ? 'text-signal-danger' : 'text-ink'
}`}
>
{overdueCount}
</p>
</Link>
<div className="card p-5">
<p className="label">Companii noi (7z)</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing?.weekInReview.newOrganizations ?? 0}
</p>
</div>
<div className="card p-5">
<p className="label">Segmente noi</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing?.weekInReview.newSegments ?? 0}
</p>
</div>
<div className="card p-5">
<p className="label">Research nou</p>
<p className="font-display text-3xl font-semibold text-ink">
{briefing?.weekInReview.newResearchBriefs ?? 0}
</p>
</div>
</div>
{briefing?.aiExplanation && (
<section className="card mb-8 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>
<Link href="/dashboard/briefing" className="mt-3 inline-block text-xs font-medium text-bronze-deep hover:underline">
Vezi briefingul complet
</Link>
</section>
)}
<div className="mt-8 grid grid-cols-3 gap-4">
<Link href="/dashboard/companies" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
Caută companii
</Link>
<Link href="/dashboard/segments" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
Segmente salvate
</Link>
<Link href="/dashboard/research" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
Research briefs
</Link>
</div>
<section>
<h2 className="label mb-2">Secțiuni active</h2>
<div className="mb-6 grid grid-cols-3 gap-3">
{readyItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="card block p-4 transition-shadow hover:shadow-overlay"
>
<p className="text-[11px] uppercase tracking-wide text-ink-faint">{item.section}</p>
<p className="text-sm font-medium text-ink">{item.label}</p>
</Link>
))}
</div>
<p className="text-xs text-ink-faint">
{progress.ready} din {progress.total} secțiuni sunt active. Restul apar în meniu cu un
punct și explică exact ce lipsește pentru fiecare.
</p>
</section>
</div>
);
}

View file

@ -5,16 +5,7 @@ import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useSession } from './session-provider';
import { TenantSwitcher } from './tenant-switcher';
const NAV_ITEMS = [
{ href: '/dashboard', label: 'Briefing' },
{ href: '/dashboard/organizations', label: 'Organizațiile mele' },
{ href: '/dashboard/tasks', label: 'Sarcini' },
{ href: '/dashboard/companies', label: 'Căutare companii' },
{ href: '/dashboard/segments', label: 'Segmente' },
{ href: '/dashboard/research', label: 'Research' },
{ href: '/dashboard/members', label: 'Echipă' },
];
import { NAV_SECTIONS, countByStatus } from '../lib/navigation';
/**
* Guard + shell pentru tot ce e sub /dashboard: redirecteaza catre /login sau
@ -26,6 +17,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const { authSession, authLoading, me, meLoading, activeTenant, signOut } = useSession();
const progress = countByStatus();
useEffect(() => {
if (authLoading) return;
@ -52,23 +44,50 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
<div className="mb-6 px-2">
<span className="font-display text-lg font-semibold text-ink">CEO OS</span>
</div>
<nav className="flex-1 space-y-1">
{NAV_ITEMS.map((item) => {
const isActive = item.href === '/dashboard' ? pathname === item.href : pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`block rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive ? 'bg-bronze-wash text-bronze-deep' : 'text-ink-soft hover:bg-paper-sunken hover:text-ink'
}`}
>
{item.label}
</Link>
);
})}
<nav className="flex-1 space-y-5 overflow-y-auto pr-1">
{NAV_SECTIONS.map((section) => (
<div key={section.label}>
<p className="mb-1 px-3 text-[11px] font-semibold uppercase tracking-wider text-ink-faint">
{section.label}
</p>
<div className="space-y-0.5">
{section.items.map((item) => {
const isActive =
item.href === '/dashboard' ? pathname === item.href : pathname === item.href;
const isPlanned = item.status === 'planned';
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center justify-between rounded-lg px-3 py-1.5 text-sm transition-colors ${
isActive
? 'bg-bronze-wash font-medium text-bronze-deep'
: isPlanned
? 'text-ink-faint hover:bg-paper-sunken hover:text-ink-soft'
: 'font-medium text-ink-soft hover:bg-paper-sunken hover:text-ink'
}`}
>
<span>{item.label}</span>
{/* Punctul marcheaza ce nu e construit inca -- forma produsului
ramane vizibila, dar nu pretindem ca exista. */}
{isPlanned && (
<span
aria-label="neconstruit"
title="Neconstruit încă"
className="ml-2 h-1.5 w-1.5 shrink-0 rounded-full bg-ink-line"
/>
)}
</Link>
);
})}
</div>
</div>
))}
</nav>
<div className="border-t border-ink-line pt-3">
<div className="mt-4 border-t border-ink-line pt-3">
<p className="mb-2 px-2 text-[11px] text-ink-faint">
{progress.ready} din {progress.total} secțiuni active
</p>
<p className="truncate px-2 text-xs text-ink-faint">{me?.email}</p>
<button type="button" onClick={() => void signOut()} className="mt-1 w-full px-2 py-1.5 text-left text-sm text-ink-faint hover:text-ink">
Deconectare

331
src/lib/navigation.ts Normal file
View file

@ -0,0 +1,331 @@
/**
* Sursa unica de adevar pentru arhitectura de informatie a platformei
* (cele 11 sectiuni din specul de navigatie). Sidebar-ul si ruta catch-all
* pentru sectiunile neconstruite citesc amandoua de aici, ca sa nu existe
* doua liste care se desincronizeaza.
*
* status:
* 'ready' -> pagina exista si e conectata la ceo-api
* 'planned' -> ruta cade pe catch-all si afiseaza explicit ce lipseste
*/
export type NavStatus = 'ready' | 'planned';
export interface NavItem {
label: string;
href: string;
status: NavStatus;
/** Ce lipseste concret ca sa devina 'ready' -- afisat pe pagina planned. */
missing?: string;
}
export interface NavSection {
label: string;
items: NavItem[];
}
const BASE = '/dashboard';
export const NAV_SECTIONS: NavSection[] = [
{
label: 'Home',
items: [
{ label: 'Executive Dashboard', href: BASE, status: 'ready' },
{ label: 'Daily Briefing', href: `${BASE}/briefing`, status: 'ready' },
{
label: 'Alerts',
href: `${BASE}/alerts`,
status: 'planned',
missing: 'Motor de alerte peste Event Fabric (praguri, deduplicare, canale de notificare).',
},
],
},
{
label: 'My Life',
items: [
{
label: 'Goals',
href: `${BASE}/goals`,
status: 'planned',
missing: 'Tabela goals exista in schema; lipsesc endpointurile /v1/goals si UI-ul.',
},
{
label: 'Calendar',
href: `${BASE}/calendar`,
status: 'planned',
missing: 'Connector de calendar (CalDAV/Google) prin Connector Fabric.',
},
{
label: 'Routines',
href: `${BASE}/routines`,
status: 'planned',
missing: 'Model de rutine + scheduler recurent.',
},
{
label: 'Energy & Performance',
href: `${BASE}/energy`,
status: 'planned',
missing: 'Telemetrie din Device Fabric (BLE gateway) -> observations.',
},
{
label: 'Personal KPI',
href: `${BASE}/personal-kpi`,
status: 'planned',
missing: 'Definitii de metrici personale peste observations.',
},
{
label: 'Family',
href: `${BASE}/family`,
status: 'planned',
missing: 'Rolul Family Member + partajare cu scope restrans (Policy Fabric).',
},
],
},
{
label: 'Business',
items: [
{ label: 'Companies', href: `${BASE}/organizations`, status: 'ready' },
{
label: 'CRM',
href: `${BASE}/crm`,
status: 'planned',
missing: 'Pipeline de leaduri + adapter ERPNext (blueprint ERP-001).',
},
{
label: 'Projects',
href: `${BASE}/projects`,
status: 'planned',
missing: 'Model de proiecte legat de organizations si tasks.',
},
{ label: 'Tasks', href: `${BASE}/tasks`, status: 'ready' },
{
label: 'Transactions',
href: `${BASE}/transactions`,
status: 'planned',
missing: 'Tabela transactions exista in schema; lipsesc /v1/transactions si UI-ul.',
},
{
label: 'Cash-flow',
href: `${BASE}/cash-flow`,
status: 'planned',
missing: 'Proiectii peste transactions + import din ERPNext.',
},
{
label: 'Contracts',
href: `${BASE}/contracts`,
status: 'planned',
missing: 'Tip de document dedicat + termene si reinnoiri.',
},
{
label: 'Accountant Pack',
href: `${BASE}/accountant-pack`,
status: 'planned',
missing: 'Export pe perioada + verificare de evidence gaps + partajare limitata in timp.',
},
],
},
{
label: 'Documents',
items: [
{
label: 'Archive',
href: `${BASE}/documents`,
status: 'planned',
missing: 'Paperless-ngx ruleaza pe server dar nu e conectat la ceo-api (adapter DOC-001).',
},
{
label: 'OCR Inbox',
href: `${BASE}/ocr-inbox`,
status: 'planned',
missing: 'Webhook Paperless -> status OCR -> extragere campuri candidate.',
},
{
label: 'Expirations',
href: `${BASE}/expirations`,
status: 'planned',
missing: 'Scan programat pe documents.expires_at -> alerta -> task.',
},
{
label: 'Evidence Center',
href: `${BASE}/evidence`,
status: 'planned',
missing: 'Legaturi document <-> tranzactie/termen cu verification status.',
},
],
},
{
label: 'Intelligence',
items: [
{ label: 'Research', href: `${BASE}/research`, status: 'ready' },
{ label: 'Data Search', href: `${BASE}/companies`, status: 'ready' },
{ label: 'Saved Segments', href: `${BASE}/segments`, status: 'ready' },
{
label: 'Insights',
href: `${BASE}/insights`,
status: 'planned',
missing: 'Detectie de tipare peste Event Fabric, cu sursa si confidence.',
},
{
label: 'Decisions',
href: `${BASE}/decisions`,
status: 'planned',
missing: 'Tabela decisions exista; lipsesc /v1/decisions, scenarii si review post-decizie.',
},
{
label: 'Opportunities',
href: `${BASE}/opportunities`,
status: 'planned',
missing: 'Tabela opportunities exista; lipseste motorul de candidati + owner review.',
},
{
label: 'Digital Twin',
href: `${BASE}/digital-twin`,
status: 'planned',
missing: 'Model dinamic al utilizatorului peste Memory Fabric. Faza A4.',
},
],
},
{
label: 'Relationships',
items: [
{
label: 'Contacts',
href: `${BASE}/contacts`,
status: 'planned',
missing: 'intelligence-api are /contacts/match; lipsesc contactele proprii ale tenantului.',
},
{
label: 'Follow-ups',
href: `${BASE}/follow-ups`,
status: 'planned',
missing: 'Taskuri legate de contacte, cu cadenta.',
},
{
label: 'Promises',
href: `${BASE}/promises`,
status: 'planned',
missing: 'Angajamente extrase din conversatii/documente, cu termen si dovada.',
},
{
label: 'Introductions',
href: `${BASE}/introductions`,
status: 'planned',
missing: 'Flux de intermediere intre contacte, cu consimtamant explicit.',
},
{
label: 'Network Map',
href: `${BASE}/network-map`,
status: 'planned',
missing: 'Graf de relatii peste memory-graph (Apache AGE ruleaza deja pe server).',
},
],
},
{
label: 'Trust & Reputation',
items: [
{
label: 'Credibility Profile',
href: `${BASE}/credibility`,
status: 'planned',
missing: 'Factori de reputatie derivati DOAR din dovezi verificate (Trust Engine).',
},
{
label: 'Social Reputation',
href: `${BASE}/social-reputation`,
status: 'planned',
missing: 'Semnale publice, cu sursa si data; explicit nu scor oficial.',
},
{
label: 'Certifications',
href: `${BASE}/certifications`,
status: 'planned',
missing: 'Certificari cu document doveditor si status de verificare.',
},
{
label: 'Projects & Outcomes',
href: `${BASE}/outcomes`,
status: 'planned',
missing: 'Rezultate masurabile legate de proiecte.',
},
{
label: 'References',
href: `${BASE}/references`,
status: 'planned',
missing: 'Recomandari cu sursa verificabila.',
},
{
label: 'Community Contributions',
href: `${BASE}/community`,
status: 'planned',
missing: 'Componenta 15 (Community & Professional Network), neinceputa.',
},
{
label: 'Compliance',
href: `${BASE}/compliance`,
status: 'planned',
missing: 'Obligatii de conformitate cu termene si dovezi.',
},
{
label: 'Disputes',
href: `${BASE}/disputes`,
status: 'planned',
missing: 'Litigii/dispute cu istoric si status.',
},
{
label: 'Credit Readiness',
href: `${BASE}/credit-readiness`,
status: 'planned',
missing: 'Pregatire pentru finantare. NU scor de credit oficial (exclus explicit din blueprint).',
},
],
},
{
label: 'Platform',
items: [
{
label: 'AI Assistant',
href: `${BASE}/assistant`,
status: 'planned',
missing:
'AI Gateway exista si Hermes ruleaza izolat pe Hetzner, dar ceo-api nu expune inca ' +
'un server MCP cu uneltele CEO OS -- agentul nu poate atinge datele tenantului.',
},
{
label: 'Automations',
href: `${BASE}/automations`,
status: 'planned',
missing: 'n8n ruleaza pe Hetzner; lipsesc callback-urile semnate si coada de aprobare.',
},
{
label: 'Integrations',
href: `${BASE}/integrations`,
status: 'planned',
missing: 'Registru de connectori (ERPNext, Paperless, calendar, BLE) cu status si credentiale.',
},
{ label: 'Privacy & Audit', href: `${BASE}/privacy-audit`, status: 'planned', missing: 'audit_log si consent_records exista in baza; lipseste UI-ul de consultare si export/stergere.' },
{ label: 'Echipă', href: `${BASE}/members`, status: 'ready' },
],
},
];
const ITEMS_BY_HREF = new Map<string, { section: NavSection; item: NavItem }>();
for (const section of NAV_SECTIONS) {
for (const item of section.items) {
ITEMS_BY_HREF.set(item.href, { section, item });
}
}
export function findNavItem(href: string) {
return ITEMS_BY_HREF.get(href);
}
export function countByStatus() {
let ready = 0;
let planned = 0;
for (const section of NAV_SECTIONS) {
for (const item of section.items) {
if (item.status === 'ready') ready += 1;
else planned += 1;
}
}
return { ready, planned, total: ready + planned };
}