129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
import 'server-only'
|
|
import { sendEmail } from '@/lib/notifications/mailer'
|
|
import { asAdmin } from '@/lib/db/with-user'
|
|
import { capaActions, incidents, users } from '@/lib/db/schema'
|
|
import { and, eq, inArray, isNotNull, lt } from 'drizzle-orm'
|
|
import { sendWhatsAppMessage } from '@/lib/notifications/whatsapp'
|
|
import { getApiKey } from '@/lib/settings'
|
|
|
|
const ROUND_LABELS = ['30-day', '60-day', '90-day'] as const
|
|
const NEXT_INTERVAL_DAYS: (number | null)[] = [60, 90, null]
|
|
|
|
export function getRoundLabel(round: number): string {
|
|
return ROUND_LABELS[round] ?? '90-day'
|
|
}
|
|
|
|
export function getNextRecheckDate(verifiedAt: string, currentRound: number): string | null {
|
|
const days = NEXT_INTERVAL_DAYS[currentRound]
|
|
if (days === null || days === undefined) return null
|
|
const base = new Date(verifiedAt)
|
|
base.setUTCDate(base.getUTCDate() + days)
|
|
return base.toISOString().split('T')[0]
|
|
}
|
|
|
|
export function shouldSendRecheck(
|
|
recheckDate: string | null,
|
|
round: number,
|
|
today: string = new Date().toISOString().split('T')[0],
|
|
): boolean {
|
|
if (!recheckDate || round >= 3) return false
|
|
return recheckDate <= today
|
|
}
|
|
|
|
export async function sendEffectivenessRecheckNotifications(): Promise<{ notified: number }> {
|
|
const today = new Date().toISOString().split('T')[0]
|
|
|
|
const capas = await asAdmin(db =>
|
|
db.select({
|
|
id: capaActions.id,
|
|
description: capaActions.description,
|
|
effectivenessRecheckDate: capaActions.effectivenessRecheckDate,
|
|
effectivenessRecheckRound: capaActions.effectivenessRecheckRound,
|
|
verifiedAt: capaActions.verifiedAt,
|
|
incidentId: capaActions.incidentId,
|
|
incidentRef: incidents.referenceNo,
|
|
verifierEmail: users.email,
|
|
verifierName: users.name,
|
|
verifierPhone: users.phone,
|
|
})
|
|
.from(capaActions)
|
|
.leftJoin(incidents, eq(capaActions.incidentId, incidents.id))
|
|
.leftJoin(users, eq(capaActions.verifiedBy, users.id))
|
|
.where(and(
|
|
inArray(capaActions.status, ['verified', 'closed']),
|
|
isNotNull(capaActions.effectivenessRecheckDate),
|
|
lt(capaActions.effectivenessRecheckRound, 3),
|
|
))
|
|
)
|
|
|
|
if (!capas || capas.length === 0) return { notified: 0 }
|
|
|
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
|
|
|
|
let whatsappPhoneId: string | null = null
|
|
let whatsappToken: string | null = null
|
|
try {
|
|
whatsappPhoneId = await getApiKey('META_WHATSAPP_PHONE_NUMBER_ID')
|
|
whatsappToken = await getApiKey('META_WHATSAPP_ACCESS_TOKEN')
|
|
} catch {
|
|
// WhatsApp not configured — email only
|
|
}
|
|
|
|
let notified = 0
|
|
|
|
for (const capa of capas) {
|
|
const recheckDate = capa.effectivenessRecheckDate
|
|
const round = capa.effectivenessRecheckRound
|
|
|
|
if (!shouldSendRecheck(recheckDate, round, today)) continue
|
|
|
|
const verifierEmail = capa.verifierEmail
|
|
const verifierName = capa.verifierName ?? 'HSE Officer'
|
|
const verifierPhone = capa.verifierPhone ?? ''
|
|
if (!verifierEmail) continue
|
|
|
|
const incidentRef = capa.incidentRef ?? capa.incidentId
|
|
const roundLabel = getRoundLabel(round)
|
|
const capaDesc = capa.description
|
|
const capaUrl = `${siteUrl}/hse/capa/${capa.id}`
|
|
|
|
const subject = `[IMS] ${roundLabel} effectiveness check — ${incidentRef}`
|
|
const html = `
|
|
<p>Hi ${verifierName},</p>
|
|
<p>This is the <strong>${roundLabel} effectiveness check</strong> for a corrective action you verified on incident <strong>${incidentRef}</strong>.</p>
|
|
<p><strong>Action:</strong> ${capaDesc}</p>
|
|
<p>Please confirm the corrective action is still in place and effective.</p>
|
|
<p><a href="${capaUrl}">View CAPA</a></p>
|
|
`
|
|
const text = `${roundLabel} effectiveness check for ${incidentRef}\nAction: ${capaDesc}\n${capaUrl}`
|
|
|
|
await sendEmail({ to: verifierEmail, subject, html, text })
|
|
|
|
// WhatsApp (non-blocking, best-effort)
|
|
if (whatsappPhoneId && whatsappToken && verifierPhone) {
|
|
sendWhatsAppMessage(
|
|
verifierPhone,
|
|
'ims_effectiveness_recheck',
|
|
[roundLabel, incidentRef ?? '', capaDesc],
|
|
whatsappPhoneId,
|
|
whatsappToken,
|
|
).catch(err => console.error('WhatsApp recheck error:', err))
|
|
}
|
|
|
|
// Advance round
|
|
const nextDate = getNextRecheckDate(capa.verifiedAt!.toISOString(), round)
|
|
await asAdmin(db =>
|
|
db.update(capaActions)
|
|
.set({
|
|
effectivenessRecheckRound: round + 1,
|
|
effectivenessRecheckDate: nextDate ?? null,
|
|
})
|
|
.where(eq(capaActions.id, capa.id))
|
|
)
|
|
|
|
notified++
|
|
}
|
|
|
|
return { notified }
|
|
}
|