From c0c2c15919be09cf05bf32d8affab1413c92583b Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 17:34:55 +0000 Subject: [PATCH] feat(CC-085): add Introductions & Networking page (task-based intro requests with status) --- src/app/dashboard/introductions/page.tsx | 156 +++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/app/dashboard/introductions/page.tsx diff --git a/src/app/dashboard/introductions/page.tsx b/src/app/dashboard/introductions/page.tsx new file mode 100644 index 0000000..3758afc --- /dev/null +++ b/src/app/dashboard/introductions/page.tsx @@ -0,0 +1,156 @@ +'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 Contact { id: string; firstName: string; lastName: string | null; role: string | null; organization: string | null; tags: string[]; email: string | null; createdAt: string; } +interface Task { id: string; title: string; status: string; tags: string[]; createdAt: string; } + +const INTRO_TAGS = ['introduction', 'intro', 'introducere', 'conectare', 'networking']; + +export default function IntroductionsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [showRequest, setShowRequest] = useState(false); + const [form, setForm] = useState({ from: '', to: '', reason: '', priority: 'normal' }); + + const { data: contacts = [] } = useQuery({ queryKey: ['intro-contacts', tenantId], queryFn: () => apiFetch('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: tasks = [] } = useQuery({ queryKey: ['intro-tasks', tenantId], queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + + const introTasks = useMemo(() => + tasks.filter((t) => t.tags.some((tag) => INTRO_TAGS.includes(tag.toLowerCase()))) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()), + [tasks]); + + const introContacts = useMemo(() => + contacts.filter((c) => c.tags.some((t) => INTRO_TAGS.includes(t.toLowerCase()))), + [contacts]); + + const createMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: `Introducere: ${form.from} ↔ ${form.to}`, + description: form.reason || undefined, + priority: form.priority, + tags: ['introduction', 'intro'], + status: 'todo', + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['intro-tasks', tenantId] }); setShowRequest(false); setForm({ from: '', to: '', reason: '', priority: 'normal' }); }, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['intro-tasks', tenantId] }), + }); + + const statusCls: Record = { + todo: 'bg-muted text-ink-faint', + in_progress: 'bg-primary/10 text-primary', + completed: 'bg-signal-ok/10 text-signal-ok', + cancelled: 'bg-signal-danger/10 text-signal-danger', + }; + + return ( +
+
+
+

Introduceri & Networking

+

+ {introTasks.length} cereri de introducere urmărite · {introContacts.length} contacte cu tag intro +

+
+ +
+ + {showRequest && ( +
+

Cerere introducere nouă

+
+
+ + setForm((p) => ({ ...p, from: 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" /> +
+
+ + setForm((p) => ({ ...p, to: 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" /> +
+ setForm((p) => ({ ...p, reason: 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" /> + +
+
+ + +
+
+ )} + + {/* Stats */} +
+
+

{introTasks.filter((t) => t.status === 'todo' || t.status === 'in_progress').length}

+

în așteptare

+
+
+

{introTasks.filter((t) => t.status === 'completed').length}

+

realizate

+
+
+

{introContacts.length}

+

contacte intro

+
+
+ + {introTasks.length === 0 ? ( +
+

🤝

+

Nicio cerere de introducere. Adaugă prima.

+

Task-urile cu tag introduction sau intro apar automat.

+
+ ) : ( +
+ {introTasks.map((t) => ( +
+
+

{t.title}

+

+ {new Date(t.createdAt).toLocaleDateString('ro-RO', { dateStyle: 'medium' })} +

+
+
+ + {t.status} + + {t.status !== 'completed' && t.status !== 'cancelled' && ( + + )} +
+
+ ))} +
+ )} +
+ ); +}