feat: Scheduled Maintenance feature complete
This commit is contained in:
@@ -6,7 +6,10 @@ import type {
|
||||
NotificationSummary,
|
||||
DueMaintenanceItem,
|
||||
ExpiringDocument,
|
||||
TemplateKey
|
||||
TemplateKey,
|
||||
UserNotification,
|
||||
UnreadNotificationCount,
|
||||
SentNotificationRecord
|
||||
} from '../domain/notifications.types';
|
||||
|
||||
export class NotificationsRepository {
|
||||
@@ -42,7 +45,10 @@ export class NotificationsRepository {
|
||||
nextDueMileage: row.next_due_mileage,
|
||||
isDueSoon: row.is_due_soon,
|
||||
isOverdue: row.is_overdue,
|
||||
emailNotifications: row.email_notifications
|
||||
emailNotifications: row.email_notifications,
|
||||
reminderDays1: row.reminder_1_days,
|
||||
reminderDays2: row.reminder_2_days,
|
||||
reminderDays3: row.reminder_3_days
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +66,33 @@ export class NotificationsRepository {
|
||||
};
|
||||
}
|
||||
|
||||
private mapUserNotification(row: any): UserNotification {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
notificationType: row.notification_type,
|
||||
title: row.title,
|
||||
message: row.message,
|
||||
referenceType: row.reference_type,
|
||||
referenceId: row.reference_id,
|
||||
vehicleId: row.vehicle_id,
|
||||
isRead: row.is_read,
|
||||
createdAt: row.created_at,
|
||||
readAt: row.read_at
|
||||
};
|
||||
}
|
||||
|
||||
private mapSentNotificationRecord(row: any): SentNotificationRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
scheduleId: row.schedule_id,
|
||||
notificationDate: row.notification_date,
|
||||
reminderLevel: row.reminder_level,
|
||||
deliveryMethod: row.delivery_method,
|
||||
createdAt: row.created_at
|
||||
};
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Email Templates
|
||||
// ========================
|
||||
@@ -208,6 +241,9 @@ export class NotificationsRepository {
|
||||
ms.next_due_date,
|
||||
ms.next_due_mileage,
|
||||
ms.email_notifications,
|
||||
ms.reminder_1_days,
|
||||
ms.reminder_2_days,
|
||||
ms.reminder_3_days,
|
||||
CASE
|
||||
WHEN ms.next_due_date <= CURRENT_DATE THEN true
|
||||
WHEN ms.next_due_mileage IS NOT NULL AND v.odometer >= ms.next_due_mileage THEN true
|
||||
@@ -269,4 +305,175 @@ export class NotificationsRepository {
|
||||
);
|
||||
return res.rows.map(row => this.mapExpiringDocument(row));
|
||||
}
|
||||
|
||||
// ========================
|
||||
// User Notifications
|
||||
// ========================
|
||||
|
||||
async insertUserNotification(notification: {
|
||||
userId: string;
|
||||
notificationType: string;
|
||||
title: string;
|
||||
message: string;
|
||||
referenceType?: string;
|
||||
referenceId?: string;
|
||||
vehicleId?: string;
|
||||
}): Promise<UserNotification> {
|
||||
const res = await this.db.query(
|
||||
`INSERT INTO user_notifications (
|
||||
id, user_id, notification_type, title, message,
|
||||
reference_type, reference_id, vehicle_id
|
||||
) VALUES (uuid_generate_v4(), $1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[
|
||||
notification.userId,
|
||||
notification.notificationType,
|
||||
notification.title,
|
||||
notification.message,
|
||||
notification.referenceType ?? null,
|
||||
notification.referenceId ?? null,
|
||||
notification.vehicleId ?? null
|
||||
]
|
||||
);
|
||||
return this.mapUserNotification(res.rows[0]);
|
||||
}
|
||||
|
||||
async findUserNotifications(
|
||||
userId: string,
|
||||
limit: number = 20,
|
||||
includeRead: boolean = false
|
||||
): Promise<UserNotification[]> {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM user_notifications
|
||||
WHERE user_id = $1
|
||||
${includeRead ? '' : 'AND is_read = false'}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`;
|
||||
|
||||
const res = await this.db.query(sql, [userId, limit]);
|
||||
return res.rows.map(row => this.mapUserNotification(row));
|
||||
}
|
||||
|
||||
async getUnreadCount(userId: string): Promise<UnreadNotificationCount> {
|
||||
const res = await this.db.query(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE notification_type = 'maintenance') as maintenance,
|
||||
COUNT(*) FILTER (WHERE notification_type = 'document') as documents
|
||||
FROM user_notifications
|
||||
WHERE user_id = $1
|
||||
AND is_read = false`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const row = res.rows[0];
|
||||
return {
|
||||
total: parseInt(row.total || '0', 10),
|
||||
maintenance: parseInt(row.maintenance || '0', 10),
|
||||
documents: parseInt(row.documents || '0', 10)
|
||||
};
|
||||
}
|
||||
|
||||
async markAsRead(id: string, userId: string): Promise<UserNotification | null> {
|
||||
const res = await this.db.query(
|
||||
`UPDATE user_notifications
|
||||
SET is_read = true, read_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING *`,
|
||||
[id, userId]
|
||||
);
|
||||
return res.rows[0] ? this.mapUserNotification(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
async markAllAsRead(userId: string): Promise<number> {
|
||||
const res = await this.db.query(
|
||||
`UPDATE user_notifications
|
||||
SET is_read = true, read_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1 AND is_read = false
|
||||
RETURNING id`,
|
||||
[userId]
|
||||
);
|
||||
return res.rowCount || 0;
|
||||
}
|
||||
|
||||
async deleteUserNotification(id: string, userId: string): Promise<void> {
|
||||
await this.db.query(
|
||||
`DELETE FROM user_notifications
|
||||
WHERE id = $1 AND user_id = $2`,
|
||||
[id, userId]
|
||||
);
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Sent Notification Tracker
|
||||
// ========================
|
||||
|
||||
async hasNotificationBeenSent(
|
||||
scheduleId: string,
|
||||
notificationDate: string,
|
||||
reminderLevel: 1 | 2 | 3
|
||||
): Promise<boolean> {
|
||||
const res = await this.db.query(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM sent_notification_tracker
|
||||
WHERE schedule_id = $1
|
||||
AND notification_date = $2
|
||||
AND reminder_level = $3
|
||||
) as exists`,
|
||||
[scheduleId, notificationDate, reminderLevel]
|
||||
);
|
||||
return res.rows[0]?.exists || false;
|
||||
}
|
||||
|
||||
async recordSentNotification(record: {
|
||||
scheduleId: string;
|
||||
notificationDate: string;
|
||||
reminderLevel: 1 | 2 | 3;
|
||||
deliveryMethod: 'email' | 'in_app' | 'both';
|
||||
}): Promise<SentNotificationRecord> {
|
||||
const res = await this.db.query(
|
||||
`INSERT INTO sent_notification_tracker (
|
||||
schedule_id, notification_date, reminder_level, delivery_method
|
||||
) VALUES ($1, $2, $3, $4)
|
||||
RETURNING *`,
|
||||
[
|
||||
record.scheduleId,
|
||||
record.notificationDate,
|
||||
record.reminderLevel,
|
||||
record.deliveryMethod
|
||||
]
|
||||
);
|
||||
return this.mapSentNotificationRecord(res.rows[0]);
|
||||
}
|
||||
|
||||
async getUsersWithActiveSchedules(): Promise<Array<{
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
}>> {
|
||||
const res = await this.db.query(
|
||||
`SELECT DISTINCT
|
||||
ms.user_id,
|
||||
up.email as user_email,
|
||||
up.name as user_name
|
||||
FROM maintenance_schedules ms
|
||||
JOIN user_profiles up ON ms.user_id = up.user_id
|
||||
WHERE ms.is_active = true
|
||||
AND (
|
||||
ms.reminder_1_days IS NOT NULL
|
||||
OR ms.reminder_2_days IS NOT NULL
|
||||
OR ms.reminder_3_days IS NOT NULL
|
||||
)
|
||||
ORDER BY ms.user_id`
|
||||
);
|
||||
|
||||
return res.rows.map(row => ({
|
||||
userId: row.user_id,
|
||||
userEmail: row.user_email,
|
||||
userName: row.user_name
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user