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:
parent
b2260fe839
commit
fc5c34fd61
5 changed files with 580 additions and 109 deletions
56
src/app/dashboard/[...slug]/page.tsx
Normal file
56
src/app/dashboard/[...slug]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/app/dashboard/briefing/page.tsx
Normal file
76
src/app/dashboard/briefing/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
331
src/lib/navigation.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Reference in a new issue