ceo-web/src/app/dashboard/events/page.tsx

214 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 Task { id: string; title: string; description: string | null; status: string; priority: string | null; tags: string[]; targetDate: string | null; createdAt: string; }
const EVENT_TAGS = ['eveniment','event','conferinta','conference','workshop','meetup','webinar','summit','hackathon','networking'];
const EVENT_TYPES: Record<string, string> = {
conferinta:'🎤', conference:'🎤', workshop:'🛠️', meetup:'🤝',
webinar:'💻', summit:'🏔️', hackathon:'⚡', networking:'🌐',
eveniment:'📅', event:'📅',
};
function daysUntil(iso: string | null): number | null {
if (!iso) return null;
return Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000);
}
export default function EventsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const [tab, setTab] = useState<'upcoming' | 'past'>('upcoming');
const [showAdd, setShowAdd] = useState(false);
const [form, setForm] = useState({ title: '', type: 'conferinta', date: '', location: '', url: '', prep: false });
const { data: tasks = [], isLoading } = useQuery({
queryKey: ['events', tenantId],
queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const events = useMemo(() =>
tasks.filter((t) => t.tags.some((tag) => EVENT_TAGS.includes(tag.toLowerCase()))),
[tasks]);
const upcoming = useMemo(() =>
events
.filter((e) => !e.targetDate || new Date(e.targetDate) >= new Date(new Date().toDateString()))
.filter((e) => e.status !== 'cancelled')
.sort((a, b) => (a.targetDate ?? '9999').localeCompare(b.targetDate ?? '9999')),
[events]);
const past = useMemo(() =>
events
.filter((e) => e.targetDate && new Date(e.targetDate) < new Date(new Date().toDateString()))
.sort((a, b) => b.targetDate!.localeCompare(a.targetDate!)),
[events]);
const urgent = upcoming.filter((e) => {
const d = daysUntil(e.targetDate);
return d !== null && d <= 14;
});
const addMut = useMutation({
mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: {
title: form.title,
description: [form.location && `📍 ${form.location}`, form.url && `🔗 ${form.url}`].filter(Boolean).join('\n') || undefined,
tags: ['eveniment', form.type, ...(form.prep ? ['prep-needed'] : [])],
status: 'todo', priority: 'normal',
targetDate: form.date || undefined,
}}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['events', tenantId] });
setShowAdd(false);
setForm({ title: '', type: 'conferinta', date: '', location: '', url: '', prep: false });
},
});
const patchMut = useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status } }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['events', tenantId] }),
});
function eventType(t: Task): string {
return t.tags.find((tag) => EVENT_TYPES[tag.toLowerCase()]) ?? 'event';
}
const displayList = tab === 'upcoming' ? upcoming : past;
return (
<div className="max-w-4xl space-y-6 p-6">
<div className="flex items-start justify-between flex-wrap gap-3">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Event Tracker</h1>
<p className="text-sm text-ink-faint mt-1">{upcoming.length} viitoare · {past.length} trecute</p>
</div>
<button onClick={() => setShowAdd(true)}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90">
+ Eveniment
</button>
</div>
{urgent.length > 0 && (
<div className="card p-4 border-warn/30 bg-warn/5 space-y-2">
<p className="text-xs font-semibold text-warn"> În următoarele 14 zile</p>
{urgent.map((e) => {
const d = daysUntil(e.targetDate);
return (
<div key={e.id} className="flex items-center justify-between">
<p className="text-sm text-ink">{EVENT_TYPES[eventType(e)] ?? '📅'} {e.title}</p>
<span className={`text-xs font-bold shrink-0 ${d !== null && d <= 3 ? 'text-signal-danger' : 'text-warn'}`}>
{d === 0 ? 'Azi!' : d === 1 ? 'Mâine' : `${d} zile`}
</span>
</div>
);
})}
</div>
)}
{showAdd && (
<div className="card p-5 space-y-3">
<p className="text-sm font-semibold text-ink">Eveniment nou</p>
<input placeholder="Titlu eveniment*" value={form.title}
onChange={(e) => setForm((p) => ({ ...p, title: 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" />
<div className="grid gap-3 sm:grid-cols-2">
<select value={form.type} onChange={(e) => setForm((p) => ({ ...p, type: 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">
{Object.entries(EVENT_TYPES).filter(([k]) => !['eveniment','event'].includes(k)).map(([k, icon]) => (
<option key={k} value={k}>{icon} {k}</option>
))}
</select>
<input type="date" value={form.date}
onChange={(e) => setForm((p) => ({ ...p, date: 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" />
<input placeholder="Locație (ex: Berlin sau Online)" value={form.location}
onChange={(e) => setForm((p) => ({ ...p, location: 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" />
<input placeholder="URL eveniment (opțional)" value={form.url}
onChange={(e) => setForm((p) => ({ ...p, url: e.target.value }))}
className="rounded-lg border bg-background px-3 py-2 text-sm font-mono text-xs text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<label className="flex items-center gap-2 text-sm text-ink cursor-pointer">
<input type="checkbox" checked={form.prep} onChange={(e) => setForm((p) => ({ ...p, prep: e.target.checked }))}
className="rounded" />
Necesită pregătire / prezentare
</label>
<div className="flex gap-2">
<button onClick={() => addMut.mutate()} disabled={!form.title || addMut.isPending}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-50">
{addMut.isPending ? '…' : 'Adaugă'}
</button>
<button onClick={() => setShowAdd(false)} className="rounded-lg border px-4 py-2 text-sm text-ink">Anulează</button>
</div>
</div>
)}
{/* Tabs */}
<div className="flex gap-1 border-b border-border/50">
{(['upcoming', 'past'] as const).map((t) => (
<button key={t} onClick={() => setTab(t)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${tab === t ? 'border-primary text-primary' : 'border-transparent text-ink-faint hover:text-ink'}`}>
{t === 'upcoming' ? `Viitoare (${upcoming.length})` : `Trecute (${past.length})`}
</button>
))}
</div>
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-8">Se încarcă</div>
) : displayList.length === 0 ? (
<div className="card p-8 text-center space-y-2">
<p className="text-3xl">🗓</p>
<p className="text-sm text-ink-faint">{tab === 'upcoming' ? 'Niciun eveniment viitor.' : 'Niciun eveniment trecut.'}</p>
</div>
) : (
<div className="card divide-y divide-border/50">
{displayList.map((e) => {
const d = daysUntil(e.targetDate);
const icon = EVENT_TYPES[eventType(e)] ?? '📅';
const needsPrep = e.tags.includes('prep-needed');
const [loc, url] = (e.description ?? '').split('\n');
return (
<div key={e.id} className="flex items-start gap-3 p-4">
<span className="text-xl shrink-0 mt-0.5">{icon}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-ink">{e.title}</p>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
{e.targetDate && <p className="text-[10px] text-ink-faint">📅 {new Date(e.targetDate).toLocaleDateString('ro-RO', { weekday:'short', day:'numeric', month:'short' })}</p>}
{loc?.startsWith('📍') && <p className="text-[10px] text-ink-faint">{loc}</p>}
{needsPrep && <span className="text-[10px] text-warn font-medium">🛠 Prep needed</span>}
</div>
{url?.startsWith('🔗') && (
<p className="text-[10px] text-primary truncate mt-0.5">{url.replace('🔗 ', '')}</p>
)}
</div>
<div className="text-right shrink-0 space-y-1">
{d !== null && tab === 'upcoming' && (
<p className={`text-xs font-bold ${d <= 3 ? 'text-signal-danger' : d <= 14 ? 'text-warn' : 'text-ink-faint'}`}>
{d === 0 ? 'Azi' : d === 1 ? 'Mâine' : `${d}z`}
</p>
)}
{tab === 'past' && e.status !== 'completed' && (
<button onClick={() => patchMut.mutate({ id: e.id, status: 'completed' })}
className="rounded border px-2 py-0.5 text-[10px] text-signal-ok border-signal-ok/30 hover:bg-signal-ok/10">
Participat
</button>
)}
{tab === 'past' && e.status === 'completed' && (
<span className="text-[10px] text-signal-ok"> Participat</span>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
}