import {
Reminder,
CreateReminderInput,
ReminderStatus,
ReminderPriority,
ApiResponse
} from '../types';
import { DatabaseManager } from '../database';
export interface ReminderFilter {
startDate?: Date;
endDate?: Date;
status?: ReminderStatus;
priority?: ReminderPriority;
tags?: string[];
}
export interface ReminderUpdate {
title?: string;
description?: string;
dueDateTime?: Date;
priority?: ReminderPriority;
status?: ReminderStatus;
tags?: string[];
}
export class ReminderService {
constructor(private database: DatabaseManager) {}
async createReminder(
userId: string,
reminderData: CreateReminderInput
): Promise<ApiResponse<Reminder>> {
try {
const reminder: Omit<Reminder, 'id' | 'created' | 'updated'> = {
title: reminderData.title,
description: reminderData.description,
dueDateTime: reminderData.dueDateTime ? new Date(reminderData.dueDateTime) : undefined,
priority: reminderData.priority || ReminderPriority.MEDIUM,
status: ReminderStatus.PENDING,
tags: reminderData.tags || []
};
const createdReminder = await this.database.createReminder(userId, reminder);
return {
success: true,
data: createdReminder
};
} catch (error) {
return this.handleError('CREATE_REMINDER_FAILED', error);
}
}
async getReminders(
userId: string,
filter: ReminderFilter = {}
): Promise<ApiResponse<Reminder[]>> {
try {
const reminders = await this.database.getReminders(userId, filter);
// Apply additional filtering if needed
let filteredReminders = reminders;
if (filter.status) {
filteredReminders = filteredReminders.filter(r => r.status === filter.status);
}
if (filter.priority) {
filteredReminders = filteredReminders.filter(r => r.priority === filter.priority);
}
if (filter.tags && filter.tags.length > 0) {
filteredReminders = filteredReminders.filter(r =>
filter.tags!.some(tag => r.tags?.includes(tag))
);
}
if (filter.startDate) {
filteredReminders = filteredReminders.filter(r =>
r.dueDateTime && r.dueDateTime >= filter.startDate!
);
}
if (filter.endDate) {
filteredReminders = filteredReminders.filter(r =>
r.dueDateTime && r.dueDateTime <= filter.endDate!
);
}
// Sort by priority and due date
filteredReminders.sort((a, b) => {
// First sort by priority (urgent > high > medium > low)
const priorityOrder = {
[ReminderPriority.URGENT]: 4,
[ReminderPriority.HIGH]: 3,
[ReminderPriority.MEDIUM]: 2,
[ReminderPriority.LOW]: 1
};
const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority];
if (priorityDiff !== 0) return priorityDiff;
// Then sort by due date (earlier dates first)
if (a.dueDateTime && b.dueDateTime) {
return a.dueDateTime.getTime() - b.dueDateTime.getTime();
}
// Put items with due dates before items without
if (a.dueDateTime && !b.dueDateTime) return -1;
if (!a.dueDateTime && b.dueDateTime) return 1;
// Finally sort by creation date
return a.created.getTime() - b.created.getTime();
});
return {
success: true,
data: filteredReminders
};
} catch (error) {
return this.handleError('GET_REMINDERS_FAILED', error);
}
}
async updateReminder(
userId: string,
reminderId: string,
updates: ReminderUpdate
): Promise<ApiResponse<Reminder>> {
try {
// Get the existing reminder first
const existingReminder = await this.database.getReminder(userId, reminderId);
if (!existingReminder) {
return {
success: false,
error: {
code: 'REMINDER_NOT_FOUND',
message: 'Reminder not found'
}
};
}
// Apply updates
const updatedReminder: Reminder = {
...existingReminder,
title: updates.title !== undefined ? updates.title : existingReminder.title,
description: updates.description !== undefined ? updates.description : existingReminder.description,
dueDateTime: updates.dueDateTime !== undefined ? updates.dueDateTime : existingReminder.dueDateTime,
priority: updates.priority !== undefined ? updates.priority : existingReminder.priority,
status: updates.status !== undefined ? updates.status : existingReminder.status,
tags: updates.tags !== undefined ? updates.tags : existingReminder.tags,
updated: new Date()
};
const result = await this.database.updateReminder(userId, reminderId, updatedReminder);
return {
success: true,
data: result
};
} catch (error) {
return this.handleError('UPDATE_REMINDER_FAILED', error);
}
}
async completeReminder(
userId: string,
reminderId: string
): Promise<ApiResponse<Reminder>> {
try {
return await this.updateReminder(userId, reminderId, {
status: ReminderStatus.COMPLETED
});
} catch (error) {
return this.handleError('COMPLETE_REMINDER_FAILED', error);
}
}
async deleteReminder(
userId: string,
reminderId: string
): Promise<ApiResponse<boolean>> {
try {
const success = await this.database.deleteReminder(userId, reminderId);
return {
success,
data: success,
error: success ? undefined : {
code: 'DELETE_REMINDER_FAILED',
message: 'Failed to delete reminder'
}
};
} catch (error) {
return this.handleError('DELETE_REMINDER_FAILED', error);
}
}
// Analytics and insights
async getReminderStatistics(
userId: string,
daysBack: number = 30
): Promise<ApiResponse<{
totalReminders: number;
completedReminders: number;
overdue: number;
dueSoon: number; // Due within next 24 hours
completionRate: number;
averageCompletionTime: number; // In days
priorityBreakdown: Record<ReminderPriority, number>;
tagFrequency: Record<string, number>;
}>> {
try {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const allReminders = await this.database.getReminders(userId, {
startDate,
endDate
});
const now = new Date();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const statistics = {
totalReminders: allReminders.length,
completedReminders: allReminders.filter(r => r.status === ReminderStatus.COMPLETED).length,
overdue: allReminders.filter(r =>
r.dueDateTime && r.dueDateTime < now && r.status === ReminderStatus.PENDING
).length,
dueSoon: allReminders.filter(r =>
r.dueDateTime && r.dueDateTime <= tomorrow && r.dueDateTime >= now && r.status === ReminderStatus.PENDING
).length,
completionRate: 0,
averageCompletionTime: 0,
priorityBreakdown: {
[ReminderPriority.LOW]: 0,
[ReminderPriority.MEDIUM]: 0,
[ReminderPriority.HIGH]: 0,
[ReminderPriority.URGENT]: 0
},
tagFrequency: {} as Record<string, number>
};
// Calculate completion rate
if (statistics.totalReminders > 0) {
statistics.completionRate = (statistics.completedReminders / statistics.totalReminders) * 100;
}
// Calculate average completion time
const completedWithDueDates = allReminders.filter(r =>
r.status === ReminderStatus.COMPLETED && r.dueDateTime
);
if (completedWithDueDates.length > 0) {
const totalCompletionTime = completedWithDueDates.reduce((sum, reminder) => {
const completionTime = reminder.updated.getTime() - reminder.dueDateTime!.getTime();
return sum + Math.abs(completionTime);
}, 0);
statistics.averageCompletionTime = totalCompletionTime / completedWithDueDates.length / (1000 * 60 * 60 * 24); // Convert to days
}
// Priority breakdown
allReminders.forEach(reminder => {
statistics.priorityBreakdown[reminder.priority]++;
});
// Tag frequency
allReminders.forEach(reminder => {
reminder.tags?.forEach(tag => {
statistics.tagFrequency[tag] = (statistics.tagFrequency[tag] || 0) + 1;
});
});
return {
success: true,
data: statistics
};
} catch (error) {
return this.handleError('GET_REMINDER_STATISTICS_FAILED', error);
}
}
async getUpcomingReminders(
userId: string,
hoursAhead: number = 24
): Promise<ApiResponse<Reminder[]>> {
try {
const now = new Date();
const futureDate = new Date();
futureDate.setHours(futureDate.getHours() + hoursAhead);
const reminders = await this.database.getReminders(userId, {
startDate: now,
endDate: futureDate,
status: ReminderStatus.PENDING
});
// Sort by due date and priority
reminders.sort((a, b) => {
if (a.dueDateTime && b.dueDateTime) {
return a.dueDateTime.getTime() - b.dueDateTime.getTime();
}
if (a.dueDateTime && !b.dueDateTime) return -1;
if (!a.dueDateTime && b.dueDateTime) return 1;
return 0;
});
return {
success: true,
data: reminders
};
} catch (error) {
return this.handleError('GET_UPCOMING_REMINDERS_FAILED', error);
}
}
async searchReminders(
userId: string,
query: string,
limit: number = 20
): Promise<ApiResponse<Reminder[]>> {
try {
const allReminders = await this.database.getReminders(userId);
const searchLower = query.toLowerCase();
// Search in title, description, and tags
const matchingReminders = allReminders.filter(reminder => {
const titleMatch = reminder.title.toLowerCase().includes(searchLower);
const descriptionMatch = reminder.description?.toLowerCase().includes(searchLower);
const tagMatch = reminder.tags?.some(tag => tag.toLowerCase().includes(searchLower));
return titleMatch || descriptionMatch || tagMatch;
});
// Sort by relevance (exact matches first, then partial matches)
matchingReminders.sort((a, b) => {
const aExactTitle = a.title.toLowerCase() === searchLower;
const bExactTitle = b.title.toLowerCase() === searchLower;
if (aExactTitle && !bExactTitle) return -1;
if (!aExactTitle && bExactTitle) return 1;
// Then by creation date (newer first)
return b.created.getTime() - a.created.getTime();
});
return {
success: true,
data: matchingReminders.slice(0, limit)
};
} catch (error) {
return this.handleError('SEARCH_REMINDERS_FAILED', error);
}
}
private handleError(code: string, error: any): ApiResponse<any> {
console.error(`Reminder Service Error [${code}]:`, error);
return {
success: false,
error: {
code,
message: error instanceof Error ? error.message : 'Unknown error occurred',
details: { originalError: error }
}
};
}
}