implement calendar event display with day indicators and overlay
- Add ExpandedEvent type to shared package for recurring event instances - Implement EventController and EventService with full CRUD operations - Server-side recurring event expansion via recurrenceExpander - Calendar grid shows orange dot indicator for days with events - Tap on day opens modal overlay with EventCards - EventCard component with Feather icons (calendar, clock, repeat, edit, trash) - EventsStore with Zustand for client-side event state management - Load events for visible grid range including adjacent month days - Add textPrimary, borderPrimary, eventIndicator to theme - Update test responses for multiple events on Saturdays
This commit is contained in:
@@ -6,29 +6,106 @@ export class EventController {
|
||||
constructor(private eventService: EventService) {}
|
||||
|
||||
async create(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const event = await this.eventService.create(req.user!.userId, req.body);
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error("Error creating event:", error);
|
||||
res.status(500).json({ error: "Failed to create event" });
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const event = await this.eventService.getById(
|
||||
req.params.id,
|
||||
req.user!.userId,
|
||||
);
|
||||
if (!event) {
|
||||
res.status(404).json({ error: "Event not found" });
|
||||
return;
|
||||
}
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error("Error getting event:", error);
|
||||
res.status(500).json({ error: "Failed to get event" });
|
||||
}
|
||||
}
|
||||
|
||||
async getAll(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const events = await this.eventService.getAll(req.user!.userId);
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error("Error getting events:", error);
|
||||
res.status(500).json({ error: "Failed to get events" });
|
||||
}
|
||||
}
|
||||
|
||||
async getByDateRange(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
|
||||
if (!start || !end) {
|
||||
res.status(400).json({ error: "start and end query params required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date(start as string);
|
||||
const endDate = new Date(end as string);
|
||||
|
||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||
res.status(400).json({ error: "Invalid date format" });
|
||||
return;
|
||||
}
|
||||
|
||||
const events = await this.eventService.getByDateRange(
|
||||
req.user!.userId,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error("Error getting events by range:", error);
|
||||
res.status(500).json({ error: "Failed to get events" });
|
||||
}
|
||||
}
|
||||
|
||||
async update(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const event = await this.eventService.update(
|
||||
req.params.id,
|
||||
req.user!.userId,
|
||||
req.body,
|
||||
);
|
||||
if (!event) {
|
||||
res.status(404).json({ error: "Event not found" });
|
||||
return;
|
||||
}
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error("Error updating event:", error);
|
||||
res.status(500).json({ error: "Failed to update event" });
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const deleted = await this.eventService.delete(
|
||||
req.params.id,
|
||||
req.user!.userId,
|
||||
);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: "Event not found" });
|
||||
return;
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Error deleting event:", error);
|
||||
res.status(500).json({ error: "Failed to delete event" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
||||
type TestResponse = { content: string; proposedChange?: ProposedEventChange };
|
||||
|
||||
// Test response index (cycles through responses)
|
||||
let responseIndex = 8;
|
||||
let responseIndex = 0;
|
||||
|
||||
// Static test responses (event proposals)
|
||||
const staticResponses: TestResponse[] = [
|
||||
@@ -109,7 +109,7 @@ const staticResponses: TestResponse[] = [
|
||||
'• "Verschiebe das Meeting auf Donnerstag"\n\n' +
|
||||
"Wie kann ich dir helfen?",
|
||||
},
|
||||
// Response 9: Phone call - short appointment
|
||||
// Response 9: Phone call - short appointment (Wednesday, so +2 days = Friday)
|
||||
{
|
||||
content:
|
||||
"Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:",
|
||||
@@ -117,12 +117,12 @@ const staticResponses: TestResponse[] = [
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Telefonat mit Mama",
|
||||
startTime: getDay("Sunday", 0, 11, 0),
|
||||
endTime: getDay("Sunday", 0, 11, 30),
|
||||
startTime: getDay("Wednesday", 1, 11, 0),
|
||||
endTime: getDay("Wednesday", 1, 11, 30),
|
||||
},
|
||||
},
|
||||
},
|
||||
// Response 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder)
|
||||
// Response 10: Update "Telefonat mit Mama" +3 days (DYNAMIC - placeholder)
|
||||
{ content: "" },
|
||||
// Response 11: Birthday party - evening event
|
||||
{
|
||||
@@ -137,10 +137,10 @@ const staticResponses: TestResponse[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
// Response 12: Language course - limited to 8 weeks
|
||||
// Response 12: Language course - limited to 8 weeks (Thu + Sat)
|
||||
{
|
||||
content:
|
||||
"Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
|
||||
"Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag und Samstag für die nächsten 8 Wochen:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
@@ -148,7 +148,7 @@ const staticResponses: TestResponse[] = [
|
||||
startTime: getDay("Thursday", 1, 19, 0),
|
||||
endTime: getDay("Thursday", 1, 20, 30),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=TH;COUNT=8",
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=TH,SA;COUNT=8",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -191,17 +191,18 @@ async function getTestResponse(
|
||||
}
|
||||
|
||||
if (responseIdx === 10) {
|
||||
// Update "Telefonat mit Mama" +2 days
|
||||
// Update "Telefonat mit Mama" +3 days and change time to 13:00
|
||||
const events = await eventRepo.findByUserId(userId);
|
||||
const mamaEvent = events.find((e) => e.title === "Telefonat mit Mama");
|
||||
if (mamaEvent) {
|
||||
const newStart = new Date(mamaEvent.startTime);
|
||||
newStart.setDate(newStart.getDate() + 2);
|
||||
const newEnd = new Date(mamaEvent.endTime);
|
||||
newEnd.setDate(newEnd.getDate() + 2);
|
||||
newStart.setDate(newStart.getDate() + 3);
|
||||
newStart.setHours(13, 0, 0, 0);
|
||||
const newEnd = new Date(newStart);
|
||||
newEnd.setMinutes(30);
|
||||
return {
|
||||
content:
|
||||
"Alles klar, ich verschiebe das Telefonat mit Mama um 2 Tage nach hinten:",
|
||||
"Alles klar, ich verschiebe das Telefonat mit Mama auf Samstag um 13:00 Uhr:",
|
||||
proposedChange: {
|
||||
action: "update",
|
||||
eventId: mamaEvent.id,
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
||||
import {
|
||||
CalendarEvent,
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
ExpandedEvent,
|
||||
} from "@caldav/shared";
|
||||
import { EventRepository } from "./interfaces";
|
||||
import { expandRecurringEvents } from "../utils/recurrenceExpander";
|
||||
|
||||
export class EventService {
|
||||
constructor(private eventRepo: EventRepository) {}
|
||||
|
||||
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
|
||||
throw new Error("Not implemented");
|
||||
return this.eventRepo.create(userId, data);
|
||||
}
|
||||
|
||||
async getById(id: string, userId: string): Promise<CalendarEvent | null> {
|
||||
throw new Error("Not implemented");
|
||||
const event = await this.eventRepo.findById(id);
|
||||
if (!event || event.userId !== userId) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
async getAll(userId: string): Promise<CalendarEvent[]> {
|
||||
throw new Error("Not implemented");
|
||||
return this.eventRepo.findByUserId(userId);
|
||||
}
|
||||
|
||||
async getByDateRange(
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<CalendarEvent[]> {
|
||||
throw new Error("Not implemented");
|
||||
): Promise<ExpandedEvent[]> {
|
||||
// Get all events for the user
|
||||
const allEvents = await this.eventRepo.findByUserId(userId);
|
||||
|
||||
// Separate recurring and non-recurring events
|
||||
const recurringEvents = allEvents.filter((e) => e.isRecurring);
|
||||
const nonRecurringEvents = allEvents.filter((e) => !e.isRecurring);
|
||||
|
||||
// Expand all events (recurring get multiple instances, non-recurring stay as-is)
|
||||
const expanded = expandRecurringEvents(
|
||||
[...nonRecurringEvents, ...recurringEvents],
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
async update(
|
||||
@@ -29,10 +53,18 @@ export class EventService {
|
||||
userId: string,
|
||||
data: UpdateEventDTO,
|
||||
): Promise<CalendarEvent | null> {
|
||||
throw new Error("Not implemented");
|
||||
const event = await this.eventRepo.findById(id);
|
||||
if (!event || event.userId !== userId) {
|
||||
return null;
|
||||
}
|
||||
return this.eventRepo.update(id, data);
|
||||
}
|
||||
|
||||
async delete(id: string, userId: string): Promise<boolean> {
|
||||
throw new Error("Not implemented");
|
||||
const event = await this.eventRepo.findById(id);
|
||||
if (!event || event.userId !== userId) {
|
||||
return false;
|
||||
}
|
||||
return this.eventRepo.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
DAY_TO_GERMAN,
|
||||
DAY_TO_GERMAN_SHORT,
|
||||
MONTH_TO_GERMAN,
|
||||
ExpandedEvent,
|
||||
} from "@caldav/shared";
|
||||
import { EventRepository } from "../services/interfaces";
|
||||
import { expandRecurringEvents, ExpandedEvent } from "./recurrenceExpander";
|
||||
import { expandRecurringEvents } from "./recurrenceExpander";
|
||||
|
||||
// Private formatting helpers
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { RRule, rrulestr } from "rrule";
|
||||
import { CalendarEvent } from "@caldav/shared";
|
||||
|
||||
export interface ExpandedEvent extends CalendarEvent {
|
||||
occurrenceStart: Date;
|
||||
occurrenceEnd: Date;
|
||||
}
|
||||
import { CalendarEvent, ExpandedEvent } from "@caldav/shared";
|
||||
|
||||
// Convert local time to "fake UTC" for rrule
|
||||
// rrule interprets all dates as UTC internally, so we need to trick it
|
||||
|
||||
Reference in New Issue
Block a user