format codebase with prettier

This commit is contained in:
2026-01-04 16:17:36 +01:00
parent 77f15b6dd1
commit e3f7a778c7
63 changed files with 786 additions and 542 deletions

View File

@@ -1,17 +1,17 @@
type Theme = { type Theme = {
chatBot: string, chatBot: string;
primeFg: string, primeFg: string;
primeBg: string, primeBg: string;
messageBorderBg: string, messageBorderBg: string;
placeholderBg: string, placeholderBg: string;
calenderBg: string, calenderBg: string;
confirmButton: string, confirmButton: string;
rejectButton: string, rejectButton: string;
disabledButton: string, disabledButton: string;
buttonText: string, buttonText: string;
textSecondary: string, textSecondary: string;
textMuted: string, textMuted: string;
} };
const defaultLight: Theme = { const defaultLight: Theme = {
chatBot: "#DE6C20", chatBot: "#DE6C20",
@@ -26,7 +26,7 @@ const defaultLight: Theme = {
buttonText: "#fff", buttonText: "#fff",
textSecondary: "#666", textSecondary: "#666",
textMuted: "#888", textMuted: "#888",
} };
let currentTheme: Theme = defaultLight; let currentTheme: Theme = defaultLight;
export default currentTheme; export default currentTheme;

View File

@@ -4,24 +4,30 @@ import theme from "../../Themes";
export default function TabLayout() { export default function TabLayout() {
return ( return (
<Tabs screenOptions={{ <Tabs
headerShown: false, screenOptions={{
tabBarActiveTintColor: theme.chatBot, headerShown: false,
tabBarInactiveTintColor: theme.primeFg, tabBarActiveTintColor: theme.chatBot,
tabBarStyle: { backgroundColor: theme.primeBg }, tabBarInactiveTintColor: theme.primeFg,
}}> tabBarStyle: { backgroundColor: theme.primeBg },
}}
>
<Tabs.Screen <Tabs.Screen
name="chat" name="chat"
options={{ options={{
title: 'Chat', title: "Chat",
tabBarIcon: ({ color }) => <Ionicons size={28} name="chatbubble" color={color} />, tabBarIcon: ({ color }) => (
<Ionicons size={28} name="chatbubble" color={color} />
),
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="calendar" name="calendar"
options={{ options={{
title: 'Calendar', title: "Calendar",
tabBarIcon: ({ color }) => <Ionicons size={28} name="calendar" color={color} />, tabBarIcon: ({ color }) => (
<Ionicons size={28} name="calendar" color={color} />
),
}} }}
/> />
</Tabs> </Tabs>

View File

@@ -40,13 +40,13 @@ const Chat = () => {
action: "confirm" | "reject", action: "confirm" | "reject",
messageId: string, messageId: string,
conversationId: string, conversationId: string,
proposedChange?: ProposedEventChange proposedChange?: ProposedEventChange,
) => { ) => {
// Mark message as responded (optimistic update) // Mark message as responded (optimistic update)
setMessages((prev) => setMessages((prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === messageId ? { ...msg, respondedAction: action } : msg msg.id === messageId ? { ...msg, respondedAction: action } : msg,
) ),
); );
try { try {
@@ -58,7 +58,7 @@ const Chat = () => {
proposedChange.action, proposedChange.action,
proposedChange.event, proposedChange.event,
proposedChange.eventId, proposedChange.eventId,
proposedChange.updates proposedChange.updates,
) )
: await ChatService.rejectEvent(conversationId, messageId); : await ChatService.rejectEvent(conversationId, messageId);
@@ -74,8 +74,8 @@ const Chat = () => {
// Revert on error // Revert on error
setMessages((prev) => setMessages((prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === messageId ? { ...msg, respondedAction: undefined } : msg msg.id === messageId ? { ...msg, respondedAction: undefined } : msg,
) ),
); );
} }
}; };
@@ -119,7 +119,12 @@ const Chat = () => {
proposedChange={item.proposedChange} proposedChange={item.proposedChange}
respondedAction={item.respondedAction} respondedAction={item.respondedAction}
onConfirm={() => onConfirm={() =>
handleEventResponse("confirm", item.id, item.conversationId!, item.proposedChange) handleEventResponse(
"confirm",
item.id,
item.conversationId!,
item.proposedChange,
)
} }
onReject={() => onReject={() =>
handleEventResponse("reject", item.id, item.conversationId!) handleEventResponse("reject", item.id, item.conversationId!)

View File

@@ -1,6 +1,6 @@
import { View, Text, TextInput, Pressable } from 'react-native'; import { View, Text, TextInput, Pressable } from "react-native";
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams } from "expo-router";
import BaseBackground from '../../components/BaseBackground'; import BaseBackground from "../../components/BaseBackground";
const EventDetailScreen = () => { const EventDetailScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
@@ -12,7 +12,7 @@ const EventDetailScreen = () => {
// TODO: Delete button -> EventService.delete() // TODO: Delete button -> EventService.delete()
// TODO: Link to NoteScreen for this event // TODO: Link to NoteScreen for this event
// TODO: Loading and error states // TODO: Loading and error states
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<BaseBackground> <BaseBackground>

View File

@@ -1,5 +1,5 @@
import { View, Text, TextInput, Pressable } from 'react-native'; import { View, Text, TextInput, Pressable } from "react-native";
import BaseBackground from '../components/BaseBackground'; import BaseBackground from "../components/BaseBackground";
const LoginScreen = () => { const LoginScreen = () => {
// TODO: Email input field // TODO: Email input field
@@ -8,7 +8,7 @@ const LoginScreen = () => {
// TODO: Link to RegisterScreen // TODO: Link to RegisterScreen
// TODO: Error handling and display // TODO: Error handling and display
// TODO: Navigate to Calendar on success // TODO: Navigate to Calendar on success
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<BaseBackground> <BaseBackground>

View File

@@ -1,6 +1,6 @@
import { View, Text, TextInput, Pressable } from 'react-native'; import { View, Text, TextInput, Pressable } from "react-native";
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams } from "expo-router";
import BaseBackground from '../../components/BaseBackground'; import BaseBackground from "../../components/BaseBackground";
const NoteScreen = () => { const NoteScreen = () => {
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
@@ -10,7 +10,7 @@ const NoteScreen = () => {
// TODO: Auto-save or manual save button // TODO: Auto-save or manual save button
// TODO: Save changes -> EventService.update({ note: ... }) // TODO: Save changes -> EventService.update({ note: ... })
// TODO: Loading and error states // TODO: Loading and error states
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<BaseBackground> <BaseBackground>

View File

@@ -1,5 +1,5 @@
import { View, Text, TextInput, Pressable } from 'react-native'; import { View, Text, TextInput, Pressable } from "react-native";
import BaseBackground from '../components/BaseBackground'; import BaseBackground from "../components/BaseBackground";
const RegisterScreen = () => { const RegisterScreen = () => {
// TODO: Email input field // TODO: Email input field
@@ -10,7 +10,7 @@ const RegisterScreen = () => {
// TODO: Link to LoginScreen // TODO: Link to LoginScreen
// TODO: Error handling and display // TODO: Error handling and display
// TODO: Navigate to Calendar on success // TODO: Navigate to Calendar on success
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<BaseBackground> <BaseBackground>

View File

@@ -1,11 +1,11 @@
import { View } from "react-native" import { View } from "react-native";
import currentTheme from "../Themes" import currentTheme from "../Themes";
import { ReactNode } from "react"; import { ReactNode } from "react";
type BaseBackgroundProps = { type BaseBackgroundProps = {
children?: ReactNode; children?: ReactNode;
className?: string; className?: string;
} };
const BaseBackground = (props: BaseBackgroundProps) => { const BaseBackground = (props: BaseBackgroundProps) => {
return ( return (
@@ -17,7 +17,7 @@ const BaseBackground = (props: BaseBackgroundProps) => {
> >
{props.children} {props.children}
</View> </View>
) );
} };
export default BaseBackground; export default BaseBackground;

View File

@@ -1,5 +1,5 @@
import { View, Text, Pressable } from 'react-native'; import { View, Text, Pressable } from "react-native";
import { CalendarEvent } from '@caldav/shared'; import { CalendarEvent } from "@caldav/shared";
type EventCardProps = { type EventCardProps = {
event: CalendarEvent; event: CalendarEvent;
@@ -10,7 +10,7 @@ const EventCard = ({ event: _event, onPress: _onPress }: EventCardProps) => {
// TODO: Display event title, time, and description preview // TODO: Display event title, time, and description preview
// TODO: Handle onPress to navigate to EventDetailScreen // TODO: Handle onPress to navigate to EventDetailScreen
// TODO: Style based on event type or time of day // TODO: Style based on event type or time of day
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<Pressable> <Pressable>

View File

@@ -1,5 +1,5 @@
import { View, Text, Modal, Pressable } from 'react-native'; import { View, Text, Modal, Pressable } from "react-native";
import { CreateEventDTO } from '@caldav/shared'; import { CreateEventDTO } from "@caldav/shared";
type EventConfirmDialogProps = { type EventConfirmDialogProps = {
visible: boolean; visible: boolean;
@@ -20,7 +20,7 @@ const EventConfirmDialog = ({
// TODO: Confirm button calls onConfirm and closes dialog // TODO: Confirm button calls onConfirm and closes dialog
// TODO: Reject button calls onReject and closes dialog // TODO: Reject button calls onReject and closes dialog
// TODO: Close button or backdrop tap calls onClose // TODO: Close button or backdrop tap calls onClose
throw new Error('Not implemented'); throw new Error("Not implemented");
return ( return (
<Modal visible={false} transparent animationType="fade"> <Modal visible={false} transparent animationType="fade">

View File

@@ -41,7 +41,10 @@ export const ProposedEventCard = ({
{formatDateTime(event?.startTime)} {formatDateTime(event?.startTime)}
</Text> </Text>
{event?.description && ( {event?.description && (
<Text style={{ color: currentTheme.textSecondary }} className="text-sm mt-1"> <Text
style={{ color: currentTheme.textSecondary }}
className="text-sm mt-1"
>
{event.description} {event.description}
</Text> </Text>
)} )}
@@ -65,7 +68,10 @@ export const ProposedEventCard = ({
borderColor: currentTheme.confirmButton, borderColor: currentTheme.confirmButton,
}} }}
> >
<Text style={{ color: currentTheme.buttonText }} className="font-medium"> <Text
style={{ color: currentTheme.buttonText }}
className="font-medium"
>
Annehmen Annehmen
</Text> </Text>
</Pressable> </Pressable>
@@ -81,7 +87,10 @@ export const ProposedEventCard = ({
borderColor: currentTheme.rejectButton, borderColor: currentTheme.rejectButton,
}} }}
> >
<Text style={{ color: currentTheme.buttonText }} className="font-medium"> <Text
style={{ color: currentTheme.buttonText }}
className="font-medium"
>
Ablehnen Ablehnen
</Text> </Text>
</Pressable> </Pressable>

View File

@@ -1,19 +1,19 @@
import { LoginDTO, CreateUserDTO, AuthResponse } from '@caldav/shared'; import { LoginDTO, CreateUserDTO, AuthResponse } from "@caldav/shared";
export const AuthService = { export const AuthService = {
login: async (_credentials: LoginDTO): Promise<AuthResponse> => { login: async (_credentials: LoginDTO): Promise<AuthResponse> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
register: async (_data: CreateUserDTO): Promise<AuthResponse> => { register: async (_data: CreateUserDTO): Promise<AuthResponse> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
logout: async (): Promise<void> => { logout: async (): Promise<void> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
refresh: async (): Promise<AuthResponse> => { refresh: async (): Promise<AuthResponse> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
}; };

View File

@@ -28,27 +28,32 @@ export const ChatService = {
action: EventAction, action: EventAction,
event?: CreateEventDTO, event?: CreateEventDTO,
eventId?: string, eventId?: string,
updates?: UpdateEventDTO updates?: UpdateEventDTO,
): Promise<ChatResponse> => { ): Promise<ChatResponse> => {
const body: ConfirmEventRequest = { action, event, eventId, updates }; const body: ConfirmEventRequest = { action, event, eventId, updates };
return ApiClient.post<ChatResponse>(`/chat/confirm/${conversationId}/${messageId}`, body); return ApiClient.post<ChatResponse>(
`/chat/confirm/${conversationId}/${messageId}`,
body,
);
}, },
rejectEvent: async ( rejectEvent: async (
conversationId: string, conversationId: string,
messageId: string messageId: string,
): Promise<ChatResponse> => { ): Promise<ChatResponse> => {
return ApiClient.post<ChatResponse>(`/chat/reject/${conversationId}/${messageId}`); return ApiClient.post<ChatResponse>(
`/chat/reject/${conversationId}/${messageId}`,
);
}, },
getConversations: async (): Promise<ConversationSummary[]> => { getConversations: async (): Promise<ConversationSummary[]> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
getConversation: async ( getConversation: async (
_id: string, _id: string,
_options?: GetMessagesOptions _options?: GetMessagesOptions,
): Promise<ChatMessage[]> => { ): Promise<ChatMessage[]> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
}; };

View File

@@ -1,27 +1,33 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
export const EventService = { export const EventService = {
getAll: async (): Promise<CalendarEvent[]> => { getAll: async (): Promise<CalendarEvent[]> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
getById: async (_id: string): Promise<CalendarEvent> => { getById: async (_id: string): Promise<CalendarEvent> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
getByDateRange: async (_start: Date, _end: Date): Promise<CalendarEvent[]> => { getByDateRange: async (
throw new Error('Not implemented'); _start: Date,
_end: Date,
): Promise<CalendarEvent[]> => {
throw new Error("Not implemented");
}, },
create: async (_data: CreateEventDTO): Promise<CalendarEvent> => { create: async (_data: CreateEventDTO): Promise<CalendarEvent> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
update: async (_id: string, _data: UpdateEventDTO): Promise<CalendarEvent> => { update: async (
throw new Error('Not implemented'); _id: string,
_data: UpdateEventDTO,
): Promise<CalendarEvent> => {
throw new Error("Not implemented");
}, },
delete: async (_id: string): Promise<void> => { delete: async (_id: string): Promise<void> => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
}; };

View File

@@ -1,4 +1,4 @@
export { ApiClient, API_BASE_URL } from './ApiClient'; export { ApiClient, API_BASE_URL } from "./ApiClient";
export { AuthService } from './AuthService'; export { AuthService } from "./AuthService";
export { EventService } from './EventService'; export { EventService } from "./EventService";
export { ChatService } from './ChatService'; export { ChatService } from "./ChatService";

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand'; import { create } from "zustand";
import { User } from '@caldav/shared'; import { User } from "@caldav/shared";
interface AuthState { interface AuthState {
user: User | null; user: User | null;
@@ -15,12 +15,12 @@ export const useAuthStore = create<AuthState>((set) => ({
token: null, token: null,
isAuthenticated: false, isAuthenticated: false,
login: (_user: User, _token: string) => { login: (_user: User, _token: string) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
logout: () => { logout: () => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
setToken: (_token: string) => { setToken: (_token: string) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
})); }));

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand'; import { create } from "zustand";
import { CalendarEvent } from '@caldav/shared'; import { CalendarEvent } from "@caldav/shared";
interface EventsState { interface EventsState {
events: CalendarEvent[]; events: CalendarEvent[];
@@ -12,15 +12,15 @@ interface EventsState {
export const useEventsStore = create<EventsState>((set) => ({ export const useEventsStore = create<EventsState>((set) => ({
events: [], events: [],
setEvents: (_events: CalendarEvent[]) => { setEvents: (_events: CalendarEvent[]) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
addEvent: (_event: CalendarEvent) => { addEvent: (_event: CalendarEvent) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
updateEvent: (_id: string, _event: Partial<CalendarEvent>) => { updateEvent: (_id: string, _event: Partial<CalendarEvent>) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
deleteEvent: (_id: string) => { deleteEvent: (_id: string) => {
throw new Error('Not implemented'); throw new Error("Not implemented");
}, },
})); }));

View File

@@ -1,2 +1,2 @@
export { useAuthStore } from './AuthStore'; export { useAuthStore } from "./AuthStore";
export { useEventsStore } from './EventsStore'; export { useEventsStore } from "./EventsStore";

View File

@@ -1,18 +1,21 @@
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from "@anthropic-ai/sdk";
import { AIProvider, AIContext, AIResponse } from '../services/interfaces'; import { AIProvider, AIContext, AIResponse } from "../services/interfaces";
export class ClaudeAdapter implements AIProvider { export class ClaudeAdapter implements AIProvider {
private client: Anthropic; private client: Anthropic;
private model: string; private model: string;
constructor(apiKey?: string, model: string = 'claude-3-haiku-20240307') { constructor(apiKey?: string, model: string = "claude-3-haiku-20240307") {
this.client = new Anthropic({ this.client = new Anthropic({
apiKey: apiKey || process.env.ANTHROPIC_API_KEY, apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
}); });
this.model = model; this.model = model;
} }
async processMessage(message: string, context: AIContext): Promise<AIResponse> { async processMessage(
throw new Error('Not implemented'); message: string,
context: AIContext,
): Promise<AIResponse> {
throw new Error("Not implemented");
} }
} }

View File

@@ -1 +1 @@
export * from './ClaudeAdapter'; export * from "./ClaudeAdapter";

View File

@@ -1,28 +1,35 @@
import express from 'express'; import express from "express";
import mongoose from 'mongoose'; import mongoose from "mongoose";
import 'dotenv/config' import "dotenv/config";
import { createRoutes } from './routes'; import { createRoutes } from "./routes";
import { AuthController, ChatController, EventController } from './controllers'; import { AuthController, ChatController, EventController } from "./controllers";
import { AuthService, ChatService, EventService } from './services'; import { AuthService, ChatService, EventService } from "./services";
import { MongoUserRepository, MongoEventRepository, MongoChatRepository } from './repositories'; import {
import { ClaudeAdapter } from './ai'; MongoUserRepository,
MongoEventRepository,
MongoChatRepository,
} from "./repositories";
import { ClaudeAdapter } from "./ai";
const app = express(); const app = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/caldav'; const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017/caldav";
// Middleware // Middleware
app.use(express.json()); app.use(express.json());
// CORS - only needed for web browser development // CORS - only needed for web browser development
// Native mobile apps don't send Origin headers and aren't affected by CORS // Native mobile apps don't send Origin headers and aren't affected by CORS
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== "production") {
app.use((req, res, next) => { app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header(
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); "Access-Control-Allow-Methods",
if (req.method === 'OPTIONS') { "GET, POST, PUT, DELETE, OPTIONS",
);
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") {
res.sendStatus(200); res.sendStatus(200);
return; return;
} }
@@ -49,34 +56,37 @@ const chatController = new ChatController(chatService);
const eventController = new EventController(eventService); const eventController = new EventController(eventService);
// Setup routes // Setup routes
app.use('/api', createRoutes({ app.use(
authController, "/api",
chatController, createRoutes({
eventController, authController,
})); chatController,
eventController,
}),
);
// Health check // Health check
app.get('/health', (_, res) => { app.get("/health", (_, res) => {
res.json({ status: 'ok' }); res.json({ status: "ok" });
}); });
// AI Test endpoint (for development only) // AI Test endpoint (for development only)
app.post('/api/ai/test', async (req, res) => { app.post("/api/ai/test", async (req, res) => {
try { try {
const { message } = req.body; const { message } = req.body;
if (!message) { if (!message) {
res.status(400).json({ error: 'message is required' }); res.status(400).json({ error: "message is required" });
return; return;
} }
const result = await aiProvider.processMessage(message, { const result = await aiProvider.processMessage(message, {
userId: 'test-user', userId: "test-user",
conversationHistory: [], conversationHistory: [],
existingEvents: [], existingEvents: [],
currentDate: new Date(), currentDate: new Date(),
}); });
res.json(result); res.json(result);
} catch (error) { } catch (error) {
console.error('AI test error:', error); console.error("AI test error:", error);
res.status(500).json({ error: String(error) }); res.status(500).json({ error: String(error) });
} }
}); });
@@ -85,13 +95,13 @@ app.post('/api/ai/test', async (req, res) => {
async function start() { async function start() {
try { try {
await mongoose.connect(mongoUri); await mongoose.connect(mongoUri);
console.log('Connected to MongoDB'); console.log("Connected to MongoDB");
app.listen(port, () => { app.listen(port, () => {
console.log(`Server running on port ${port}`); console.log(`Server running on port ${port}`);
}); });
} catch (error) { } catch (error) {
console.error('Failed to start server:', error); console.error("Failed to start server:", error);
process.exit(1); process.exit(1);
} }
} }

View File

@@ -1,5 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { AuthService } from '../services'; import { AuthService } from "../services";
export class AuthController { export class AuthController {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
@@ -23,10 +23,10 @@ export class AuthController {
} }
async refresh(req: Request, res: Response): Promise<void> { async refresh(req: Request, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async logout(req: Request, res: Response): Promise<void> { async logout(req: Request, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
} }

View File

@@ -1,7 +1,12 @@
import { Response } from 'express'; import { Response } from "express";
import { SendMessageDTO, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared'; import {
import { ChatService } from '../services'; SendMessageDTO,
import { AuthenticatedRequest } from '../middleware'; CreateEventDTO,
UpdateEventDTO,
EventAction,
} from "@caldav/shared";
import { ChatService } from "../services";
import { AuthenticatedRequest } from "../middleware";
export class ChatController { export class ChatController {
constructor(private chatService: ChatService) {} constructor(private chatService: ChatService) {}
@@ -13,7 +18,7 @@ export class ChatController {
const response = await this.chatService.processMessage(userId, data); const response = await this.chatService.processMessage(userId, data);
res.json(response); res.json(response);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Failed to process message' }); res.status(500).json({ error: "Failed to process message" });
} }
} }
@@ -34,11 +39,11 @@ export class ChatController {
action, action,
event, event,
eventId, eventId,
updates updates,
); );
res.json(response); res.json(response);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Failed to confirm event' }); res.status(500).json({ error: "Failed to confirm event" });
} }
} }
@@ -46,18 +51,28 @@ export class ChatController {
try { try {
const userId = req.user!.userId; const userId = req.user!.userId;
const { conversationId, messageId } = req.params; const { conversationId, messageId } = req.params;
const response = await this.chatService.rejectEvent(userId, conversationId, messageId); const response = await this.chatService.rejectEvent(
userId,
conversationId,
messageId,
);
res.json(response); res.json(response);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Failed to reject event' }); res.status(500).json({ error: "Failed to reject event" });
} }
} }
async getConversations(req: AuthenticatedRequest, res: Response): Promise<void> { async getConversations(
throw new Error('Not implemented'); req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
} }
async getConversation(req: AuthenticatedRequest, res: Response): Promise<void> { async getConversation(
throw new Error('Not implemented'); req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
} }
} }

View File

@@ -1,31 +1,34 @@
import { Response } from 'express'; import { Response } from "express";
import { EventService } from '../services'; import { EventService } from "../services";
import { AuthenticatedRequest } from '../middleware'; import { AuthenticatedRequest } from "../middleware";
export class EventController { export class EventController {
constructor(private eventService: EventService) {} constructor(private eventService: EventService) {}
async create(req: AuthenticatedRequest, res: Response): Promise<void> { async create(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getById(req: AuthenticatedRequest, res: Response): Promise<void> { async getById(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getAll(req: AuthenticatedRequest, res: Response): Promise<void> { async getAll(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getByDateRange(req: AuthenticatedRequest, res: Response): Promise<void> { async getByDateRange(
throw new Error('Not implemented'); req: AuthenticatedRequest,
res: Response,
): Promise<void> {
throw new Error("Not implemented");
} }
async update(req: AuthenticatedRequest, res: Response): Promise<void> { async update(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async delete(req: AuthenticatedRequest, res: Response): Promise<void> { async delete(req: AuthenticatedRequest, res: Response): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
} }

View File

@@ -1,3 +1,3 @@
export * from './AuthController'; export * from "./AuthController";
export * from './ChatController'; export * from "./ChatController";
export * from './EventController'; export * from "./EventController";

View File

@@ -1,16 +1,20 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { verifyToken, TokenPayload } from '../utils/jwt'; import { verifyToken, TokenPayload } from "../utils/jwt";
export interface AuthenticatedRequest extends Request { export interface AuthenticatedRequest extends Request {
user?: TokenPayload; user?: TokenPayload;
} }
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void { export function authenticate(
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
): void {
// TODO: Implement real JWT verification // TODO: Implement real JWT verification
// Fake user for testing purposes // Fake user for testing purposes
req.user = { req.user = {
userId: 'fake-user-id', userId: "fake-user-id",
email: 'test@example.com', email: "test@example.com",
}; };
next(); next();
} }

View File

@@ -1 +1 @@
export * from './AuthMiddleware'; export * from "./AuthMiddleware";

View File

@@ -1 +1 @@
export * from './mongo'; export * from "./mongo";

View File

@@ -1,23 +1,34 @@
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared'; import {
import { ChatRepository } from '../../services/interfaces'; ChatMessage,
import { ChatMessageModel, ConversationModel } from './models'; Conversation,
CreateMessageDTO,
GetMessagesOptions,
} from "@caldav/shared";
import { ChatRepository } from "../../services/interfaces";
import { ChatMessageModel, ConversationModel } from "./models";
export class MongoChatRepository implements ChatRepository { export class MongoChatRepository implements ChatRepository {
// Conversations // Conversations
async getConversationsByUser(userId: string): Promise<Conversation[]> { async getConversationsByUser(userId: string): Promise<Conversation[]> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async createConversation(userId: string): Promise<Conversation> { async createConversation(userId: string): Promise<Conversation> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
// Messages (cursor-based pagination) // Messages (cursor-based pagination)
async getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]> { async getMessages(
throw new Error('Not implemented'); conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
throw new Error("Not implemented");
} }
async createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage> { async createMessage(
throw new Error('Not implemented'); conversationId: string,
message: CreateMessageDTO,
): Promise<ChatMessage> {
throw new Error("Not implemented");
} }
} }

View File

@@ -1,6 +1,6 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
import { EventRepository } from '../../services/interfaces'; import { EventRepository } from "../../services/interfaces";
import { EventModel } from './models'; import { EventModel } from "./models";
export class MongoEventRepository implements EventRepository { export class MongoEventRepository implements EventRepository {
async findById(id: string): Promise<CalendarEvent | null> { async findById(id: string): Promise<CalendarEvent | null> {
@@ -11,15 +11,19 @@ export class MongoEventRepository implements EventRepository {
async findByUserId(userId: string): Promise<CalendarEvent[]> { async findByUserId(userId: string): Promise<CalendarEvent[]> {
const events = await EventModel.find({ userId }).sort({ startTime: 1 }); const events = await EventModel.find({ userId }).sort({ startTime: 1 });
return events.map(e => e.toJSON() as unknown as CalendarEvent); return events.map((e) => e.toJSON() as unknown as CalendarEvent);
} }
async findByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]> { async findByDateRange(
userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]> {
const events = await EventModel.find({ const events = await EventModel.find({
userId, userId,
startTime: { $gte: startDate, $lte: endDate } startTime: { $gte: startDate, $lte: endDate },
}).sort({ startTime: 1 }); }).sort({ startTime: 1 });
return events.map(e => e.toJSON() as unknown as CalendarEvent); return events.map((e) => e.toJSON() as unknown as CalendarEvent);
} }
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> { async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
@@ -28,7 +32,10 @@ export class MongoEventRepository implements EventRepository {
return event.toJSON() as unknown as CalendarEvent; return event.toJSON() as unknown as CalendarEvent;
} }
async update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null> { async update(
id: string,
data: UpdateEventDTO,
): Promise<CalendarEvent | null> {
const event = await EventModel.findByIdAndUpdate(id, data, { new: true }); const event = await EventModel.findByIdAndUpdate(id, data, { new: true });
if (!event) return null; if (!event) return null;
return event.toJSON() as unknown as CalendarEvent; return event.toJSON() as unknown as CalendarEvent;

View File

@@ -1,10 +1,10 @@
import { User } from '@caldav/shared'; import { User } from "@caldav/shared";
import { UserRepository, CreateUserData } from '../../services/interfaces'; import { UserRepository, CreateUserData } from "../../services/interfaces";
import { UserModel } from './models'; import { UserModel } from "./models";
export class MongoUserRepository implements UserRepository { export class MongoUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> { async findById(id: string): Promise<User | null> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async findByEmail(email: string): Promise<User | null> { async findByEmail(email: string): Promise<User | null> {

View File

@@ -1,3 +1,3 @@
export * from './MongoUserRepository'; export * from "./MongoUserRepository";
export * from './MongoEventRepository'; export * from "./MongoEventRepository";
export * from './MongoChatRepository'; export * from "./MongoChatRepository";

View File

@@ -1,11 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose'; import mongoose, { Schema, Document, Model } from "mongoose";
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared'; import {
import { IdVirtual } from './types'; ChatMessage,
Conversation,
CreateEventDTO,
UpdateEventDTO,
ProposedEventChange,
} from "@caldav/shared";
import { IdVirtual } from "./types";
export interface ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document { export interface ChatMessageDocument extends Omit<ChatMessage, "id">, Document {
toJSON(): ChatMessage; toJSON(): ChatMessage;
} }
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document { export interface ConversationDocument
extends Omit<Conversation, "id">, Document {
toJSON(): Conversation; toJSON(): Conversation;
} }
@@ -19,7 +26,7 @@ const EventSchema = new Schema<CreateEventDTO>(
isRecurring: { type: Boolean }, isRecurring: { type: Boolean },
recurrenceRule: { type: String }, recurrenceRule: { type: String },
}, },
{ _id: false } { _id: false },
); );
const UpdatesSchema = new Schema<UpdateEventDTO>( const UpdatesSchema = new Schema<UpdateEventDTO>(
@@ -32,20 +39,30 @@ const UpdatesSchema = new Schema<UpdateEventDTO>(
isRecurring: { type: Boolean }, isRecurring: { type: Boolean },
recurrenceRule: { type: String }, recurrenceRule: { type: String },
}, },
{ _id: false } { _id: false },
); );
const ProposedChangeSchema = new Schema<ProposedEventChange>( const ProposedChangeSchema = new Schema<ProposedEventChange>(
{ {
action: { type: String, enum: ['create', 'update', 'delete'], required: true }, action: {
type: String,
enum: ["create", "update", "delete"],
required: true,
},
eventId: { type: String }, eventId: { type: String },
event: { type: EventSchema }, event: { type: EventSchema },
updates: { type: UpdatesSchema }, updates: { type: UpdatesSchema },
}, },
{ _id: false } { _id: false },
); );
const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>( const ChatMessageSchema = new Schema<
ChatMessageDocument,
Model<ChatMessageDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{ {
conversationId: { conversationId: {
type: String, type: String,
@@ -53,7 +70,7 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
}, },
sender: { sender: {
type: String, type: String,
enum: ['user', 'assistant'], enum: ["user", "assistant"],
required: true, required: true,
}, },
content: { content: {
@@ -81,10 +98,16 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
return ret; return ret;
}, },
}, },
} },
); );
const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>( const ConversationSchema = new Schema<
ConversationDocument,
Model<ConversationDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{ {
userId: { userId: {
type: String, type: String,
@@ -109,8 +132,14 @@ const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDo
return ret; return ret;
}, },
}, },
} },
); );
export const ChatMessageModel = mongoose.model<ChatMessageDocument>('ChatMessage', ChatMessageSchema); export const ChatMessageModel = mongoose.model<ChatMessageDocument>(
export const ConversationModel = mongoose.model<ConversationDocument>('Conversation', ConversationSchema); "ChatMessage",
ChatMessageSchema,
);
export const ConversationModel = mongoose.model<ConversationDocument>(
"Conversation",
ConversationSchema,
);

View File

@@ -1,12 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose'; import mongoose, { Schema, Document, Model } from "mongoose";
import { CalendarEvent } from '@caldav/shared'; import { CalendarEvent } from "@caldav/shared";
import { IdVirtual } from './types'; import { IdVirtual } from "./types";
export interface EventDocument extends Omit<CalendarEvent, 'id'>, Document { export interface EventDocument extends Omit<CalendarEvent, "id">, Document {
toJSON(): CalendarEvent; toJSON(): CalendarEvent;
} }
const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>( const EventSchema = new Schema<
EventDocument,
Model<EventDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{ {
userId: { userId: {
type: String, type: String,
@@ -58,9 +64,9 @@ const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVir
return ret; return ret;
}, },
}, },
} },
); );
EventSchema.index({ userId: 1, startTime: 1, endTime: 1 }); EventSchema.index({ userId: 1, startTime: 1, endTime: 1 });
export const EventModel = mongoose.model<EventDocument>('Event', EventSchema); export const EventModel = mongoose.model<EventDocument>("Event", EventSchema);

View File

@@ -1,12 +1,18 @@
import mongoose, { Schema, Document, Model } from 'mongoose'; import mongoose, { Schema, Document, Model } from "mongoose";
import { User } from '@caldav/shared'; import { User } from "@caldav/shared";
import { IdVirtual } from './types'; import { IdVirtual } from "./types";
export interface UserDocument extends Omit<User, 'id'>, Document { export interface UserDocument extends Omit<User, "id">, Document {
toJSON(): User; toJSON(): User;
} }
const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>( const UserSchema = new Schema<
UserDocument,
Model<UserDocument, {}, {}, IdVirtual>,
{},
{},
IdVirtual
>(
{ {
email: { email: {
type: String, type: String,
@@ -43,7 +49,7 @@ const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtua
return ret; return ret;
}, },
}, },
} },
); );
export const UserModel = mongoose.model<UserDocument>('User', UserSchema); export const UserModel = mongoose.model<UserDocument>("User", UserSchema);

View File

@@ -1,3 +1,3 @@
export * from './UserModel'; export * from "./UserModel";
export * from './EventModel'; export * from "./EventModel";
export * from './ChatModel'; export * from "./ChatModel";

View File

@@ -1,13 +1,13 @@
import { Router } from 'express'; import { Router } from "express";
import { AuthController } from '../controllers'; import { AuthController } from "../controllers";
export function createAuthRoutes(authController: AuthController): Router { export function createAuthRoutes(authController: AuthController): Router {
const router = Router(); const router = Router();
router.post('/login', (req, res) => authController.login(req, res)); router.post("/login", (req, res) => authController.login(req, res));
router.post('/register', (req, res) => authController.register(req, res)); router.post("/register", (req, res) => authController.register(req, res));
router.post('/refresh', (req, res) => authController.refresh(req, res)); router.post("/refresh", (req, res) => authController.refresh(req, res));
router.post('/logout', (req, res) => authController.logout(req, res)); router.post("/logout", (req, res) => authController.logout(req, res));
return router; return router;
} }

View File

@@ -1,17 +1,25 @@
import { Router } from 'express'; import { Router } from "express";
import { ChatController } from '../controllers'; import { ChatController } from "../controllers";
import { authenticate } from '../middleware'; import { authenticate } from "../middleware";
export function createChatRoutes(chatController: ChatController): Router { export function createChatRoutes(chatController: ChatController): Router {
const router = Router(); const router = Router();
router.use(authenticate); router.use(authenticate);
router.post('/message', (req, res) => chatController.sendMessage(req, res)); router.post("/message", (req, res) => chatController.sendMessage(req, res));
router.post('/confirm/:conversationId/:messageId', (req, res) => chatController.confirmEvent(req, res)); router.post("/confirm/:conversationId/:messageId", (req, res) =>
router.post('/reject/:conversationId/:messageId', (req, res) => chatController.rejectEvent(req, res)); chatController.confirmEvent(req, res),
router.get('/conversations', (req, res) => chatController.getConversations(req, res)); );
router.get('/conversations/:id', (req, res) => chatController.getConversation(req, res)); router.post("/reject/:conversationId/:messageId", (req, res) =>
chatController.rejectEvent(req, res),
);
router.get("/conversations", (req, res) =>
chatController.getConversations(req, res),
);
router.get("/conversations/:id", (req, res) =>
chatController.getConversation(req, res),
);
return router; return router;
} }

View File

@@ -1,18 +1,18 @@
import { Router } from 'express'; import { Router } from "express";
import { EventController } from '../controllers'; import { EventController } from "../controllers";
import { authenticate } from '../middleware'; import { authenticate } from "../middleware";
export function createEventRoutes(eventController: EventController): Router { export function createEventRoutes(eventController: EventController): Router {
const router = Router(); const router = Router();
router.use(authenticate); router.use(authenticate);
router.post('/', (req, res) => eventController.create(req, res)); router.post("/", (req, res) => eventController.create(req, res));
router.get('/', (req, res) => eventController.getAll(req, res)); router.get("/", (req, res) => eventController.getAll(req, res));
router.get('/range', (req, res) => eventController.getByDateRange(req, res)); router.get("/range", (req, res) => eventController.getByDateRange(req, res));
router.get('/:id', (req, res) => eventController.getById(req, res)); router.get("/:id", (req, res) => eventController.getById(req, res));
router.put('/:id', (req, res) => eventController.update(req, res)); router.put("/:id", (req, res) => eventController.update(req, res));
router.delete('/:id', (req, res) => eventController.delete(req, res)); router.delete("/:id", (req, res) => eventController.delete(req, res));
return router; return router;
} }

View File

@@ -1,8 +1,12 @@
import { Router } from 'express'; import { Router } from "express";
import { createAuthRoutes } from './auth.routes'; import { createAuthRoutes } from "./auth.routes";
import { createChatRoutes } from './chat.routes'; import { createChatRoutes } from "./chat.routes";
import { createEventRoutes } from './event.routes'; import { createEventRoutes } from "./event.routes";
import { AuthController, ChatController, EventController } from '../controllers'; import {
AuthController,
ChatController,
EventController,
} from "../controllers";
export interface Controllers { export interface Controllers {
authController: AuthController; authController: AuthController;
@@ -13,13 +17,13 @@ export interface Controllers {
export function createRoutes(controllers: Controllers): Router { export function createRoutes(controllers: Controllers): Router {
const router = Router(); const router = Router();
router.use('/auth', createAuthRoutes(controllers.authController)); router.use("/auth", createAuthRoutes(controllers.authController));
router.use('/chat', createChatRoutes(controllers.chatController)); router.use("/chat", createChatRoutes(controllers.chatController));
router.use('/events', createEventRoutes(controllers.eventController)); router.use("/events", createEventRoutes(controllers.eventController));
return router; return router;
} }
export * from './auth.routes'; export * from "./auth.routes";
export * from './chat.routes'; export * from "./chat.routes";
export * from './event.routes'; export * from "./event.routes";

View File

@@ -1,7 +1,7 @@
import { User, CreateUserDTO, LoginDTO, AuthResponse } from '@caldav/shared'; import { User, CreateUserDTO, LoginDTO, AuthResponse } from "@caldav/shared";
import { UserRepository } from './interfaces'; import { UserRepository } from "./interfaces";
import * as jwt from '../utils/jwt'; import * as jwt from "../utils/jwt";
import * as password from '../utils/password'; import * as password from "../utils/password";
export class AuthService { export class AuthService {
constructor(private userRepo: UserRepository) {} constructor(private userRepo: UserRepository) {}
@@ -9,21 +9,21 @@ export class AuthService {
async login(data: LoginDTO): Promise<AuthResponse> { async login(data: LoginDTO): Promise<AuthResponse> {
const user = await this.userRepo.findByEmail(data.email); const user = await this.userRepo.findByEmail(data.email);
if (!user || !user.passwordHash) { if (!user || !user.passwordHash) {
throw new Error('Invalid credentials'); throw new Error("Invalid credentials");
} }
const isValid = await password.compare(data.password, user.passwordHash); const isValid = await password.compare(data.password, user.passwordHash);
if (!isValid) { if (!isValid) {
throw new Error('Invalid credentials'); throw new Error("Invalid credentials");
} }
return { user, accessToken: '' }; return { user, accessToken: "" };
} }
async register(data: CreateUserDTO): Promise<AuthResponse> { async register(data: CreateUserDTO): Promise<AuthResponse> {
const existingUser = await this.userRepo.findByEmail(data.email); const existingUser = await this.userRepo.findByEmail(data.email);
if (existingUser) { if (existingUser) {
throw new Error('Email already exists'); throw new Error("Email already exists");
} }
const passwordHash = await password.hash(data.password); const passwordHash = await password.hash(data.password);
@@ -33,14 +33,14 @@ export class AuthService {
passwordHash, passwordHash,
}); });
return { user, accessToken: '' }; return { user, accessToken: "" };
} }
async refreshToken(refreshToken: string): Promise<AuthResponse> { async refreshToken(refreshToken: string): Promise<AuthResponse> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async logout(userId: string): Promise<void> { async logout(userId: string): Promise<void> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
} }

View File

@@ -1,6 +1,17 @@
import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared'; import {
import { ChatRepository, EventRepository, AIProvider } from './interfaces'; ChatMessage,
import { getWeeksOverview, getMonthOverview } from '../utils/eventFormatters'; ChatResponse,
SendMessageDTO,
ConversationSummary,
GetMessagesOptions,
ProposedEventChange,
getDay,
CreateEventDTO,
UpdateEventDTO,
EventAction,
} from "@caldav/shared";
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
type TestResponse = { content: string; proposedChange?: ProposedEventChange }; type TestResponse = { content: string; proposedChange?: ProposedEventChange };
@@ -9,139 +20,147 @@ let responseIndex = 8;
// Static test responses (event proposals) // Static test responses (event proposals)
const staticResponses: TestResponse[] = [ const staticResponses: TestResponse[] = [
// {{{ // {{{
// Response 0: Meeting mit Jens - next Friday 14:00 // Response 0: Meeting mit Jens - next Friday 14:00
{ {
content: "Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:", content:
proposedChange: { "Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:",
action: 'create', proposedChange: {
event: { action: "create",
title: "Meeting mit Jens", event: {
startTime: getDay('Friday', 1, 14, 0), title: "Meeting mit Jens",
endTime: getDay('Friday', 1, 15, 0), startTime: getDay("Friday", 1, 14, 0),
description: "Arbeitstreffen", endTime: getDay("Friday", 1, 15, 0),
} description: "Arbeitstreffen",
} },
}, },
// Response 1: Recurring event - every Saturday 10:00 },
{ // Response 1: Recurring event - every Saturday 10:00
content: "Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:", {
proposedChange: { content:
action: 'create', "Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:",
event: { proposedChange: {
title: "Badezimmer putzen", action: "create",
startTime: getDay('Saturday', 1, 10, 0), event: {
endTime: getDay('Saturday', 1, 11, 0), title: "Badezimmer putzen",
isRecurring: true, startTime: getDay("Saturday", 1, 10, 0),
recurrenceRule: "FREQ=WEEKLY;BYDAY=SA", endTime: getDay("Saturday", 1, 11, 0),
} isRecurring: true,
} recurrenceRule: "FREQ=WEEKLY;BYDAY=SA",
},
}, },
// Response 2: 2-week overview (DYNAMIC - placeholder) },
{ content: '' }, // Response 2: 2-week overview (DYNAMIC - placeholder)
// Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder) { content: "" },
{ content: '' }, // Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder)
// Response 4: Doctor appointment with description { content: "" },
{ // Response 4: Doctor appointment with description
content: "Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!", {
proposedChange: { content:
action: 'create', "Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
event: { proposedChange: {
title: "Arzttermin Dr. Müller", action: "create",
startTime: getDay('Wednesday', 1, 9, 30), event: {
endTime: getDay('Wednesday', 1, 10, 30), title: "Arzttermin Dr. Müller",
description: "Routineuntersuchung - Versichertenkarte nicht vergessen", startTime: getDay("Wednesday", 1, 9, 30),
} endTime: getDay("Wednesday", 1, 10, 30),
} description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
},
}, },
// Response 5: Birthday - yearly recurring },
{ // Response 5: Birthday - yearly recurring
content: "Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:", {
proposedChange: { content:
action: 'create', "Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:",
event: { proposedChange: {
title: "Mamas Geburtstag", action: "create",
startTime: getDay('Thursday', 2, 0, 0), event: {
endTime: getDay('Thursday', 2, 23, 59), title: "Mamas Geburtstag",
isRecurring: true, startTime: getDay("Thursday", 2, 0, 0),
recurrenceRule: "FREQ=YEARLY", endTime: getDay("Thursday", 2, 23, 59),
} isRecurring: true,
} recurrenceRule: "FREQ=YEARLY",
},
}, },
// Response 6: Gym - recurring for 2 months (8 weeks) },
{ // Response 6: Gym - recurring for 2 months (8 weeks)
content: "Perfekt! Ich habe dein Probetraining eingetragen - jeden Dienstag für die nächsten 2 Monate:", {
proposedChange: { content:
action: 'create', "Perfekt! Ich habe dein Probetraining eingetragen - jeden Dienstag für die nächsten 2 Monate:",
event: { proposedChange: {
title: "Fitnessstudio Probetraining", action: "create",
startTime: getDay('Tuesday', 1, 18, 0), event: {
endTime: getDay('Tuesday', 1, 19, 30), title: "Fitnessstudio Probetraining",
isRecurring: true, startTime: getDay("Tuesday", 1, 18, 0),
recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8", endTime: getDay("Tuesday", 1, 19, 30),
} isRecurring: true,
} recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8",
},
}, },
// Response 7: 1-week overview (DYNAMIC - placeholder) },
{ content: '' }, // Response 7: 1-week overview (DYNAMIC - placeholder)
// Response 8: Help response (text only) { content: "" },
{ // Response 8: Help response (text only)
content: "Ich bin dein Kalender-Assistent! Du kannst mir einfach sagen, welche Termine du erstellen, ändern oder löschen möchtest. Zum Beispiel:\n\n" + {
"• \"Erstelle einen Termin für morgen um 15 Uhr\"\n" + content:
"• \"Was habe ich nächste Woche vor?\"\n" + "Ich bin dein Kalender-Assistent! Du kannst mir einfach sagen, welche Termine du erstellen, ändern oder löschen möchtest. Zum Beispiel:\n\n" +
"• \"Verschiebe das Meeting auf Donnerstag\"\n\n" + '• "Erstelle einen Termin für morgen um 15 Uhr"\n' +
"Wie kann ich dir helfen?", '• "Was habe ich nächste Woche vor?"\n' +
'• "Verschiebe das Meeting auf Donnerstag"\n\n' +
"Wie kann ich dir helfen?",
},
// Response 9: Phone call - short appointment
{
content:
"Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:",
proposedChange: {
action: "create",
event: {
title: "Telefonat mit Mama",
startTime: getDay("Sunday", 0, 11, 0),
endTime: getDay("Sunday", 0, 11, 30),
},
}, },
// Response 9: Phone call - short appointment },
{ // Response 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder)
content: "Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:", { content: "" },
proposedChange: { // Response 11: Birthday party - evening event
action: 'create', {
event: { content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!",
title: "Telefonat mit Mama", proposedChange: {
startTime: getDay('Sunday', 0, 11, 0), action: "create",
endTime: getDay('Sunday', 0, 11, 30), event: {
} title: "Geburtstagsfeier Lisa",
} startTime: getDay("Saturday", 2, 19, 0),
endTime: getDay("Saturday", 2, 23, 0),
description: "Geschenk: Buch über Fotografie",
},
}, },
// Response 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder) },
{ content: '' }, // Response 12: Language course - limited to 8 weeks
// Response 11: Birthday party - evening event {
{ content:
content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!", "Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
proposedChange: { proposedChange: {
action: 'create', action: "create",
event: { event: {
title: "Geburtstagsfeier Lisa", title: "Spanischkurs VHS",
startTime: getDay('Saturday', 2, 19, 0), startTime: getDay("Thursday", 1, 19, 0),
endTime: getDay('Saturday', 2, 23, 0), endTime: getDay("Thursday", 1, 20, 30),
description: "Geschenk: Buch über Fotografie", isRecurring: true,
} recurrenceRule: "FREQ=WEEKLY;BYDAY=TH;COUNT=8",
} },
}, },
// Response 12: Language course - limited to 8 weeks },
{ // Response 13: Monthly overview (DYNAMIC - placeholder)
content: "Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:", { content: "" },
proposedChange: { // }}}
action: 'create',
event: {
title: "Spanischkurs VHS",
startTime: getDay('Thursday', 1, 19, 0),
endTime: getDay('Thursday', 1, 20, 30),
isRecurring: true,
recurrenceRule: "FREQ=WEEKLY;BYDAY=TH;COUNT=8",
}
}
},
// Response 13: Monthly overview (DYNAMIC - placeholder)
{ content: '' },
// }}}
]; ];
async function getTestResponse( async function getTestResponse(
index: number, index: number,
eventRepo: EventRepository, eventRepo: EventRepository,
userId: string userId: string,
): Promise<TestResponse> { ): Promise<TestResponse> {
const responseIdx = index % staticResponses.length; const responseIdx = index % staticResponses.length;
@@ -153,14 +172,15 @@ async function getTestResponse(
if (responseIdx === 3) { if (responseIdx === 3) {
// Delete "Meeting mit Jens" // Delete "Meeting mit Jens"
const events = await eventRepo.findByUserId(userId); const events = await eventRepo.findByUserId(userId);
const jensEvent = events.find(e => e.title === 'Meeting mit Jens'); const jensEvent = events.find((e) => e.title === "Meeting mit Jens");
if (jensEvent) { if (jensEvent) {
return { return {
content: "Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:", content:
"Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:",
proposedChange: { proposedChange: {
action: 'delete', action: "delete",
eventId: jensEvent.id, eventId: jensEvent.id,
} },
}; };
} }
return { content: "Ich konnte keinen Termin 'Meeting mit Jens' finden." }; return { content: "Ich konnte keinen Termin 'Meeting mit Jens' finden." };
@@ -173,16 +193,17 @@ async function getTestResponse(
if (responseIdx === 10) { if (responseIdx === 10) {
// Update "Telefonat mit Mama" +2 days // Update "Telefonat mit Mama" +2 days
const events = await eventRepo.findByUserId(userId); const events = await eventRepo.findByUserId(userId);
const mamaEvent = events.find(e => e.title === 'Telefonat mit Mama'); const mamaEvent = events.find((e) => e.title === "Telefonat mit Mama");
if (mamaEvent) { if (mamaEvent) {
const newStart = new Date(mamaEvent.startTime); const newStart = new Date(mamaEvent.startTime);
newStart.setDate(newStart.getDate() + 2); newStart.setDate(newStart.getDate() + 2);
const newEnd = new Date(mamaEvent.endTime); const newEnd = new Date(mamaEvent.endTime);
newEnd.setDate(newEnd.getDate() + 2); newEnd.setDate(newEnd.getDate() + 2);
return { return {
content: "Alles klar, ich verschiebe das Telefonat mit Mama um 2 Tage nach hinten:", content:
"Alles klar, ich verschiebe das Telefonat mit Mama um 2 Tage nach hinten:",
proposedChange: { proposedChange: {
action: 'update', action: "update",
eventId: mamaEvent.id, eventId: mamaEvent.id,
updates: { startTime: newStart, endTime: newEnd }, updates: { startTime: newStart, endTime: newEnd },
// Include event with new times for display // Include event with new times for display
@@ -191,8 +212,8 @@ async function getTestResponse(
startTime: newStart, startTime: newStart,
endTime: newEnd, endTime: newEnd,
description: mamaEvent.description, description: mamaEvent.description,
} },
} },
}; };
} }
return { content: "Ich konnte keinen Termin 'Telefonat mit Mama' finden." }; return { content: "Ich konnte keinen Termin 'Telefonat mit Mama' finden." };
@@ -200,7 +221,14 @@ async function getTestResponse(
if (responseIdx === 13) { if (responseIdx === 13) {
const now = new Date(); const now = new Date();
return { content: await getMonthOverview(eventRepo, userId, now.getFullYear(), now.getMonth()) }; return {
content: await getMonthOverview(
eventRepo,
userId,
now.getFullYear(),
now.getMonth(),
),
};
} }
return staticResponses[responseIdx]; return staticResponses[responseIdx];
@@ -210,17 +238,24 @@ export class ChatService {
constructor( constructor(
private chatRepo: ChatRepository, private chatRepo: ChatRepository,
private eventRepo: EventRepository, private eventRepo: EventRepository,
private aiProvider: AIProvider private aiProvider: AIProvider,
) {} ) {}
async processMessage(userId: string, data: SendMessageDTO): Promise<ChatResponse> { async processMessage(
const response = await getTestResponse(responseIndex, this.eventRepo, userId); userId: string,
data: SendMessageDTO,
): Promise<ChatResponse> {
const response = await getTestResponse(
responseIndex,
this.eventRepo,
userId,
);
responseIndex++; responseIndex++;
const message: ChatMessage = { const message: ChatMessage = {
id: Date.now().toString(), id: Date.now().toString(),
conversationId: data.conversationId || 'temp-conv-id', conversationId: data.conversationId || "temp-conv-id",
sender: 'assistant', sender: "assistant",
content: response.content, content: response.content,
proposedChange: response.proposedChange, proposedChange: response.proposedChange,
}; };
@@ -235,49 +270,57 @@ export class ChatService {
action: EventAction, action: EventAction,
event?: CreateEventDTO, event?: CreateEventDTO,
eventId?: string, eventId?: string,
updates?: UpdateEventDTO updates?: UpdateEventDTO,
): Promise<ChatResponse> { ): Promise<ChatResponse> {
let content: string; let content: string;
if (action === 'create' && event) { if (action === "create" && event) {
const createdEvent = await this.eventRepo.create(userId, event); const createdEvent = await this.eventRepo.create(userId, event);
content = `Der Termin "${createdEvent.title}" wurde erstellt.`; content = `Der Termin "${createdEvent.title}" wurde erstellt.`;
} else if (action === 'update' && eventId && updates) { } else if (action === "update" && eventId && updates) {
const updatedEvent = await this.eventRepo.update(eventId, updates); const updatedEvent = await this.eventRepo.update(eventId, updates);
content = updatedEvent content = updatedEvent
? `Der Termin "${updatedEvent.title}" wurde aktualisiert.` ? `Der Termin "${updatedEvent.title}" wurde aktualisiert.`
: 'Termin nicht gefunden.'; : "Termin nicht gefunden.";
} else if (action === 'delete' && eventId) { } else if (action === "delete" && eventId) {
await this.eventRepo.delete(eventId); await this.eventRepo.delete(eventId);
content = 'Der Termin wurde gelöscht.'; content = "Der Termin wurde gelöscht.";
} else { } else {
content = 'Ungültige Aktion.'; content = "Ungültige Aktion.";
} }
const message: ChatMessage = { const message: ChatMessage = {
id: Date.now().toString(), id: Date.now().toString(),
conversationId, conversationId,
sender: 'assistant', sender: "assistant",
content, content,
}; };
return { message, conversationId }; return { message, conversationId };
} }
async rejectEvent(userId: string, conversationId: string, messageId: string): Promise<ChatResponse> { async rejectEvent(
userId: string,
conversationId: string,
messageId: string,
): Promise<ChatResponse> {
const message: ChatMessage = { const message: ChatMessage = {
id: Date.now().toString(), id: Date.now().toString(),
conversationId, conversationId,
sender: 'assistant', sender: "assistant",
content: 'Der Vorschlag wurde abgelehnt.', content: "Der Vorschlag wurde abgelehnt.",
}; };
return { message, conversationId }; return { message, conversationId };
} }
async getConversations(userId: string): Promise<ConversationSummary[]> { async getConversations(userId: string): Promise<ConversationSummary[]> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getConversation(userId: string, conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]> { async getConversation(
throw new Error('Not implemented'); userId: string,
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
throw new Error("Not implemented");
} }
} }

View File

@@ -1,30 +1,38 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
import { EventRepository } from './interfaces'; import { EventRepository } from "./interfaces";
export class EventService { export class EventService {
constructor(private eventRepo: EventRepository) {} constructor(private eventRepo: EventRepository) {}
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> { async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getById(id: string, userId: string): Promise<CalendarEvent | null> { async getById(id: string, userId: string): Promise<CalendarEvent | null> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getAll(userId: string): Promise<CalendarEvent[]> { async getAll(userId: string): Promise<CalendarEvent[]> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
async getByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]> { async getByDateRange(
throw new Error('Not implemented'); userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]> {
throw new Error("Not implemented");
} }
async update(id: string, userId: string, data: UpdateEventDTO): Promise<CalendarEvent | null> { async update(
throw new Error('Not implemented'); id: string,
userId: string,
data: UpdateEventDTO,
): Promise<CalendarEvent | null> {
throw new Error("Not implemented");
} }
async delete(id: string, userId: string): Promise<boolean> { async delete(id: string, userId: string): Promise<boolean> {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
} }

View File

@@ -1,4 +1,4 @@
export * from './AuthService'; export * from "./AuthService";
export * from './ChatService'; export * from "./ChatService";
export * from './EventService'; export * from "./EventService";
export * from './interfaces'; export * from "./interfaces";

View File

@@ -1,4 +1,8 @@
import { CalendarEvent, ChatMessage, ProposedEventChange } from '@caldav/shared'; import {
CalendarEvent,
ChatMessage,
ProposedEventChange,
} from "@caldav/shared";
export interface AIContext { export interface AIContext {
userId: string; userId: string;

View File

@@ -1,4 +1,9 @@
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared'; import {
ChatMessage,
Conversation,
CreateMessageDTO,
GetMessagesOptions,
} from "@caldav/shared";
export interface ChatRepository { export interface ChatRepository {
// Conversations // Conversations
@@ -6,6 +11,12 @@ export interface ChatRepository {
createConversation(userId: string): Promise<Conversation>; createConversation(userId: string): Promise<Conversation>;
// Messages (cursor-based pagination) // Messages (cursor-based pagination)
getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]>; getMessages(
createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage>; conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]>;
createMessage(
conversationId: string,
message: CreateMessageDTO,
): Promise<ChatMessage>;
} }

View File

@@ -1,9 +1,13 @@
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared'; import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
export interface EventRepository { export interface EventRepository {
findById(id: string): Promise<CalendarEvent | null>; findById(id: string): Promise<CalendarEvent | null>;
findByUserId(userId: string): Promise<CalendarEvent[]>; findByUserId(userId: string): Promise<CalendarEvent[]>;
findByDateRange(userId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[]>; findByDateRange(
userId: string,
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]>;
create(userId: string, data: CreateEventDTO): Promise<CalendarEvent>; create(userId: string, data: CreateEventDTO): Promise<CalendarEvent>;
update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>; update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>;
delete(id: string): Promise<boolean>; delete(id: string): Promise<boolean>;

View File

@@ -1,4 +1,4 @@
import { User } from '@caldav/shared'; import { User } from "@caldav/shared";
export interface CreateUserData { export interface CreateUserData {
email: string; email: string;

View File

@@ -1,4 +1,4 @@
export * from './AIProvider'; export * from "./AIProvider";
export * from './UserRepository'; export * from "./UserRepository";
export * from './EventRepository'; export * from "./EventRepository";
export * from './ChatRepository'; export * from "./ChatRepository";

View File

@@ -4,34 +4,37 @@ import {
DAY_TO_GERMAN, DAY_TO_GERMAN,
DAY_TO_GERMAN_SHORT, DAY_TO_GERMAN_SHORT,
MONTH_TO_GERMAN, MONTH_TO_GERMAN,
} from '@caldav/shared'; } from "@caldav/shared";
import { EventRepository } from '../services/interfaces'; import { EventRepository } from "../services/interfaces";
import { expandRecurringEvents, ExpandedEvent } from './recurrenceExpander'; import { expandRecurringEvents, ExpandedEvent } from "./recurrenceExpander";
// Private formatting helpers // Private formatting helpers
function formatTime(date: Date): string { function formatTime(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0'); const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`; return `${hours}:${minutes}`;
} }
function formatDateShort(date: Date): string { function formatDateShort(date: Date): string {
const day = date.getDate().toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${day}.${month}.`; return `${day}.${month}.`;
} }
function getWeekNumber(date: Date): number { function getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const d = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
);
const dayNum = d.getUTCDay() || 7; const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum); d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
} }
function formatWeeksText(events: ExpandedEvent[], weeks: number): string { function formatWeeksText(events: ExpandedEvent[], weeks: number): string {
const weeksText = weeks === 1 ? 'die nächste Woche' : `die nächsten ${weeks} Wochen`; const weeksText =
weeks === 1 ? "die nächste Woche" : `die nächsten ${weeks} Wochen`;
if (events.length === 0) { if (events.length === 0) {
return `Du hast für ${weeksText} keine Termine.`; return `Du hast für ${weeksText} keine Termine.`;
@@ -47,8 +50,10 @@ function formatWeeksText(events: ExpandedEvent[], weeks: number): string {
lines.push(`${weekday}, ${dateStr} - ${timeStr} Uhr: ${event.title}`); lines.push(`${weekday}, ${dateStr} - ${timeStr} Uhr: ${event.title}`);
} }
lines.push(`\nInsgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'}.`); lines.push(
return lines.join('\n'); `\nInsgesamt ${events.length} Termin${events.length === 1 ? "" : "e"}.`,
);
return lines.join("\n");
} }
function formatMonthText(events: ExpandedEvent[], monthName: string): string { function formatMonthText(events: ExpandedEvent[], monthName: string): string {
@@ -66,13 +71,17 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
weekGroups.get(weekNum)!.push(event); weekGroups.get(weekNum)!.push(event);
} }
const lines: string[] = [`Hier ist deine Monatsübersicht für ${monthName}:\n`]; const lines: string[] = [
`Hier ist deine Monatsübersicht für ${monthName}:\n`,
];
// Sort weeks and format // Sort weeks and format
const sortedWeeks = Array.from(weekGroups.keys()).sort((a, b) => a - b); const sortedWeeks = Array.from(weekGroups.keys()).sort((a, b) => a - b);
for (const weekNum of sortedWeeks) { for (const weekNum of sortedWeeks) {
const weekEvents = weekGroups.get(weekNum)!; const weekEvents = weekGroups.get(weekNum)!;
lines.push(`KW ${weekNum}: ${weekEvents.length} Termin${weekEvents.length === 1 ? '' : 'e'}`); lines.push(
`KW ${weekNum}: ${weekEvents.length} Termin${weekEvents.length === 1 ? "" : "e"}`,
);
for (const event of weekEvents) { for (const event of weekEvents) {
const day = DAY_INDEX_TO_DAY[event.occurrenceStart.getDay()]; const day = DAY_INDEX_TO_DAY[event.occurrenceStart.getDay()];
@@ -81,11 +90,13 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
const timeStr = formatTime(event.occurrenceStart); const timeStr = formatTime(event.occurrenceStart);
lines.push(`${weekdayShort} ${dateStr}, ${timeStr}: ${event.title}`); lines.push(`${weekdayShort} ${dateStr}, ${timeStr}: ${event.title}`);
} }
lines.push(''); lines.push("");
} }
lines.push(`Insgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'} im ${monthName}.`); lines.push(
return lines.join('\n'); `Insgesamt ${events.length} Termin${events.length === 1 ? "" : "e"} im ${monthName}.`,
);
return lines.join("\n");
} }
// Public API // Public API
@@ -97,7 +108,7 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
export async function getWeeksOverview( export async function getWeeksOverview(
eventRepo: EventRepository, eventRepo: EventRepository,
userId: string, userId: string,
weeks: number weeks: number,
): Promise<string> { ): Promise<string> {
const now = new Date(); const now = new Date();
const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000); const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000);
@@ -114,7 +125,7 @@ export async function getMonthOverview(
eventRepo: EventRepository, eventRepo: EventRepository,
userId: string, userId: string,
year: number, year: number,
month: number month: number,
): Promise<string> { ): Promise<string> {
const startOfMonth = new Date(year, month, 1); const startOfMonth = new Date(year, month, 1);
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59); const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);

View File

@@ -1,2 +1,2 @@
export * from './jwt'; export * from "./jwt";
export * from './password'; export * from "./password";

View File

@@ -1,4 +1,4 @@
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
export interface TokenPayload { export interface TokenPayload {
userId: string; userId: string;
@@ -10,17 +10,17 @@ export interface JWTConfig {
expiresIn: string; expiresIn: string;
} }
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h'; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
export function signToken(payload: TokenPayload): string { export function signToken(payload: TokenPayload): string {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
export function verifyToken(token: string): TokenPayload { export function verifyToken(token: string): TokenPayload {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
export function decodeToken(token: string): TokenPayload | null { export function decodeToken(token: string): TokenPayload | null {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }

View File

@@ -1,4 +1,4 @@
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
const SALT_ROUNDS = 10; const SALT_ROUNDS = 10;
@@ -6,6 +6,9 @@ export async function hash(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS); return bcrypt.hash(password, SALT_ROUNDS);
} }
export async function compare(password: string, hash: string): Promise<boolean> { export async function compare(
password: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(password, hash); return bcrypt.compare(password, hash);
} }

View File

@@ -1,5 +1,5 @@
import { RRule, rrulestr } from 'rrule'; import { RRule, rrulestr } from "rrule";
import { CalendarEvent } from '@caldav/shared'; import { CalendarEvent } from "@caldav/shared";
export interface ExpandedEvent extends CalendarEvent { export interface ExpandedEvent extends CalendarEvent {
occurrenceStart: Date; occurrenceStart: Date;
@@ -9,14 +9,16 @@ export interface ExpandedEvent extends CalendarEvent {
// Convert local time to "fake UTC" for rrule // Convert local time to "fake UTC" for rrule
// rrule interprets all dates as UTC internally, so we need to trick it // rrule interprets all dates as UTC internally, so we need to trick it
function toRRuleDate(date: Date): Date { function toRRuleDate(date: Date): Date {
return new Date(Date.UTC( return new Date(
date.getFullYear(), Date.UTC(
date.getMonth(), date.getFullYear(),
date.getDate(), date.getMonth(),
date.getHours(), date.getDate(),
date.getMinutes(), date.getHours(),
date.getSeconds() date.getMinutes(),
)); date.getSeconds(),
),
);
} }
// Convert rrule result back to local time // Convert rrule result back to local time
@@ -27,7 +29,7 @@ function fromRRuleDate(date: Date): Date {
date.getUTCDate(), date.getUTCDate(),
date.getUTCHours(), date.getUTCHours(),
date.getUTCMinutes(), date.getUTCMinutes(),
date.getUTCSeconds() date.getUTCSeconds(),
); );
} }
@@ -38,7 +40,7 @@ function fromRRuleDate(date: Date): Date {
export function expandRecurringEvents( export function expandRecurringEvents(
events: CalendarEvent[], events: CalendarEvent[],
rangeStart: Date, rangeStart: Date,
rangeEnd: Date rangeEnd: Date,
): ExpandedEvent[] { ): ExpandedEvent[] {
const expanded: ExpandedEvent[] = []; const expanded: ExpandedEvent[] = [];
@@ -61,13 +63,15 @@ export function expandRecurringEvents(
// Recurring event: parse RRULE and expand // Recurring event: parse RRULE and expand
try { try {
const rule = rrulestr(`DTSTART:${formatRRuleDateString(startTime)}\nRRULE:${event.recurrenceRule}`); const rule = rrulestr(
`DTSTART:${formatRRuleDateString(startTime)}\nRRULE:${event.recurrenceRule}`,
);
// Get occurrences within the range (using fake UTC dates) // Get occurrences within the range (using fake UTC dates)
const occurrences = rule.between( const occurrences = rule.between(
toRRuleDate(rangeStart), toRRuleDate(rangeStart),
toRRuleDate(rangeEnd), toRRuleDate(rangeEnd),
true // inclusive true, // inclusive
); );
for (const occurrence of occurrences) { for (const occurrence of occurrences) {
@@ -82,7 +86,10 @@ export function expandRecurringEvents(
} }
} catch (error) { } catch (error) {
// If RRULE parsing fails, include the event as a single occurrence // If RRULE parsing fails, include the event as a single occurrence
console.error(`Failed to parse recurrence rule for event ${event.id}:`, error); console.error(
`Failed to parse recurrence rule for event ${event.id}:`,
error,
);
expanded.push({ expanded.push({
...event, ...event,
occurrenceStart: startTime, occurrenceStart: startTime,
@@ -92,7 +99,9 @@ export function expandRecurringEvents(
} }
// Sort by occurrence start time // Sort by occurrence start time
expanded.sort((a, b) => a.occurrenceStart.getTime() - b.occurrenceStart.getTime()); expanded.sort(
(a, b) => a.occurrenceStart.getTime() - b.occurrenceStart.getTime(),
);
return expanded; return expanded;
} }
@@ -100,10 +109,10 @@ export function expandRecurringEvents(
// Format date as RRULE DTSTART string (YYYYMMDDTHHMMSS) // Format date as RRULE DTSTART string (YYYYMMDDTHHMMSS)
function formatRRuleDateString(date: Date): string { function formatRRuleDateString(date: Date): string {
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}${month}${day}T${hours}${minutes}${seconds}`; return `${year}${month}${day}T${hours}${minutes}${seconds}`;
} }

2
package-lock.json generated
View File

@@ -13,6 +13,7 @@
], ],
"devDependencies": { "devDependencies": {
"eslint": "^9.25.0", "eslint": "^9.25.0",
"prettier": "^3.7.4",
"typescript": "~5.9.2" "typescript": "~5.9.2"
} }
}, },
@@ -11757,7 +11758,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },

View File

@@ -8,6 +8,7 @@
], ],
"devDependencies": { "devDependencies": {
"eslint": "^9.25.0", "eslint": "^9.25.0",
"prettier": "^3.7.4",
"typescript": "~5.9.2" "typescript": "~5.9.2"
} }
} }

View File

@@ -1,2 +1,2 @@
export * from './models'; export * from "./models";
export * from './utils'; export * from "./utils";

View File

@@ -1,14 +1,14 @@
import { CreateEventDTO, UpdateEventDTO } from './CalendarEvent'; import { CreateEventDTO, UpdateEventDTO } from "./CalendarEvent";
export type MessageSender = 'user' | 'assistant'; export type MessageSender = "user" | "assistant";
export type EventAction = 'create' | 'update' | 'delete'; export type EventAction = "create" | "update" | "delete";
export interface ProposedEventChange { export interface ProposedEventChange {
action: EventAction; action: EventAction;
eventId?: string; // Required for update/delete eventId?: string; // Required for update/delete
event?: CreateEventDTO; // Required for create event?: CreateEventDTO; // Required for create
updates?: UpdateEventDTO; // Required for update updates?: UpdateEventDTO; // Required for update
} }
export interface ChatMessage { export interface ChatMessage {
@@ -39,8 +39,8 @@ export interface CreateMessageDTO {
} }
export interface GetMessagesOptions { export interface GetMessagesOptions {
before?: string; // Message ID - load messages before this one before?: string; // Message ID - load messages before this one
limit?: number; // Default: 20 limit?: number; // Default: 20
} }
export interface ChatResponse { export interface ChatResponse {

View File

@@ -39,40 +39,48 @@ export const DAY_INDEX: Record<Day, number> = {
}; };
// Mapping from Date.getDay() index (0=Sunday) to Day type // Mapping from Date.getDay() index (0=Sunday) to Day type
export const DAY_INDEX_TO_DAY: Day[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; export const DAY_INDEX_TO_DAY: Day[] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
// German translations // German translations
export const DAY_TO_GERMAN: Record<Day, string> = { export const DAY_TO_GERMAN: Record<Day, string> = {
Monday: 'Montag', Monday: "Montag",
Tuesday: 'Dienstag', Tuesday: "Dienstag",
Wednesday: 'Mittwoch', Wednesday: "Mittwoch",
Thursday: 'Donnerstag', Thursday: "Donnerstag",
Friday: 'Freitag', Friday: "Freitag",
Saturday: 'Samstag', Saturday: "Samstag",
Sunday: 'Sonntag', Sunday: "Sonntag",
}; };
export const DAY_TO_GERMAN_SHORT: Record<Day, string> = { export const DAY_TO_GERMAN_SHORT: Record<Day, string> = {
Monday: 'Mo', Monday: "Mo",
Tuesday: 'Di', Tuesday: "Di",
Wednesday: 'Mi', Wednesday: "Mi",
Thursday: 'Do', Thursday: "Do",
Friday: 'Fr', Friday: "Fr",
Saturday: 'Sa', Saturday: "Sa",
Sunday: 'So', Sunday: "So",
}; };
export const MONTH_TO_GERMAN: Record<Month, string> = { export const MONTH_TO_GERMAN: Record<Month, string> = {
January: 'Januar', January: "Januar",
February: 'Februar', February: "Februar",
March: 'März', March: "März",
April: 'April', April: "April",
May: 'Mai', May: "Mai",
June: 'Juni', June: "Juni",
July: 'Juli', July: "Juli",
August: 'August', August: "August",
September: 'September', September: "September",
October: 'Oktober', October: "Oktober",
November: 'November', November: "November",
December: 'Dezember', December: "Dezember",
}; };

View File

@@ -19,7 +19,7 @@ export interface LoginDTO {
} }
export interface AuthResponse { export interface AuthResponse {
user: Omit<User, 'passwordHash'>; user: Omit<User, "passwordHash">;
accessToken: string; accessToken: string;
refreshToken?: string; refreshToken?: string;
} }

View File

@@ -1,4 +1,4 @@
export * from './User'; export * from "./User";
export * from './CalendarEvent'; export * from "./CalendarEvent";
export * from './ChatMessage'; export * from "./ChatMessage";
export * from './Constants'; export * from "./Constants";

View File

@@ -1,4 +1,4 @@
import { Day, DAY_INDEX } from '../models/Constants'; import { Day, DAY_INDEX } from "../models/Constants";
/** /**
* Get a date for a specific weekday relative to today. * Get a date for a specific weekday relative to today.
@@ -7,7 +7,12 @@ import { Day, DAY_INDEX } from '../models/Constants';
* @param hour - Hour of day (0-23) * @param hour - Hour of day (0-23)
* @param minute - Minute (0-59) * @param minute - Minute (0-59)
*/ */
export function getDay(day: Day, offset: number, hour: number, minute: number): Date { export function getDay(
day: Day,
offset: number,
hour: number,
minute: number,
): Date {
const today = new Date(); const today = new Date();
const currentDay = today.getDay(); const currentDay = today.getDay();
const targetDay = DAY_INDEX[day]; const targetDay = DAY_INDEX[day];

View File

@@ -1 +1 @@
export * from './dateHelpers'; export * from "./dateHelpers";