format codebase with prettier
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { AIProvider, AIContext, AIResponse } from '../services/interfaces';
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { AIProvider, AIContext, AIResponse } from "../services/interfaces";
|
||||
|
||||
export class ClaudeAdapter implements AIProvider {
|
||||
private client: Anthropic;
|
||||
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({
|
||||
apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
|
||||
});
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
async processMessage(message: string, context: AIContext): Promise<AIResponse> {
|
||||
throw new Error('Not implemented');
|
||||
async processMessage(
|
||||
message: string,
|
||||
context: AIContext,
|
||||
): Promise<AIResponse> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './ClaudeAdapter';
|
||||
export * from "./ClaudeAdapter";
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import express from 'express';
|
||||
import mongoose from 'mongoose';
|
||||
import 'dotenv/config'
|
||||
import express from "express";
|
||||
import mongoose from "mongoose";
|
||||
import "dotenv/config";
|
||||
|
||||
import { createRoutes } from './routes';
|
||||
import { AuthController, ChatController, EventController } from './controllers';
|
||||
import { AuthService, ChatService, EventService } from './services';
|
||||
import { MongoUserRepository, MongoEventRepository, MongoChatRepository } from './repositories';
|
||||
import { ClaudeAdapter } from './ai';
|
||||
import { createRoutes } from "./routes";
|
||||
import { AuthController, ChatController, EventController } from "./controllers";
|
||||
import { AuthService, ChatService, EventService } from "./services";
|
||||
import {
|
||||
MongoUserRepository,
|
||||
MongoEventRepository,
|
||||
MongoChatRepository,
|
||||
} from "./repositories";
|
||||
import { ClaudeAdapter } from "./ai";
|
||||
|
||||
const app = express();
|
||||
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
|
||||
app.use(express.json());
|
||||
|
||||
// CORS - only needed for web browser development
|
||||
// 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) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS",
|
||||
);
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
if (req.method === "OPTIONS") {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
@@ -49,34 +56,37 @@ const chatController = new ChatController(chatService);
|
||||
const eventController = new EventController(eventService);
|
||||
|
||||
// Setup routes
|
||||
app.use('/api', createRoutes({
|
||||
authController,
|
||||
chatController,
|
||||
eventController,
|
||||
}));
|
||||
app.use(
|
||||
"/api",
|
||||
createRoutes({
|
||||
authController,
|
||||
chatController,
|
||||
eventController,
|
||||
}),
|
||||
);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (_, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
app.get("/health", (_, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
// AI Test endpoint (for development only)
|
||||
app.post('/api/ai/test', async (req, res) => {
|
||||
app.post("/api/ai/test", async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
if (!message) {
|
||||
res.status(400).json({ error: 'message is required' });
|
||||
res.status(400).json({ error: "message is required" });
|
||||
return;
|
||||
}
|
||||
const result = await aiProvider.processMessage(message, {
|
||||
userId: 'test-user',
|
||||
userId: "test-user",
|
||||
conversationHistory: [],
|
||||
existingEvents: [],
|
||||
currentDate: new Date(),
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('AI test error:', error);
|
||||
console.error("AI test error:", error);
|
||||
res.status(500).json({ error: String(error) });
|
||||
}
|
||||
});
|
||||
@@ -85,13 +95,13 @@ app.post('/api/ai/test', async (req, res) => {
|
||||
async function start() {
|
||||
try {
|
||||
await mongoose.connect(mongoUri);
|
||||
console.log('Connected to MongoDB');
|
||||
console.log("Connected to MongoDB");
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthService } from '../services';
|
||||
import { Request, Response } from "express";
|
||||
import { AuthService } from "../services";
|
||||
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
@@ -23,10 +23,10 @@ export class AuthController {
|
||||
}
|
||||
|
||||
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> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Response } from 'express';
|
||||
import { SendMessageDTO, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared';
|
||||
import { ChatService } from '../services';
|
||||
import { AuthenticatedRequest } from '../middleware';
|
||||
import { Response } from "express";
|
||||
import {
|
||||
SendMessageDTO,
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
EventAction,
|
||||
} from "@caldav/shared";
|
||||
import { ChatService } from "../services";
|
||||
import { AuthenticatedRequest } from "../middleware";
|
||||
|
||||
export class ChatController {
|
||||
constructor(private chatService: ChatService) {}
|
||||
@@ -13,7 +18,7 @@ export class ChatController {
|
||||
const response = await this.chatService.processMessage(userId, data);
|
||||
res.json(response);
|
||||
} 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,
|
||||
event,
|
||||
eventId,
|
||||
updates
|
||||
updates,
|
||||
);
|
||||
res.json(response);
|
||||
} 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 {
|
||||
const userId = req.user!.userId;
|
||||
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);
|
||||
} 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> {
|
||||
throw new Error('Not implemented');
|
||||
async getConversations(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async getConversation(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
async getConversation(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { Response } from 'express';
|
||||
import { EventService } from '../services';
|
||||
import { AuthenticatedRequest } from '../middleware';
|
||||
import { Response } from "express";
|
||||
import { EventService } from "../services";
|
||||
import { AuthenticatedRequest } from "../middleware";
|
||||
|
||||
export class EventController {
|
||||
constructor(private eventService: EventService) {}
|
||||
|
||||
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> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
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> {
|
||||
throw new Error('Not implemented');
|
||||
async getByDateRange(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
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> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './AuthController';
|
||||
export * from './ChatController';
|
||||
export * from './EventController';
|
||||
export * from "./AuthController";
|
||||
export * from "./ChatController";
|
||||
export * from "./EventController";
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyToken, TokenPayload } from '../utils/jwt';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { verifyToken, TokenPayload } from "../utils/jwt";
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
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
|
||||
// Fake user for testing purposes
|
||||
req.user = {
|
||||
userId: 'fake-user-id',
|
||||
email: 'test@example.com',
|
||||
userId: "fake-user-id",
|
||||
email: "test@example.com",
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './AuthMiddleware';
|
||||
export * from "./AuthMiddleware";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './mongo';
|
||||
export * from "./mongo";
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared';
|
||||
import { ChatRepository } from '../../services/interfaces';
|
||||
import { ChatMessageModel, ConversationModel } from './models';
|
||||
import {
|
||||
ChatMessage,
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
} from "@caldav/shared";
|
||||
import { ChatRepository } from "../../services/interfaces";
|
||||
import { ChatMessageModel, ConversationModel } from "./models";
|
||||
|
||||
export class MongoChatRepository implements ChatRepository {
|
||||
// Conversations
|
||||
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async createConversation(userId: string): Promise<Conversation> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
// Messages (cursor-based pagination)
|
||||
async getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]> {
|
||||
throw new Error('Not implemented');
|
||||
async getMessages(
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage> {
|
||||
throw new Error('Not implemented');
|
||||
async createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
|
||||
import { EventRepository } from '../../services/interfaces';
|
||||
import { EventModel } from './models';
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
||||
import { EventRepository } from "../../services/interfaces";
|
||||
import { EventModel } from "./models";
|
||||
|
||||
export class MongoEventRepository implements EventRepository {
|
||||
async findById(id: string): Promise<CalendarEvent | null> {
|
||||
@@ -11,15 +11,19 @@ export class MongoEventRepository implements EventRepository {
|
||||
|
||||
async findByUserId(userId: string): Promise<CalendarEvent[]> {
|
||||
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({
|
||||
userId,
|
||||
startTime: { $gte: startDate, $lte: endDate }
|
||||
startTime: { $gte: startDate, $lte: endDate },
|
||||
}).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> {
|
||||
@@ -28,7 +32,10 @@ export class MongoEventRepository implements EventRepository {
|
||||
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 });
|
||||
if (!event) return null;
|
||||
return event.toJSON() as unknown as CalendarEvent;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { User } from '@caldav/shared';
|
||||
import { UserRepository, CreateUserData } from '../../services/interfaces';
|
||||
import { UserModel } from './models';
|
||||
import { User } from "@caldav/shared";
|
||||
import { UserRepository, CreateUserData } from "../../services/interfaces";
|
||||
import { UserModel } from "./models";
|
||||
|
||||
export class MongoUserRepository implements UserRepository {
|
||||
async findById(id: string): Promise<User | null> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './MongoUserRepository';
|
||||
export * from './MongoEventRepository';
|
||||
export * from './MongoChatRepository';
|
||||
export * from "./MongoUserRepository";
|
||||
export * from "./MongoEventRepository";
|
||||
export * from "./MongoChatRepository";
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared';
|
||||
import { IdVirtual } from './types';
|
||||
import mongoose, { Schema, Document, Model } from "mongoose";
|
||||
import {
|
||||
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;
|
||||
}
|
||||
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {
|
||||
export interface ConversationDocument
|
||||
extends Omit<Conversation, "id">, Document {
|
||||
toJSON(): Conversation;
|
||||
}
|
||||
|
||||
@@ -19,7 +26,7 @@ const EventSchema = new Schema<CreateEventDTO>(
|
||||
isRecurring: { type: Boolean },
|
||||
recurrenceRule: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const UpdatesSchema = new Schema<UpdateEventDTO>(
|
||||
@@ -32,20 +39,30 @@ const UpdatesSchema = new Schema<UpdateEventDTO>(
|
||||
isRecurring: { type: Boolean },
|
||||
recurrenceRule: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
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 },
|
||||
event: { type: EventSchema },
|
||||
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: {
|
||||
type: String,
|
||||
@@ -53,7 +70,7 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
|
||||
},
|
||||
sender: {
|
||||
type: String,
|
||||
enum: ['user', 'assistant'],
|
||||
enum: ["user", "assistant"],
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
@@ -81,10 +98,16 @@ const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocum
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||
const ConversationSchema = new Schema<
|
||||
ConversationDocument,
|
||||
Model<ConversationDocument, {}, {}, IdVirtual>,
|
||||
{},
|
||||
{},
|
||||
IdVirtual
|
||||
>(
|
||||
{
|
||||
userId: {
|
||||
type: String,
|
||||
@@ -109,8 +132,14 @@ const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDo
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const ChatMessageModel = mongoose.model<ChatMessageDocument>('ChatMessage', ChatMessageSchema);
|
||||
export const ConversationModel = mongoose.model<ConversationDocument>('Conversation', ConversationSchema);
|
||||
export const ChatMessageModel = mongoose.model<ChatMessageDocument>(
|
||||
"ChatMessage",
|
||||
ChatMessageSchema,
|
||||
);
|
||||
export const ConversationModel = mongoose.model<ConversationDocument>(
|
||||
"Conversation",
|
||||
ConversationSchema,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||
import { CalendarEvent } from '@caldav/shared';
|
||||
import { IdVirtual } from './types';
|
||||
import mongoose, { Schema, Document, Model } from "mongoose";
|
||||
import { CalendarEvent } from "@caldav/shared";
|
||||
import { IdVirtual } from "./types";
|
||||
|
||||
export interface EventDocument extends Omit<CalendarEvent, 'id'>, Document {
|
||||
export interface EventDocument extends Omit<CalendarEvent, "id">, Document {
|
||||
toJSON(): CalendarEvent;
|
||||
}
|
||||
|
||||
const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||
const EventSchema = new Schema<
|
||||
EventDocument,
|
||||
Model<EventDocument, {}, {}, IdVirtual>,
|
||||
{},
|
||||
{},
|
||||
IdVirtual
|
||||
>(
|
||||
{
|
||||
userId: {
|
||||
type: String,
|
||||
@@ -58,9 +64,9 @@ const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVir
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
EventSchema.index({ userId: 1, startTime: 1, endTime: 1 });
|
||||
|
||||
export const EventModel = mongoose.model<EventDocument>('Event', EventSchema);
|
||||
export const EventModel = mongoose.model<EventDocument>("Event", EventSchema);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||
import { User } from '@caldav/shared';
|
||||
import { IdVirtual } from './types';
|
||||
import mongoose, { Schema, Document, Model } from "mongoose";
|
||||
import { User } from "@caldav/shared";
|
||||
import { IdVirtual } from "./types";
|
||||
|
||||
export interface UserDocument extends Omit<User, 'id'>, Document {
|
||||
export interface UserDocument extends Omit<User, "id">, Document {
|
||||
toJSON(): User;
|
||||
}
|
||||
|
||||
const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||
const UserSchema = new Schema<
|
||||
UserDocument,
|
||||
Model<UserDocument, {}, {}, IdVirtual>,
|
||||
{},
|
||||
{},
|
||||
IdVirtual
|
||||
>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
@@ -43,7 +49,7 @@ const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtua
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const UserModel = mongoose.model<UserDocument>('User', UserSchema);
|
||||
export const UserModel = mongoose.model<UserDocument>("User", UserSchema);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './UserModel';
|
||||
export * from './EventModel';
|
||||
export * from './ChatModel';
|
||||
export * from "./UserModel";
|
||||
export * from "./EventModel";
|
||||
export * from "./ChatModel";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { AuthController } from '../controllers';
|
||||
import { Router } from "express";
|
||||
import { AuthController } from "../controllers";
|
||||
|
||||
export function createAuthRoutes(authController: AuthController): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', (req, res) => authController.login(req, res));
|
||||
router.post('/register', (req, res) => authController.register(req, res));
|
||||
router.post('/refresh', (req, res) => authController.refresh(req, res));
|
||||
router.post('/logout', (req, res) => authController.logout(req, res));
|
||||
router.post("/login", (req, res) => authController.login(req, res));
|
||||
router.post("/register", (req, res) => authController.register(req, res));
|
||||
router.post("/refresh", (req, res) => authController.refresh(req, res));
|
||||
router.post("/logout", (req, res) => authController.logout(req, res));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import { ChatController } from '../controllers';
|
||||
import { authenticate } from '../middleware';
|
||||
import { Router } from "express";
|
||||
import { ChatController } from "../controllers";
|
||||
import { authenticate } from "../middleware";
|
||||
|
||||
export function createChatRoutes(chatController: ChatController): Router {
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.post('/message', (req, res) => chatController.sendMessage(req, res));
|
||||
router.post('/confirm/:conversationId/:messageId', (req, res) => chatController.confirmEvent(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));
|
||||
router.post("/message", (req, res) => chatController.sendMessage(req, res));
|
||||
router.post("/confirm/:conversationId/:messageId", (req, res) =>
|
||||
chatController.confirmEvent(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;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Router } from 'express';
|
||||
import { EventController } from '../controllers';
|
||||
import { authenticate } from '../middleware';
|
||||
import { Router } from "express";
|
||||
import { EventController } from "../controllers";
|
||||
import { authenticate } from "../middleware";
|
||||
|
||||
export function createEventRoutes(eventController: EventController): Router {
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.post('/', (req, res) => eventController.create(req, res));
|
||||
router.get('/', (req, res) => eventController.getAll(req, res));
|
||||
router.get('/range', (req, res) => eventController.getByDateRange(req, res));
|
||||
router.get('/:id', (req, res) => eventController.getById(req, res));
|
||||
router.put('/:id', (req, res) => eventController.update(req, res));
|
||||
router.delete('/:id', (req, res) => eventController.delete(req, res));
|
||||
router.post("/", (req, res) => eventController.create(req, res));
|
||||
router.get("/", (req, res) => eventController.getAll(req, res));
|
||||
router.get("/range", (req, res) => eventController.getByDateRange(req, res));
|
||||
router.get("/:id", (req, res) => eventController.getById(req, res));
|
||||
router.put("/:id", (req, res) => eventController.update(req, res));
|
||||
router.delete("/:id", (req, res) => eventController.delete(req, res));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import { createAuthRoutes } from './auth.routes';
|
||||
import { createChatRoutes } from './chat.routes';
|
||||
import { createEventRoutes } from './event.routes';
|
||||
import { AuthController, ChatController, EventController } from '../controllers';
|
||||
import { Router } from "express";
|
||||
import { createAuthRoutes } from "./auth.routes";
|
||||
import { createChatRoutes } from "./chat.routes";
|
||||
import { createEventRoutes } from "./event.routes";
|
||||
import {
|
||||
AuthController,
|
||||
ChatController,
|
||||
EventController,
|
||||
} from "../controllers";
|
||||
|
||||
export interface Controllers {
|
||||
authController: AuthController;
|
||||
@@ -13,13 +17,13 @@ export interface Controllers {
|
||||
export function createRoutes(controllers: Controllers): Router {
|
||||
const router = Router();
|
||||
|
||||
router.use('/auth', createAuthRoutes(controllers.authController));
|
||||
router.use('/chat', createChatRoutes(controllers.chatController));
|
||||
router.use('/events', createEventRoutes(controllers.eventController));
|
||||
router.use("/auth", createAuthRoutes(controllers.authController));
|
||||
router.use("/chat", createChatRoutes(controllers.chatController));
|
||||
router.use("/events", createEventRoutes(controllers.eventController));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
export * from './auth.routes';
|
||||
export * from './chat.routes';
|
||||
export * from './event.routes';
|
||||
export * from "./auth.routes";
|
||||
export * from "./chat.routes";
|
||||
export * from "./event.routes";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { User, CreateUserDTO, LoginDTO, AuthResponse } from '@caldav/shared';
|
||||
import { UserRepository } from './interfaces';
|
||||
import * as jwt from '../utils/jwt';
|
||||
import * as password from '../utils/password';
|
||||
import { User, CreateUserDTO, LoginDTO, AuthResponse } from "@caldav/shared";
|
||||
import { UserRepository } from "./interfaces";
|
||||
import * as jwt from "../utils/jwt";
|
||||
import * as password from "../utils/password";
|
||||
|
||||
export class AuthService {
|
||||
constructor(private userRepo: UserRepository) {}
|
||||
@@ -9,21 +9,21 @@ export class AuthService {
|
||||
async login(data: LoginDTO): Promise<AuthResponse> {
|
||||
const user = await this.userRepo.findByEmail(data.email);
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new Error('Invalid credentials');
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const isValid = await password.compare(data.password, user.passwordHash);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid credentials');
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return { user, accessToken: '' };
|
||||
return { user, accessToken: "" };
|
||||
}
|
||||
|
||||
async register(data: CreateUserDTO): Promise<AuthResponse> {
|
||||
const existingUser = await this.userRepo.findByEmail(data.email);
|
||||
if (existingUser) {
|
||||
throw new Error('Email already exists');
|
||||
throw new Error("Email already exists");
|
||||
}
|
||||
|
||||
const passwordHash = await password.hash(data.password);
|
||||
@@ -33,14 +33,14 @@ export class AuthService {
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
return { user, accessToken: '' };
|
||||
return { user, accessToken: "" };
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthResponse> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async logout(userId: string): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay, CreateEventDTO, UpdateEventDTO, EventAction } from '@caldav/shared';
|
||||
import { ChatRepository, EventRepository, AIProvider } from './interfaces';
|
||||
import { getWeeksOverview, getMonthOverview } from '../utils/eventFormatters';
|
||||
import {
|
||||
ChatMessage,
|
||||
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 };
|
||||
|
||||
@@ -9,139 +20,147 @@ let responseIndex = 8;
|
||||
|
||||
// Static test responses (event proposals)
|
||||
const staticResponses: TestResponse[] = [
|
||||
// {{{
|
||||
// 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:",
|
||||
proposedChange: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Meeting mit Jens",
|
||||
startTime: getDay('Friday', 1, 14, 0),
|
||||
endTime: getDay('Friday', 1, 15, 0),
|
||||
description: "Arbeitstreffen",
|
||||
}
|
||||
}
|
||||
// {{{
|
||||
// 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:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Meeting mit Jens",
|
||||
startTime: getDay("Friday", 1, 14, 0),
|
||||
endTime: getDay("Friday", 1, 15, 0),
|
||||
description: "Arbeitstreffen",
|
||||
},
|
||||
},
|
||||
// Response 1: Recurring event - every Saturday 10:00
|
||||
{
|
||||
content: "Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:",
|
||||
proposedChange: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Badezimmer putzen",
|
||||
startTime: getDay('Saturday', 1, 10, 0),
|
||||
endTime: getDay('Saturday', 1, 11, 0),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=SA",
|
||||
}
|
||||
}
|
||||
},
|
||||
// Response 1: Recurring event - every Saturday 10:00
|
||||
{
|
||||
content:
|
||||
"Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Badezimmer putzen",
|
||||
startTime: getDay("Saturday", 1, 10, 0),
|
||||
endTime: getDay("Saturday", 1, 11, 0),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=SA",
|
||||
},
|
||||
},
|
||||
// Response 2: 2-week overview (DYNAMIC - placeholder)
|
||||
{ content: '' },
|
||||
// Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder)
|
||||
{ content: '' },
|
||||
// Response 4: Doctor appointment with description
|
||||
{
|
||||
content: "Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
|
||||
proposedChange: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Arzttermin Dr. Müller",
|
||||
startTime: getDay('Wednesday', 1, 9, 30),
|
||||
endTime: getDay('Wednesday', 1, 10, 30),
|
||||
description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
|
||||
}
|
||||
}
|
||||
},
|
||||
// Response 2: 2-week overview (DYNAMIC - placeholder)
|
||||
{ content: "" },
|
||||
// Response 3: Delete "Meeting mit Jens" (DYNAMIC - placeholder)
|
||||
{ content: "" },
|
||||
// Response 4: Doctor appointment with description
|
||||
{
|
||||
content:
|
||||
"Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Arzttermin Dr. Müller",
|
||||
startTime: getDay("Wednesday", 1, 9, 30),
|
||||
endTime: getDay("Wednesday", 1, 10, 30),
|
||||
description: "Routineuntersuchung - Versichertenkarte nicht vergessen",
|
||||
},
|
||||
},
|
||||
// Response 5: Birthday - yearly recurring
|
||||
{
|
||||
content: "Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:",
|
||||
proposedChange: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Mamas Geburtstag",
|
||||
startTime: getDay('Thursday', 2, 0, 0),
|
||||
endTime: getDay('Thursday', 2, 23, 59),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=YEARLY",
|
||||
}
|
||||
}
|
||||
},
|
||||
// Response 5: Birthday - yearly recurring
|
||||
{
|
||||
content:
|
||||
"Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Mamas Geburtstag",
|
||||
startTime: getDay("Thursday", 2, 0, 0),
|
||||
endTime: getDay("Thursday", 2, 23, 59),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=YEARLY",
|
||||
},
|
||||
},
|
||||
// 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: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Fitnessstudio Probetraining",
|
||||
startTime: getDay('Tuesday', 1, 18, 0),
|
||||
endTime: getDay('Tuesday', 1, 19, 30),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8",
|
||||
}
|
||||
}
|
||||
},
|
||||
// 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: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Fitnessstudio Probetraining",
|
||||
startTime: getDay("Tuesday", 1, 18, 0),
|
||||
endTime: getDay("Tuesday", 1, 19, 30),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8",
|
||||
},
|
||||
},
|
||||
// Response 7: 1-week overview (DYNAMIC - placeholder)
|
||||
{ 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" +
|
||||
"• \"Was habe ich nächste Woche vor?\"\n" +
|
||||
"• \"Verschiebe das Meeting auf Donnerstag\"\n\n" +
|
||||
"Wie kann ich dir helfen?",
|
||||
},
|
||||
// Response 7: 1-week overview (DYNAMIC - placeholder)
|
||||
{ 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' +
|
||||
'• "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
|
||||
{
|
||||
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 10: Update "Telefonat mit Mama" +2 days (DYNAMIC - placeholder)
|
||||
{ content: "" },
|
||||
// Response 11: Birthday party - evening event
|
||||
{
|
||||
content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
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 11: Birthday party - evening event
|
||||
{
|
||||
content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!",
|
||||
proposedChange: {
|
||||
action: 'create',
|
||||
event: {
|
||||
title: "Geburtstagsfeier Lisa",
|
||||
startTime: getDay('Saturday', 2, 19, 0),
|
||||
endTime: getDay('Saturday', 2, 23, 0),
|
||||
description: "Geschenk: Buch über Fotografie",
|
||||
}
|
||||
}
|
||||
},
|
||||
// Response 12: Language course - limited to 8 weeks
|
||||
{
|
||||
content:
|
||||
"Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
|
||||
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 12: Language course - limited to 8 weeks
|
||||
{
|
||||
content: "Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag für die nächsten 8 Wochen:",
|
||||
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: '' },
|
||||
// }}}
|
||||
},
|
||||
// Response 13: Monthly overview (DYNAMIC - placeholder)
|
||||
{ content: "" },
|
||||
// }}}
|
||||
];
|
||||
|
||||
async function getTestResponse(
|
||||
index: number,
|
||||
eventRepo: EventRepository,
|
||||
userId: string
|
||||
userId: string,
|
||||
): Promise<TestResponse> {
|
||||
const responseIdx = index % staticResponses.length;
|
||||
|
||||
@@ -153,14 +172,15 @@ async function getTestResponse(
|
||||
if (responseIdx === 3) {
|
||||
// Delete "Meeting mit Jens"
|
||||
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) {
|
||||
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: {
|
||||
action: 'delete',
|
||||
action: "delete",
|
||||
eventId: jensEvent.id,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return { content: "Ich konnte keinen Termin 'Meeting mit Jens' finden." };
|
||||
@@ -173,16 +193,17 @@ async function getTestResponse(
|
||||
if (responseIdx === 10) {
|
||||
// Update "Telefonat mit Mama" +2 days
|
||||
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) {
|
||||
const newStart = new Date(mamaEvent.startTime);
|
||||
newStart.setDate(newStart.getDate() + 2);
|
||||
const newEnd = new Date(mamaEvent.endTime);
|
||||
newEnd.setDate(newEnd.getDate() + 2);
|
||||
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: {
|
||||
action: 'update',
|
||||
action: "update",
|
||||
eventId: mamaEvent.id,
|
||||
updates: { startTime: newStart, endTime: newEnd },
|
||||
// Include event with new times for display
|
||||
@@ -191,8 +212,8 @@ async function getTestResponse(
|
||||
startTime: newStart,
|
||||
endTime: newEnd,
|
||||
description: mamaEvent.description,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return { content: "Ich konnte keinen Termin 'Telefonat mit Mama' finden." };
|
||||
@@ -200,7 +221,14 @@ async function getTestResponse(
|
||||
|
||||
if (responseIdx === 13) {
|
||||
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];
|
||||
@@ -210,17 +238,24 @@ export class ChatService {
|
||||
constructor(
|
||||
private chatRepo: ChatRepository,
|
||||
private eventRepo: EventRepository,
|
||||
private aiProvider: AIProvider
|
||||
private aiProvider: AIProvider,
|
||||
) {}
|
||||
|
||||
async processMessage(userId: string, data: SendMessageDTO): Promise<ChatResponse> {
|
||||
const response = await getTestResponse(responseIndex, this.eventRepo, userId);
|
||||
async processMessage(
|
||||
userId: string,
|
||||
data: SendMessageDTO,
|
||||
): Promise<ChatResponse> {
|
||||
const response = await getTestResponse(
|
||||
responseIndex,
|
||||
this.eventRepo,
|
||||
userId,
|
||||
);
|
||||
responseIndex++;
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId: data.conversationId || 'temp-conv-id',
|
||||
sender: 'assistant',
|
||||
conversationId: data.conversationId || "temp-conv-id",
|
||||
sender: "assistant",
|
||||
content: response.content,
|
||||
proposedChange: response.proposedChange,
|
||||
};
|
||||
@@ -235,49 +270,57 @@ export class ChatService {
|
||||
action: EventAction,
|
||||
event?: CreateEventDTO,
|
||||
eventId?: string,
|
||||
updates?: UpdateEventDTO
|
||||
updates?: UpdateEventDTO,
|
||||
): Promise<ChatResponse> {
|
||||
let content: string;
|
||||
|
||||
if (action === 'create' && event) {
|
||||
if (action === "create" && event) {
|
||||
const createdEvent = await this.eventRepo.create(userId, event);
|
||||
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);
|
||||
content = updatedEvent
|
||||
? `Der Termin "${updatedEvent.title}" wurde aktualisiert.`
|
||||
: 'Termin nicht gefunden.';
|
||||
} else if (action === 'delete' && eventId) {
|
||||
: "Termin nicht gefunden.";
|
||||
} else if (action === "delete" && eventId) {
|
||||
await this.eventRepo.delete(eventId);
|
||||
content = 'Der Termin wurde gelöscht.';
|
||||
content = "Der Termin wurde gelöscht.";
|
||||
} else {
|
||||
content = 'Ungültige Aktion.';
|
||||
content = "Ungültige Aktion.";
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
sender: 'assistant',
|
||||
sender: "assistant",
|
||||
content,
|
||||
};
|
||||
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 = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
sender: 'assistant',
|
||||
content: 'Der Vorschlag wurde abgelehnt.',
|
||||
sender: "assistant",
|
||||
content: "Der Vorschlag wurde abgelehnt.",
|
||||
};
|
||||
return { message, conversationId };
|
||||
}
|
||||
|
||||
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[]> {
|
||||
throw new Error('Not implemented');
|
||||
async getConversation(
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
|
||||
import { EventRepository } from './interfaces';
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
||||
import { EventRepository } from "./interfaces";
|
||||
|
||||
export class EventService {
|
||||
constructor(private eventRepo: EventRepository) {}
|
||||
|
||||
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> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
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[]> {
|
||||
throw new Error('Not implemented');
|
||||
async getByDateRange(
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<CalendarEvent[]> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async update(id: string, userId: string, data: UpdateEventDTO): Promise<CalendarEvent | null> {
|
||||
throw new Error('Not implemented');
|
||||
async update(
|
||||
id: string,
|
||||
userId: string,
|
||||
data: UpdateEventDTO,
|
||||
): Promise<CalendarEvent | null> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async delete(id: string, userId: string): Promise<boolean> {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './AuthService';
|
||||
export * from './ChatService';
|
||||
export * from './EventService';
|
||||
export * from './interfaces';
|
||||
export * from "./AuthService";
|
||||
export * from "./ChatService";
|
||||
export * from "./EventService";
|
||||
export * from "./interfaces";
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CalendarEvent, ChatMessage, ProposedEventChange } from '@caldav/shared';
|
||||
import {
|
||||
CalendarEvent,
|
||||
ChatMessage,
|
||||
ProposedEventChange,
|
||||
} from "@caldav/shared";
|
||||
|
||||
export interface AIContext {
|
||||
userId: string;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChatMessage, Conversation, CreateMessageDTO, GetMessagesOptions } from '@caldav/shared';
|
||||
import {
|
||||
ChatMessage,
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
} from "@caldav/shared";
|
||||
|
||||
export interface ChatRepository {
|
||||
// Conversations
|
||||
@@ -6,6 +11,12 @@ export interface ChatRepository {
|
||||
createConversation(userId: string): Promise<Conversation>;
|
||||
|
||||
// Messages (cursor-based pagination)
|
||||
getMessages(conversationId: string, options?: GetMessagesOptions): Promise<ChatMessage[]>;
|
||||
createMessage(conversationId: string, message: CreateMessageDTO): Promise<ChatMessage>;
|
||||
getMessages(
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]>;
|
||||
createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from '@caldav/shared';
|
||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
||||
|
||||
export interface EventRepository {
|
||||
findById(id: string): Promise<CalendarEvent | null>;
|
||||
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>;
|
||||
update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>;
|
||||
delete(id: string): Promise<boolean>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@caldav/shared';
|
||||
import { User } from "@caldav/shared";
|
||||
|
||||
export interface CreateUserData {
|
||||
email: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './AIProvider';
|
||||
export * from './UserRepository';
|
||||
export * from './EventRepository';
|
||||
export * from './ChatRepository';
|
||||
export * from "./AIProvider";
|
||||
export * from "./UserRepository";
|
||||
export * from "./EventRepository";
|
||||
export * from "./ChatRepository";
|
||||
|
||||
@@ -4,34 +4,37 @@ import {
|
||||
DAY_TO_GERMAN,
|
||||
DAY_TO_GERMAN_SHORT,
|
||||
MONTH_TO_GERMAN,
|
||||
} from '@caldav/shared';
|
||||
import { EventRepository } from '../services/interfaces';
|
||||
import { expandRecurringEvents, ExpandedEvent } from './recurrenceExpander';
|
||||
} from "@caldav/shared";
|
||||
import { EventRepository } from "../services/interfaces";
|
||||
import { expandRecurringEvents, ExpandedEvent } from "./recurrenceExpander";
|
||||
|
||||
// Private formatting helpers
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function formatDateShort(date: Date): string {
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
return `${day}.${month}.`;
|
||||
}
|
||||
|
||||
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;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
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 {
|
||||
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) {
|
||||
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(`\nInsgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'}.`);
|
||||
return lines.join('\n');
|
||||
lines.push(
|
||||
`\nInsgesamt ${events.length} Termin${events.length === 1 ? "" : "e"}.`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatMonthText(events: ExpandedEvent[], monthName: string): string {
|
||||
@@ -66,13 +71,17 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
|
||||
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
|
||||
const sortedWeeks = Array.from(weekGroups.keys()).sort((a, b) => a - b);
|
||||
for (const weekNum of sortedWeeks) {
|
||||
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) {
|
||||
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);
|
||||
lines.push(` • ${weekdayShort} ${dateStr}, ${timeStr}: ${event.title}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push(`Insgesamt ${events.length} Termin${events.length === 1 ? '' : 'e'} im ${monthName}.`);
|
||||
return lines.join('\n');
|
||||
lines.push(
|
||||
`Insgesamt ${events.length} Termin${events.length === 1 ? "" : "e"} im ${monthName}.`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Public API
|
||||
@@ -97,7 +108,7 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
|
||||
export async function getWeeksOverview(
|
||||
eventRepo: EventRepository,
|
||||
userId: string,
|
||||
weeks: number
|
||||
weeks: number,
|
||||
): Promise<string> {
|
||||
const now = new Date();
|
||||
const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000);
|
||||
@@ -114,7 +125,7 @@ export async function getMonthOverview(
|
||||
eventRepo: EventRepository,
|
||||
userId: string,
|
||||
year: number,
|
||||
month: number
|
||||
month: number,
|
||||
): Promise<string> {
|
||||
const startOfMonth = new Date(year, month, 1);
|
||||
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './jwt';
|
||||
export * from './password';
|
||||
export * from "./jwt";
|
||||
export * from "./password";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export interface TokenPayload {
|
||||
userId: string;
|
||||
@@ -10,17 +10,17 @@ export interface JWTConfig {
|
||||
expiresIn: string;
|
||||
}
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
|
||||
|
||||
export function signToken(payload: TokenPayload): string {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): TokenPayload {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
export function decodeToken(token: string): TokenPayload | null {
|
||||
throw new Error('Not implemented');
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
@@ -6,6 +6,9 @@ export async function hash(password: string): Promise<string> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RRule, rrulestr } from 'rrule';
|
||||
import { CalendarEvent } from '@caldav/shared';
|
||||
import { RRule, rrulestr } from "rrule";
|
||||
import { CalendarEvent } from "@caldav/shared";
|
||||
|
||||
export interface ExpandedEvent extends CalendarEvent {
|
||||
occurrenceStart: Date;
|
||||
@@ -9,14 +9,16 @@ export interface ExpandedEvent extends CalendarEvent {
|
||||
// Convert local time to "fake UTC" for rrule
|
||||
// rrule interprets all dates as UTC internally, so we need to trick it
|
||||
function toRRuleDate(date: Date): Date {
|
||||
return new Date(Date.UTC(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
date.getSeconds()
|
||||
));
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
date.getSeconds(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert rrule result back to local time
|
||||
@@ -27,7 +29,7 @@ function fromRRuleDate(date: Date): Date {
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds()
|
||||
date.getUTCSeconds(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@ function fromRRuleDate(date: Date): Date {
|
||||
export function expandRecurringEvents(
|
||||
events: CalendarEvent[],
|
||||
rangeStart: Date,
|
||||
rangeEnd: Date
|
||||
rangeEnd: Date,
|
||||
): ExpandedEvent[] {
|
||||
const expanded: ExpandedEvent[] = [];
|
||||
|
||||
@@ -61,13 +63,15 @@ export function expandRecurringEvents(
|
||||
|
||||
// Recurring event: parse RRULE and expand
|
||||
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)
|
||||
const occurrences = rule.between(
|
||||
toRRuleDate(rangeStart),
|
||||
toRRuleDate(rangeEnd),
|
||||
true // inclusive
|
||||
true, // inclusive
|
||||
);
|
||||
|
||||
for (const occurrence of occurrences) {
|
||||
@@ -82,7 +86,10 @@ export function expandRecurringEvents(
|
||||
}
|
||||
} catch (error) {
|
||||
// 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({
|
||||
...event,
|
||||
occurrenceStart: startTime,
|
||||
@@ -92,7 +99,9 @@ export function expandRecurringEvents(
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -100,10 +109,10 @@ export function expandRecurringEvents(
|
||||
// Format date as RRULE DTSTART string (YYYYMMDDTHHMMSS)
|
||||
function formatRRuleDateString(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user