From 7fab4a052765bf0fa156bcda9892bf29a2d0554c Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:41:12 +0000 Subject: [PATCH] feat(CC-075): add Follow-ups page (tasks tagged follow-up, grouped overdue/today/upcoming) --- src/app/dashboard/follow-ups/page.tsx | 215 ++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/app/dashboard/follow-ups/page.tsx diff --git a/src/app/dashboard/follow-ups/page.tsx b/src/app/dashboard/follow-ups/page.tsx new file mode 100644 index 0000000..861df93 --- /dev/null +++ b/src/app/dashboard/follow-ups/page.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { + id: string; title: string; status: string; priority: string; + dueDate: string | null; tags: string[]; description: string | null; + assignedTo: string | null; createdAt: string; +} + +const FOLLOWUP_TAGS = ['follow-up', 'followup', 'follow_up', 'urmarire', 'callback', 'recontact']; + +function isFollowUp(task: Task) { + return task.tags.some((t) => FOLLOWUP_TAGS.includes(t.toLowerCase())) + || task.title.toLowerCase().includes('follow') || task.title.toLowerCase().includes('urmarire') + || task.title.toLowerCase().includes('contact'); +} + +function daysUntil(dateStr: string) { + return Math.ceil((new Date(dateStr).getTime() - Date.now()) / 86400_000); +} + +const PRIORITY_DOT: Record = { + critical: 'bg-signal-danger', high: 'bg-warn', medium: 'bg-primary', low: 'bg-muted-foreground', +}; + +export default function FollowUpsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [tab, setTab] = useState<'pending' | 'done'>('pending'); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ title: '', dueDate: '', priority: 'medium', assignedTo: '', description: '' }); + + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ['followups', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, + }); + + const followups = tasks.filter(isFollowUp); + const pending = followups.filter((t) => t.status !== 'completed' && t.status !== 'cancelled'); + const done = followups.filter((t) => t.status === 'completed'); + + const overdue = pending.filter((t) => t.dueDate && daysUntil(t.dueDate) < 0); + const today = pending.filter((t) => t.dueDate && daysUntil(t.dueDate) === 0); + const upcoming = pending.filter((t) => t.dueDate && daysUntil(t.dueDate) > 0); + const noDate = pending.filter((t) => !t.dueDate); + + const completeMut = useMutation({ + mutationFn: (id: string) => + apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status: 'completed' } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['followups', tenantId] }), + }); + + const createMut = useMutation({ + mutationFn: (body: Record) => + apiFetch('/v1/tasks', { tenantId, method: 'POST', body }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['followups', tenantId] }); + setShowCreate(false); + setForm({ title: '', dueDate: '', priority: 'medium', assignedTo: '', description: '' }); + }, + }); + + const renderTask = (t: Task) => { + const days = t.dueDate ? daysUntil(t.dueDate) : null; + return ( +
+ +
+ + {/* Tabs */} +
+ + +
+ + {/* Create form */} + {showCreate && ( +
+

Follow-up nou

+ setForm({ ...form, title: e.target.value })} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ setForm({ ...form, dueDate: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + + setForm({ ...form, assignedTo: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ setForm({ ...form, description: e.target.value })} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + +
+
+ )} + + {isLoading ? ( +
Se încarcă…
+ ) : tab === 'pending' ? ( +
+ {overdue.length > 0 && ( +
+

Depășite ({overdue.length})

+
{overdue.map(renderTask)}
+
+ )} + {today.length > 0 && ( +
+

Azi ({today.length})

+
{today.map(renderTask)}
+
+ )} + {upcoming.length > 0 && ( +
+

Urmează ({upcoming.length})

+
{upcoming.map(renderTask)}
+
+ )} + {noDate.length > 0 && ( +
+

Fără termen ({noDate.length})

+
{noDate.map(renderTask)}
+
+ )} + {pending.length === 0 && ( +
+

📞

+

Niciun follow-up în așteptare. Adaugă cu butonul de sus.

+
+ )} +
+ ) : ( +
+ {done.slice(0, 20).map((t) => ( +
+
+

{t.title}

+ {t.dueDate && ( + + {new Date(t.dueDate).toLocaleDateString('ro-RO', { day: '2-digit', month: 'short' })} + + )} +
+ ))} + {done.length === 0 && ( +
+

Niciun follow-up finalizat.

+
+ )} +
+ )} + + ); +}