diff --git a/package-lock.json b/package-lock.json index 60fb075..a41a8ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@mui/icons-material": "^6.4.1", "@mui/material": "^6.4.1", "@mui/x-date-pickers": "^7.24.0", + "@swc/core": "^1.10.11", "@zl-asica/react": "^0.3.10", "dayjs": "^1.11.13", "es-toolkit": "^1.29.0", @@ -3364,10 +3365,9 @@ } }, "node_modules/@swc/core": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.6.tgz", - "integrity": "sha512-zgXXsI6SAVwr6XsXyMnqlyLoa1lT+r09bAWI1xT3679ejWqI1Vnl14eJG0GjWYXCEMKHCNytfMq3OOQ62C39QQ==", - "dev": true, + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.11.tgz", + "integrity": "sha512-3zGU5y3S20cAwot9ZcsxVFNsSVaptG+dKdmAxORSE3EX7ixe1Xn5kUwLlgIsM4qrwTUWCJDLNhRS+2HLFivcDg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3382,16 +3382,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.6", - "@swc/core-darwin-x64": "1.10.6", - "@swc/core-linux-arm-gnueabihf": "1.10.6", - "@swc/core-linux-arm64-gnu": "1.10.6", - "@swc/core-linux-arm64-musl": "1.10.6", - "@swc/core-linux-x64-gnu": "1.10.6", - "@swc/core-linux-x64-musl": "1.10.6", - "@swc/core-win32-arm64-msvc": "1.10.6", - "@swc/core-win32-ia32-msvc": "1.10.6", - "@swc/core-win32-x64-msvc": "1.10.6" + "@swc/core-darwin-arm64": "1.10.11", + "@swc/core-darwin-x64": "1.10.11", + "@swc/core-linux-arm-gnueabihf": "1.10.11", + "@swc/core-linux-arm64-gnu": "1.10.11", + "@swc/core-linux-arm64-musl": "1.10.11", + "@swc/core-linux-x64-gnu": "1.10.11", + "@swc/core-linux-x64-musl": "1.10.11", + "@swc/core-win32-arm64-msvc": "1.10.11", + "@swc/core-win32-ia32-msvc": "1.10.11", + "@swc/core-win32-x64-msvc": "1.10.11" }, "peerDependencies": { "@swc/helpers": "*" @@ -3403,13 +3403,12 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.6.tgz", - "integrity": "sha512-USbMvT8Rw5PvIfF6HyTm+yW84J9c45emzmHBDIWY76vZHkFsS5MepNi+JLQyBzBBgE7ScwBRBNhRx6VNhkSoww==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.11.tgz", + "integrity": "sha512-ZpgEaNcx2e5D+Pd0yZGVbpSrEDOEubn7r2JXoNBf0O85lPjUm3HDzGRfLlV/MwxRPAkwm93eLP4l7gYnc50l3g==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3420,13 +3419,12 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.6.tgz", - "integrity": "sha512-7t2IozcZN4r1p27ei+Kb8IjN4aLoBDn107fPi+aPLcVp2uFgJEUzhCDuZXBNW2057Mx1OHcjzrkaleRpECz3Xg==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.11.tgz", + "integrity": "sha512-szObinnq2o7spXMDU5pdunmUeLrfV67Q77rV+DyojAiGJI1RSbEQotLOk+ONOLpoapwGUxOijFG4IuX1xiwQ2g==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3437,13 +3435,12 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.6.tgz", - "integrity": "sha512-CPgWT+D0bDp/qhXsLkIJ54LmKU1/zvyGaf/yz8A4iR+YoF6R5CSXENXhNJY8cIrb6+uNWJZzHJ+gefB5V51bpA==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.11.tgz", + "integrity": "sha512-tVE8aXQwd8JUB9fOGLawFJa76nrpvp3dvErjozMmWSKWqtoeO7HV83aOrVtc8G66cj4Vq7FjTE9pOJeV1FbKRw==", "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3454,13 +3451,12 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.6.tgz", - "integrity": "sha512-5qZ6hVnqO/ShETXdGSzvdGUVx372qydlj1YWSYiaxQzTAepEBc8TC1NVUgYtOHOKVRkky1d7p6GQ9lymsd4bHw==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.11.tgz", + "integrity": "sha512-geFkENU5GMEKO7FqHOaw9HVlpQEW10nICoM6ubFc0hXBv8dwRXU4vQbh9s/isLSFRftw1m4jEEWixAnXSw8bxQ==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3471,13 +3467,12 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.6.tgz", - "integrity": "sha512-hB2xZFmXCKf2iJF5y2z01PSuLqEoUP3jIX/XlIHN+/AIP7PkSKsValE63LnjlnWPnSEI0IxUyRE3T3FzWE/fQQ==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.11.tgz", + "integrity": "sha512-2mMscXe/ivq8c4tO3eQSbQDFBvagMJGlalXCspn0DgDImLYTEnt/8KHMUMGVfh0gMJTZ9q4FlGLo7mlnbx99MQ==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3488,13 +3483,12 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.6.tgz", - "integrity": "sha512-PRGPp0I22+oJ8RMGg8M4hXYxEffH3ayu0WoSDPOjfol1F51Wj1tfTWN4wVa2RibzJjkBwMOT0KGLGb/hSEDDXQ==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.11.tgz", + "integrity": "sha512-eu2apgDbC4xwsigpl6LS+iyw6a3mL6kB4I+6PZMbFF2nIb1Dh7RGnu70Ai6mMn1o80fTmRSKsCT3CKMfVdeNFg==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3505,13 +3499,12 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.6.tgz", - "integrity": "sha512-SoNBxlA86lnoV9vIz/TCyakLkdRhFSHx6tFMKNH8wAhz1kKYbZfDmpYoIzeQqdTh0tpx8e/Zu1zdK4smovsZqQ==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.11.tgz", + "integrity": "sha512-0n+wPWpDigwqRay4IL2JIvAqSKCXv6nKxPig9M7+epAlEQlqX+8Oq/Ap3yHtuhjNPb7HmnqNJLCXT1Wx+BZo0w==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3522,13 +3515,12 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.6.tgz", - "integrity": "sha512-6L5Y2E+FVvM+BtoA+mJFjf/SjpFr73w2kHBxINxwH8/PkjAjkePDr5m0ibQhPXV61bTwX49+1otzTY85EsUW9Q==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.11.tgz", + "integrity": "sha512-7+bMSIoqcbXKosIVd314YjckDRPneA4OpG1cb3/GrkQTEDXmWT3pFBBlJf82hzJfw7b6lfv6rDVEFBX7/PJoLA==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3539,13 +3531,12 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.6.tgz", - "integrity": "sha512-kxK3tW8DJwEkAkwy0vhwoBAShRebH1QTe0mvH9tlBQ21rToVZQn+GCV/I44dind80hYPw0Tw2JKFVfoEJyBszg==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.11.tgz", + "integrity": "sha512-6hkLl4+3KjP/OFTryWxpW7YFN+w4R689TSPwiII4fFgsFNupyEmLWWakKfkGgV2JVA59L4Oi02elHy/O1sbgtw==", "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3556,13 +3547,12 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.6.tgz", - "integrity": "sha512-4pJka/+t8XcHee12G/R5VWcilkp5poT2EJhrybpuREkpQ7iC/4WOlOVrohbWQ4AhDQmojYQI/iS+gdF2JFLzTQ==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.11.tgz", + "integrity": "sha512-kKNE2BGu/La2k2WFHovenqZvGQAHRIU+rd2/6a7D6EiQ6EyimtbhUqjCCZ+N1f5fIAnvM+sMdLiQJq4jdd/oOQ==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3576,14 +3566,12 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" diff --git a/package.json b/package.json index bceaf3d..94ffbdb 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@mui/icons-material": "^6.4.1", "@mui/material": "^6.4.1", "@mui/x-date-pickers": "^7.24.0", + "@swc/core": "^1.10.11", "@zl-asica/react": "^0.3.10", "dayjs": "^1.11.13", "es-toolkit": "^1.29.0", diff --git a/src/components/Home/Calendar/AddTaskButton/index.tsx b/src/components/Home/Calendar/AddTaskButton/index.tsx index afe41d2..03c68b8 100644 --- a/src/components/Home/Calendar/AddTaskButton/index.tsx +++ b/src/components/Home/Calendar/AddTaskButton/index.tsx @@ -8,9 +8,11 @@ import { DialogContent, DialogTitle, FormControl, + FormControlLabel, InputLabel, MenuItem, Select, + Switch, } from '@mui/material' import { generateUniqueId, useToggle } from '@zl-asica/react' import { useCallback, useEffect, useState } from 'react' @@ -20,6 +22,11 @@ interface AddTaskButtonProps { selectedDate: Dayjs | null } +interface TimeOption { + value: string + label: string +} + const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { const addTask = useScheduleStore(state => state.addTask) @@ -35,6 +42,21 @@ const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { const [endTime, setEndTime] = useState('') const [loading, setLoading] = useState(false) + // New recurring task states + const [isRecurring, setIsRecurring] = useState(false) + const [recurrenceType, setRecurrenceType] = useState<'daily' | 'weekly' | 'monthly'>('daily') + const [recurrenceInterval, setRecurrenceInterval] = useState(1) + const [recurrenceEndDate, setRecurrenceEndDate] = useState('') + + // Generate time options in 30-minute intervals + const timeOptions: TimeOption[] = Array.from({ length: 48 }, (_, i) => { + const hour = Math.floor(i / 2) + const minute = (i % 2) * 30 + const value = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}` + const label = `${hour === 0 ? 12 : hour > 12 ? hour - 12 : hour}:${minute.toString().padStart(2, '0')} ${hour >= 12 ? 'PM' : 'AM'}` + return { value, label } + }) + useEffect(() => { setDate(selectedDate?.format('YYYY-MM-DD') ?? new Date().toISOString().split('T')[0]) }, [selectedDate]) @@ -49,6 +71,10 @@ const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { setDate(selectedDate?.format('YYYY-MM-DD') ?? new Date().toISOString().split('T')[0]) setStartTime('') setEndTime('') + setIsRecurring(false) + setRecurrenceType('daily') + setRecurrenceInterval(1) + setRecurrenceEndDate('') }, [selectedDate]) const handleAddTask = useCallback(async () => { @@ -63,6 +89,14 @@ const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { date, timeRange: startTime && endTime ? { start: startTime, end: endTime } : undefined, status: 'pending' as const, + isRecurring, + ...(isRecurring && { + recurrencePattern: { + type: recurrenceType, + interval: recurrenceInterval, + ...(recurrenceEndDate && { endDate: recurrenceEndDate }), + }, + }), } try { @@ -76,7 +110,22 @@ const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { finally { setLoading(false) } - }, [title, description, category, priority, date, startTime, endTime, addTask, resetStates, toggleOpen]) + }, [ + title, + description, + category, + priority, + date, + startTime, + endTime, + isRecurring, + recurrenceType, + recurrenceInterval, + recurrenceEndDate, + addTask, + resetStates, + toggleOpen, + ]) return ( <> @@ -103,11 +152,95 @@ const AddTaskButton = ({ selectedDate }: AddTaskButtonProps) => { Add Task - - + + + Start Time + setStartTime(e.target.value)} + label="Start Time" + > + {timeOptions.map(option => ( + + {option.label} + + ))} + + + + + End Time + setEndTime(e.target.value)} + label="End Time" + error={!!startTime && !!endTime && startTime >= endTime} + > + {timeOptions.map(option => ( + + {option.label} + + ))} + + + + + setIsRecurring(e.target.checked)} + /> + )} + label="Recurring Task" + sx={{ mt: 2, mb: 1 }} + /> + + {isRecurring && ( + <> + + Repeat + setRecurrenceType(e.target.value as 'daily' | 'weekly' | 'monthly')} + > + Daily + Weekly + Monthly + + + + + Every + setRecurrenceInterval(Number(e.target.value))} + > + {[1, 2, 3, 4, 5, 6, 7].map(num => ( + + {num} + + ))} + + + + + > + )} + Category * { School + Priority * { + if (!task.isRecurring || !task.recurrencePattern) { + return [task.date] + } + + const dates: string[] = [] + const startDate = dayjs(task.date) + const endDate = task.recurrencePattern.endDate + ? dayjs(task.recurrencePattern.endDate) + // If no end date, generate for next 6 months + : dayjs().add(6, 'month') + + let currentDate = startDate + const { type, interval } = task.recurrencePattern + + while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) { + dates.push(currentDate.format('YYYY-MM-DD')) + + switch (type) { + case 'daily': + currentDate = currentDate.add(interval, 'day') + break + case 'weekly': + currentDate = currentDate.add(interval, 'week') + break + case 'monthly': + currentDate = currentDate.add(interval, 'month') + break + } + } + + return dates +} + const ServerDay = ( props: PickersDayProps & { highlightedDays?: string[] }, ) => { const { highlightedDays = defaultHighlightedDays, day, outsideCurrentMonth, ...other } = props - const isSelected - = !outsideCurrentMonth - && highlightedDays.includes(day.format('YYYY-MM-DD')) + const isSelected = !outsideCurrentMonth && highlightedDays.includes(day.format('YYYY-MM-DD')) return ( { - // Extract unique days with tasks + // Generate all dates including recurring ones const highlightedDays = useMemo(() => { - return schedule - .map((task) => { - return dayjs(task.date).format('YYYY-MM-DD') - }) - .filter(Boolean) + const allDates = schedule.flatMap(task => generateRecurringDates(task)) + + // Remove duplicates and sort + return [...new Set(allDates)].sort() }, [schedule]) return ( diff --git a/src/components/Home/Calendar/TaskDialog.tsx b/src/components/Home/Calendar/TaskDialog.tsx index 31660a9..7784059 100644 --- a/src/components/Home/Calendar/TaskDialog.tsx +++ b/src/components/Home/Calendar/TaskDialog.tsx @@ -1,3 +1,4 @@ +/* eslint-disable ts/strict-boolean-expressions */ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' import dayjs from 'dayjs' @@ -9,16 +10,26 @@ interface TaskDialogProps { const TaskDialog = ({ open, selectedTask, handleClose }: TaskDialogProps) => { let actualDateTime = dayjs(selectedTask?.date).format('MMMM D, YYYY') - if (selectedTask?.timeRange) { const [hour, minute] = selectedTask.timeRange.start.split(':').map(Number) const isPM = hour >= 12 - const formattedHour = isPM ? hour - 12 || 12 : hour || 12 // Converts 0 to 12 for AM/PM notation + const formattedHour = isPM ? hour - 12 || 12 : hour || 12 const formattedMinute = minute ? `:${minute.toString().padStart(2, '0')}` : '' - actualDateTime = `${formattedHour}${formattedMinute} ${isPM ? 'PM' : 'AM'}, ${actualDateTime}` } + const getRecurrenceText = (pattern: RecurrencePattern): string => { + const intervalText = pattern.interval === 1 ? '' : `every ${pattern.interval} ` + const typeText = `${pattern.type}${pattern.interval === 1 ? 'ly' : 's'}` + let text = `Repeats ${intervalText}${typeText}` + + if (pattern.endDate) { + text += ` until ${dayjs(pattern.endDate).format('MMMM D, YYYY')}` + } + + return text + } + return ( { {actualDateTime} + {selectedTask.isRecurring && selectedTask.recurrencePattern && ( + <> + + Recurrence: + + + {getRecurrenceText(selectedTask.recurrencePattern)} + + > + )} + {selectedTask.description !== '' && ( <> diff --git a/src/components/Home/Calendar/WeekCalendar.tsx b/src/components/Home/Calendar/WeekCalendar.tsx index 302165c..952a060 100644 --- a/src/components/Home/Calendar/WeekCalendar.tsx +++ b/src/components/Home/Calendar/WeekCalendar.tsx @@ -1,3 +1,4 @@ +/* eslint-disable ts/strict-boolean-expressions */ import type { Dayjs } from 'dayjs' import dayGridPlugin from '@fullcalendar/daygrid' import interactionPlugin from '@fullcalendar/interaction' @@ -7,7 +8,6 @@ import { Box } from '@mui/material' import { useToggle } from '@zl-asica/react' import dayjs from 'dayjs' import { useCallback, useMemo, useState } from 'react' - import TaskDialog from './TaskDialog' interface ScheduleCalendarProps { @@ -29,30 +29,67 @@ const calendarStyles = { }, } -const computeHighlightedDays = (schedule: Schedule) => { - return schedule.map((task) => { - const start = task.timeRange - ? `${task.date}T${task.timeRange.start}` - : task.date - const end = task.timeRange - ? `${task.date}T${task.timeRange.end}` - : undefined - - return { +const generateRecurringEvents = (task: Task) => { + if (!task.isRecurring || !task.recurrencePattern) { + return [{ ...task, id: task.taskId, - start, - end, + start: task.timeRange ? `${task.date}T${task.timeRange.start}` : task.date, + end: task.timeRange ? `${task.date}T${task.timeRange.end}` : undefined, + backgroundColor: '#4E2A84', + borderColor: '#4E2A84', + title: task.title, + }] + } + + const events = [] + const startDate = dayjs(task.date) + const endDate = task.recurrencePattern.endDate + ? dayjs(task.recurrencePattern.endDate) + : dayjs().add(6, 'month') // Default to 6 months if no end date + + let currentDate = startDate + const { type, interval } = task.recurrencePattern + + while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) { + const currentDateStr = currentDate.format('YYYY-MM-DD') + events.push({ + ...task, + id: `${task.taskId}-${currentDateStr}`, // Unique ID for each occurrence + start: task.timeRange ? `${currentDateStr}T${task.timeRange.start}` : currentDateStr, + end: task.timeRange ? `${currentDateStr}T${task.timeRange.end}` : undefined, backgroundColor: '#4E2A84', borderColor: '#4E2A84', + title: `${task.title} (Recurring)`, + extendedProps: { + originalTaskId: task.taskId, + isRecurringInstance: true, + }, + }) + + switch (type) { + case 'daily': + currentDate = currentDate.add(interval, 'day') + break + case 'weekly': + currentDate = currentDate.add(interval, 'week') + break + case 'monthly': + currentDate = currentDate.add(interval, 'month') + break } - }) + } + + return events +} + +const computeHighlightedDays = (schedule: Schedule) => { + return schedule.flatMap(task => generateRecurringEvents(task)) } const computeTimeRange = (schedule: Schedule, defaultStart: string, defaultEnd: string) => { let earliestTime = defaultStart let latestTime = defaultEnd - schedule.forEach((task) => { if (task.timeRange) { if (task.timeRange.start < earliestTime) { @@ -63,7 +100,6 @@ const computeTimeRange = (schedule: Schedule, defaultStart: string, defaultEnd: } } }) - return { slotMinTime: earliestTime, slotMaxTime: latestTime } } @@ -92,8 +128,10 @@ const CalendarComponent = ({ schedule, setSelectedDate }: ScheduleCalendarProps) ) const handleEventClick = useCallback( - ({ event }: { event: { id: string } }) => { - const task = schedule.find(v => v.taskId === event.id) + ({ event }: { event: { id: string, extendedProps?: { originalTaskId?: string } } }) => { + // For recurring instances, find the original task + const taskId = event.extendedProps?.originalTaskId || event.id.split('-')[0] + const task = schedule.find(v => v.taskId === taskId) handleTaskClick(task) }, [schedule, handleTaskClick], diff --git a/src/types/user.d.ts b/src/types/user.d.ts index ee8ceae..45e6256 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -18,6 +18,12 @@ interface TaskTimeRange { end: string } +interface RecurrencePattern { + type: 'daily' | 'weekly' | 'monthly' + interval: number + endDate?: string +} + interface Task { taskId: string title: string @@ -27,6 +33,8 @@ interface Task { timeRange?: TaskTimeRange priority: TaskPriority status: 'pending' | 'completed' + isRecurring: boolean + recurrencePattern: RecurrencePattern | null } type Schedule = Task[]