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:
Till-JS 2025-12-11 16:00:08 +01:00 committed by Wuesteon
parent 307f1ae22e
commit 0ecbf69ebc
50 changed files with 5791 additions and 53 deletions

View 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.)?