Notification updates
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import { Pool } from 'pg';
|
||||
import pool from '../../../core/config/database';
|
||||
import type {
|
||||
EmailTemplate,
|
||||
NotificationLog,
|
||||
NotificationSummary,
|
||||
DueMaintenanceItem,
|
||||
ExpiringDocument,
|
||||
TemplateKey
|
||||
} from '../domain/notifications.types';
|
||||
|
||||
export class NotificationsRepository {
|
||||
constructor(private readonly db: Pool = pool) {}
|
||||
|
||||
// ========================
|
||||
// Row Mappers
|
||||
// ========================
|
||||
|
||||
private mapEmailTemplate(row: any): EmailTemplate {
|
||||
return {
|
||||
id: row.id,
|
||||
templateKey: row.template_key,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
subject: row.subject,
|
||||
body: row.body,
|
||||
variables: row.variables,
|
||||
isActive: row.is_active,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
private mapDueMaintenanceItem(row: any): DueMaintenanceItem {
|
||||
return {
|
||||
id: row.id,
|
||||
vehicleId: row.vehicle_id,
|
||||
vehicleName: row.vehicle_name,
|
||||
category: row.category,
|
||||
subtypes: row.subtypes,
|
||||
nextDueDate: row.next_due_date,
|
||||
nextDueMileage: row.next_due_mileage,
|
||||
isDueSoon: row.is_due_soon,
|
||||
isOverdue: row.is_overdue,
|
||||
emailNotifications: row.email_notifications
|
||||
};
|
||||
}
|
||||
|
||||
private mapExpiringDocument(row: any): ExpiringDocument {
|
||||
return {
|
||||
id: row.id,
|
||||
vehicleId: row.vehicle_id,
|
||||
vehicleName: row.vehicle_name,
|
||||
documentType: row.document_type,
|
||||
title: row.title,
|
||||
expirationDate: row.expiration_date,
|
||||
isExpiringSoon: row.is_expiring_soon,
|
||||
isExpired: row.is_expired,
|
||||
emailNotifications: row.email_notifications
|
||||
};
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Email Templates
|
||||
// ========================
|
||||
|
||||
async getEmailTemplates(): Promise<EmailTemplate[]> {
|
||||
const res = await this.db.query(
|
||||
`SELECT * FROM email_templates ORDER BY template_key`
|
||||
);
|
||||
return res.rows.map(row => this.mapEmailTemplate(row));
|
||||
}
|
||||
|
||||
async getEmailTemplateByKey(key: TemplateKey): Promise<EmailTemplate | null> {
|
||||
const res = await this.db.query(
|
||||
`SELECT * FROM email_templates WHERE template_key = $1`,
|
||||
[key]
|
||||
);
|
||||
return res.rows[0] ? this.mapEmailTemplate(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
async updateEmailTemplate(
|
||||
key: TemplateKey,
|
||||
updates: { subject?: string; body?: string; isActive?: boolean }
|
||||
): Promise<EmailTemplate | null> {
|
||||
const fields: string[] = [];
|
||||
const params: (string | boolean)[] = [];
|
||||
let i = 1;
|
||||
|
||||
if (updates.subject !== undefined) {
|
||||
fields.push(`subject = $${i++}`);
|
||||
params.push(updates.subject);
|
||||
}
|
||||
if (updates.body !== undefined) {
|
||||
fields.push(`body = $${i++}`);
|
||||
params.push(updates.body);
|
||||
}
|
||||
if (updates.isActive !== undefined) {
|
||||
fields.push(`is_active = $${i++}`);
|
||||
params.push(updates.isActive);
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return this.getEmailTemplateByKey(key);
|
||||
}
|
||||
|
||||
fields.push(`updated_at = CURRENT_TIMESTAMP`);
|
||||
params.push(key);
|
||||
|
||||
const sql = `
|
||||
UPDATE email_templates
|
||||
SET ${fields.join(', ')}
|
||||
WHERE template_key = $${i}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const res = await this.db.query(sql, params);
|
||||
return res.rows[0] ? this.mapEmailTemplate(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Notification Logs
|
||||
// ========================
|
||||
|
||||
async insertNotificationLog(log: {
|
||||
user_id: string;
|
||||
notification_type: 'email' | 'toast';
|
||||
template_key: TemplateKey;
|
||||
recipient_email?: string;
|
||||
subject?: string;
|
||||
reference_type?: string;
|
||||
reference_id?: string;
|
||||
status?: 'pending' | 'sent' | 'failed';
|
||||
error_message?: string;
|
||||
}): Promise<NotificationLog> {
|
||||
const res = await this.db.query(
|
||||
`INSERT INTO notification_logs (
|
||||
user_id, notification_type, template_key, recipient_email, subject,
|
||||
reference_type, reference_id, status, error_message
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *`,
|
||||
[
|
||||
log.user_id,
|
||||
log.notification_type,
|
||||
log.template_key,
|
||||
log.recipient_email ?? null,
|
||||
log.subject ?? null,
|
||||
log.reference_type ?? null,
|
||||
log.reference_id ?? null,
|
||||
log.status ?? 'sent',
|
||||
log.error_message ?? null,
|
||||
]
|
||||
);
|
||||
return res.rows[0] as NotificationLog;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Notification Summary
|
||||
// ========================
|
||||
|
||||
async getNotificationSummary(userId: string): Promise<NotificationSummary> {
|
||||
// Get counts of due soon vs overdue maintenance items
|
||||
const maintenanceRes = await this.db.query(
|
||||
`SELECT
|
||||
COUNT(*) FILTER (WHERE ms.next_due_date <= CURRENT_DATE) as overdue_count,
|
||||
COUNT(*) FILTER (WHERE ms.next_due_date > CURRENT_DATE AND ms.next_due_date <= CURRENT_DATE + INTERVAL '30 days') as due_soon_count
|
||||
FROM maintenance_schedules ms
|
||||
WHERE ms.user_id = $1
|
||||
AND ms.is_active = true
|
||||
AND ms.next_due_date IS NOT NULL
|
||||
AND ms.next_due_date <= CURRENT_DATE + INTERVAL '30 days'`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
// Get counts of expiring soon vs expired documents
|
||||
const documentRes = await this.db.query(
|
||||
`SELECT
|
||||
COUNT(*) FILTER (WHERE d.expiration_date < CURRENT_DATE) as expired_count,
|
||||
COUNT(*) FILTER (WHERE d.expiration_date >= CURRENT_DATE AND d.expiration_date <= CURRENT_DATE + INTERVAL '30 days') as expiring_soon_count
|
||||
FROM documents d
|
||||
WHERE d.user_id = $1
|
||||
AND d.deleted_at IS NULL
|
||||
AND d.expiration_date IS NOT NULL
|
||||
AND d.expiration_date <= CURRENT_DATE + INTERVAL '30 days'`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
return {
|
||||
maintenanceDueSoon: parseInt(maintenanceRes.rows[0]?.due_soon_count || '0', 10),
|
||||
maintenanceOverdue: parseInt(maintenanceRes.rows[0]?.overdue_count || '0', 10),
|
||||
documentsExpiringSoon: parseInt(documentRes.rows[0]?.expiring_soon_count || '0', 10),
|
||||
documentsExpired: parseInt(documentRes.rows[0]?.expired_count || '0', 10),
|
||||
};
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Due Maintenance Items
|
||||
// ========================
|
||||
|
||||
async getDueMaintenanceItems(userId: string): Promise<DueMaintenanceItem[]> {
|
||||
const res = await this.db.query(
|
||||
`SELECT
|
||||
ms.id,
|
||||
ms.vehicle_id,
|
||||
v.name as vehicle_name,
|
||||
ms.category,
|
||||
ms.subtypes,
|
||||
ms.next_due_date,
|
||||
ms.next_due_mileage,
|
||||
ms.email_notifications,
|
||||
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
|
||||
ELSE false
|
||||
END as is_overdue,
|
||||
CASE
|
||||
WHEN ms.next_due_date <= CURRENT_DATE + INTERVAL '30 days' AND ms.next_due_date > CURRENT_DATE THEN true
|
||||
WHEN ms.next_due_mileage IS NOT NULL AND v.odometer >= ms.next_due_mileage - 500 AND v.odometer < ms.next_due_mileage THEN true
|
||||
ELSE false
|
||||
END as is_due_soon
|
||||
FROM maintenance_schedules ms
|
||||
JOIN vehicles v ON ms.vehicle_id = v.id
|
||||
WHERE ms.user_id = $1
|
||||
AND ms.is_active = true
|
||||
AND (
|
||||
ms.next_due_date <= CURRENT_DATE + INTERVAL '30 days'
|
||||
OR (ms.next_due_mileage IS NOT NULL AND v.odometer >= ms.next_due_mileage - 500)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN ms.next_due_date <= CURRENT_DATE THEN 0 ELSE 1 END,
|
||||
ms.next_due_date ASC NULLS LAST`,
|
||||
[userId]
|
||||
);
|
||||
return res.rows.map(row => this.mapDueMaintenanceItem(row));
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Expiring Documents
|
||||
// ========================
|
||||
|
||||
async getExpiringDocuments(userId: string): Promise<ExpiringDocument[]> {
|
||||
const res = await this.db.query(
|
||||
`SELECT
|
||||
d.id,
|
||||
d.vehicle_id,
|
||||
v.name as vehicle_name,
|
||||
d.document_type,
|
||||
d.title,
|
||||
d.expiration_date,
|
||||
d.email_notifications,
|
||||
CASE
|
||||
WHEN d.expiration_date < CURRENT_DATE THEN true
|
||||
ELSE false
|
||||
END as is_expired,
|
||||
CASE
|
||||
WHEN d.expiration_date >= CURRENT_DATE AND d.expiration_date <= CURRENT_DATE + INTERVAL '30 days' THEN true
|
||||
ELSE false
|
||||
END as is_expiring_soon
|
||||
FROM documents d
|
||||
JOIN vehicles v ON d.vehicle_id = v.id
|
||||
WHERE d.user_id = $1
|
||||
AND d.deleted_at IS NULL
|
||||
AND d.expiration_date IS NOT NULL
|
||||
AND d.expiration_date <= CURRENT_DATE + INTERVAL '30 days'
|
||||
ORDER BY
|
||||
CASE WHEN d.expiration_date < CURRENT_DATE THEN 0 ELSE 1 END,
|
||||
d.expiration_date ASC`,
|
||||
[userId]
|
||||
);
|
||||
return res.rows.map(row => this.mapExpiringDocument(row));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user