diff --git a/.cursor/rules/app-rule.mdc b/.cursor/rules/app-rule.mdc index 8c9b9e5..5cae140 100644 --- a/.cursor/rules/app-rule.mdc +++ b/.cursor/rules/app-rule.mdc @@ -172,7 +172,6 @@ generator client { /src /lib /calendar - parser.ts # ICS parsing utilities sync.ts # Feed synchronization events.ts # Event management types.ts # Type definitions diff --git a/package-lock.json b/package-lock.json index 284769c..4666891 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "fluid-calendar", "version": "0.1.0", + "license": "MIT", "dependencies": { "@auth/core": "^0.34.2", "@auth/prisma-adapter": "^2.7.4", @@ -36,7 +37,6 @@ "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", "googleapis": "^144.0.0", - "ical.js": "^2.1.0", "next": "15.1.6", "next-auth": "^4.24.11", "react": "^19.0.0", @@ -58,6 +58,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.6", + "eslint-formatter-compact": "^8.40.0", "postcss": "^8", "prisma": "^6.3.1", "tailwindcss": "^3.4.1", @@ -3419,6 +3420,15 @@ } } }, + "node_modules/eslint-formatter-compact": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-compact/-/eslint-formatter-compact-8.40.0.tgz", + "integrity": "sha512-cwGUs113TgmTQXecx5kfRjB7m0y2wkDLSadPTE2pK6M/wO4N8PjmUaoWOFNCP9MHgsiZwgqd5bZFnDCnszC56Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -4335,11 +4345,6 @@ "node": ">= 14" } }, - "node_modules/ical.js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.1.0.tgz", - "integrity": "sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index bbcabc6..72a7d3e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", "googleapis": "^144.0.0", - "ical.js": "^2.1.0", "next": "15.1.6", "next-auth": "^4.24.11", "react": "^19.0.0", @@ -70,6 +69,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.6", + "eslint-formatter-compact": "^8.40.0", "postcss": "^8", "prisma": "^6.3.1", "tailwindcss": "^3.4.1", diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 878db6f..d6ade45 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,4 +1,4 @@ -import NextAuth, { AuthOptions } from "next-auth"; +import NextAuth from "next-auth"; import GoogleProvider from "next-auth/providers/google"; declare module "next-auth" { @@ -23,7 +23,7 @@ declare module "next-auth/jwt" { } } -export const authOptions: AuthOptions = { +const handler = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, @@ -82,8 +82,6 @@ export const authOptions: AuthOptions = { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 days }, -}; - -const handler = NextAuth(authOptions); +}); export { handler as GET, handler as POST }; diff --git a/src/app/api/calendar/google/[id]/route.ts b/src/app/api/calendar/google/[id]/route.ts index 9067c61..0b6d34d 100644 --- a/src/app/api/calendar/google/[id]/route.ts +++ b/src/app/api/calendar/google/[id]/route.ts @@ -10,12 +10,12 @@ interface UpdateRequest { // Update a Google Calendar feed export async function PATCH( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; const feed = await prisma.calendarFeed.findUnique({ - where: { id: id }, + where: { id }, include: { account: true }, }); @@ -30,7 +30,7 @@ export async function PATCH( // Update only local properties const updatedFeed = await prisma.calendarFeed.update({ - where: { id: id }, + where: { id }, data: { enabled: updates.enabled, color: updates.color, @@ -56,12 +56,12 @@ export async function PATCH( // Delete a Google Calendar feed export async function DELETE( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; const feed = await prisma.calendarFeed.findUnique({ - where: { id: id }, + where: { id }, include: { account: true }, }); @@ -74,7 +74,7 @@ export async function DELETE( // Delete the feed and all its events await prisma.calendarFeed.delete({ - where: { id: id }, + where: { id }, }); return NextResponse.json({ success: true }); diff --git a/src/app/api/events/[id]/route.ts b/src/app/api/events/[id]/route.ts index 065d587..c2fb35c 100644 --- a/src/app/api/events/[id]/route.ts +++ b/src/app/api/events/[id]/route.ts @@ -1,14 +1,11 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -interface RouteParams { - params: { - id: string; - }; -} - // Get a specific event -export async function GET(request: Request, { params }: RouteParams) { +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { const { id } = await params; const event = await prisma.calendarEvent.findUnique({ @@ -30,7 +27,10 @@ export async function GET(request: Request, { params }: RouteParams) { } // Update a specific event -export async function PATCH(request: Request, { params }: RouteParams) { +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { const { id } = await params; const updates = await request.json(); @@ -49,7 +49,10 @@ export async function PATCH(request: Request, { params }: RouteParams) { } // Delete a specific event -export async function DELETE(request: Request, { params }: RouteParams) { +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { const { id } = await params; await prisma.calendarEvent.delete({ diff --git a/src/app/api/feeds/[id]/route.ts b/src/app/api/feeds/[id]/route.ts index d6d3efc..fc1edfe 100644 --- a/src/app/api/feeds/[id]/route.ts +++ b/src/app/api/feeds/[id]/route.ts @@ -1,17 +1,15 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -interface RouteParams { - params: { - id: string; - }; -} - // Get a specific feed -export async function GET(request: Request, { params }: RouteParams) { +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { + const { id } = await params; const feed = await prisma.calendarFeed.findUnique({ - where: { id: params.id }, + where: { id }, include: { events: true }, }); @@ -30,11 +28,15 @@ export async function GET(request: Request, { params }: RouteParams) { } // Update a specific feed -export async function PATCH(request: Request, { params }: RouteParams) { +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { + const { id } = await params; const updates = await request.json(); const updated = await prisma.calendarFeed.update({ - where: { id: params.id }, + where: { id }, data: updates, }); return NextResponse.json(updated); @@ -48,11 +50,15 @@ export async function PATCH(request: Request, { params }: RouteParams) { } // Delete a specific feed -export async function DELETE(request: Request, { params }: RouteParams) { +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { + const { id } = await params; // The feed's events will be automatically deleted due to the cascade delete in the schema await prisma.calendarFeed.delete({ - where: { id: params.id }, + where: { id }, }); return NextResponse.json({ success: true }); } catch (error) { diff --git a/src/app/api/feeds/[id]/sync/route.ts b/src/app/api/feeds/[id]/sync/route.ts index eb54745..be832d3 100644 --- a/src/app/api/feeds/[id]/sync/route.ts +++ b/src/app/api/feeds/[id]/sync/route.ts @@ -1,16 +1,21 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -interface RouteParams { - params: { - id: string; - }; +interface CalendarEventInput { + start: string | Date; + end: string | Date; + created?: string | Date; + lastModified?: string | Date; + [key: string]: unknown; } -export async function POST(request: Request, { params }: RouteParams) { +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { try { const { events } = await request.json(); - const feedId = params.id; + const { id: feedId } = await params; // Start a transaction to ensure data consistency await prisma.$transaction(async (tx) => { @@ -22,7 +27,7 @@ export async function POST(request: Request, { params }: RouteParams) { // Insert new events if (events && events.length > 0) { await tx.calendarEvent.createMany({ - data: events.map((event: any) => ({ + data: events.map((event: CalendarEventInput) => ({ ...event, feedId, // Convert Date objects to strings for database storage @@ -47,10 +52,7 @@ export async function POST(request: Request, { params }: RouteParams) { return NextResponse.json({ success: true }); } catch (error) { - console.error("Failed to sync feed events:", error); - return NextResponse.json( - { error: "Failed to sync feed events" }, - { status: 500 } - ); + console.error("Failed to sync feed:", error); + return NextResponse.json({ error: "Failed to sync feed" }, { status: 500 }); } } diff --git a/src/app/api/feeds/route.ts b/src/app/api/feeds/route.ts index 5ec7727..6f41145 100644 --- a/src/app/api/feeds/route.ts +++ b/src/app/api/feeds/route.ts @@ -1,6 +1,12 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +interface CalendarFeedUpdate { + id: string; + enabled?: boolean; + color?: string | null; +} + // List all calendar feeds export async function GET() { try { @@ -53,7 +59,7 @@ export async function PUT(request: Request) { // Use transaction to ensure all updates succeed or none do await prisma.$transaction( - feeds.map((feed: any) => + feeds.map((feed: CalendarFeedUpdate) => prisma.calendarFeed.update({ where: { id: feed.id }, data: feed, diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts index 7fc42bd..0c2f5ed 100644 --- a/src/app/api/projects/[id]/route.ts +++ b/src/app/api/projects/[id]/route.ts @@ -1,13 +1,12 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { ProjectStatus } from "@/types/project"; export async function GET( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { - const { id } = params; + const { id } = await params; const project = await prisma.project.findUnique({ where: { id }, include: { @@ -31,10 +30,10 @@ export async function GET( export async function PUT( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { - const { id } = params; + const { id } = await params; const json = await request.json(); const project = await prisma.project.update({ @@ -61,10 +60,10 @@ export async function PUT( export async function DELETE( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { - const { id } = params; + const { id } = await params; // First, remove project reference from all tasks await prisma.task.updateMany({ diff --git a/src/app/api/tags/[id]/route.ts b/src/app/api/tags/[id]/route.ts index 609bb04..0b620e1 100644 --- a/src/app/api/tags/[id]/route.ts +++ b/src/app/api/tags/[id]/route.ts @@ -3,12 +3,13 @@ import { prisma } from "@/lib/prisma"; export async function GET( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { + const { id } = await params; const tag = await prisma.tag.findUnique({ where: { - id: params.id, + id, }, }); @@ -25,12 +26,13 @@ export async function GET( export async function PUT( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { + const { id } = await params; const tag = await prisma.tag.findUnique({ where: { - id: params.id, + id, }, }); @@ -46,7 +48,7 @@ export async function PUT( const existingTag = await prisma.tag.findFirst({ where: { name, - id: { not: params.id }, // Exclude current tag + id: { not: id }, // Exclude current tag }, }); @@ -59,7 +61,7 @@ export async function PUT( const updatedTag = await prisma.tag.update({ where: { - id: params.id, + id, }, data: { ...(name && { name }), @@ -76,12 +78,13 @@ export async function PUT( export async function DELETE( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { + const { id } = await params; const tag = await prisma.tag.findUnique({ where: { - id: params.id, + id, }, }); @@ -91,7 +94,7 @@ export async function DELETE( await prisma.tag.delete({ where: { - id: params.id, + id, }, }); diff --git a/src/app/api/tags/route.ts b/src/app/api/tags/route.ts index c829b41..2451a04 100644 --- a/src/app/api/tags/route.ts +++ b/src/app/api/tags/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -export async function GET(request: Request) { +export async function GET() { try { const tags = await prisma.tag.findMany({ orderBy: { diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index 6bc95dc..72f6255 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -5,7 +5,7 @@ import { TaskStatus } from "@/types/task"; export async function GET( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; @@ -32,7 +32,7 @@ export async function GET( export async function PUT( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; @@ -50,6 +50,7 @@ export async function PUT( } const json = await request.json(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { tagIds, project, projectId, ...updates } = json; // Handle recurring task completion @@ -146,7 +147,7 @@ export async function PUT( export async function DELETE( request: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index f78d715..c41361c 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -64,7 +64,7 @@ export async function POST(request: Request) { try { // Attempt to parse the RRule string to validate it RRule.fromString(recurrenceRule); - } catch (error) { + } catch { return new NextResponse("Invalid recurrence rule", { status: 400 }); } } diff --git a/src/app/tasks/page.tsx b/src/app/tasks/page.tsx index f1fe8e1..f16f028 100644 --- a/src/app/tasks/page.tsx +++ b/src/app/tasks/page.tsx @@ -180,7 +180,6 @@ export default function TasksPage() { }} onDelete={handleDeleteTask} onStatusChange={handleStatusChange} - onInlineEdit={handleInlineEdit} /> )} diff --git a/src/components/projects/ProjectModal.tsx b/src/components/projects/ProjectModal.tsx index 211559d..728be25 100644 --- a/src/components/projects/ProjectModal.tsx +++ b/src/components/projects/ProjectModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { IoClose } from "react-icons/io5"; -import { Project, NewProject, ProjectStatus } from "@/types/project"; +import { Project, ProjectStatus } from "@/types/project"; import { useProjectStore } from "@/store/project"; interface ProjectModalProps { diff --git a/src/components/settings/AccountManager.tsx b/src/components/settings/AccountManager.tsx index 16d1b1e..d360c12 100644 --- a/src/components/settings/AccountManager.tsx +++ b/src/components/settings/AccountManager.tsx @@ -102,10 +102,7 @@ export function AccountManager() { {showAvailableFor === account.id && ( - + )} ))} diff --git a/src/components/settings/AvailableCalendars.tsx b/src/components/settings/AvailableCalendars.tsx index 8b84d98..6838b5b 100644 --- a/src/components/settings/AvailableCalendars.tsx +++ b/src/components/settings/AvailableCalendars.tsx @@ -11,10 +11,9 @@ interface AvailableCalendar { interface Props { accountId: string; - accountEmail: string; } -export function AvailableCalendars({ accountId, accountEmail }: Props) { +export function AvailableCalendars({ accountId }: Props) { const [isLoading, setIsLoading] = useState(true); const [calendars, setCalendars] = useState([]); const [addingCalendars, setAddingCalendars] = useState>( diff --git a/src/components/settings/UserSettings.tsx b/src/components/settings/UserSettings.tsx index 32dc08f..80409e6 100644 --- a/src/components/settings/UserSettings.tsx +++ b/src/components/settings/UserSettings.tsx @@ -1,6 +1,7 @@ import { useSettingsStore } from "@/store/settings"; import { SettingsSection, SettingRow } from "./SettingsSection"; import { useSession } from "next-auth/react"; +import Image from "next/image"; import { ThemeMode, TimeFormat, @@ -62,10 +63,12 @@ export function UserSettings() {
{session.user.image && ( - {session.user.name )}
diff --git a/src/components/tasks/BoardView/BoardTask.tsx b/src/components/tasks/BoardView/BoardTask.tsx index 9737c61..f2ebba9 100644 --- a/src/components/tasks/BoardView/BoardTask.tsx +++ b/src/components/tasks/BoardView/BoardTask.tsx @@ -1,6 +1,6 @@ "use client"; -import { Task, EnergyLevel, TimePreference } from "@/types/task"; +import { Task, TimePreference } from "@/types/task"; import { useDraggable } from "@dnd-kit/core"; import { HiPencil, HiTrash, HiClock, HiLockClosed } from "react-icons/hi"; import { format, isToday, isTomorrow, isThisWeek, isThisYear } from "date-fns"; @@ -9,7 +9,6 @@ interface BoardTaskProps { task: Task; onEdit: (task: Task) => void; onDelete: (taskId: string) => void; - onInlineEdit: (task: Task) => void; } const energyLevelColors = { @@ -65,7 +64,6 @@ export function BoardTask({ task, onEdit, onDelete, - onInlineEdit, }: BoardTaskProps) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ diff --git a/src/components/tasks/BoardView/BoardView.tsx b/src/components/tasks/BoardView/BoardView.tsx index 1c720f4..e5a0a90 100644 --- a/src/components/tasks/BoardView/BoardView.tsx +++ b/src/components/tasks/BoardView/BoardView.tsx @@ -5,14 +5,13 @@ import { useProjectStore } from "@/store/project"; import { useTaskListViewSettings } from "@/store/taskListViewSettings"; import { useMemo } from "react"; import { Column } from "./Column"; -import { DndContext, DragEndEvent, DragOverEvent } from "@dnd-kit/core"; +import { DndContext, DragEndEvent} from "@dnd-kit/core"; interface BoardViewProps { tasks: Task[]; onEdit: (task: Task) => void; onDelete: (taskId: string) => void; onStatusChange: (taskId: string, status: TaskStatus) => void; - onInlineEdit: (task: Task) => void; } export function BoardView({ @@ -20,7 +19,6 @@ export function BoardView({ onEdit, onDelete, onStatusChange, - onInlineEdit, }: BoardViewProps) { const { activeProject } = useProjectStore(); const { energyLevel, timePreference, tagIds, search } = @@ -107,7 +105,6 @@ export function BoardView({ tasks={columns[status]} onEdit={onEdit} onDelete={onDelete} - onInlineEdit={onInlineEdit} /> ))} diff --git a/src/components/tasks/BoardView/Column.tsx b/src/components/tasks/BoardView/Column.tsx index 3b6acd9..f7403c3 100644 --- a/src/components/tasks/BoardView/Column.tsx +++ b/src/components/tasks/BoardView/Column.tsx @@ -9,7 +9,6 @@ interface ColumnProps { tasks: Task[]; onEdit: (task: Task) => void; onDelete: (taskId: string) => void; - onInlineEdit: (task: Task) => void; } const statusColors = { @@ -38,7 +37,6 @@ export function Column({ tasks, onEdit, onDelete, - onInlineEdit, }: ColumnProps) { const { setNodeRef, isOver } = useDroppable({ id: status, @@ -71,7 +69,6 @@ export function Column({ task={task} onEdit={onEdit} onDelete={onDelete} - onInlineEdit={onInlineEdit} /> ))}
diff --git a/src/components/tasks/TaskList.tsx b/src/components/tasks/TaskList.tsx index 4699afc..66fb1a9 100644 --- a/src/components/tasks/TaskList.tsx +++ b/src/components/tasks/TaskList.tsx @@ -201,6 +201,7 @@ function StatusFilter({ interface EditableCellProps { task: Task; field: keyof Task; + // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; onSave: (task: Task) => void; } @@ -488,7 +489,7 @@ function EditableCell({ task, field, value, onSave }: EditableCellProps) { const utcDate = new Date( Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) ); - onSave({ ...task, [field]: utcDate.toISOString() }); + onSave({ ...task, [field]: utcDate }); } else { onSave({ ...task, [field]: undefined }); } @@ -496,7 +497,7 @@ function EditableCell({ task, field, value, onSave }: EditableCellProps) { }} onClickOutside={() => setIsEditing(false)} open={isEditing} - onInputClick={(e: React.MouseEvent) => e.stopPropagation()} + onInputClick={() => {}} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" dateFormat="yyyy-MM-dd" isClearable @@ -805,7 +806,7 @@ export function TaskList({ setFilters, resetFilters, } = useTaskListViewSettings(); - const { activeProject, projects } = useProjectStore(); + const { activeProject } = useProjectStore(); const handleSort = (column: typeof sortBy) => { if (sortBy === column) { diff --git a/src/components/tasks/TaskModal.tsx b/src/components/tasks/TaskModal.tsx index b7090ab..25e7974 100644 --- a/src/components/tasks/TaskModal.tsx +++ b/src/components/tasks/TaskModal.tsx @@ -11,8 +11,7 @@ import { Tag, } from "@/types/task"; import { useProjectStore } from "@/store/project"; -import { Project } from "@/types/project"; -import { RRule, Frequency } from "rrule"; +import { RRule } from "rrule"; import { Switch } from "@/components/ui/switch"; import { format } from "date-fns"; diff --git a/src/lib/calendar/parser.ts b/src/lib/calendar/parser.ts deleted file mode 100644 index 9951592..0000000 --- a/src/lib/calendar/parser.ts +++ /dev/null @@ -1,115 +0,0 @@ -import ICAL from "ical.js"; -import { zonedTimeToUtc, toZonedTime } from "date-fns-tz"; -import { v4 as uuidv4 } from "uuid"; -import { CalendarEvent, EventStatus, AttendeeStatus } from "@/types/calendar"; - -export async function parseICalFeed( - feedId: string, - url: string -): Promise { - try { - const response = await fetch("/api/calendar", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ url }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error( - error.error || `Failed to fetch calendar feed: ${response.statusText}` - ); - } - - const { data: icalData } = await response.json(); - const jcalData = ICAL.parse(icalData); - const comp = new ICAL.Component(jcalData); - const vevents = comp.getAllSubcomponents("vevent"); - - return vevents.map((vevent) => parseEvent(feedId, vevent)); - } catch (error) { - console.error("Error parsing iCal feed:", error); - throw error; - } -} - -function parseEvent(feedId: string, vevent: ICAL.Component): CalendarEvent { - const event = new ICAL.Event(vevent); - const timezone = event.startDate?.timezone || "UTC"; - - const startDate = event.startDate - ? toZonedTime(event.startDate.toJSDate(), timezone) - : new Date(); - - const endDate = event.endDate - ? toZonedTime(event.endDate.toJSDate(), timezone) - : new Date(startDate.getTime() + 3600000); // Default 1 hour duration - - const attendees = vevent.getAllProperties("attendee").map((attendee) => ({ - name: attendee.getParameter("cn"), - email: attendee.getFirstValue(), - status: parseAttendeeStatus(attendee.getParameter("partstat")), - })); - - const organizer = vevent.getFirstProperty("organizer"); - const organizerInfo = organizer - ? { - name: organizer.getParameter("cn"), - email: organizer.getFirstValue(), - } - : undefined; - - return { - id: uuidv4(), // Generate a unique ID for the event - feedId, - uid: event.uid, - title: event.summary || "Untitled Event", - description: event.description, - start: startDate, - end: endDate, - location: event.location, - isRecurring: !!event.recurrenceId, - recurrenceRule: event.recurrenceRule?.toString(), - allDay: event.startDate?.isDate || false, // isDate true means it's an all-day event - status: parseEventStatus(vevent.getFirstPropertyValue("status")), - created: event.created?.toJSDate(), - lastModified: event.lastModified?.toJSDate(), - sequence: event.sequence, - organizer: organizerInfo, - attendees, - }; -} - -function parseEventStatus(status?: string): EventStatus | undefined { - if (!status) return undefined; - - switch (status.toUpperCase()) { - case "CONFIRMED": - return EventStatus.CONFIRMED; - case "TENTATIVE": - return EventStatus.TENTATIVE; - case "CANCELLED": - return EventStatus.CANCELLED; - default: - return undefined; - } -} - -function parseAttendeeStatus(status?: string): AttendeeStatus | undefined { - if (!status) return undefined; - - switch (status.toUpperCase()) { - case "ACCEPTED": - return AttendeeStatus.ACCEPTED; - case "TENTATIVE": - return AttendeeStatus.TENTATIVE; - case "DECLINED": - return AttendeeStatus.DECLINED; - case "NEEDS-ACTION": - return AttendeeStatus.NEEDS_ACTION; - default: - return undefined; - } -} diff --git a/src/lib/logger.ts b/src/lib/logger.ts index accce05..f161014 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -15,14 +15,16 @@ class Logger { ); } - log(message: string, data?: any) { - if (process.env.NODE_ENV === "development" && process.env.LOG_LEVEL === "debug") { + log(message: string, data?: Record) { + if ( + process.env.NODE_ENV === "development" && + process.env.LOG_LEVEL === "debug" + ) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}${ data ? "\n" + JSON.stringify(data, null, 2) : "" }\n`; fs.appendFileSync(this.logFile, logMessage); - } } } diff --git a/src/services/scheduling/CalendarServiceImpl.ts b/src/services/scheduling/CalendarServiceImpl.ts index ebf53c4..e1fa2a8 100644 --- a/src/services/scheduling/CalendarServiceImpl.ts +++ b/src/services/scheduling/CalendarServiceImpl.ts @@ -1,7 +1,7 @@ -import { CalendarEvent, PrismaClient, Task } from "@prisma/client"; +import { CalendarEvent, PrismaClient } from "@prisma/client"; import { TimeSlot, Conflict } from "@/types/scheduling"; import { CalendarService } from "./CalendarService"; -import { isWithinInterval, areIntervalsOverlapping } from "date-fns"; +import { areIntervalsOverlapping } from "date-fns"; import { logger } from "@/lib/logger"; export class CalendarServiceImpl implements CalendarService { @@ -29,16 +29,15 @@ export class CalendarServiceImpl implements CalendarService { logger.log(`[DEBUG] Found ${events.length} calendar events in range`); if (events.length > 0) { - logger.log( - "[DEBUG] Calendar events:", - events.map((e) => ({ + logger.log("[DEBUG] Calendar events:", { + events: events.map((e) => ({ id: e.id, title: e.title, start: e.start, end: e.end, feedId: e.feedId, - })) - ); + })), + }); } for (const event of events) { @@ -84,15 +83,14 @@ export class CalendarServiceImpl implements CalendarService { `[DEBUG] Found ${scheduledTasks.length} scheduled tasks to check` ); if (scheduledTasks.length > 0) { - logger.log( - "[DEBUG] Scheduled tasks:", - scheduledTasks.map((t) => ({ + logger.log("[DEBUG] Scheduled tasks:", { + tasks: scheduledTasks.map((t) => ({ id: t.id, title: t.title, start: t.scheduledStart, end: t.scheduledEnd, - })) - ); + })), + }); } for (const task of scheduledTasks) { @@ -138,7 +136,9 @@ export class CalendarServiceImpl implements CalendarService { return []; } - logger.log("[DEBUG] Fetching events for calendars:", selectedCalendarIds); + logger.log("[DEBUG] Fetching events for calendars:", { + calendarIds: selectedCalendarIds, + }); return this.prisma.calendarEvent.findMany({ where: { feedId: { diff --git a/src/services/scheduling/SchedulingService.ts b/src/services/scheduling/SchedulingService.ts index 2e28e8f..b5b75e4 100644 --- a/src/services/scheduling/SchedulingService.ts +++ b/src/services/scheduling/SchedulingService.ts @@ -1,6 +1,5 @@ -import { PrismaClient, Task } from "@prisma/client"; +import { PrismaClient, Task, AutoScheduleSettings } from "@prisma/client"; import { TimeSlotManagerImpl, TimeSlotManager } from "./TimeSlotManager"; -import { TaskAnalyzer } from "./TaskAnalyzer"; import { CalendarServiceImpl } from "./CalendarServiceImpl"; import { useSettingsStore } from "@/store/settings"; import { addDays } from "date-fns"; @@ -11,13 +10,11 @@ const DEFAULT_TASK_DURATION = 30; // Default duration in minutes export class SchedulingService { private prisma: PrismaClient; private calendarService: CalendarServiceImpl; - private taskAnalyzer: TaskAnalyzer; private settings: AutoScheduleSettings | null; constructor(settings?: AutoScheduleSettings) { this.prisma = new PrismaClient(); this.calendarService = new CalendarServiceImpl(this.prisma); - this.taskAnalyzer = new TaskAnalyzer(); this.settings = settings || null; } @@ -53,15 +50,14 @@ export class SchedulingService { } async scheduleMultipleTasks(tasks: Task[]): Promise { - logger.log( - "Starting to schedule multiple tasks", - tasks.map((t) => ({ + logger.log("Starting to schedule multiple tasks", { + tasks: tasks.map((t) => ({ id: t.id, title: t.title, duration: t.duration || DEFAULT_TASK_DURATION, dueDate: t.dueDate, - })) - ); + })), + }); // Clear existing schedules for non-locked tasks const tasksToSchedule = tasks.filter((t) => !t.scheduleLocked); @@ -90,15 +86,14 @@ export class SchedulingService { if (!b.dueDate) return -1; return a.dueDate.getTime() - b.dueDate.getTime(); }); - logger.log( - "Sorted tasks by due date", - sortedTasks.map((t) => ({ + logger.log("Sorted tasks by due date", { + tasks: sortedTasks.map((t) => ({ id: t.id, title: t.title, dueDate: t.dueDate, duration: t.duration || DEFAULT_TASK_DURATION, - })) - ); + })), + }); const timeSlotManager = this.getTimeSlotManager(); const updatedTasks: Task[] = []; diff --git a/src/services/scheduling/SlotScorer.ts b/src/services/scheduling/SlotScorer.ts index 48d14b1..5dd98ff 100644 --- a/src/services/scheduling/SlotScorer.ts +++ b/src/services/scheduling/SlotScorer.ts @@ -1,5 +1,5 @@ import { TimeSlot, SlotScore, EnergyLevel } from "@/types/scheduling"; -import { AutoScheduleSettings, Task, CalendarEvent } from "@prisma/client"; +import { AutoScheduleSettings, Task } from "@prisma/client"; import { getEnergyLevelForTime } from "@/lib/autoSchedule"; import { differenceInMinutes, differenceInHours } from "date-fns"; import { logger } from "@/lib/logger"; diff --git a/src/services/scheduling/TaskAnalyzer.ts b/src/services/scheduling/TaskAnalyzer.ts deleted file mode 100644 index 8a2a585..0000000 --- a/src/services/scheduling/TaskAnalyzer.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Task, Project } from "@prisma/client"; -import { differenceInDays, differenceInMinutes } from "date-fns"; - -export interface TaskDependency { - taskId: string; - type: "hard" | "soft"; // hard = must complete before, soft = should complete before - reason: string; -} - -export interface RecurrencePattern { - type: "daily" | "weekly" | "monthly"; - interval: number; - daysOfWeek?: number[]; - endDate?: Date; - occurrences?: number; -} - -export interface TaskPriority { - score: number; - factors: { - dueDate: number; - importance: number; - dependencies: number; - userInteraction: number; - }; -} - -export interface TaskAnalysis { - priority: TaskPriority; - estimatedDuration: number; - dependencies: TaskDependency[]; - recurrencePattern?: RecurrencePattern; - complexity: "low" | "medium" | "high"; -} - -export class TaskAnalyzer { - constructor(private completedTasks: Task[] = []) {} - - async analyzeTask(task: Task, project?: Project): Promise { - return { - priority: await this.calculatePriority(task), - estimatedDuration: this.estimateDuration(task), - dependencies: await this.analyzeDependencies(task), - recurrencePattern: this.analyzeRecurringPattern(task), - complexity: this.assessComplexity(task), - }; - } - - private async calculatePriority(task: Task): Promise { - const factors = { - dueDate: this.calculateDueDatePriority(task), - importance: this.calculateImportancePriority(task), - dependencies: await this.calculateDependencyPriority(task), - userInteraction: this.calculateUserInteractionPriority(task), - }; - - // Weighted average of factors - const weights = { - dueDate: 2.0, - importance: 1.5, - dependencies: 1.0, - userInteraction: 0.5, - }; - - const totalWeight = Object.values(weights).reduce((a, b) => a + b, 0); - const weightedSum = Object.entries(factors).reduce( - (sum, [key, value]) => sum + value * weights[key as keyof typeof weights], - 0 - ); - - return { - score: weightedSum / totalWeight, - factors, - }; - } - - private calculateDueDatePriority(task: Task): number { - if (!task.dueDate) return 0.5; // Neutral priority if no due date - - const daysToDeadline = differenceInDays(task.dueDate, new Date()); - if (daysToDeadline < 0) return 1; // Highest priority if overdue - - // Exponential increase in priority as deadline approaches - return Math.min(1, Math.exp(-daysToDeadline / 7)); // 7 days as half-life - } - - private calculateImportancePriority(task: Task): number { - // Factors that indicate importance: - // 1. Task has dependencies (other tasks depend on it) - // 2. Part of a project - // 3. Has a specific energy level requirement - // 4. Has been rescheduled multiple times - - let importance = 0.5; // Start with neutral importance - - if (task.projectId) importance += 0.2; - if (task.energyLevel) importance += 0.1; - if (task.lastScheduled) importance += 0.1; - - return Math.min(1, importance); - } - - private async calculateDependencyPriority(task: Task): Promise { - const dependencies = await this.analyzeDependencies(task); - if (dependencies.length === 0) return 0.5; - - // Higher priority if task has many dependencies or hard dependencies - const hardDependencies = dependencies.filter( - (d) => d.type === "hard" - ).length; - return Math.min( - 1, - 0.5 + hardDependencies * 0.2 + dependencies.length * 0.1 - ); - } - - private calculateUserInteractionPriority(task: Task): number { - // Factors that indicate user interest: - // 1. Recently viewed/edited - // 2. Manually scheduled before - // 3. Has detailed description/notes - // 4. Part of an active project - - let interactionScore = 0.5; - - if (task.scheduleLocked) interactionScore += 0.2; // User cares about timing - if (task.description) interactionScore += 0.1; // User provided details - if (task.preferredTime) interactionScore += 0.1; // User specified preference - - return Math.min(1, interactionScore); - } - - private estimateDuration(task: Task): number { - if (task.duration) return task.duration; // Use explicit duration if set - - // Find similar completed tasks - const similarTasks = this.completedTasks.filter( - (t) => - t.projectId === task.projectId || t.energyLevel === task.energyLevel - ); - - if (similarTasks.length > 0) { - // Calculate average duration of similar tasks - const totalDuration = similarTasks.reduce((sum, t) => { - if (t.scheduledStart && t.scheduledEnd) { - return sum + differenceInMinutes(t.scheduledEnd, t.scheduledStart); - } - return sum + (t.duration || 60); - }, 0); - return Math.round(totalDuration / similarTasks.length); - } - - return 60; // Default to 1 hour if no better estimate available - } - - private async analyzeDependencies(task: Task): Promise { - const dependencies: TaskDependency[] = []; - - // For now, return empty array - // This will be implemented when we add task dependency features - - return dependencies; - } - - private analyzeRecurringPattern(task: Task): RecurrencePattern | undefined { - if (!task.isRecurring || !task.recurrenceRule) return undefined; - - // Basic pattern detection - will be enhanced later - if (task.recurrenceRule.includes("FREQ=DAILY")) { - return { - type: "daily", - interval: 1, - }; - } - - if (task.recurrenceRule.includes("FREQ=WEEKLY")) { - return { - type: "weekly", - interval: 1, - daysOfWeek: [task.scheduledStart?.getDay() || 0], - }; - } - - return undefined; - } - - private assessComplexity(task: Task): "low" | "medium" | "high" { - let complexityScore = 0; - - // Factors that indicate complexity: - if (task.duration && task.duration > 120) complexityScore++; // Long duration - if (task.description?.length || 0 > 100) complexityScore++; // Detailed description - if (task.projectId) complexityScore++; // Part of a project - if (task.energyLevel === "high") complexityScore++; // Requires high energy - if (task.isRecurring) complexityScore++; // Recurring task - - return complexityScore <= 1 - ? "low" - : complexityScore <= 3 - ? "medium" - : "high"; - } -} diff --git a/src/services/scheduling/TimeSlotManager.ts b/src/services/scheduling/TimeSlotManager.ts index 442bb9c..46c46e4 100644 --- a/src/services/scheduling/TimeSlotManager.ts +++ b/src/services/scheduling/TimeSlotManager.ts @@ -6,7 +6,6 @@ import { isWithinInterval, setHours, setMinutes, - getHours, getDay, differenceInHours, } from "date-fns"; @@ -104,14 +103,13 @@ export class TimeSlotManagerImpl implements TimeSlotManager { logger.log(`[DEBUG] Found ${availableSlots.length} available slots`); if (availableSlots.length > 0) { - logger.log( - "[DEBUG] Available slots:", - availableSlots.map((slot) => ({ + logger.log("[DEBUG] Available slots:", { + slots: availableSlots.map((slot) => ({ start: slot.start, end: slot.end, score: this.scoreSlot(slot), - })) - ); + })), + }); } // 4. Apply buffer times @@ -130,9 +128,8 @@ export class TimeSlotManagerImpl implements TimeSlotManager { return false; } - // Check for calendar conflicts with a dummy task - const dummyTask = { id: "dummy" } as Task; - const conflicts = await this.findConflicts(slot, dummyTask); + // Check for calendar conflicts + const conflicts = await this.findCalendarConflicts(slot); return conflicts.length === 0; } @@ -283,7 +280,7 @@ export class TimeSlotManagerImpl implements TimeSlotManager { ); } - private async findConflicts(slot: TimeSlot, task: Task): Promise { + private async findCalendarConflicts(slot: TimeSlot): Promise { const selectedCalendars = parseSelectedCalendars( this.settings.selectedCalendars ); @@ -293,7 +290,9 @@ export class TimeSlotManagerImpl implements TimeSlotManager { return []; } - logger.log("[DEBUG] Checking conflicts with calendars:", selectedCalendars); + logger.log("[DEBUG] Checking conflicts with calendars:", { + calendarIds: selectedCalendars, + }); return this.calendarService.findConflicts(slot, selectedCalendars); } @@ -314,7 +313,7 @@ export class TimeSlotManagerImpl implements TimeSlotManager { }); for (const slot of slots) { - const conflicts = await this.findConflicts(slot, task); + const conflicts = await this.findCalendarConflicts(slot); // Check for conflicts with other scheduled tasks const hasTaskConflict = scheduledTasks.some( @@ -335,11 +334,11 @@ export class TimeSlotManagerImpl implements TimeSlotManager { ...(hasTaskConflict ? [ { - type: "task", + type: "task" as const, start: slot.start, end: slot.end, title: "Conflict with another scheduled task", - source: { type: "task", id: "conflict" }, + source: { type: "task" as const, id: "conflict" }, }, ] : []), @@ -350,10 +349,17 @@ export class TimeSlotManagerImpl implements TimeSlotManager { return availableSlots; } + // TODO: Buffer time implementation needs improvement: + // 1. Currently only checks if buffers fit within work hours but doesn't prevent scheduling in buffer times + // 2. Should check for conflicts during buffer periods + // 3. Consider adjusting slot times to include the buffers + // 4. Could factor buffer availability into slot scoring private applyBufferTimes(slots: TimeSlot[]): TimeSlot[] { return slots.map((slot) => { const { beforeBuffer, afterBuffer } = this.calculateBufferTimes(slot); - slot.hasBufferTime = true; + // Only mark as having buffer time if both buffers are within work hours + slot.hasBufferTime = + beforeBuffer.isWithinWorkHours && afterBuffer.isWithinWorkHours; return slot; }); } diff --git a/src/store/calendar.ts b/src/store/calendar.ts index 7cc1378..f1f54bb 100644 --- a/src/store/calendar.ts +++ b/src/store/calendar.ts @@ -9,7 +9,6 @@ import { CalendarView, CalendarViewState, } from "@/types/calendar"; -import { parseICalFeed } from "@/lib/calendar/parser"; import { CalendarType } from "@/lib/calendar/init"; import { useTaskStore } from "@/store/task"; import { useSettingsStore } from "@/store/settings"; @@ -548,34 +547,6 @@ export const useCalendarStore = create()((set, get) => ({ await scheduleAllTasks(); return; } - - // For iCal feeds, parse the feed - if (feed.url) { - const events = await parseICalFeed(feed.url, id); - - // Update events in database - const response = await fetch(`/api/feeds/${id}/sync`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ events }), - }); - - if (!response.ok) { - throw new Error("Failed to sync feed with database"); - } - - // Update feed sync status - await get().updateFeed(id, { - lastSync: new Date(), - error: undefined, - }); - - // Reload events from database - await get().loadFromDatabase(); - // Trigger auto-scheduling after event is created - const { scheduleAllTasks } = useTaskStore.getState(); - await scheduleAllTasks(); - } } catch (error) { console.error("Failed to sync feed:", error); // Update feed with error @@ -736,18 +707,19 @@ export const useCalendarStore = create()((set, get) => ({ id: `${task.id}`, feedId: "tasks", title: task.title, - description: task.description, + description: task.description || undefined, start: new Date(task.scheduledStart), end: new Date(task.scheduledEnd), isRecurring: false, + isMaster: false, allDay: false, color: task.tags[0]?.color || "#4f46e5", extendedProps: { isTask: true, taskId: task.id, status: task.status, - energyLevel: task.energyLevel, - preferredTime: task.preferredTime, + energyLevel: task.energyLevel?.toString() || undefined, + preferredTime: task.preferredTime?.toString(), tags: task.tags, isAutoScheduled: true, scheduleScore: task.scheduleScore, @@ -770,20 +742,21 @@ export const useCalendarStore = create()((set, get) => ({ id: `${task.id}`, feedId: "tasks", title: task.title, - description: task.description, + description: task.description || undefined, start: eventDate, end: task.duration ? new Date(eventDate.getTime() + task.duration * 60000) : new Date(eventDate.getTime() + 3600000), isRecurring: false, + isMaster: false, allDay: true, color: task.tags[0]?.color || "#4f46e5", extendedProps: { isTask: true, taskId: task.id, status: task.status, - energyLevel: task.energyLevel, - preferredTime: task.preferredTime, + energyLevel: task.energyLevel?.toString() || undefined, + preferredTime: task.preferredTime?.toString(), tags: task.tags, isAutoScheduled: false, dueDate: task.dueDate diff --git a/src/types/project.ts b/src/types/project.ts index 05624cf..218e269 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -20,4 +20,4 @@ export interface NewProject { status?: ProjectStatus; } -export interface UpdateProject extends Partial {} +export type UpdateProject = Partial; diff --git a/src/types/task.ts b/src/types/task.ts index a4d6cb7..0626034 100644 --- a/src/types/task.ts +++ b/src/types/task.ts @@ -64,7 +64,7 @@ export interface UpdateTask tagIds?: string[]; } -export interface NewTag extends Omit {} +export type NewTag = Omit; export interface TaskFilters { status?: TaskStatus[];