- {t('about.version', 'Version')} {version} -
-+ {t('about.version', 'Version')} {version} +
+- {t('about.description', 'Self-hosted task management with hierarchical organization, multi-language support, and Telegram integration. Built with love for productivity enthusiasts.')} -
-+ {t( + 'about.description', + 'Self-hosted task management with hierarchical organization, multi-language support, and Telegram integration. Built with love for productivity enthusiasts.' + )} +
+- {t('about.appreciation', 'Thank you for using tududi! Your support helps keep this project alive and growing. If you find it useful, consider supporting the development.')} -
-+ {t( + 'about.appreciation', + 'Thank you for using tududi! Your support helps keep this project alive and growing. If you find it useful, consider supporting the development.' + )} +
+- {t('about.builtBy', 'Built by')} Chris Veleris -
+ {/* Footer */} ++ {t('about.builtBy', 'Built by')}{' '} + + Chris Veleris + +
++ {area?.description} +
+ + {t('areas.viewProjects', { name: area?.name })} + +{area?.description}
- - {t('areas.viewProjects', { name: area?.name })} - -{t('areas.noAreasFound')}
- ) : ( -- {area.description} + {/* Areas List */} + {areas.length === 0 ? ( +
+ {t('areas.noAreasFound')}
- )} -+ {area.description} +
+ )} +- {isDemoMode - ? 'Demo mode: Google Calendar integration simulated for testing purposes.' - : t('calendar.googleDescription') + // Handle different API response formats + let tasks; + if (Array.isArray(data)) { + tasks = data; + } else if (data && Array.isArray(data.tasks)) { + tasks = data.tasks; + } else if (data && data.data && Array.isArray(data.data)) { + tasks = data.data; + } else { + console.error('Unexpected API response format:', data); + tasks = []; } -
-
- {t('calendar.googleStatus')}:
- {googleStatus.connected ? (
-
- {t('calendar.connected')}
- {googleStatus.email && ` (${googleStatus.email})`}
-
- ) : (
- {t('calendar.notConnected')}
+
+ // Store the original tasks for later reference
+ setAllTasks(tasks);
+
+ const taskEvents = convertTasksToEvents(tasks);
+ setEvents(taskEvents);
+ } else {
+ console.error('Failed to load tasks, status:', response.status);
+ }
+ } catch (error) {
+ console.error('Error loading tasks:', error);
+ } finally {
+ setIsLoadingTasks(false);
+ }
+ };
+
+ const convertTasksToEvents = (tasks: any[]): CalendarEvent[] => {
+ const taskEvents: CalendarEvent[] = [];
+
+ if (!Array.isArray(tasks)) {
+ console.error('convertTasksToEvents received non-array:', tasks);
+ return [];
+ }
+
+ tasks.forEach((task) => {
+ // Add tasks with due dates
+ if (task.due_date) {
+ const dueDate = new Date(task.due_date);
+ const taskEvent = {
+ id: `task-${task.id}`,
+ title: task.name || task.title || `Task ${task.id}`,
+ start: dueDate,
+ end: new Date(dueDate.getTime() + 60 * 60 * 1000), // 1 hour duration
+ type: 'task' as const,
+ color: task.completed_at ? '#22c55e' : '#ef4444', // Green if completed, red if not
+ };
+ taskEvents.push(taskEvent);
+ }
+
+ // Add tasks scheduled for today (if they don't have due_date)
+ if (!task.due_date && task.created_at) {
+ const createdDate = new Date(task.created_at);
+ const today = new Date();
+
+ // Show tasks created today on the calendar
+ if (createdDate.toDateString() === today.toDateString()) {
+ const taskEvent = {
+ id: `task-created-${task.id}`,
+ title: `📝 ${task.name || task.title || `Task ${task.id}`}`,
+ start: createdDate,
+ end: new Date(createdDate.getTime() + 30 * 60 * 1000), // 30 min duration
+ type: 'task' as const,
+ color: task.completed_at ? '#22c55e' : '#3b82f6', // Green if completed, blue if not
+ };
+ taskEvents.push(taskEvent);
+ }
+ }
+
+ // Always add tasks to calendar for easier debugging
+ if (!task.due_date && !task.created_at) {
+ const taskEvent = {
+ id: `task-fallback-${task.id}`,
+ title: `📌 ${task.name || task.title || `Task ${task.id}`}`,
+ start: new Date(), // Today
+ end: new Date(Date.now() + 30 * 60 * 1000), // 30 min duration
+ type: 'task' as const,
+ color: task.completed_at ? '#22c55e' : '#8b5cf6', // Green if completed, purple if not
+ };
+ taskEvents.push(taskEvent);
+ }
+ });
+
+ return taskEvents;
+ };
+
+ const loadProjects = async () => {
+ try {
+ const response = await fetch('/api/projects', {
+ credentials: 'include',
+ });
+ if (response.ok) {
+ const projectsData = await response.json();
+ setProjects(Array.isArray(projectsData) ? projectsData : []);
+ }
+ } catch (error) {
+ console.error('Error loading projects:', error);
+ }
+ };
+
+ const connectGoogleCalendar = async () => {
+ if (isConnecting) return;
+
+ setIsConnecting(true);
+ try {
+ const response = await fetch('/api/calendar/auth', {
+ credentials: 'include',
+ });
+ if (response.ok) {
+ const result = await response.json();
+ if (result.demo) {
+ // Demo mode - simulate connection
+ setGoogleStatus({
+ connected: true,
+ email: 'demo@example.com',
+ });
+ setIsDemoMode(true);
+ } else {
+ // Real Google OAuth - redirect to auth URL
+ window.location.href = result.authUrl;
+ }
+ } else {
+ throw new Error('Failed to get authorization URL');
+ }
+ } catch (error) {
+ console.error('Error connecting to Google Calendar:', error);
+ alert(t('calendar.connectionError'));
+ } finally {
+ setIsConnecting(false);
+ }
+ };
+
+ const disconnectGoogleCalendar = async () => {
+ try {
+ if (isDemoMode) {
+ // Demo mode - just update local state
+ setGoogleStatus({ connected: false });
+ setIsDemoMode(false);
+ return;
+ }
+
+ // Real disconnect API call
+ const response = await fetch('/api/calendar/disconnect', {
+ method: 'POST',
+ credentials: 'include',
+ });
+ if (response.ok) {
+ setGoogleStatus({ connected: false });
+ } else {
+ throw new Error('Failed to disconnect');
+ }
+ } catch (error) {
+ console.error('Error disconnecting Google Calendar:', error);
+ alert(t('calendar.disconnectionError'));
+ }
+ };
+
+ const navigate = (direction: 'prev' | 'next') => {
+ setCurrentDate((prev) => {
+ if (view === 'month') {
+ const newDate = new Date(prev);
+ if (direction === 'prev') {
+ newDate.setMonth(prev.getMonth() - 1);
+ } else {
+ newDate.setMonth(prev.getMonth() + 1);
+ }
+ return newDate;
+ } else if (view === 'week') {
+ return direction === 'prev'
+ ? addWeeks(prev, -1)
+ : addWeeks(prev, 1);
+ } else {
+ // day
+ return direction === 'prev'
+ ? addDays(prev, -1)
+ : addDays(prev, 1);
+ }
+ });
+ };
+
+ const goToToday = () => {
+ setCurrentDate(new Date());
+ };
+
+ const handleDateClick = () => {
+ // Date click handler - can be used for future functionality
+ };
+
+ const handleEventClick = (event: CalendarEvent) => {
+ // Handle task events
+ if (event.type === 'task') {
+ // Extract task ID from event ID
+ const taskId = event.id.replace(/^task(-created|-fallback)?-/, '');
+ const task = allTasks.find((t) => t.id.toString() === taskId);
+
+ if (task) {
+ // Convert task to proper Task entity format for TaskModal
+ const taskEntity: Task = {
+ ...task,
+ name: task.name || task.title || `Task ${task.id}`,
+ // Ensure all required Task properties are present
+ priority: task.priority || 'medium',
+ status: task.status || 'not_started',
+ tags: task.tags || [],
+ note: task.note || task.description || '',
+ due_date: task.due_date,
+ created_at: task.created_at,
+ completed_at: task.completed_at,
+ project_id: task.project_id,
+ };
+
+ setSelectedTask(taskEntity);
+ setIsEventDetailModalOpen(true);
+ }
+ }
+ };
+
+ const handleTimeSlotClick = () => {
+ // Time slot click handler - can be used for future functionality
+ };
+
+ const handleEditTask = () => {
+ setIsEventDetailModalOpen(false);
+ setIsTaskModalOpen(true);
+ };
+
+ const handleTaskSave = (updatedTask: Task) => {
+ // Update the task in allTasks
+ setAllTasks((prev) =>
+ prev.map((t) => (t.id === updatedTask.id ? updatedTask : t))
+ );
+ // Refresh calendar
+ loadTasks();
+ // Close modal
+ setIsTaskModalOpen(false);
+ setSelectedTask(null);
+ };
+
+ const handleTaskDelete = async (taskId: number) => {
+ try {
+ await deleteTask(taskId);
+ // Remove task from allTasks
+ setAllTasks((prev) => prev.filter((t) => t.id !== taskId));
+ // Refresh calendar
+ loadTasks();
+ // Close modal
+ setIsTaskModalOpen(false);
+ setSelectedTask(null);
+ } catch (error) {
+ console.error('Failed to delete task:', error);
+ }
+ };
+
+ const handleCreateProject = async (name: string): Promise
+ {isDemoMode
+ ? 'Demo mode: Google Calendar integration simulated for testing purposes.'
+ : t('calendar.googleDescription')}
+
+ {t('calendar.googleStatus')}:
+ {googleStatus.connected ? (
+
+ {t('calendar.connected')}
+ {googleStatus.email &&
+ ` (${googleStatus.email})`}
+
+ ) : (
+
+ {t('calendar.notConnected')}
+
+ )}
+
+
+
+ {format(currentDate, 'MMMM yyyy', { locale })}
+
+
+ {t('calendar.googleIntegration')}
+
+
+ {task.name || `Task ${task.id}`} +
++ {format(new Date(task.due_date), 'PPP', { + locale: locale, + })} +
++ {task.Project.name} +
++ {task.note} +
++ {format(new Date(task.created_at), 'PPp', { + locale: locale, + })} +
+- {task.name || `Task ${task.id}`} -
-- {format(new Date(task.due_date), 'PPP', { locale: locale })} -
-- {task.Project.name} -
-- {task.note} -
-- {format(new Date(task.created_at), 'PPp', { locale: locale })} -
-- {item.content} -
- - {/* Tags display */} - {hashtags.length > 0 && ( -+ {item.content} +
+ + {/* Tags display */} + {hashtags.length > 0 && ( +{t('inbox.emptyDescription')}
-- {t('taskViews.inbox', 'Inbox is where all uncategorized tasks are located. Tasks that have not been assigned to a project or don\'t have a due date will appear here. This is your \'brain dump\' area where you can quickly note down tasks and organize them later.')} -
- -{t('inbox.emptyDescription')}
++ {t( + 'taskViews.inbox', + "Inbox is where all uncategorized tasks are located. Tasks that have not been assigned to a project or don't have a due date will appear here. This is your 'brain dump' area where you can quickly note down tasks and organize them later." + )} +
+ +