Files
task-team/apps/tasks/app/calendar/page.tsx
Admin b4b8439f80 Feature batch: Projects, Recurrence, Group settings, Bug fixes
- Projects CRUD API + invite members
- Task recurrence (daily/weekly/monthly) with auto-creation
- Group time zones + GPS locations settings
- i18n fallback fix (no more undefined labels)
- UX: action buttons in one row
- Chat/Calendar: relative API URLs
- DB: task_assignments, projects tables, recurrence column

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:49:25 +00:00

107 lines
3.0 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import { useTranslation } from '@/lib/i18n';
import { useAuth } from '@/lib/auth';
import { useRouter } from 'next/navigation';
interface CalTask {
id: string;
title: string;
scheduled_at: string | null;
due_at: string | null;
status: string;
group_name: string;
group_color: string;
}
const LOCALE_MAP: Record<string, string> = {
cs: 'cs',
he: 'he',
ru: 'ru',
ua: 'uk',
};
export default function CalendarPage() {
const [tasks, setTasks] = useState<CalTask[]>([]);
const [error, setError] = useState<string | null>(null);
const { t, locale } = useTranslation();
const { token } = useAuth();
const router = useRouter();
useEffect(() => {
if (!token) {
router.replace('/login');
return;
}
fetch('/api/v1/tasks?limit=100', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
})
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(d => setTasks(d.data || []))
.catch(err => setError(err.message));
}, [token, router]);
const events = tasks
.filter((tk) => tk.scheduled_at !== null || tk.due_at !== null)
.map(tk => ({
id: tk.id,
title: tk.title,
start: (tk.scheduled_at || tk.due_at) as string,
end: (tk.due_at || tk.scheduled_at) as string,
backgroundColor: tk.group_color || '#3B82F6',
borderColor: tk.group_color || '#3B82F6',
extendedProps: { status: tk.status, group: tk.group_name },
}));
// Build background events from unique groups
const groupColors = new Map<string, string>();
tasks.forEach(tk => {
if (tk.group_name && tk.group_color) {
groupColors.set(tk.group_name, tk.group_color);
}
});
if (!token) return null;
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">{t('calendar.title')}</h1>
{error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
{error}
</div>
)}
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="timeGridWeek"
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay',
}}
events={events}
editable={true}
selectable={true}
locale={LOCALE_MAP[locale] || 'cs'}
direction={locale === 'he' ? 'rtl' : 'ltr'}
firstDay={1}
height="auto"
slotMinTime="06:00:00"
slotMaxTime="23:00:00"
/>
</div>
</div>
);
}