diff --git a/src/app/dashboard/reports/business/page.tsx b/src/app/dashboard/reports/business/page.tsx new file mode 100644 index 0000000..b54af4d --- /dev/null +++ b/src/app/dashboard/reports/business/page.tsx @@ -0,0 +1,228 @@ +'use client'; + +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import type { Goal, Decision, Task } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +const B = '/dashboard'; + +interface Organization { id: string; name: string; country?: string | null; createdAt: string; } +interface Opportunity { id: string; source: string; probability?: string | null; expiresAt?: string | null; } + +function StatCard({ label, value, sub, href, color = 'text-ink' }: { + label: string; value: number | string; sub?: string; href?: string; color?: string; +}) { + const inner = ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); + return href ? {inner} : inner; +} + +function ProgressBar({ value, total, color = 'bg-primary' }: { value: number; total: number; color?: string }) { + const pct = total > 0 ? Math.round((value / total) * 100) : 0; + return ( +
+
+
+
+ {pct}% +
+ ); +} + +export default function BusinessReportsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const opts = { enabled: Boolean(tenantId), staleTime: 120_000 }; + + const { data: orgs = [] } = useQuery({ + queryKey: ['rep-orgs', tenantId], + queryFn: () => apiFetch('/v1/organizations', { tenantId }), + ...opts, + }); + const { data: tasks = [] } = useQuery({ + queryKey: ['rep-tasks', tenantId], + queryFn: () => apiFetch('/v1/tasks', { tenantId }), + ...opts, + }); + const { data: goals = [] } = useQuery({ + queryKey: ['rep-goals', tenantId], + queryFn: () => apiFetch('/v1/goals', { tenantId }), + ...opts, + }); + const { data: decisions = [] } = useQuery({ + queryKey: ['rep-decisions', tenantId], + queryFn: () => apiFetch('/v1/decisions', { tenantId }), + ...opts, + }); + const { data: opps = [] } = useQuery({ + queryKey: ['rep-opps', tenantId], + queryFn: () => apiFetch('/v1/opportunities', { tenantId }), + ...opts, + }); + + // Task stats + const taskDone = tasks.filter((t) => t.status === 'done').length; + const taskOpen = tasks.filter((t) => t.status === 'open').length; + const taskInProg = tasks.filter((t) => t.status === 'in_progress').length; + const taskBlocked = tasks.filter((t) => t.status === 'blocked').length; + const taskRate = tasks.length > 0 ? Math.round((taskDone / tasks.length) * 100) : 0; + + // Goal stats + const goalAchieved = goals.filter((g) => g.status === 'achieved').length; + const goalOnTrack = goals.filter((g) => g.status === 'on_track').length; + const goalAtRisk = goals.filter((g) => g.status === 'at_risk').length; + const goalNotStart = goals.filter((g) => g.status === 'not_started').length; + + // Decision stats + const decDecided = decisions.filter((d) => d.selectedOption).length; + const decPending = decisions.filter((d) => !d.selectedOption).length; + const decWithReview = decisions.filter((d) => d.outcomeReview).length; + + // Opportunity stats + const now = new Date(); + const oppsActive = opps.filter((o) => !o.expiresAt || new Date(o.expiresAt) > now).length; + const oppsExpiring = opps.filter((o) => { + if (!o.expiresAt) return false; + const exp = new Date(o.expiresAt); + const in7 = new Date(now.getTime() + 7 * 86400_000); + return exp > now && exp <= in7; + }).length; + + // Recent orgs (last 30 days) + const in30 = new Date(now.getTime() - 30 * 86400_000); + const recentOrgs = orgs.filter((o) => new Date(o.createdAt) > in30).length; + + return ( +
+
+

Raport Business

+

+ Metrici cheie — organizații, taskuri, obiective, decizii, oportunități +

+
+ + {/* KPI grid */} +
+ + + 0 ? 'text-signal-warn' : 'text-ink'} /> + 0 ? `${oppsExpiring} expiră curând` : 'toate active'} + href={`${B}/opportunities`} color={oppsExpiring > 0 ? 'text-signal-warn' : 'text-ink'} /> +
+ + {/* Tasks breakdown */} +
+
+

Taskuri

+ Deschide → +
+
+ {[ + { label: 'Finalizate', count: taskDone, color: 'bg-signal-ok' }, + { label: 'În progres', count: taskInProg, color: 'bg-primary' }, + { label: 'Deschise', count: taskOpen, color: 'bg-amber-400' }, + { label: 'Blocate', count: taskBlocked, color: 'bg-signal-danger' }, + ].map(({ label, count, color }) => ( +
+
+ {label} + {count} +
+ +
+ ))} +
+
+ Rată completare + {taskRate}% +
+
+ + {/* Goals + Decisions in 2 cols */} +
+ {/* Goals breakdown */} +
+
+

Obiective

+ Deschide → +
+ {goals.length === 0 ? ( +

Niciun obiectiv înregistrat

+ ) : ( +
+ {[ + { label: 'Realizate', count: goalAchieved, cls: 'text-signal-ok' }, + { label: 'Pe plan', count: goalOnTrack, cls: 'text-primary' }, + { label: 'Neîncepute', count: goalNotStart, cls: 'text-ink-faint' }, + { label: 'La risc', count: goalAtRisk, cls: 'text-signal-warn' }, + ].map(({ label, count, cls }) => ( +
+ {label} + {count} +
+ ))} +
+ )} +
+ + {/* Decisions breakdown */} +
+
+

Decizii

+ Deschide → +
+ {decisions.length === 0 ? ( +

Nicio decizie înregistrată

+ ) : ( +
+ {[ + { label: 'Total decizii', count: decisions.length, cls: 'text-ink' }, + { label: 'Pronunțate', count: decDecided, cls: 'text-signal-ok' }, + { label: 'În analiză', count: decPending, cls: 'text-signal-warn' }, + { label: 'Cu review', count: decWithReview, cls: 'text-primary' }, + ].map(({ label, count, cls }) => ( +
+ {label} + {count} +
+ ))} +
+ )} +
+
+ + {/* Recent organizations */} + {orgs.length > 0 && ( +
+

Organizații recente

+
+ {orgs.slice(0, 8).map((org) => ( +
+
+

{org.name}

+ {org.country &&

{org.country}

} +
+

+ {new Date(org.createdAt).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short', year: 'numeric' })} +

+
+ ))} +
+ {orgs.length > 8 && ( + + Vezi toate {orgs.length} organizații → + + )} +
+ )} +
+ ); +}