227 lines
12 KiB
TypeScript
227 lines
12 KiB
TypeScript
'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[]; assignedTo: string | null;
|
|
description: string | null; createdAt: string;
|
|
}
|
|
|
|
const PRIORITY_DOT: Record<string, string> = {
|
|
critical: 'bg-signal-danger', high: 'bg-warn',
|
|
medium: 'bg-primary', low: 'bg-muted-foreground',
|
|
};
|
|
|
|
const ROUTINE_TAGS = ['routine', 'daily', 'weekly', 'monthly', 'recurring', 'habit'];
|
|
|
|
function isRoutine(task: Task) {
|
|
return task.tags.some((t) => ROUTINE_TAGS.includes(t.toLowerCase()));
|
|
}
|
|
|
|
const TODAY_LABEL: Record<string, string> = {
|
|
Mon: 'Lu', Tue: 'Ma', Wed: 'Mi', Thu: 'Jo', Fri: 'Vi', Sat: 'Sâ', Sun: 'Du',
|
|
};
|
|
|
|
export default function RoutinesPage() {
|
|
const { activeTenant } = useSession();
|
|
const tenantId = activeTenant?.tenantId ?? '';
|
|
const qc = useQueryClient();
|
|
|
|
const [tab, setTab] = useState<'routines' | 'all'>('routines');
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [form, setForm] = useState({ title: '', priority: 'medium', dueDate: '', tags: 'routine,daily', description: '' });
|
|
|
|
const { data: tasks = [], isLoading } = useQuery({
|
|
queryKey: ['tasks-routines', tenantId],
|
|
queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=200', { tenantId }),
|
|
enabled: Boolean(tenantId), staleTime: 30_000,
|
|
});
|
|
|
|
const completeMut = useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiFetch<Task>(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status: 'completed' } }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tasks-routines', tenantId] }),
|
|
});
|
|
|
|
const createMut = useMutation({
|
|
mutationFn: (body: Record<string, string | string[]>) =>
|
|
apiFetch<Task>('/v1/tasks', { tenantId, method: 'POST', body }),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['tasks-routines', tenantId] });
|
|
setShowCreate(false);
|
|
setForm({ title: '', priority: 'medium', dueDate: '', tags: 'routine,daily', description: '' });
|
|
},
|
|
});
|
|
|
|
const routines = tasks.filter(isRoutine);
|
|
const active = routines.filter((t) => t.status !== 'completed' && t.status !== 'cancelled');
|
|
const completed = routines.filter((t) => t.status === 'completed');
|
|
|
|
const today = new Date().toLocaleDateString('en-US', { weekday: 'short' });
|
|
const todayLabel = TODAY_LABEL[today] ?? today;
|
|
|
|
// All active tasks for the "all tasks" view
|
|
const allActive = tasks.filter((t) => t.status !== 'completed' && t.status !== 'cancelled');
|
|
const overdueAll = allActive.filter((t) => t.dueDate && new Date(t.dueDate) < new Date());
|
|
|
|
return (
|
|
<div className="max-w-3xl space-y-6 p-6">
|
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
<div>
|
|
<h1 className="font-display text-2xl font-semibold text-ink">Tasks & Rutine</h1>
|
|
<p className="text-sm text-ink-faint mt-1">
|
|
{active.length} rutine active · {todayLabel} · {allActive.length} taskuri totale
|
|
{overdueAll.length > 0 && <span className="ml-1 text-signal-danger font-semibold">{overdueAll.length} depășite</span>}
|
|
</p>
|
|
</div>
|
|
<button onClick={() => setShowCreate(!showCreate)}
|
|
className="btn btn-primary px-4 py-2 text-sm rounded-lg">
|
|
+ Task nou
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-1 border-b border-border/50">
|
|
<button onClick={() => setTab('routines')}
|
|
className={`pb-2 px-3 text-sm font-medium border-b-2 transition-colors ${tab === 'routines' ? 'border-primary text-ink' : 'border-transparent text-ink-faint hover:text-ink'}`}>
|
|
Rutine ({routines.length})
|
|
</button>
|
|
<button onClick={() => setTab('all')}
|
|
className={`pb-2 px-3 text-sm font-medium border-b-2 transition-colors ${tab === 'all' ? 'border-primary text-ink' : 'border-transparent text-ink-faint hover:text-ink'}`}>
|
|
Toate ({tasks.length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* Create form */}
|
|
{showCreate && (
|
|
<div className="card p-5 space-y-3">
|
|
<h2 className="text-sm font-semibold text-ink">Task / rutină nouă</h2>
|
|
<input placeholder="Titlu *" value={form.title}
|
|
onChange={(e) => 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" />
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: 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">
|
|
{['low','medium','high','critical'].map((p) => <option key={p}>{p}</option>)}
|
|
</select>
|
|
<input type="date" value={form.dueDate}
|
|
onChange={(e) => 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" />
|
|
</div>
|
|
<input placeholder="Taguri (virgulă) — ex: routine,daily" value={form.tags}
|
|
onChange={(e) => setForm({ ...form, tags: 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" />
|
|
<input placeholder="Descriere (opțional)" value={form.description}
|
|
onChange={(e) => 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" />
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => createMut.mutate({ title: form.title, priority: form.priority, dueDate: form.dueDate || undefined, tags: form.tags.split(',').map(t => t.trim()).filter(Boolean), description: form.description || undefined } as Record<string,string|string[]>)}
|
|
disabled={!form.title || createMut.isPending}
|
|
className="btn btn-primary px-4 py-1.5 text-sm rounded-lg disabled:opacity-50">
|
|
{createMut.isPending ? 'Se creează…' : 'Salvează'}
|
|
</button>
|
|
<button onClick={() => setShowCreate(false)} className="text-sm text-ink-faint hover:text-ink px-2">Anulează</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className="text-center text-sm text-ink-faint py-8">Se încarcă…</div>
|
|
) : tab === 'routines' ? (
|
|
<>
|
|
{active.length === 0 && (
|
|
<div className="card p-8 text-center space-y-2">
|
|
<p className="text-2xl">🔄</p>
|
|
<p className="text-sm text-ink-faint">Nicio rutină activă. Adaugă un task cu tag-ul "routine" sau "daily".</p>
|
|
</div>
|
|
)}
|
|
{active.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className="text-[10px] font-semibold uppercase tracking-wider text-ink-faint">Active ({active.length})</p>
|
|
<div className="card divide-y divide-border/50">
|
|
{active.map((t) => {
|
|
const overdue = t.dueDate && new Date(t.dueDate) < new Date();
|
|
return (
|
|
<div key={t.id} className="flex items-center gap-3 px-4 py-3">
|
|
<button onClick={() => completeMut.mutate(t.id)}
|
|
className="w-5 h-5 shrink-0 rounded-full border-2 border-border hover:border-signal-ok hover:bg-signal-ok/10 transition-colors" />
|
|
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${PRIORITY_DOT[t.priority] ?? 'bg-muted'}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-ink">{t.title}</p>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{t.tags.map((tag) => (
|
|
<span key={tag} className="text-[9px] rounded-full bg-primary/10 px-1.5 py-0.5 text-ink">{tag}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{t.dueDate && (
|
|
<span className={`text-[10px] shrink-0 ${overdue ? 'text-signal-danger font-semibold' : 'text-ink-faint'}`}>
|
|
{new Date(t.dueDate).toLocaleDateString('ro-RO', { day: '2-digit', month: 'short' })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{completed.length > 0 && (
|
|
<div className="space-y-2 opacity-60">
|
|
<p className="text-[10px] font-semibold uppercase tracking-wider text-ink-faint">Completate recent ({completed.length})</p>
|
|
<div className="card divide-y divide-border/50">
|
|
{completed.slice(0, 5).map((t) => (
|
|
<div key={t.id} className="flex items-center gap-3 px-4 py-2.5">
|
|
<div className="w-5 h-5 shrink-0 rounded-full bg-signal-ok/20 flex items-center justify-center text-[10px] text-signal-ok">✓</div>
|
|
<p className="text-sm text-ink-faint line-through">{t.title}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
/* All tasks view */
|
|
<div className="space-y-3">
|
|
{overdueAll.length > 0 && (
|
|
<div className="space-y-1">
|
|
<p className="text-[10px] font-semibold uppercase tracking-wider text-signal-danger">Depășite ({overdueAll.length})</p>
|
|
<div className="card divide-y divide-border/50 border-signal-danger/20">
|
|
{overdueAll.map((t) => (
|
|
<div key={t.id} className="flex items-center gap-3 px-4 py-3">
|
|
<button onClick={() => completeMut.mutate(t.id)}
|
|
className="w-5 h-5 shrink-0 rounded-full border-2 border-signal-danger/50 hover:bg-signal-ok/10 transition-colors" />
|
|
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${PRIORITY_DOT[t.priority] ?? 'bg-muted'}`} />
|
|
<p className="flex-1 text-sm text-ink">{t.title}</p>
|
|
<span className="text-[10px] text-signal-danger font-semibold shrink-0">
|
|
{new Date(t.dueDate!).toLocaleDateString('ro-RO', { day: '2-digit', month: 'short' })}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="card divide-y divide-border/50">
|
|
{allActive.filter((t) => !overdueAll.includes(t)).map((t) => (
|
|
<div key={t.id} className="flex items-center gap-3 px-4 py-3">
|
|
<button onClick={() => completeMut.mutate(t.id)}
|
|
className="w-5 h-5 shrink-0 rounded-full border-2 border-border hover:border-signal-ok hover:bg-signal-ok/10 transition-colors" />
|
|
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${PRIORITY_DOT[t.priority] ?? 'bg-muted'}`} />
|
|
<p className="flex-1 text-sm text-ink">{t.title}</p>
|
|
{t.dueDate && (
|
|
<span className="text-[10px] text-ink-faint shrink-0">
|
|
{new Date(t.dueDate).toLocaleDateString('ro-RO', { day: '2-digit', month: 'short' })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|