implement event persistence and improve Mongoose TypeScript patterns
- Add event persistence: confirmed events are now saved to MongoDB - Refactor Mongoose models to use virtuals for id field with IdVirtual interface - Update repositories to use toJSON() with consistent type casting - Add more test responses for chat (doctor, birthday, gym, etc.) - Show event description in ProposedEventCard - Change mongo-express port to 8083 - Update CLAUDE.md with Mongoose model pattern documentation
This commit is contained in:
42
CLAUDE.md
42
CLAUDE.md
@@ -124,6 +124,7 @@ src/
|
|||||||
│ ├── index.ts # Re-exports from ./mongo
|
│ ├── index.ts # Re-exports from ./mongo
|
||||||
│ └── mongo/ # MongoDB implementation
|
│ └── mongo/ # MongoDB implementation
|
||||||
│ ├── models/ # Mongoose schemas
|
│ ├── models/ # Mongoose schemas
|
||||||
|
│ │ ├── types.ts # Shared types (IdVirtual interface)
|
||||||
│ │ ├── UserModel.ts
|
│ │ ├── UserModel.ts
|
||||||
│ │ ├── EventModel.ts
|
│ │ ├── EventModel.ts
|
||||||
│ │ └── ChatModel.ts
|
│ │ └── ChatModel.ts
|
||||||
@@ -193,6 +194,35 @@ The repository pattern allows swapping databases:
|
|||||||
- **Implementations** (`repositories/mongo/`) are DB-specific
|
- **Implementations** (`repositories/mongo/`) are DB-specific
|
||||||
- To add MySQL: create `repositories/mysql/` with TypeORM entities
|
- To add MySQL: create `repositories/mysql/` with TypeORM entities
|
||||||
|
|
||||||
|
### Mongoose Model Pattern
|
||||||
|
|
||||||
|
All Mongoose models use a consistent pattern for TypeScript-safe `id` virtuals:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IdVirtual } from './types';
|
||||||
|
|
||||||
|
const Schema = new Schema<Doc, Model<Doc, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||||
|
{ /* fields */ },
|
||||||
|
{
|
||||||
|
virtuals: {
|
||||||
|
id: {
|
||||||
|
get() { return this._id.toString(); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toJSON: {
|
||||||
|
virtuals: true,
|
||||||
|
transform: (_, ret) => {
|
||||||
|
delete ret._id;
|
||||||
|
delete ret.__v;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Repositories use `doc.toJSON() as unknown as Type` casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output).
|
||||||
|
|
||||||
## MVP Feature Scope
|
## MVP Feature Scope
|
||||||
|
|
||||||
### Must-Have
|
### Must-Have
|
||||||
@@ -218,7 +248,7 @@ docker compose up -d # Start MongoDB + Mongo Express
|
|||||||
docker compose down # Stop services
|
docker compose down # Stop services
|
||||||
```
|
```
|
||||||
- MongoDB: `localhost:27017` (root/mongoose)
|
- MongoDB: `localhost:27017` (root/mongoose)
|
||||||
- Mongo Express UI: `localhost:8081` (admin/admin)
|
- Mongo Express UI: `localhost:8083` (admin/admin)
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
Server requires `.env` file in `apps/server/`:
|
Server requires `.env` file in `apps/server/`:
|
||||||
@@ -239,16 +269,16 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
|||||||
- `utils/jwt`: signToken() (verifyToken() pending)
|
- `utils/jwt`: signToken() (verifyToken() pending)
|
||||||
- `dotenv` integration for environment variables
|
- `dotenv` integration for environment variables
|
||||||
- `ChatController`: sendMessage(), confirmEvent(), rejectEvent()
|
- `ChatController`: sendMessage(), confirmEvent(), rejectEvent()
|
||||||
- `ChatService`: processMessage() with test responses, confirmEvent(), rejectEvent()
|
- `ChatService`: processMessage() with test responses, confirmEvent() saves events to DB, rejectEvent()
|
||||||
- **Stubbed (TODO):**
|
- **Stubbed (TODO):**
|
||||||
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
||||||
- `AuthController`: refresh(), logout()
|
- `AuthController`: refresh(), logout()
|
||||||
- `AuthService`: refreshToken()
|
- `AuthService`: refreshToken()
|
||||||
- `ChatController`: getConversations(), getConversation()
|
- `ChatController`: getConversations(), getConversation()
|
||||||
- `MongoChatRepository`: Database persistence for chat
|
- `MongoChatRepository`: Database persistence for chat
|
||||||
- All Event functionality
|
- `MongoEventRepository`: Only create() implemented, rest stubbed
|
||||||
- **Not started:**
|
- **Not started:**
|
||||||
- `EventController`, `EventService`, `MongoEventRepository`
|
- `EventController`, `EventService`
|
||||||
- `ClaudeAdapter` (AI integration - currently using test responses)
|
- `ClaudeAdapter` (AI integration - currently using test responses)
|
||||||
|
|
||||||
**Shared:** Types, DTOs, constants (Day, Month), and date utilities defined and exported.
|
**Shared:** Types, DTOs, constants (Day, Month), and date utilities defined and exported.
|
||||||
@@ -258,8 +288,8 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
|||||||
- Calendar screen has month navigation and grid display (partially functional)
|
- Calendar screen has month navigation and grid display (partially functional)
|
||||||
- Chat screen functional with FlashList, message sending, and event confirm/reject
|
- Chat screen functional with FlashList, message sending, and event confirm/reject
|
||||||
- `ApiClient`: get(), post() implemented
|
- `ApiClient`: get(), post() implemented
|
||||||
- `ChatService`: sendMessage(), confirmEvent(), rejectEvent() implemented
|
- `ChatService`: sendMessage(), confirmEvent(convId, msgId, event), rejectEvent() - confirmEvent sends CreateEventDTO in body
|
||||||
- `ProposedEventCard`: Displays proposed events with confirm/reject buttons, theming support
|
- `ProposedEventCard`: Displays proposed events (title, date, description, recurring indicator) with confirm/reject buttons
|
||||||
- `Themes.tsx`: Centralized color definitions including button colors
|
- `Themes.tsx`: Centralized color definitions including button colors
|
||||||
- Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
|
- Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
|
||||||
- Zustand stores (AuthStore, EventsStore) defined with `throw new Error('Not implemented')`
|
- Zustand stores (AuthStore, EventsStore) defined with `throw new Error('Not implemented')`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Header from "../../components/Header";
|
|||||||
import BaseBackground from "../../components/BaseBackground";
|
import BaseBackground from "../../components/BaseBackground";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { ChatService } from "../../services";
|
import { ChatService } from "../../services";
|
||||||
import { ProposedEventChange } from "@caldav/shared";
|
import { ProposedEventChange, CreateEventDTO } from "@caldav/shared";
|
||||||
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
||||||
|
|
||||||
// TODO: better shadows for everything
|
// TODO: better shadows for everything
|
||||||
@@ -39,7 +39,8 @@ const Chat = () => {
|
|||||||
const handleEventResponse = async (
|
const handleEventResponse = async (
|
||||||
action: "confirm" | "reject",
|
action: "confirm" | "reject",
|
||||||
messageId: string,
|
messageId: string,
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
event?: CreateEventDTO
|
||||||
) => {
|
) => {
|
||||||
// Mark message as responded (optimistic update)
|
// Mark message as responded (optimistic update)
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
@@ -50,8 +51,8 @@ const Chat = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response =
|
const response =
|
||||||
action === "confirm"
|
action === "confirm" && event
|
||||||
? await ChatService.confirmEvent(conversationId, messageId)
|
? await ChatService.confirmEvent(conversationId, messageId, event)
|
||||||
: await ChatService.rejectEvent(conversationId, messageId);
|
: await ChatService.rejectEvent(conversationId, messageId);
|
||||||
|
|
||||||
const botMessage: MessageData = {
|
const botMessage: MessageData = {
|
||||||
@@ -111,7 +112,7 @@ const Chat = () => {
|
|||||||
proposedChange={item.proposedChange}
|
proposedChange={item.proposedChange}
|
||||||
respondedAction={item.respondedAction}
|
respondedAction={item.respondedAction}
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
handleEventResponse("confirm", item.id, item.conversationId!)
|
handleEventResponse("confirm", item.id, item.conversationId!, item.proposedChange?.event)
|
||||||
}
|
}
|
||||||
onReject={() =>
|
onReject={() =>
|
||||||
handleEventResponse("reject", item.id, item.conversationId!)
|
handleEventResponse("reject", item.id, item.conversationId!)
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export const ProposedEventCard = ({
|
|||||||
<Text style={{ color: currentTheme.textSecondary }}>
|
<Text style={{ color: currentTheme.textSecondary }}>
|
||||||
{formatDateTime(event?.startTime)}
|
{formatDateTime(event?.startTime)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{event?.description && (
|
||||||
|
<Text style={{ color: currentTheme.textSecondary }} className="text-sm mt-1">
|
||||||
|
{event.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
{event?.isRecurring && (
|
{event?.isRecurring && (
|
||||||
<Text style={{ color: currentTheme.textMuted }} className="text-sm">
|
<Text style={{ color: currentTheme.textMuted }} className="text-sm">
|
||||||
Wiederkehrend
|
Wiederkehrend
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ChatMessage,
|
ChatMessage,
|
||||||
ConversationSummary,
|
ConversationSummary,
|
||||||
GetMessagesOptions,
|
GetMessagesOptions,
|
||||||
|
CreateEventDTO,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ApiClient } from "./ApiClient";
|
import { ApiClient } from "./ApiClient";
|
||||||
|
|
||||||
@@ -14,9 +15,10 @@ export const ChatService = {
|
|||||||
|
|
||||||
confirmEvent: async (
|
confirmEvent: async (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messageId: string
|
messageId: string,
|
||||||
|
event: CreateEventDTO
|
||||||
): Promise<ChatResponse> => {
|
): Promise<ChatResponse> => {
|
||||||
return ApiClient.post<ChatResponse>(`/chat/confirm/${conversationId}/${messageId}`);
|
return ApiClient.post<ChatResponse>(`/chat/confirm/${conversationId}/${messageId}`, event);
|
||||||
},
|
},
|
||||||
|
|
||||||
rejectEvent: async (
|
rejectEvent: async (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
image: mongo-express:latest
|
image: mongo-express:latest
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8083:8083"
|
- "8083:8081"
|
||||||
environment:
|
environment:
|
||||||
ME_CONFIG_MONGODB_URL: mongodb://root:mongoose@mongo:27017/
|
ME_CONFIG_MONGODB_URL: mongodb://root:mongoose@mongo:27017/
|
||||||
ME_CONFIG_BASICAUTH_ENABLED: true
|
ME_CONFIG_BASICAUTH_ENABLED: true
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { SendMessageDTO } from '@caldav/shared';
|
import { SendMessageDTO, CreateEventDTO } from '@caldav/shared';
|
||||||
import { ChatService } from '../services';
|
import { ChatService } from '../services';
|
||||||
import { AuthenticatedRequest } from '../middleware';
|
import { AuthenticatedRequest } from '../middleware';
|
||||||
|
|
||||||
@@ -21,7 +21,8 @@ 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.confirmEvent(userId, conversationId, messageId);
|
const event: CreateEventDTO = req.body;
|
||||||
|
const response = await this.chatService.confirmEvent(userId, conversationId, messageId, event);
|
||||||
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' });
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export class MongoEventRepository implements EventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
|
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
|
||||||
throw new Error('Not implemented');
|
const event = await EventModel.create({ userId, ...data });
|
||||||
|
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
|
||||||
|
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> {
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
import { User } from '@caldav/shared';
|
import { User } from '@caldav/shared';
|
||||||
import { UserRepository, CreateUserData } from '../../services/interfaces';
|
import { UserRepository, CreateUserData } from '../../services/interfaces';
|
||||||
import { UserModel, UserDocument } from './models';
|
import { UserModel } from './models';
|
||||||
|
|
||||||
export class MongoUserRepository implements UserRepository {
|
export class MongoUserRepository implements UserRepository {
|
||||||
private toUser(doc: UserDocument): User {
|
|
||||||
return {
|
|
||||||
id: doc._id.toString(),
|
|
||||||
email: doc.email,
|
|
||||||
displayName: doc.displayName,
|
|
||||||
createdAt: doc.createdAt,
|
|
||||||
updatedAt: doc.updatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
||||||
const user = await UserModel.findOne({ email: email.toLowerCase() });
|
const user = await UserModel.findOne({ email: email.toLowerCase() });
|
||||||
return user ? this.toUser(user) : null;
|
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
|
||||||
|
return (user?.toJSON() as unknown as User) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateUserData): Promise<User> {
|
async create(data: CreateUserData): Promise<User> {
|
||||||
const user = await UserModel.create(data);
|
const user = await UserModel.create(data);
|
||||||
return this.toUser(user);
|
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field
|
||||||
|
return user.toJSON() as unknown as User;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||||
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared';
|
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 {
|
||||||
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {}
|
toJSON(): ChatMessage;
|
||||||
|
}
|
||||||
|
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {
|
||||||
|
toJSON(): Conversation;
|
||||||
|
}
|
||||||
|
|
||||||
const EventSchema = new Schema<CreateEventDTO>(
|
const EventSchema = new Schema<CreateEventDTO>(
|
||||||
{
|
{
|
||||||
@@ -40,7 +45,7 @@ const ProposedChangeSchema = new Schema<ProposedEventChange>(
|
|||||||
{ _id: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const ChatMessageSchema = new Schema<ChatMessageDocument>(
|
const ChatMessageSchema = new Schema<ChatMessageDocument, Model<ChatMessageDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||||
{
|
{
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -61,9 +66,16 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
virtuals: {
|
||||||
|
id: {
|
||||||
|
get() {
|
||||||
|
return this._id.toString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
toJSON: {
|
toJSON: {
|
||||||
|
virtuals: true,
|
||||||
transform: (_, ret: Record<string, unknown>) => {
|
transform: (_, ret: Record<string, unknown>) => {
|
||||||
ret.id = String(ret._id);
|
|
||||||
delete ret._id;
|
delete ret._id;
|
||||||
delete ret.__v;
|
delete ret.__v;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -72,7 +84,7 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const ConversationSchema = new Schema<ConversationDocument>(
|
const ConversationSchema = new Schema<ConversationDocument, Model<ConversationDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||||
{
|
{
|
||||||
userId: {
|
userId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -82,9 +94,16 @@ const ConversationSchema = new Schema<ConversationDocument>(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
virtuals: {
|
||||||
|
id: {
|
||||||
|
get() {
|
||||||
|
return this._id.toString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
toJSON: {
|
toJSON: {
|
||||||
|
virtuals: true,
|
||||||
transform: (_, ret: Record<string, unknown>) => {
|
transform: (_, ret: Record<string, unknown>) => {
|
||||||
ret.id = String(ret._id);
|
|
||||||
delete ret._id;
|
delete ret._id;
|
||||||
delete ret.__v;
|
delete ret.__v;
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||||
import { CalendarEvent } from '@caldav/shared';
|
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>(
|
const EventSchema = new Schema<EventDocument, Model<EventDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||||
{
|
{
|
||||||
userId: {
|
userId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -40,9 +43,16 @@ const EventSchema = new Schema<EventDocument>(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
virtuals: {
|
||||||
|
id: {
|
||||||
|
get() {
|
||||||
|
return this._id.toString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
toJSON: {
|
toJSON: {
|
||||||
|
virtuals: true,
|
||||||
transform: (_, ret: Record<string, unknown>) => {
|
transform: (_, ret: Record<string, unknown>) => {
|
||||||
ret.id = String(ret._id);
|
|
||||||
delete ret._id;
|
delete ret._id;
|
||||||
delete ret.__v;
|
delete ret.__v;
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||||
import { User } from '@caldav/shared';
|
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>(
|
const UserSchema = new Schema<UserDocument, Model<UserDocument, {}, {}, IdVirtual>, {}, {}, IdVirtual>(
|
||||||
{
|
{
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,9 +27,16 @@ const UserSchema = new Schema<UserDocument>(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
virtuals: {
|
||||||
|
id: {
|
||||||
|
get() {
|
||||||
|
return this._id.toString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
toJSON: {
|
toJSON: {
|
||||||
|
virtuals: true,
|
||||||
transform: (_, ret: Record<string, unknown>) => {
|
transform: (_, ret: Record<string, unknown>) => {
|
||||||
ret.id = String(ret._id);
|
|
||||||
delete ret._id;
|
delete ret._id;
|
||||||
delete ret.__v;
|
delete ret.__v;
|
||||||
delete ret.passwordHash;
|
delete ret.passwordHash;
|
||||||
|
|||||||
4
apps/server/src/repositories/mongo/models/types.ts
Normal file
4
apps/server/src/repositories/mongo/models/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Common virtual interface for all Mongoose models
|
||||||
|
export interface IdVirtual {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay } from '@caldav/shared';
|
import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay, CreateEventDTO } from '@caldav/shared';
|
||||||
import { ChatRepository, EventRepository, AIProvider } from './interfaces';
|
import { ChatRepository, EventRepository, AIProvider } from './interfaces';
|
||||||
|
|
||||||
// Test responses array (cycles through responses)
|
// Test responses array (cycles through responses)
|
||||||
@@ -41,6 +41,120 @@ const testResponses: Array<{ content: string; proposedChange?: ProposedEventChan
|
|||||||
"Samstag, 18.01. - 10:00 Uhr: Badezimmer putzen\n\n" +
|
"Samstag, 18.01. - 10:00 Uhr: Badezimmer putzen\n\n" +
|
||||||
"Insgesamt 3 Termine.",
|
"Insgesamt 3 Termine.",
|
||||||
},
|
},
|
||||||
|
// 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 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: Weekly calendar overview (text only)
|
||||||
|
{
|
||||||
|
content: "Hier ist dein Überblick für nächste Woche:\n\n" +
|
||||||
|
"Montag - Keine Termine\n" +
|
||||||
|
"Dienstag, 18:00 Uhr: Fitnessstudio\n" +
|
||||||
|
"Mittwoch, 09:30 Uhr: Arzttermin Dr. Müller\n" +
|
||||||
|
"Donnerstag - Keine Termine\n" +
|
||||||
|
"Freitag, 14:00 Uhr: Meeting mit Jens\n" +
|
||||||
|
"Samstag, 10:00 Uhr: Badezimmer putzen\n" +
|
||||||
|
"Sonntag, 11:00 Uhr: Telefonat mit Mama\n\n" +
|
||||||
|
"Insgesamt 5 Termine nächste Woche.",
|
||||||
|
},
|
||||||
|
// 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 10: 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 11: 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: Monthly calendar overview (text only)
|
||||||
|
{
|
||||||
|
content: "Hier ist deine Monatsübersicht für Januar:\n\n" +
|
||||||
|
"KW 2: 3 Termine\n" +
|
||||||
|
" • Mi 08.01., 09:30: Arzttermin Dr. Müller\n" +
|
||||||
|
" • Fr 10.01., 14:00: Meeting mit Jens\n" +
|
||||||
|
" • Sa 11.01., 10:00: Badezimmer putzen\n\n" +
|
||||||
|
"KW 3: 4 Termine\n" +
|
||||||
|
" • Di 14.01., 18:00: Fitnessstudio\n" +
|
||||||
|
" • Do 16.01., 19:00: Spanischkurs VHS\n" +
|
||||||
|
" • Sa 18.01., 10:00: Badezimmer putzen\n" +
|
||||||
|
" • Sa 18.01., 19:00: Geburtstagsfeier Lisa\n\n" +
|
||||||
|
"Insgesamt 7 Termine im Januar.",
|
||||||
|
},
|
||||||
// }}}
|
// }}}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -66,12 +180,14 @@ export class ChatService {
|
|||||||
return { message, conversationId: message.conversationId };
|
return { message, conversationId: message.conversationId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmEvent(userId: string, conversationId: string, messageId: string): Promise<ChatResponse> {
|
async confirmEvent(userId: string, conversationId: string, messageId: string, event: CreateEventDTO): Promise<ChatResponse> {
|
||||||
|
const createdEvent = await this.eventRepo.create(userId, event);
|
||||||
|
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
conversationId,
|
conversationId,
|
||||||
sender: 'assistant',
|
sender: 'assistant',
|
||||||
content: 'Der Vorschlag wurde angenommen.',
|
content: `Der Termin "${createdEvent.title}" wurde erstellt.`,
|
||||||
};
|
};
|
||||||
return { message, conversationId };
|
return { message, conversationId };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user