fix(types): resolve TypeScript errors across multiple packages

- bot-services: Add registerAsync to AI, Calendar, Clock, Todo modules
- bot-services: Add convenience methods to ClockService for bot handlers
- bot-services: Make CreateEventInput.endTime optional with sensible defaults
- bot-services: Fix empty interface ESLint errors (use type aliases)
- questions-backend: Add missing schema columns (isDefault, sortOrder, deletedAt)
- questions-backend: Fix or() return type handling in question service
- questions-web: Add guard for undefined question ID in route params
- skilltree-web: Fix DBSchema type by not extending idb interface directly
- calendar-web: Fix Check icon prop (use weight instead of strokeWidth)
- matrix-mana-bot: Update clock handler to use new service methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 13:33:01 +01:00
parent 91143a497b
commit 1733580d05
14 changed files with 314 additions and 37 deletions

View file

@ -1,8 +1,13 @@
import { Module, DynamicModule } from '@nestjs/common';
import { Module, DynamicModule, Provider, Type, ModuleMetadata } from '@nestjs/common';
import { ClockService } from './clock.service';
import { ClockServiceConfig } from './types';
export interface ClockModuleOptions extends Partial<ClockServiceConfig> {}
export type ClockModuleOptions = Partial<ClockServiceConfig>;
export interface ClockModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useFactory: (...args: unknown[]) => Promise<ClockModuleOptions> | ClockModuleOptions;
inject?: (Type<unknown> | string | symbol)[];
}
@Module({})
export class ClockModule {
@ -42,4 +47,29 @@ export class ClockModule {
exports: [ClockService],
};
}
/**
* Register asynchronously with factory function
*/
static registerAsync(options: ClockModuleAsyncOptions): DynamicModule {
const configProvider: Provider = {
provide: 'CLOCK_SERVICE_CONFIG',
useFactory: options.useFactory,
inject: options.inject || [],
};
return {
module: ClockModule,
imports: options.imports || [],
providers: [
configProvider,
{
provide: ClockService,
useFactory: (config: Partial<ClockServiceConfig>) => new ClockService(config),
inject: ['CLOCK_SERVICE_CONFIG'],
},
],
exports: [ClockService],
};
}
}

View file

@ -267,4 +267,116 @@ export class ClockService {
return parts.join(' ');
}
// ===== Convenience Methods for Bot Handlers =====
/**
* Start a timer from natural language input
* Parses duration and optional label from input like "25m Pomodoro"
*/
async startTimerForUser(userId: string, input: string): Promise<Timer & { name?: string }> {
const token = this.getUserToken(userId);
if (!token) {
throw new Error('Nicht authentifiziert. Bitte zuerst anmelden.');
}
// Parse duration from input
const durationSeconds = this.parseDuration(input);
if (!durationSeconds) {
throw new Error('Ungültiges Dauer-Format. Beispiele: 25m, 1h30m, 90s');
}
// Extract label (everything after duration pattern)
const label = input.replace(/\d+\s*[hms]?(?:in)?/gi, '').trim() || null;
const timer = await this.createTimer({ durationSeconds, label }, token);
// Start the timer immediately
const started = await this.startTimer(timer.id, token);
return { ...started, name: started.label ?? undefined };
}
/**
* Stop the running timer for a user
*/
async stopTimerForUser(userId: string, timerName?: string): Promise<Timer & { name?: string }> {
const token = this.getUserToken(userId);
if (!token) {
throw new Error('Nicht authentifiziert. Bitte zuerst anmelden.');
}
const timers = await this.getTimers(token);
let timer: Timer | undefined;
if (timerName) {
timer = timers.find(
(t) =>
(t.status === 'running' || t.status === 'paused') &&
t.label?.toLowerCase().includes(timerName.toLowerCase())
);
} else {
timer = timers.find((t) => t.status === 'running' || t.status === 'paused');
}
if (!timer) {
throw new Error('Kein aktiver Timer gefunden.');
}
await this.deleteTimer(timer.id, token);
return { ...timer, name: timer.label ?? undefined };
}
/**
* Set an alarm from natural language input
* Parses time and optional label from input like "14:30 Meeting"
*/
async setAlarmForUser(userId: string, input: string): Promise<Alarm & { name?: string }> {
const token = this.getUserToken(userId);
if (!token) {
throw new Error('Nicht authentifiziert. Bitte zuerst anmelden.');
}
const time = this.parseAlarmTime(input);
if (!time) {
throw new Error('Ungültiges Zeit-Format. Beispiele: 14:30, 9:00, 14 Uhr 30');
}
// Extract label (everything after time pattern)
const label =
input
.replace(/\d{1,2}:\d{2}(:\d{2})?/g, '')
.replace(/\d{1,2}\s*uhr(\s*\d{1,2})?/gi, '')
.trim() || null;
const alarm = await this.createAlarm({ time, label }, token);
return { ...alarm, name: alarm.label ?? undefined };
}
/**
* Get time for a specific city/timezone
*/
async getWorldClockTime(city: string): Promise<{ city: string; time: string; date: string }> {
// Search for timezone
const results = await this.searchTimezones(city);
if (results.length === 0) {
throw new Error(`Stadt "${city}" nicht gefunden.`);
}
const tz = results[0];
const now = new Date();
const time = now.toLocaleTimeString('de-DE', {
timeZone: tz.timezone,
hour: '2-digit',
minute: '2-digit',
});
const date = now.toLocaleDateString('de-DE', {
timeZone: tz.timezone,
weekday: 'long',
day: 'numeric',
month: 'long',
});
return { city: tz.city, time, date };
}
}