diff --git a/src/app/dashboard/reports/custom/page.tsx b/src/app/dashboard/reports/custom/page.tsx new file mode 100644 index 0000000..9ba5c26 --- /dev/null +++ b/src/app/dashboard/reports/custom/page.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { id: string; status: string; priority: string; dueDate: string | null; createdAt: string; } +interface Goal { id: string; status: string; progress: number | null; createdAt: string; } +interface Observation { id: string; metric: string; value: string; subjectType: string; createdAt: string; observedAt: string | null; } +interface Decision { id: string; status: string; impact: string | null; createdAt: string; } +interface Transaction { id: string; amount: string; type: string; createdAt: string; } +interface Contact { id: string; createdAt: string; } + +type Metric = 'tasks' | 'goals' | 'observations' | 'decisions' | 'finance' | 'contacts'; +type Period = '7' | '30' | '90' | '180' | '365'; +type Grouping = 'day' | 'week' | 'month'; + +const METRIC_LABELS: Record = { + tasks: 'Taskuri', goals: 'Obiective', observations: 'Observații', + decisions: 'Decizii', finance: 'Tranzacții', contacts: 'Contacte', +}; + +function toDate(s: string) { return new Date(s); } +function weekKey(d: Date) { + const start = new Date(d); start.setDate(d.getDate() - d.getDay()); + return start.toISOString().slice(0, 10); +} +function monthKey(d: Date) { return d.toISOString().slice(0, 7); } + +function groupDates(dates: Date[], grouping: Grouping): Record { + const map: Record = {}; + for (const d of dates) { + const key = grouping === 'day' ? d.toISOString().slice(0, 10) + : grouping === 'week' ? weekKey(d) : monthKey(d); + map[key] = (map[key] ?? 0) + 1; + } + return map; +} + +export default function CustomReportsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [metrics, setMetrics] = useState(['tasks', 'observations']); + const [period, setPeriod] = useState('30'); + const [grouping, setGrouping] = useState('week'); + + const { data: tasks = [] } = useQuery({ queryKey: ['cr-tasks', tenantId], queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: goals = [] } = useQuery({ queryKey: ['cr-goals', tenantId], queryFn: () => apiFetch('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: obs = [] } = useQuery({ queryKey: ['cr-obs', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: decisions = [] } = useQuery({ queryKey: ['cr-dec', tenantId], queryFn: () => apiFetch('/v1/decisions?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: transactions = [] } = useQuery({ queryKey: ['cr-txn', tenantId], queryFn: () => apiFetch('/v1/transactions?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: contacts = [] } = useQuery({ queryKey: ['cr-con', tenantId], queryFn: () => apiFetch('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + + const cutoff = useMemo(() => new Date(Date.now() - parseInt(period) * 86400_000), [period]); + + const DATA_MAP: Record = { + tasks, goals, observations: obs.map((o) => ({ ...o, date: o.observedAt ?? o.createdAt })), + decisions, finance: transactions, contacts, + }; + + const charts = useMemo(() => { + return metrics.map((m) => { + const items = DATA_MAP[m] + .filter((item) => { + const d = toDate((item as Record).observedAt ?? (item as Record).date ?? (item as Record).createdAt); + return d >= cutoff; + }) + .map((item) => { + const dateStr = (item as Record).observedAt ?? (item as Record).date ?? (item as Record).createdAt; + return toDate(dateStr); + }); + const grouped = groupDates(items, grouping); + const sorted = Object.entries(grouped).sort((a, b) => a[0].localeCompare(b[0])); + const max = Math.max(...sorted.map(([, c]) => c), 1); + const total = sorted.reduce((s, [, c]) => s + c, 0); + return { metric: m, sorted, max, total }; + }); + }, [metrics, cutoff, grouping, tasks, goals, obs, decisions, transactions, contacts]); + + function toggleMetric(m: Metric) { + setMetrics((prev) => prev.includes(m) ? prev.filter((x) => x !== m) : [...prev, m]); + } + + function exportCSV() { + const header = ['Metric', 'Period', 'Count'].join(','); + const rows = charts.flatMap(({ metric, sorted }) => + sorted.map(([period, count]) => `${metric},${period},${count}`) + ); + const csv = [header, ...rows].join('\n'); + const el = document.createElement('a'); + el.href = `data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`; + el.download = `ceo-os-report-${new Date().toISOString().slice(0, 10)}.csv`; + el.click(); + } + + return ( +
+
+
+

Rapoarte Personalizate

+

Construiește rapoarte combinate din orice modul CEO OS.

+
+ +
+ + {/* Controls */} +
+
+

Metrici

+
+ {(Object.keys(METRIC_LABELS) as Metric[]).map((m) => ( + + ))} +
+
+ +
+
+

Perioadă

+
+ {(['7', '30', '90', '180', '365'] as Period[]).map((p) => ( + + ))} +
+
+
+

Grupare

+
+ {(['day', 'week', 'month'] as Grouping[]).map((g) => ( + + ))} +
+
+
+
+ + {/* Charts */} + {metrics.length === 0 ? ( +
+

📊

+

Selectează cel puțin o metrică de mai sus.

+
+ ) : ( +
+ {charts.map(({ metric, sorted, max, total }) => ( +
+
+

{METRIC_LABELS[metric]}

+
+ Total: {total} + Perioade: {sorted.length} +
+
+ {sorted.length === 0 ? ( +

Niciun dat în perioadă.

+ ) : ( +
+ {sorted.map(([key, count]) => ( +
+ {key} +
+
+
+ {count} +
+ ))} +
+ )} +
+ ))} +
+ )} +
+ ); +}