diff --git a/src/app/dashboard/obligations/page.tsx b/src/app/dashboard/obligations/page.tsx new file mode 100644 index 0000000..2ffad8b --- /dev/null +++ b/src/app/dashboard/obligations/page.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Obligation { + id: string; title: string; description: string | null; + category: string; status: string; dueDate: string | null; + owner: string | null; riskLevel: string; createdAt: string; +} + +const CAT_META: Record = { + contractual: '📋 Contractual', fiscal: '🧾 Fiscal', legal: '⚖️ Legal', regulatory: '🏛️ Reglementar', +}; +const STATUS_META: Record = { + pending: { label: 'Pending', cls: 'bg-signal-warn/10 text-signal-warn' }, + fulfilled: { label: 'Îndeplinit', cls: 'bg-signal-ok/10 text-signal-ok' }, + overdue: { label: 'Restant', cls: 'bg-signal-danger/10 text-signal-danger' }, + waived: { label: 'Renunțat', cls: 'bg-muted text-ink-faint' }, +}; +const RISK_META: Record = { + low: { cls: 'text-sky-600' }, medium: { cls: 'text-signal-warn' }, + high: { cls: 'text-orange-600' }, critical: { cls: 'text-signal-danger font-bold' }, +}; + +export default function ObligationsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [filterStatus, setFilterStatus] = useState('pending'); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ title: '', category: 'contractual', dueDate: '', owner: '', riskLevel: 'medium' }); + + const { data: obligations = [], isLoading } = useQuery({ + queryKey: ['obligations', tenantId, filterStatus], + queryFn: () => apiFetch(`/v1/obligations${filterStatus !== 'all' ? `?status=${filterStatus}` : ''}`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + + const { mutate: create, isPending } = useMutation({ + mutationFn: () => apiFetch('/v1/obligations', { method: 'POST', body: form, tenantId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['obligations', tenantId] }); + setShowCreate(false); + setForm({ title: '', category: 'contractual', dueDate: '', owner: '', riskLevel: 'medium' }); + }, + }); + + const { mutate: fulfill } = useMutation({ + mutationFn: (id: string) => apiFetch(`/v1/obligations/${id}`, { method: 'PATCH', body: { status: 'fulfilled' }, tenantId }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['obligations', tenantId] }), + }); + + const now = new Date(); + const overdueIds = new Set( + obligations.filter((o) => o.dueDate && new Date(o.dueDate) < now && o.status === 'pending').map((o) => o.id) + ); + const sorted = [...obligations].sort((a, b) => { + if (!a.dueDate) return 1; + if (!b.dueDate) return -1; + return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); + }); + + return ( +
+
+
+

Termene & Obligații

+

+ {overdueIds.size > 0 && {overdueIds.size} restante · } + {obligations.length} obligații +

+
+ +
+ + {showCreate && ( +
+

Obligație nouă

+
+
+ + setForm((f) => ({ ...f, title: e.target.value }))} + placeholder="Depunere declarație TVA Q1…" + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ {[ + { key: 'category', label: 'Categorie', opts: Object.entries(CAT_META).map(([k, v]) => ({ value: k, label: v })) }, + { key: 'riskLevel', label: 'Nivel risc', opts: [['low','Scăzut'],['medium','Mediu'],['high','Ridicat'],['critical','Critic']].map(([k, l]) => ({ value: k, label: l })) }, + ].map(({ key, label, opts }) => ( +
+ + +
+ ))} + {[{ key: 'dueDate', label: 'Termen', type: 'date' }, { key: 'owner', label: 'Responsabil', type: 'text' }].map(({key, label, type}) => ( +
+ + )[key]} + onChange={(e) => setForm((f) => ({ ...f, [key]: e.target.value }))} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ ))} +
+
+ + +
+
+ )} + +
+ {[['pending','Pending'], ['fulfilled','Îndeplinite'], ['overdue','Restante'], ['all','Toate']].map(([s, l]) => ( + + ))} +
+ + {isLoading ?
Se încarcă…
+ : sorted.length === 0 ? ( +
+

+

Nicio obligație înregistrată.

+
+ ) : ( +
+ {sorted.map((ob) => { + const status = STATUS_META[ob.status] ?? STATUS_META.pending; + const risk = RISK_META[ob.riskLevel] ?? RISK_META.medium; + const isOv = overdueIds.has(ob.id); + return ( +
+
+
+

{ob.title}

+ {status.label} + {isOv && ⏰ RESTANT} +
+
+ {CAT_META[ob.category] ?? ob.category} + {ob.dueDate && Termen: {ob.dueDate}} + {ob.owner && Resp.: {ob.owner}} + Risc: {ob.riskLevel} +
+
+ {ob.status === 'pending' && ( + + )} +
+ ); + })} +
+ ) + } +
+ ); +}