mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-28 21:37:43 +02:00
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:
parent
91143a497b
commit
1733580d05
14 changed files with 314 additions and 37 deletions
|
|
@ -53,7 +53,7 @@
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="spinner"></span>
|
<span class="spinner"></span>
|
||||||
{:else if checked}
|
{:else if checked}
|
||||||
<Check size={sizes[size].icon} strokeWidth={3} />
|
<Check size={sizes[size].icon} weight="bold" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { pgTable, uuid, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
import { pgTable, uuid, text, boolean, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
export const collections = pgTable('collections', {
|
export const collections = pgTable('collections', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
|
@ -9,11 +9,15 @@ export const collections = pgTable('collections', {
|
||||||
color: text('color').default('#6366f1'),
|
color: text('color').default('#6366f1'),
|
||||||
icon: text('icon').default('folder'),
|
icon: text('icon').default('folder'),
|
||||||
|
|
||||||
|
isDefault: boolean('is_default').default(false),
|
||||||
|
sortOrder: integer('sort_order').default(0),
|
||||||
|
|
||||||
isShared: boolean('is_shared').default(false),
|
isShared: boolean('is_shared').default(false),
|
||||||
shareToken: text('share_token').unique(),
|
shareToken: text('share_token').unique(),
|
||||||
|
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
||||||
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Collection = typeof collections.$inferSelect;
|
export type Collection = typeof collections.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const questions = pgTable('questions', {
|
||||||
// Soft delete
|
// Soft delete
|
||||||
isArchived: boolean('is_archived').default(false),
|
isArchived: boolean('is_archived').default(false),
|
||||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||||
|
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Question = typeof questions.$inferSelect;
|
export type Question = typeof questions.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { CreateQuestionDto, UpdateQuestionDto } from './dto';
|
||||||
export class QuestionService {
|
export class QuestionService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('DATABASE_CONNECTION')
|
@Inject('DATABASE_CONNECTION')
|
||||||
private readonly db: NodePgDatabase,
|
private readonly db: NodePgDatabase
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(userId: string, dto: CreateQuestionDto): Promise<Question> {
|
async create(userId: string, dto: CreateQuestionDto): Promise<Question> {
|
||||||
|
|
@ -35,7 +35,7 @@ export class QuestionService {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
},
|
}
|
||||||
): Promise<{ data: Question[]; total: number }> {
|
): Promise<{ data: Question[]; total: number }> {
|
||||||
const conditions = [eq(questions.userId, userId), isNull(questions.deletedAt)];
|
const conditions = [eq(questions.userId, userId), isNull(questions.deletedAt)];
|
||||||
|
|
||||||
|
|
@ -48,12 +48,13 @@ export class QuestionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.search) {
|
if (options?.search) {
|
||||||
conditions.push(
|
const searchCondition = or(
|
||||||
or(
|
ilike(questions.title, `%${options.search}%`),
|
||||||
ilike(questions.title, `%${options.search}%`),
|
ilike(questions.description, `%${options.search}%`)
|
||||||
ilike(questions.description, `%${options.search}%`),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (searchCondition) {
|
||||||
|
conditions.push(searchCondition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = options?.limit || 20;
|
const limit = options?.limit || 20;
|
||||||
|
|
@ -137,8 +138,8 @@ export class QuestionService {
|
||||||
and(
|
and(
|
||||||
eq(questions.userId, userId),
|
eq(questions.userId, userId),
|
||||||
eq(questions.collectionId, collectionId),
|
eq(questions.collectionId, collectionId),
|
||||||
isNull(questions.deletedAt),
|
isNull(questions.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.orderBy(desc(questions.createdAt));
|
.orderBy(desc(questions.createdAt));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = page.params.id;
|
const id = page.params.id;
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Question ID is required');
|
||||||
|
}
|
||||||
question = await questionsApi.getById(id);
|
question = await questionsApi.getById(id);
|
||||||
researchResults = await researchApi.getByQuestion(id);
|
researchResults = await researchApi.getByQuestion(id);
|
||||||
sources = await sourcesApi.getByQuestion(id);
|
sources = await sourcesApi.getByQuestion(id);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
|
import { openDB, type IDBPDatabase } from 'idb';
|
||||||
import type { Skill, Activity, UserStats } from '$lib/types';
|
import type { Skill, Activity, UserStats } from '$lib/types';
|
||||||
|
|
||||||
interface SkillTreeDB extends DBSchema {
|
interface SkillTreeDB {
|
||||||
skills: {
|
skills: {
|
||||||
key: string;
|
key: string;
|
||||||
value: Skill;
|
value: Skill;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { Module, DynamicModule } from '@nestjs/common';
|
import { Module, DynamicModule, Provider, Type, ModuleMetadata } from '@nestjs/common';
|
||||||
import { AiService } from './ai.service';
|
import { AiService } from './ai.service';
|
||||||
import { AiServiceConfig } from './types';
|
import { AiServiceConfig } from './types';
|
||||||
|
|
||||||
export interface AiModuleOptions extends Partial<AiServiceConfig> {}
|
export type AiModuleOptions = Partial<AiServiceConfig>;
|
||||||
|
|
||||||
|
export interface AiModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
||||||
|
useFactory: (...args: unknown[]) => Promise<AiModuleOptions> | AiModuleOptions;
|
||||||
|
inject?: (Type<unknown> | string | symbol)[];
|
||||||
|
}
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class AiModule {
|
export class AiModule {
|
||||||
|
|
@ -42,4 +47,29 @@ export class AiModule {
|
||||||
exports: [AiService],
|
exports: [AiService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register asynchronously with factory function
|
||||||
|
*/
|
||||||
|
static registerAsync(options: AiModuleAsyncOptions): DynamicModule {
|
||||||
|
const configProvider: Provider = {
|
||||||
|
provide: 'AI_SERVICE_CONFIG',
|
||||||
|
useFactory: options.useFactory,
|
||||||
|
inject: options.inject || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: AiModule,
|
||||||
|
imports: options.imports || [],
|
||||||
|
providers: [
|
||||||
|
configProvider,
|
||||||
|
{
|
||||||
|
provide: AiService,
|
||||||
|
useFactory: (config: Partial<AiServiceConfig>) => new AiService(config),
|
||||||
|
inject: ['AI_SERVICE_CONFIG'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [AiService],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Module, DynamicModule } from '@nestjs/common';
|
import { Module, DynamicModule, Provider, Type, ModuleMetadata } from '@nestjs/common';
|
||||||
import { CalendarService, CALENDAR_STORAGE_PROVIDER } from './calendar.service';
|
import { CalendarService, CALENDAR_STORAGE_PROVIDER } from './calendar.service';
|
||||||
import { StorageProvider } from '../shared/types';
|
import { StorageProvider } from '../shared/types';
|
||||||
import { FileStorageProvider } from '../shared/storage';
|
import { FileStorageProvider } from '../shared/storage';
|
||||||
|
|
@ -9,6 +9,11 @@ export interface CalendarModuleOptions {
|
||||||
storageProvider?: StorageProvider<CalendarData>;
|
storageProvider?: StorageProvider<CalendarData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CalendarModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
||||||
|
useFactory: (...args: unknown[]) => Promise<CalendarModuleOptions> | CalendarModuleOptions;
|
||||||
|
inject?: (Type<unknown> | string | symbol)[];
|
||||||
|
}
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class CalendarModule {
|
export class CalendarModule {
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,7 +29,8 @@ export class CalendarModule {
|
||||||
{
|
{
|
||||||
provide: CALENDAR_STORAGE_PROVIDER,
|
provide: CALENDAR_STORAGE_PROVIDER,
|
||||||
useValue:
|
useValue:
|
||||||
options?.storageProvider ?? new FileStorageProvider<CalendarData>(storagePath, defaultData),
|
options?.storageProvider ??
|
||||||
|
new FileStorageProvider<CalendarData>(storagePath, defaultData),
|
||||||
},
|
},
|
||||||
CalendarService,
|
CalendarService,
|
||||||
],
|
],
|
||||||
|
|
@ -48,4 +54,30 @@ export class CalendarModule {
|
||||||
exports: [CalendarService],
|
exports: [CalendarService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register asynchronously with factory function
|
||||||
|
*/
|
||||||
|
static registerAsync(options: CalendarModuleAsyncOptions): DynamicModule {
|
||||||
|
const storageProvider: Provider = {
|
||||||
|
provide: CALENDAR_STORAGE_PROVIDER,
|
||||||
|
useFactory: async (...args: unknown[]) => {
|
||||||
|
const moduleOptions = await options.useFactory(...args);
|
||||||
|
const storagePath = moduleOptions?.storagePath ?? './data/calendar-data.json';
|
||||||
|
const defaultData: CalendarData = { events: [], calendars: [] };
|
||||||
|
return (
|
||||||
|
moduleOptions?.storageProvider ??
|
||||||
|
new FileStorageProvider<CalendarData>(storagePath, defaultData)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inject: options.inject || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: CalendarModule,
|
||||||
|
imports: options.imports || [],
|
||||||
|
providers: [storageProvider, CalendarService],
|
||||||
|
exports: [CalendarService],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@ export class CalendarService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
this.storage =
|
this.storage =
|
||||||
storage ||
|
storage ||
|
||||||
new FileStorageProvider<CalendarData>('./data/calendar-data.json', { events: [], calendars: [] });
|
new FileStorageProvider<CalendarData>('./data/calendar-data.json', {
|
||||||
|
events: [],
|
||||||
|
calendars: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
|
|
@ -46,7 +49,9 @@ export class CalendarService implements OnModuleInit {
|
||||||
private async loadData(): Promise<void> {
|
private async loadData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.data = await this.storage.load();
|
this.data = await this.storage.load();
|
||||||
this.logger.log(`Loaded ${this.data.events.length} events, ${this.data.calendars.length} calendars`);
|
this.logger.log(
|
||||||
|
`Loaded ${this.data.events.length} events, ${this.data.calendars.length} calendars`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to load calendar data:', error);
|
this.logger.error('Failed to load calendar data:', error);
|
||||||
this.data = { events: [], calendars: [] };
|
this.data = { events: [], calendars: [] };
|
||||||
|
|
@ -81,6 +86,19 @@ export class CalendarService implements OnModuleInit {
|
||||||
async createEvent(userId: string, input: CreateEventInput): Promise<CalendarEvent> {
|
async createEvent(userId: string, input: CreateEventInput): Promise<CalendarEvent> {
|
||||||
const calendar = this.ensureDefaultCalendar(userId);
|
const calendar = this.ensureDefaultCalendar(userId);
|
||||||
|
|
||||||
|
// Calculate endTime if not provided
|
||||||
|
let endTime: Date;
|
||||||
|
if (input.endTime) {
|
||||||
|
endTime = input.endTime;
|
||||||
|
} else if (input.isAllDay) {
|
||||||
|
// For all-day events, end at end of the same day
|
||||||
|
endTime = new Date(input.startTime);
|
||||||
|
endTime.setHours(23, 59, 59, 999);
|
||||||
|
} else {
|
||||||
|
// Default to 1 hour after start
|
||||||
|
endTime = new Date(input.startTime.getTime() + 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
const event: CalendarEvent = {
|
const event: CalendarEvent = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
userId,
|
userId,
|
||||||
|
|
@ -88,7 +106,7 @@ export class CalendarService implements OnModuleInit {
|
||||||
description: input.description ?? null,
|
description: input.description ?? null,
|
||||||
location: input.location ?? null,
|
location: input.location ?? null,
|
||||||
startTime: input.startTime.toISOString(),
|
startTime: input.startTime.toISOString(),
|
||||||
endTime: input.endTime.toISOString(),
|
endTime: endTime.toISOString(),
|
||||||
isAllDay: input.isAllDay ?? false,
|
isAllDay: input.isAllDay ?? false,
|
||||||
calendarId: input.calendarId ?? calendar.id,
|
calendarId: input.calendarId ?? calendar.id,
|
||||||
calendarName: calendar.name,
|
calendarName: calendar.name,
|
||||||
|
|
@ -101,7 +119,11 @@ export class CalendarService implements OnModuleInit {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEvent(userId: string, eventId: string, input: UpdateEventInput): Promise<CalendarEvent | null> {
|
async updateEvent(
|
||||||
|
userId: string,
|
||||||
|
eventId: string,
|
||||||
|
input: UpdateEventInput
|
||||||
|
): Promise<CalendarEvent | null> {
|
||||||
const event = this.data.events.find((e) => e.id === eventId && e.userId === userId);
|
const event = this.data.events.find((e) => e.id === eventId && e.userId === userId);
|
||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
|
|
||||||
|
|
@ -197,7 +219,7 @@ export class CalendarService implements OnModuleInit {
|
||||||
return this.getEventsInRange(userId, today, weekEnd);
|
return this.getEventsInRange(userId, today, weekEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUpcomingEvents(userId: string, days: number = 7): Promise<CalendarEvent[]> {
|
async getUpcomingEvents(userId: string, days = 7): Promise<CalendarEvent[]> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const endDate = addDays(now, days);
|
const endDate = addDays(now, days);
|
||||||
return this.getEventsInRange(userId, now, endDate);
|
return this.getEventsInRange(userId, now, endDate);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { UserEntity } from '../shared/types';
|
import { type UserEntity } from '../shared/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calendar event entity
|
* Calendar event entity
|
||||||
|
|
@ -38,7 +38,7 @@ export interface CalendarData {
|
||||||
export interface CreateEventInput {
|
export interface CreateEventInput {
|
||||||
title: string;
|
title: string;
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
endTime: Date;
|
endTime?: Date; // Optional - defaults to startTime + 1 hour, or end of day for all-day events
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
isAllDay?: boolean;
|
isAllDay?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -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 { ClockService } from './clock.service';
|
||||||
import { ClockServiceConfig } from './types';
|
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({})
|
@Module({})
|
||||||
export class ClockModule {
|
export class ClockModule {
|
||||||
|
|
@ -42,4 +47,29 @@ export class ClockModule {
|
||||||
exports: [ClockService],
|
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],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,4 +267,116 @@ export class ClockService {
|
||||||
|
|
||||||
return parts.join(' ');
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Module, DynamicModule } from '@nestjs/common';
|
import { Module, DynamicModule, Provider, Type, ModuleMetadata } from '@nestjs/common';
|
||||||
import { TodoService, TODO_STORAGE_PROVIDER } from './todo.service';
|
import { TodoService, TODO_STORAGE_PROVIDER } from './todo.service';
|
||||||
import { StorageProvider } from '../shared/types';
|
import { StorageProvider } from '../shared/types';
|
||||||
import { FileStorageProvider } from '../shared/storage';
|
import { FileStorageProvider } from '../shared/storage';
|
||||||
|
|
@ -9,6 +9,11 @@ export interface TodoModuleOptions {
|
||||||
storageProvider?: StorageProvider<TodoData>;
|
storageProvider?: StorageProvider<TodoData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TodoModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
||||||
|
useFactory: (...args: unknown[]) => Promise<TodoModuleOptions> | TodoModuleOptions;
|
||||||
|
inject?: (Type<unknown> | string | symbol)[];
|
||||||
|
}
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class TodoModule {
|
export class TodoModule {
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,7 +28,8 @@ export class TodoModule {
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: TODO_STORAGE_PROVIDER,
|
provide: TODO_STORAGE_PROVIDER,
|
||||||
useValue: options?.storageProvider ?? new FileStorageProvider<TodoData>(storagePath, defaultData),
|
useValue:
|
||||||
|
options?.storageProvider ?? new FileStorageProvider<TodoData>(storagePath, defaultData),
|
||||||
},
|
},
|
||||||
TodoService,
|
TodoService,
|
||||||
],
|
],
|
||||||
|
|
@ -47,4 +53,30 @@ export class TodoModule {
|
||||||
exports: [TodoService],
|
exports: [TodoService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register asynchronously with factory function
|
||||||
|
*/
|
||||||
|
static registerAsync(options: TodoModuleAsyncOptions): DynamicModule {
|
||||||
|
const storageProvider: Provider = {
|
||||||
|
provide: TODO_STORAGE_PROVIDER,
|
||||||
|
useFactory: async (...args: unknown[]) => {
|
||||||
|
const moduleOptions = await options.useFactory(...args);
|
||||||
|
const storagePath = moduleOptions?.storagePath ?? './data/todo-data.json';
|
||||||
|
const defaultData: TodoData = { tasks: [], projects: [] };
|
||||||
|
return (
|
||||||
|
moduleOptions?.storageProvider ??
|
||||||
|
new FileStorageProvider<TodoData>(storagePath, defaultData)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
inject: options.inject || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: TodoModule,
|
||||||
|
imports: options.imports || [],
|
||||||
|
providers: [storageProvider, TodoService],
|
||||||
|
exports: [TodoService],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export class ClockHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.clockService.startTimer(ctx.userId, input);
|
const result = await this.clockService.startTimerForUser(ctx.userId, input);
|
||||||
this.logger.log(`Started timer for ${ctx.userId}: ${result.name}`);
|
this.logger.log(`Started timer for ${ctx.userId}: ${result.name}`);
|
||||||
|
|
||||||
const durationStr = this.formatDuration(result.durationSeconds);
|
const durationStr = this.formatDuration(result.durationSeconds);
|
||||||
|
|
@ -33,7 +33,12 @@ export class ClockHandler {
|
||||||
|
|
||||||
async listTimers(ctx: CommandContext): Promise<string> {
|
async listTimers(ctx: CommandContext): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const timers = await this.clockService.getTimers(ctx.userId);
|
const token = this.clockService.getUserToken(ctx.userId);
|
||||||
|
if (!token) {
|
||||||
|
return '❌ Nicht authentifiziert. Bitte zuerst anmelden.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const timers = await this.clockService.getTimers(token);
|
||||||
|
|
||||||
if (timers.length === 0) {
|
if (timers.length === 0) {
|
||||||
return '⏱️ Keine aktiven Timer.\n\nStarte einen mit `!timer [Dauer]`';
|
return '⏱️ Keine aktiven Timer.\n\nStarte einen mit `!timer [Dauer]`';
|
||||||
|
|
@ -42,8 +47,8 @@ export class ClockHandler {
|
||||||
let response = '⏱️ **Aktive Timer:**\n\n';
|
let response = '⏱️ **Aktive Timer:**\n\n';
|
||||||
for (const timer of timers) {
|
for (const timer of timers) {
|
||||||
const remaining = this.formatDuration(timer.remainingSeconds);
|
const remaining = this.formatDuration(timer.remainingSeconds);
|
||||||
const status = timer.isPaused ? '⏸️' : '▶️';
|
const status = timer.status === 'paused' ? '⏸️' : '▶️';
|
||||||
response += `${status} **${timer.name || 'Timer'}** - ${remaining} verbleibend\n`;
|
response += `${status} **${timer.label || 'Timer'}** - ${remaining} verbleibend\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
response += '\n`!stop` zum Beenden';
|
response += '\n`!stop` zum Beenden';
|
||||||
|
|
@ -55,7 +60,7 @@ export class ClockHandler {
|
||||||
|
|
||||||
async stopTimer(ctx: CommandContext, args: string): Promise<string> {
|
async stopTimer(ctx: CommandContext, args: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const result = await this.clockService.stopTimer(ctx.userId, args.trim() || undefined);
|
const result = await this.clockService.stopTimerForUser(ctx.userId, args.trim() || undefined);
|
||||||
return `⏹️ Timer gestoppt: **${result.name || 'Timer'}**`;
|
return `⏹️ Timer gestoppt: **${result.name || 'Timer'}**`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `❌ ${error instanceof Error ? error.message : 'Kein aktiver Timer gefunden'}`;
|
return `❌ ${error instanceof Error ? error.message : 'Kein aktiver Timer gefunden'}`;
|
||||||
|
|
@ -73,7 +78,7 @@ export class ClockHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.clockService.setAlarm(ctx.userId, input);
|
const result = await this.clockService.setAlarmForUser(ctx.userId, input);
|
||||||
this.logger.log(`Set alarm for ${ctx.userId}: ${result.name} at ${result.time}`);
|
this.logger.log(`Set alarm for ${ctx.userId}: ${result.name} at ${result.time}`);
|
||||||
|
|
||||||
return `⏰ Alarm gesetzt: **${result.name || 'Alarm'}**\nZeit: ${result.time}`;
|
return `⏰ Alarm gesetzt: **${result.name || 'Alarm'}**\nZeit: ${result.time}`;
|
||||||
|
|
@ -84,7 +89,12 @@ export class ClockHandler {
|
||||||
|
|
||||||
async listAlarms(ctx: CommandContext): Promise<string> {
|
async listAlarms(ctx: CommandContext): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const alarms = await this.clockService.getAlarms(ctx.userId);
|
const token = this.clockService.getUserToken(ctx.userId);
|
||||||
|
if (!token) {
|
||||||
|
return '❌ Nicht authentifiziert. Bitte zuerst anmelden.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const alarms = await this.clockService.getAlarms(token);
|
||||||
|
|
||||||
if (alarms.length === 0) {
|
if (alarms.length === 0) {
|
||||||
return '⏰ Keine aktiven Alarme.\n\nSetze einen mit `!alarm [Zeit]`';
|
return '⏰ Keine aktiven Alarme.\n\nSetze einen mit `!alarm [Zeit]`';
|
||||||
|
|
@ -93,7 +103,7 @@ export class ClockHandler {
|
||||||
let response = '⏰ **Aktive Alarme:**\n\n';
|
let response = '⏰ **Aktive Alarme:**\n\n';
|
||||||
for (const alarm of alarms) {
|
for (const alarm of alarms) {
|
||||||
const status = alarm.enabled ? '🔔' : '🔕';
|
const status = alarm.enabled ? '🔔' : '🔕';
|
||||||
response += `${status} **${alarm.name || 'Alarm'}** - ${alarm.time}\n`;
|
response += `${status} **${alarm.label || 'Alarm'}** - ${alarm.time}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -130,7 +140,7 @@ export class ClockHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.clockService.getWorldClock(city);
|
const result = await this.clockService.getWorldClockTime(city);
|
||||||
return `🕐 **${result.city}:** ${result.time}\n📅 ${result.date}`;
|
return `🕐 **${result.city}:** ${result.time}\n📅 ${result.date}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `❌ Stadt "${city}" nicht gefunden.\n\nVersuche: Berlin, London, New York, Tokyo, Sydney`;
|
return `❌ Stadt "${city}" nicht gefunden.\n\nVersuche: Berlin, London, New York, Tokyo, Sydney`;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue