From 6cafc2ea9d507d610f00297889ef6ff97fd4128d Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:58:11 +0000 Subject: [PATCH] feat(CC-079): add Scheduled Actions page (approval queue with approve/reject + schedule info) --- src/app/dashboard/ai/scheduled/page.tsx | 145 ++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/app/dashboard/ai/scheduled/page.tsx diff --git a/src/app/dashboard/ai/scheduled/page.tsx b/src/app/dashboard/ai/scheduled/page.tsx new file mode 100644 index 0000000..923e69e --- /dev/null +++ b/src/app/dashboard/ai/scheduled/page.tsx @@ -0,0 +1,145 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface Approval { + id: string; title: string; description: string | null; status: string; + priority: string | null; metadata: Record | null; + requestedAt: string; scheduledFor: string | null; +} + +const STATUS_BADGE: Record = { + pending: 'bg-warn/10 text-warn', + approved: 'bg-signal-ok/10 text-signal-ok', + rejected: 'bg-signal-danger/10 text-signal-danger', + running: 'bg-primary/10 text-primary', + done: 'bg-muted text-ink-faint', +}; + +function relTime(s: string) { + const diff = (new Date(s).getTime() - Date.now()) / 1000; + if (diff < -86400 * 7) return new Date(s).toLocaleDateString('ro-RO'); + if (diff < -3600) return `acum ${Math.floor(-diff/3600)}h`; + if (diff < -60) return `acum ${Math.floor(-diff/60)}min`; + if (diff < 0) return 'acum'; + if (diff < 3600) return `în ${Math.floor(diff/60)}min`; + if (diff < 86400) return `în ${Math.floor(diff/3600)}h`; + return `în ${Math.floor(diff/86400)} zile`; +} + +export default function ScheduledActionsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [statusFilter, setStatusFilter] = useState('all'); + + const { data: approvals = [], isLoading } = useQuery({ + queryKey: ['scheduled-approvals', tenantId], + queryFn: () => apiFetch('/v1/approvals?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, refetchInterval: 30_000, + }); + + const scheduled = useMemo(() => approvals.filter((a) => a.scheduledFor), [approvals]); + const unscheduled = useMemo(() => approvals.filter((a) => !a.scheduledFor), [approvals]); + + const approveMut = useMutation({ + mutationFn: (id: string) => apiFetch(`/v1/approvals/${id}/approve`, { tenantId, method: 'POST', body: { notes: 'Aprobat din Scheduled Actions' } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['scheduled-approvals', tenantId] }), + }); + const rejectMut = useMutation({ + mutationFn: (id: string) => apiFetch(`/v1/approvals/${id}/reject`, { tenantId, method: 'POST', body: { notes: 'Respins din Scheduled Actions' } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['scheduled-approvals', tenantId] }), + }); + + const filtered = (statusFilter === 'all' ? approvals : approvals.filter((a) => a.status === statusFilter)); + const pending = approvals.filter((a) => a.status === 'pending'); + + return ( +
+
+

Acțiuni Programate

+

+ {isLoading ? 'Se încarcă…' : `${approvals.length} total · ${pending.length} în așteptare · ${scheduled.length} programate`} +

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

⏳ {pending.length} acțiuni necesită aprobare

+

Revizuiește și aprobă sau respinge acțiunile pendinte.

+
+ )} + + {/* Stats */} +
+ {Object.entries(STATUS_BADGE).map(([status, cls]) => { + const count = approvals.filter((a) => a.status === status).length; + return ( + + ); + })} +
+ + {/* Filter tabs */} +
+ {([['all', 'Toate'], ['pending', 'Pending'], ['approved', 'Aprobate'], ['rejected', 'Respinse']] as const).map(([key, label]) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

🤖

+

Nicio acțiune AI în această categorie.

+
+ ) : ( +
+ {filtered.map((a) => { + const badge = STATUS_BADGE[a.status] ?? 'bg-muted text-ink-faint'; + return ( +
+
+
+
+ {a.status} +

{a.title}

+
+ {a.description &&

{a.description}

} +
+ Solicitat: {relTime(a.requestedAt)} + {a.scheduledFor && Programat: {relTime(a.scheduledFor)}} +
+
+ {a.status === 'pending' && ( +
+ + +
+ )} +
+
+ ); + })} +
+ )} +
+ ); +}