From 05ea4b849c49c5b7f260b384f4c94057dea3a0e3 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:24:25 +0000 Subject: [PATCH] feat(CC-064): add Contracts page (list/create, expiry alerts, type/status filters) --- src/app/dashboard/contracts/page.tsx | 172 +++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/app/dashboard/contracts/page.tsx diff --git a/src/app/dashboard/contracts/page.tsx b/src/app/dashboard/contracts/page.tsx new file mode 100644 index 0000000..4d65ab2 --- /dev/null +++ b/src/app/dashboard/contracts/page.tsx @@ -0,0 +1,172 @@ +'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 Contract { + id: string; title: string; contractType: string; status: string; + parties: string[]; organizationId: string | null; + startDate: string | null; endDate: string | null; + valueMinorUnits: number | null; currency: string | null; + notes: string | null; tags: string[]; createdAt: string; +} +interface Organization { id: string; name: string; } + +const TYPE_META: Record = { + revenue: '💰 Venituri', expense: '💸 Cheltuieli', nda: '🔒 NDA', + employment: '👔 Angajare', other: '📄 Altele', +}; +const STATUS_META: Record = { + draft: { label: 'Schiță', cls: 'bg-muted text-ink-faint' }, + active: { label: 'Activ', cls: 'bg-signal-ok/10 text-signal-ok' }, + expired: { label: 'Expirat', cls: 'bg-signal-danger/10 text-signal-danger' }, + terminated: { label: 'Reziliat', cls: 'bg-muted text-ink-faint' }, +}; + +export default function ContractsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [filterStatus, setFilterStatus] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ + title: '', contractType: 'other', startDate: '', endDate: '', parties: '', notes: '', + }); + + const { data: contracts = [], isLoading } = useQuery({ + queryKey: ['contracts', tenantId, filterStatus], + queryFn: () => apiFetch(`/v1/contracts${filterStatus ? `?status=${filterStatus}` : ''}`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + + const { mutate: create, isPending } = useMutation({ + mutationFn: () => apiFetch('/v1/contracts', { + method: 'POST', + body: { ...form, parties: form.parties.split(',').map((p) => p.trim()).filter(Boolean) }, + tenantId, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['contracts', tenantId] }); + setShowCreate(false); + setForm({ title: '', contractType: 'other', startDate: '', endDate: '', parties: '', notes: '' }); + }, + }); + + const now = new Date(); + const expiringSoon = contracts.filter((c) => { + if (!c.endDate || c.status !== 'active') return false; + const exp = new Date(c.endDate); + return exp < new Date(now.getTime() + 30 * 86400_000) && exp > now; + }); + + return ( +
+
+
+

Contracte

+

{contracts.length} contracte înregistrate

+
+ +
+ + {expiringSoon.length > 0 && ( +
+

⚠ {expiringSoon.length} contracte expiră în 30 zile

+ {expiringSoon.map((c) => ( +

{c.title} — expiră {c.endDate}

+ ))} +
+ )} + + {showCreate && ( +
+

Contract nou

+
+
+ + setForm((f) => ({ ...f, title: e.target.value }))} + placeholder="Contract prestări servicii…" + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+ + setForm((f) => ({ ...f, parties: e.target.value }))} + placeholder="Firma A, Firma B" + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ {[{key: 'startDate', label: 'Data start'}, {key: 'endDate', label: 'Dată expirare'}].map(({key, label}) => ( +
+ + )[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" /> +
+ ))} +
+
+ + +
+
+ )} + +
+ {[['', 'Toate'], ['draft', 'Schiță'], ['active', 'Active'], ['expired', 'Expirate']].map(([s, l]) => ( + + ))} +
+ + {isLoading ?
Se încarcă…
+ : contracts.length === 0 ? ( +
+

📋

+

Niciun contract înregistrat.

+ +
+ ) : ( +
+ {contracts.map((c) => { + const status = STATUS_META[c.status] ?? STATUS_META.draft; + const isExpiring = expiringSoon.some((e) => e.id === c.id); + return ( +
+
+
+

{c.title}

+ {status.label} + {isExpiring && Expiră curând} +
+
+ {TYPE_META[c.contractType] ?? c.contractType} + {c.endDate && ⏳ {c.endDate}} + {c.parties.length > 0 && {c.parties.slice(0, 2).join(', ')}{c.parties.length > 2 ? '…' : ''}} +
+
+ + {new Date(c.createdAt).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' })} + +
+ ); + })} +
+ ) + } +
+ ); +}