mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(contacts): integrate contacts into Todo and Calendar apps
- Add ContactSelector, ContactBadge, ContactAvatar to shared-ui - Add ContactsClient API service to shared-auth - Add ContactReference, ContactSummary types to shared-types - Todo: Add assignee and involvedContacts to tasks with UI in TaskEditModal - Todo: Display contacts in TaskItem and KanbanTaskCard - Calendar: Add AttendeeSelector with RSVP status support - Calendar: Integrate attendees in EventForm - Calendar: Add task drag-drop to calendar views (Day/Week/MultiDay) - Contacts: Add ContactTasks component to show related tasks - Backend: Add findByContact endpoint to Todo task service - UI polish: glassmorphism styling, keyboard navigation, auto-focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
307f1ae22e
commit
0ecbf69ebc
50 changed files with 5791 additions and 53 deletions
418
docs/optimizable/foundation-layer-improvements.md
Normal file
418
docs/optimizable/foundation-layer-improvements.md
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
# Foundation Layer - Verbesserungsvorschläge
|
||||
|
||||
> **Stand:** Dezember 2024
|
||||
> **Betrifft:** Contacts, Todo, Calendar (Foundation Services)
|
||||
|
||||
## Aktuelle Architektur (Gut!)
|
||||
|
||||
Die drei Foundation Services sind korrekt als **separate Services mit eigenen Datenbanken** aufgesetzt:
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Contacts │ │ Todo │ │ Calendar │
|
||||
│ :3010 │ │ :3011 │ │ :3012 │
|
||||
│ │ │ │ │ │
|
||||
│ contacts DB │ │ todo DB │ │ calendar DB │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**Warum das richtig ist:**
|
||||
- Unabhängige Deployments
|
||||
- Failure Isolation
|
||||
- Unabhängige Skalierung
|
||||
- Keine Schema-Konflikte zwischen Teams
|
||||
|
||||
---
|
||||
|
||||
## Verbesserungsvorschläge
|
||||
|
||||
### 1. Foundation Clients Package
|
||||
|
||||
**Aufwand:** Mittel | **Priorität:** Hoch
|
||||
|
||||
Einheitlicher API-Client für alle Consumer Apps (Chat, Picture, Clock, etc.).
|
||||
|
||||
**Neues Package:** `packages/foundation-clients/`
|
||||
|
||||
```typescript
|
||||
// packages/foundation-clients/src/index.ts
|
||||
export class FoundationClients {
|
||||
contacts: ContactsClient;
|
||||
todo: TodoClient;
|
||||
calendar: CalendarClient;
|
||||
|
||||
constructor(config: FoundationConfig) {
|
||||
this.contacts = new ContactsClient(config);
|
||||
this.todo = new TodoClient(config);
|
||||
this.calendar = new CalendarClient(config);
|
||||
}
|
||||
}
|
||||
|
||||
// packages/foundation-clients/src/contacts.client.ts
|
||||
export class ContactsClient {
|
||||
private baseUrl: string;
|
||||
private cache: Map<string, CachedContact> = new Map();
|
||||
|
||||
async get(id: string): Promise<Contact | null> {
|
||||
// Mit Caching
|
||||
const cached = this.cache.get(id);
|
||||
if (cached && !this.isStale(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/contacts/${id}`, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
const contact = await response.json();
|
||||
this.cache.set(id, { data: contact, fetchedAt: Date.now() });
|
||||
return contact;
|
||||
}
|
||||
|
||||
async search(query: string): Promise<ContactSummary[]> {
|
||||
// Für Autocomplete in anderen Apps
|
||||
}
|
||||
|
||||
async getBulk(ids: string[]): Promise<Contact[]> {
|
||||
// Effizient für Listen
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Nutzung in Consumer Apps:**
|
||||
|
||||
```typescript
|
||||
// apps/chat/apps/backend/src/chat.service.ts
|
||||
import { FoundationClients } from '@manacore/foundation-clients';
|
||||
|
||||
@Injectable()
|
||||
export class ChatService {
|
||||
private foundation: FoundationClients;
|
||||
|
||||
constructor(configService: ConfigService) {
|
||||
this.foundation = new FoundationClients({
|
||||
contactsUrl: configService.get('CONTACTS_API_URL'),
|
||||
todoUrl: configService.get('TODO_API_URL'),
|
||||
calendarUrl: configService.get('CALENDAR_API_URL'),
|
||||
});
|
||||
}
|
||||
|
||||
async getMessageWithContact(messageId: string) {
|
||||
const message = await this.getMessage(messageId);
|
||||
const sender = await this.foundation.contacts.get(message.senderId);
|
||||
return { ...message, sender };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Event Bus (Redis Pub/Sub)
|
||||
|
||||
**Aufwand:** Mittel | **Priorität:** Mittel
|
||||
|
||||
Ermöglicht reaktive Updates zwischen Services ohne Polling.
|
||||
|
||||
**Events definieren:**
|
||||
|
||||
```typescript
|
||||
// packages/foundation-events/src/index.ts
|
||||
export const FoundationEvents = {
|
||||
// Contacts
|
||||
CONTACT_CREATED: 'contact.created',
|
||||
CONTACT_UPDATED: 'contact.updated',
|
||||
CONTACT_DELETED: 'contact.deleted',
|
||||
|
||||
// Todo
|
||||
TASK_CREATED: 'task.created',
|
||||
TASK_COMPLETED: 'task.completed',
|
||||
TASK_DELETED: 'task.deleted',
|
||||
|
||||
// Calendar
|
||||
EVENT_CREATED: 'event.created',
|
||||
EVENT_UPDATED: 'event.updated',
|
||||
EVENT_DELETED: 'event.deleted',
|
||||
} as const;
|
||||
|
||||
export interface TaskCompletedEvent {
|
||||
taskId: string;
|
||||
userId: string;
|
||||
completedAt: string;
|
||||
linkedCalendarEventId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Publisher (Todo Service):**
|
||||
|
||||
```typescript
|
||||
// apps/todo/apps/backend/src/task/task.service.ts
|
||||
import { RedisService } from '@manacore/shared-redis';
|
||||
import { FoundationEvents } from '@manacore/foundation-events';
|
||||
|
||||
@Injectable()
|
||||
export class TaskService {
|
||||
constructor(private redis: RedisService) {}
|
||||
|
||||
async completeTask(taskId: string, userId: string) {
|
||||
const task = await this.markCompleted(taskId);
|
||||
|
||||
// Event publizieren
|
||||
await this.redis.publish(FoundationEvents.TASK_COMPLETED, {
|
||||
taskId: task.id,
|
||||
userId,
|
||||
completedAt: new Date().toISOString(),
|
||||
linkedCalendarEventId: task.metadata?.linkedCalendarEventId,
|
||||
});
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Subscriber (Calendar Service):**
|
||||
|
||||
```typescript
|
||||
// apps/calendar/apps/backend/src/calendar.module.ts
|
||||
import { FoundationEvents } from '@manacore/foundation-events';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarEventSubscriber implements OnModuleInit {
|
||||
constructor(
|
||||
private redis: RedisService,
|
||||
private eventService: EventService
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.redis.subscribe(FoundationEvents.TASK_COMPLETED, async (data) => {
|
||||
if (data.linkedCalendarEventId) {
|
||||
await this.eventService.markLinkedTaskCompleted(
|
||||
data.linkedCalendarEventId
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
| Event | Reaktion |
|
||||
|-------|----------|
|
||||
| `task.completed` | Calendar markiert verknüpftes Event |
|
||||
| `contact.updated` | Chat aktualisiert Sender-Anzeige |
|
||||
| `event.deleted` | Todo entfernt `linkedCalendarEventId` |
|
||||
| `contact.deleted` | Alle Apps entfernen Kontakt-Referenzen |
|
||||
|
||||
---
|
||||
|
||||
### 3. Bulk-Endpoints
|
||||
|
||||
**Aufwand:** Klein | **Priorität:** Hoch
|
||||
|
||||
Reduziert N+1 API-Calls bei Listen-Ansichten.
|
||||
|
||||
**Contacts Service:**
|
||||
|
||||
```typescript
|
||||
// apps/contacts/apps/backend/src/contact/contact.controller.ts
|
||||
@Controller('contacts')
|
||||
export class ContactController {
|
||||
@Post('bulk')
|
||||
async getBulk(@Body() body: { ids: string[] }): Promise<Contact[]> {
|
||||
return this.contactService.findByIds(body.ids);
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
async search(
|
||||
@Query('q') query: string,
|
||||
@Query('limit') limit = 10
|
||||
): Promise<ContactSummary[]> {
|
||||
return this.contactService.search(query, limit);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Todo Service:**
|
||||
|
||||
```typescript
|
||||
// apps/todo/apps/backend/src/task/task.controller.ts
|
||||
@Controller('tasks')
|
||||
export class TaskController {
|
||||
@Post('bulk')
|
||||
async getBulk(@Body() body: { ids: string[] }): Promise<Task[]> {
|
||||
return this.taskService.findByIds(body.ids);
|
||||
}
|
||||
|
||||
@Get('by-contact/:contactId')
|
||||
async getByContact(@Param('contactId') contactId: string): Promise<Task[]> {
|
||||
// Tasks die mit einem Kontakt verknüpft sind
|
||||
return this.taskService.findByLinkedContact(contactId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Calendar Service:**
|
||||
|
||||
```typescript
|
||||
// apps/calendar/apps/backend/src/event/event.controller.ts
|
||||
@Controller('events')
|
||||
export class EventController {
|
||||
@Post('bulk')
|
||||
async getBulk(@Body() body: { ids: string[] }): Promise<Event[]> {
|
||||
return this.eventService.findByIds(body.ids);
|
||||
}
|
||||
|
||||
@Get('by-attendee')
|
||||
async getByAttendee(@Query('email') email: string): Promise<Event[]> {
|
||||
return this.eventService.findByAttendeeEmail(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Caching-Layer
|
||||
|
||||
**Aufwand:** Klein | **Priorität:** Mittel
|
||||
|
||||
Kontakte ändern sich selten - perfekt für Caching.
|
||||
|
||||
**In Foundation Clients (Client-Side Cache):**
|
||||
|
||||
```typescript
|
||||
// packages/foundation-clients/src/cache.ts
|
||||
export class SimpleCache<T> {
|
||||
private cache = new Map<string, { data: T; expiresAt: number }>();
|
||||
private ttlMs: number;
|
||||
|
||||
constructor(ttlSeconds = 300) {
|
||||
this.ttlMs = ttlSeconds * 1000;
|
||||
}
|
||||
|
||||
get(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
set(key: string, data: T): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expiresAt: Date.now() + this.ttlMs,
|
||||
});
|
||||
}
|
||||
|
||||
invalidate(key: string): void {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Redis Cache (Server-Side):**
|
||||
|
||||
```typescript
|
||||
// apps/contacts/apps/backend/src/contact/contact.service.ts
|
||||
@Injectable()
|
||||
export class ContactService {
|
||||
private readonly CACHE_TTL = 300; // 5 Minuten
|
||||
|
||||
async findById(id: string): Promise<Contact | null> {
|
||||
// 1. Redis Cache prüfen
|
||||
const cached = await this.redis.get(`contact:${id}`);
|
||||
if (cached) return JSON.parse(cached);
|
||||
|
||||
// 2. DB Query
|
||||
const contact = await this.db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (contact[0]) {
|
||||
// 3. In Cache speichern
|
||||
await this.redis.setex(
|
||||
`contact:${id}`,
|
||||
this.CACHE_TTL,
|
||||
JSON.stringify(contact[0])
|
||||
);
|
||||
}
|
||||
|
||||
return contact[0] || null;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateContactDto): Promise<Contact> {
|
||||
const updated = await this.db
|
||||
.update(contacts)
|
||||
.set(data)
|
||||
.where(eq(contacts.id, id))
|
||||
.returning();
|
||||
|
||||
// Cache invalidieren
|
||||
await this.redis.del(`contact:${id}`);
|
||||
|
||||
return updated[0];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementierungs-Reihenfolge
|
||||
|
||||
| Phase | Task | Abhängigkeiten |
|
||||
|-------|------|----------------|
|
||||
| **1** | Bulk-Endpoints hinzufügen | Keine |
|
||||
| **2** | Foundation Clients Package erstellen | Bulk-Endpoints |
|
||||
| **3** | Client-Side Caching in Foundation Clients | Foundation Clients |
|
||||
| **4** | Redis Cache in Services | Redis Setup |
|
||||
| **5** | Event Bus Setup | Redis Setup |
|
||||
| **6** | Event Publisher/Subscriber | Event Bus |
|
||||
|
||||
---
|
||||
|
||||
## Neue Package-Struktur
|
||||
|
||||
```
|
||||
packages/
|
||||
├── foundation-clients/ # NEU: API Clients
|
||||
│ ├── src/
|
||||
│ │ ├── contacts.client.ts
|
||||
│ │ ├── todo.client.ts
|
||||
│ │ ├── calendar.client.ts
|
||||
│ │ ├── cache.ts
|
||||
│ │ └── index.ts
|
||||
│ └── package.json
|
||||
│
|
||||
├── foundation-events/ # NEU: Event Definitions
|
||||
│ ├── src/
|
||||
│ │ ├── contact.events.ts
|
||||
│ │ ├── task.events.ts
|
||||
│ │ ├── calendar.events.ts
|
||||
│ │ └── index.ts
|
||||
│ └── package.json
|
||||
│
|
||||
├── shared-types/ # Existiert bereits
|
||||
│ └── src/
|
||||
│ ├── contact.ts # ContactReference, ContactSummary
|
||||
│ └── ...
|
||||
│
|
||||
└── shared-redis/ # NEU oder erweitern
|
||||
└── src/
|
||||
├── redis.service.ts
|
||||
├── pub-sub.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen
|
||||
|
||||
- [ ] Welche Consumer Apps werden als erste integriert?
|
||||
- [ ] Redis bereits im Stack oder neu einführen?
|
||||
- [ ] Cache TTL pro Entity-Typ oder einheitlich?
|
||||
- [ ] Event Bus: Redis Pub/Sub vs. dediziertes System (Bull, etc.)?
|
||||
Loading…
Add table
Add a link
Reference in a new issue