- Migrate due_date/next_occurrence columns from TIMESTAMPTZ to DATE - Update serializeRow() to distinguish DATE vs TIMESTAMPTZ serialization - Simplify frontend date parsing (no more timezone workarounds) - Add Vitest + Testing Library test infrastructure - Add initial date parsing/formatting unit tests - Update package.json with dev dependencies (vitest, testing-library, jsdom)
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useState, useCallback, type ReactNode, type Dispatch, type SetStateAction } from 'react';
|
|
|
|
export type ViewType = 'list' | 'kanban' | 'calendar';
|
|
|
|
export interface Task {
|
|
id: string;
|
|
projectId: string | null;
|
|
title: string;
|
|
description: string;
|
|
completed: boolean;
|
|
priority: 'low' | 'medium' | 'high' | 'urgent';
|
|
dueDate: string | null;
|
|
status: 'todo' | 'in_progress' | 'done';
|
|
parentTaskId: string | null;
|
|
recurrenceRule: 'none' | 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly';
|
|
recurrenceInterval: number;
|
|
nextOccurrence: string | null;
|
|
sortOrder: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface Project {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
color: string;
|
|
sortOrder: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface AppState {
|
|
view: ViewType;
|
|
selectedProject: string | null;
|
|
selectedTask: Task | null;
|
|
searchQuery: string;
|
|
sidebarOpen: boolean;
|
|
showNewTaskModal: boolean;
|
|
showEditTaskModal: boolean;
|
|
showNewProjectModal: boolean;
|
|
showRecurrenceModal: boolean;
|
|
editingTask: Task | null;
|
|
filterStatus: string;
|
|
filterPriority: string;
|
|
filterDueBefore: string;
|
|
filterDueAfter: string;
|
|
filterCompleted: string;
|
|
tasks: Task[];
|
|
projects: Project[];
|
|
setView: (view: ViewType) => void;
|
|
setSelectedProject: (project: string | null) => void;
|
|
setSelectedTask: (task: Task | null) => void;
|
|
setSearchQuery: (query: string) => void;
|
|
setSidebarOpen: (open: boolean) => void;
|
|
setShowNewTaskModal: (show: boolean) => void;
|
|
setShowEditTaskModal: (show: boolean) => void;
|
|
setShowNewProjectModal: (show: boolean) => void;
|
|
setShowRecurrenceModal: (show: boolean) => void;
|
|
setEditingTask: (task: Task | null) => void;
|
|
setFilterStatus: (status: string) => void;
|
|
setFilterPriority: (priority: string) => void;
|
|
setFilterDueBefore: (date: string) => void;
|
|
setFilterDueAfter: (date: string) => void;
|
|
setFilterCompleted: (completed: string) => void;
|
|
setTasks: Dispatch<SetStateAction<Task[]>>;
|
|
setProjects: Dispatch<SetStateAction<Project[]>>;
|
|
refreshTasks: () => Promise<void>;
|
|
refreshProjects: () => Promise<void>;
|
|
}
|
|
|
|
const AppContext = createContext<AppState | null>(null);
|
|
|
|
export function useApp() {
|
|
const context = useContext(AppContext);
|
|
if (!context) throw new Error('useApp must be used within AppProvider');
|
|
return context;
|
|
}
|
|
|
|
export default function AppProvider({ children }: { children: ReactNode }) {
|
|
const [view, setView] = useState<ViewType>('list');
|
|
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
const [showNewTaskModal, setShowNewTaskModal] = useState(false);
|
|
const [showEditTaskModal, setShowEditTaskModal] = useState(false);
|
|
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
|
|
const [showRecurrenceModal, setShowRecurrenceModal] = useState(false);
|
|
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
|
const [filterStatus, setFilterStatus] = useState('');
|
|
const [filterPriority, setFilterPriority] = useState('');
|
|
const [filterDueBefore, setFilterDueBefore] = useState('');
|
|
const [filterDueAfter, setFilterDueAfter] = useState('');
|
|
const [filterCompleted, setFilterCompleted] = useState('');
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
const refreshTasks = useCallback(async () => {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (selectedProject) params.set('project', selectedProject);
|
|
if (searchQuery) params.set('search', searchQuery);
|
|
if (filterStatus) params.set('status', filterStatus);
|
|
if (filterPriority) params.set('priority', filterPriority);
|
|
if (filterDueBefore) params.set('due_before', filterDueBefore);
|
|
if (filterDueAfter) params.set('due_after', filterDueAfter);
|
|
if (filterCompleted) params.set('completed', filterCompleted);
|
|
|
|
const res = await fetch(`/api/tasks?${params}`);
|
|
const data = await res.json();
|
|
setTasks(data);
|
|
} catch (error) {
|
|
console.error('Failed to refresh tasks:', error);
|
|
}
|
|
}, [selectedProject, searchQuery, filterStatus, filterPriority, filterDueBefore, filterDueAfter, filterCompleted]);
|
|
|
|
const refreshProjects = useCallback(async () => {
|
|
try {
|
|
const res = await fetch('/api/projects');
|
|
const data = await res.json();
|
|
setProjects(data);
|
|
} catch (error) {
|
|
console.error('Failed to refresh projects:', error);
|
|
}
|
|
}, []);
|
|
|
|
const value: AppState = {
|
|
view, selectedProject, selectedTask, searchQuery, sidebarOpen,
|
|
showNewTaskModal, showEditTaskModal, showNewProjectModal, showRecurrenceModal,
|
|
editingTask, filterStatus, filterPriority, filterDueBefore, filterDueAfter, filterCompleted,
|
|
tasks, projects,
|
|
setView, setSelectedProject, setSelectedTask, setSearchQuery, setSidebarOpen,
|
|
setShowNewTaskModal, setShowEditTaskModal, setShowNewProjectModal, setShowRecurrenceModal,
|
|
setEditingTask, setFilterStatus, setFilterPriority, setFilterDueBefore, setFilterDueAfter,
|
|
setFilterCompleted, setTasks, setProjects,
|
|
refreshTasks, refreshProjects,
|
|
};
|
|
|
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
}
|