diff --git a/src/app/dashboard/applications/page.tsx b/src/app/dashboard/applications/page.tsx new file mode 100644 index 0000000..830cf0d --- /dev/null +++ b/src/app/dashboard/applications/page.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import Link from 'next/link'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { + id: string; title: string; description: string | null; status: string; + priority: string | null; tags: string[]; dueDate: string | null; + assignedTo: string | null; createdAt: string; +} + +const APP_TAGS = ['application', 'aplicatie', 'candidatura', 'grant', 'bursă', 'bursa', 'concurs', 'tender']; +const STATUS_LABELS: Record = { + todo: { label: 'De făcut', cls: 'bg-muted text-ink-faint' }, + in_progress: { label: 'În lucru', cls: 'bg-primary/10 text-primary' }, + review: { label: 'Review', cls: 'bg-warn/10 text-warn' }, + completed: { label: 'Depus', cls: 'bg-signal-ok/10 text-signal-ok' }, + cancelled: { label: 'Anulat', cls: 'bg-signal-danger/10 text-signal-danger' }, +}; + +function daysLeft(due: string | null): number | null { + if (!due) return null; + return Math.ceil((new Date(due).getTime() - Date.now()) / 86400_000); +} + +export default function ApplicationsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [statusFilter, setStatusFilter] = useState('all'); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ title: '', description: '', priority: 'normal', dueDate: '', tag: 'application' }); + + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ['apps', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const apps = useMemo(() => + tasks.filter((t) => t.tags.some((tag) => APP_TAGS.includes(tag.toLowerCase()))) + .sort((a, b) => { + if (a.dueDate && b.dueDate) return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); + if (a.dueDate) return -1; + if (b.dueDate) return 1; + return 0; + }), + [tasks]); + + const filtered = statusFilter === 'all' ? apps : apps.filter((a) => a.status === statusFilter); + + const stats = useMemo(() => ({ + total: apps.length, + active: apps.filter((a) => !['completed','cancelled'].includes(a.status)).length, + submitted: apps.filter((a) => a.status === 'completed').length, + urgent: apps.filter((a) => { const d = daysLeft(a.dueDate); return d !== null && d <= 7 && d >= 0 && a.status !== 'completed'; }).length, + }), [apps]); + + const createMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: form.title, description: form.description || undefined, + priority: form.priority, dueDate: form.dueDate || undefined, + tags: [form.tag, 'application'], status: 'todo', + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['apps', tenantId] }); setShowAdd(false); setForm({ title: '', description: '', priority: 'normal', dueDate: '', tag: 'application' }); }, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['apps', tenantId] }), + }); + + return ( +
+
+
+

Aplicații & Termene

+

+ {isLoading ? 'Se încarcă…' : `${apps.length} aplicații urmărite`} +

+
+ +
+ + {stats.urgent > 0 && ( +
+ ⚠️ +

+ {stats.urgent} aplicație/aplicații cu termen în mai puțin de 7 zile! +

+
+ )} + + {/* Stats */} +
+ {[ + { label: 'Total', value: stats.total }, + { label: 'Active', value: stats.active }, + { label: 'Depuse', value: stats.submitted }, + { label: 'Urgente', value: stats.urgent }, + ].map((s) => ( +
+

{s.value}

+

{s.label}

+
+ ))} +
+ + {/* Add form */} + {showAdd && ( +
+

Aplicație nouă

+
+ setForm((p) => ({ ...p, title: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring sm:col-span-2" /> + setForm((p) => ({ ...p, description: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring sm:col-span-2" /> +
+ + setForm((p) => ({ ...p, dueDate: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+
+ + +
+
+ )} + + {/* Filters */} +
+ {['all', 'todo', 'in_progress', 'review', 'completed', 'cancelled'].map((s) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

📋

+

Nicio aplicație. Adaugă prima aplicație.

+

Task-urile cu tag-ul application, grant sau bursa apar automat.

+
+ ) : ( +
+ {filtered.map((app) => { + const dl = daysLeft(app.dueDate); + const urgentFlag = dl !== null && dl <= 7 && dl >= 0 && app.status !== 'completed'; + const sl = STATUS_LABELS[app.status] ?? { label: app.status, cls: 'bg-muted text-ink-faint' }; + return ( +
+
+

{app.title}

+ {sl.label} +
+
+ {app.dueDate && ( + + 📅 {new Date(app.dueDate).toLocaleDateString('ro-RO', { dateStyle: 'medium' })} + {dl !== null && dl >= 0 && ` (${dl}z)`} + {dl !== null && dl < 0 && ' (expirat)'} + + )} + {app.tags.filter((t) => t !== 'application').map((t) => ( + {t} + ))} +
+ {app.status !== 'completed' && app.status !== 'cancelled' && ( +
+ {app.status === 'todo' && ( + + )} + + +
+ )} +
+ ); + })} +
+ )} +
+ ); +}