diff --git a/src/app/dashboard/ai/approvals/page.tsx b/src/app/dashboard/ai/approvals/page.tsx index 9305e5a..9b90e73 100644 --- a/src/app/dashboard/ai/approvals/page.tsx +++ b/src/app/dashboard/ai/approvals/page.tsx @@ -1,168 +1,191 @@ 'use client'; import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiFetch } from '../../../../lib/api'; import { useSession } from '../../../../components/session-provider'; -interface AiRequest { - id: string; - purpose: string; - actionClass: string; - model: string; - templateVersion: string | null; - resultStatus: string; - costUsd: string | null; - contextManifestHash: string; - createdAt: string; - completedAt: string | null; +interface Approval { + id: string; tenantId: string; requestType: string; title: string; + description: string | null; requestedBy: string | null; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + priority: 'low' | 'medium' | 'high' | 'critical'; + expiresAt: string | null; approvedAt: string | null; + rejectedAt: string | null; reviewNotes: string | null; + metadata: Record | null; createdAt: string; } -const RISK_BY_CLASS: Record = { - read: { label: 'Citire', cls: 'bg-sky-500/10 text-sky-700 dark:text-sky-300' }, - write: { label: 'Scriere', cls: 'bg-amber-500/10 text-amber-700 dark:text-amber-300' }, - send: { label: 'Trimitere', cls: 'bg-orange-500/10 text-orange-700 dark:text-orange-300' }, - delete: { label: 'Ștergere', cls: 'bg-signal-danger/10 text-signal-danger' }, - high_risk: { label: 'Risc înalt', cls: 'bg-signal-danger/10 text-signal-danger font-bold' }, +const STATUS_META: Record = { + pending: { label: 'În așteptare', cls: 'bg-warn/10 text-warn border-warn/30' }, + approved: { label: 'Aprobat', cls: 'bg-signal-ok/10 text-signal-ok border-signal-ok/30' }, + rejected: { label: 'Respins', cls: 'bg-signal-danger/10 text-signal-danger border-signal-danger/30' }, + expired: { label: 'Expirat', cls: 'bg-muted text-ink-faint border-border' }, }; -export default function ApprovalQueuePage() { +const PRIORITY_CLS: Record = { + critical: 'text-signal-danger', high: 'text-warn', + medium: 'text-ink', low: 'text-ink-faint', +}; + +export default function AIApprovalsPage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; const qc = useQueryClient(); - const [rejecting, setRejecting] = useState(null); - const { data: pending = [], isLoading } = useQuery({ - queryKey: ['ai-approvals', tenantId], - queryFn: () => apiFetch('/v1/ai-requests?status=pending&limit=100', { tenantId }), - enabled: Boolean(tenantId), - refetchInterval: 15_000, + const [filter, setFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('pending'); + const [reviewNotes, setReviewNotes] = useState>({}); + const [expanded, setExpanded] = useState(null); + + const { data: approvals = [], isLoading } = useQuery({ + queryKey: ['approvals', tenantId, filter], + queryFn: () => { + const q = filter !== 'all' ? `?status=${filter}` : ''; + return apiFetch(`/v1/approvals${q}`, { tenantId }); + }, + enabled: Boolean(tenantId), staleTime: 15_000, refetchInterval: 30_000, }); - const { mutate: updateStatus, isPending: isUpdating } = useMutation({ - mutationFn: ({ id, resultStatus }: { id: string; resultStatus: 'completed' | 'cancelled' }) => - apiFetch(`/v1/ai-requests/${id}/status`, { method: 'PATCH', body: { resultStatus }, tenantId }), - onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-approvals', tenantId] }), + const approveMut = useMutation({ + mutationFn: ({ id, notes }: { id: string; notes: string }) => + apiFetch(`/v1/approvals/${id}/approve`, { tenantId, method: 'POST', body: { reviewNotes: notes } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['approvals', tenantId] }), }); - const approve = (id: string) => updateStatus({ id, resultStatus: 'completed' }); - const reject = (id: string) => { updateStatus({ id, resultStatus: 'cancelled' }); setRejecting(null); }; + const rejectMut = useMutation({ + mutationFn: ({ id, notes }: { id: string; notes: string }) => + apiFetch(`/v1/approvals/${id}/reject`, { tenantId, method: 'POST', body: { reviewNotes: notes } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['approvals', tenantId] }), + }); + + const pending = approvals.filter((a) => a.status === 'pending').length; return ( -
+
-

Coadă de Aprobare

+

Aprobări AI

- Acțiuni AI care necesită autorizare înainte de execuție + Acțiuni generate de AI care necesită confirmare umană. + {pending > 0 && {pending} în așteptare}

- {pending.length > 0 && ( - - - {pending.length} în așteptare - - )}
+ {/* Filter tabs */} +
+ {(['pending', 'approved', 'rejected', 'all'] as const).map((s) => ( + + ))} +
+ + {/* List */} {isLoading ? ( -
Se încarcă…
- ) : pending.length === 0 ? ( -
-

-

Coada este goală

-

- Nicio acțiune AI nu este în așteptare de aprobare. +

Se încarcă…
+ ) : approvals.length === 0 ? ( +
+

{filter === 'pending' ? '✅' : '📋'}

+

+ {filter === 'pending' ? 'Nicio aprobare în așteptare. Totul e la zi.' : 'Niciun rezultat.'}

) : (
- {pending.map((req) => { - const risk = RISK_BY_CLASS[req.actionClass] ?? { label: req.actionClass, cls: 'bg-muted text-ink-faint' }; - const estimatedCost = req.costUsd ? `$${parseFloat(req.costUsd).toFixed(5)}` : null; - const waitingSince = new Date(req.createdAt); - const minsWaiting = Math.round((Date.now() - waitingSince.getTime()) / 60_000); - const isConfirmingReject = rejecting === req.id; + {approvals.map((a) => { + const sm = STATUS_META[a.status] ?? STATUS_META.pending; + const isExp = expanded === a.id; + const notes = reviewNotes[a.id] ?? ''; + const isExpired = a.expiresAt && new Date(a.expiresAt) < new Date(); return ( -
-
-
-
-

{req.purpose}

- - {risk.label} +
+
+ {/* Priority indicator */} +
+ +
+
+
+

{a.title}

+

+ {a.requestType} · {a.priority} +

+
+ + {sm.label}
-
- {req.model.split('/').pop()} - {estimatedCost && cuv. estimat {estimatedCost}} - {req.templateVersion && template v{req.templateVersion}} - în așteptare {minsWaiting > 60 ? `${Math.round(minsWaiting / 60)}h` : `${minsWaiting}m`} + + {a.description && ( +

{a.description}

+ )} + +
+ {a.requestedBy && de: {a.requestedBy}} + {new Date(a.createdAt).toLocaleDateString('ro-RO')} + {a.expiresAt && ( + + {isExpired ? '⚠ expirat' : `expiră ${new Date(a.expiresAt).toLocaleDateString('ro-RO')}`} + + )}
+ + {/* Metadata preview */} + {a.metadata && Object.keys(a.metadata).length > 0 && ( + + )} + + {isExp && a.metadata && ( +
+                        {JSON.stringify(a.metadata, null, 2)}
+                      
+ )} + + {/* Actions for pending */} + {a.status === 'pending' && !isExpired && ( +
+