Final MVP valid
This commit is contained in:
123
app/api/downtime/actions/reminders/route.ts
Normal file
123
app/api/downtime/actions/reminders/route.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireSession } from "@/lib/auth/requireSession";
|
||||
import { buildDowntimeActionReminderEmail, sendEmail } from "@/lib/email";
|
||||
import { getBaseUrl } from "@/lib/appUrl";
|
||||
|
||||
const DEFAULT_DUE_DAYS = 7;
|
||||
const DEFAULT_LIMIT = 100;
|
||||
const MS_PER_HOUR = 60 * 60 * 1000;
|
||||
|
||||
type ReminderStage = "week" | "day" | "hour" | "overdue";
|
||||
|
||||
function formatDueDate(value?: Date | null) {
|
||||
if (!value) return null;
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getReminderStage(dueDate: Date, now: Date): ReminderStage | null {
|
||||
const diffMs = dueDate.getTime() - now.getTime();
|
||||
if (diffMs <= 0) return "overdue";
|
||||
if (diffMs <= MS_PER_HOUR) return "hour";
|
||||
if (diffMs <= 24 * MS_PER_HOUR) return "day";
|
||||
if (diffMs <= 7 * 24 * MS_PER_HOUR) return "week";
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildActionUrl(baseUrl: string, action: { machineId: string | null; reasonCode: string | null; hmDay: number | null; hmHour: number | null }) {
|
||||
const params = new URLSearchParams();
|
||||
if (action.machineId) params.set("machineId", action.machineId);
|
||||
if (action.reasonCode) params.set("reasonCode", action.reasonCode);
|
||||
if (action.hmDay != null && action.hmHour != null) {
|
||||
params.set("hmDay", String(action.hmDay));
|
||||
params.set("hmHour", String(action.hmHour));
|
||||
}
|
||||
const qs = params.toString();
|
||||
return qs ? `${baseUrl}/downtime?${qs}` : `${baseUrl}/downtime`;
|
||||
}
|
||||
|
||||
async function authorizeRequest(req: Request) {
|
||||
const secret = process.env.DOWNTIME_ACTION_REMINDER_SECRET;
|
||||
if (!secret) {
|
||||
const session = await requireSession();
|
||||
return { ok: !!session };
|
||||
}
|
||||
const authHeader = req.headers.get("authorization") || "";
|
||||
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : null;
|
||||
const urlToken = new URL(req.url).searchParams.get("token");
|
||||
return { ok: token === secret || urlToken === secret };
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const auth = await authorizeRequest(req);
|
||||
if (!auth.ok) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const dueInDays = Number(sp.get("dueInDays") || DEFAULT_DUE_DAYS);
|
||||
const limit = Number(sp.get("limit") || DEFAULT_LIMIT);
|
||||
|
||||
const now = new Date();
|
||||
const dueBy = new Date(now.getTime() + dueInDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const actions = await prisma.downtimeAction.findMany({
|
||||
where: {
|
||||
status: { not: "done" },
|
||||
ownerUserId: { not: null },
|
||||
dueDate: { not: null, lte: dueBy },
|
||||
},
|
||||
include: {
|
||||
ownerUser: { select: { name: true, email: true } },
|
||||
org: { select: { name: true } },
|
||||
},
|
||||
orderBy: { dueDate: "asc" },
|
||||
take: Number.isFinite(limit) ? Math.max(1, Math.min(500, limit)) : DEFAULT_LIMIT,
|
||||
});
|
||||
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const sentIds: string[] = [];
|
||||
const failures: Array<{ id: string; error: string }> = [];
|
||||
|
||||
for (const action of actions) {
|
||||
const email = action.ownerUser?.email;
|
||||
if (!email) continue;
|
||||
if (!action.dueDate) continue;
|
||||
const stage = getReminderStage(action.dueDate, now);
|
||||
if (!stage) continue;
|
||||
if (action.reminderStage === stage) continue;
|
||||
try {
|
||||
const content = buildDowntimeActionReminderEmail({
|
||||
appName: "MIS Control Tower",
|
||||
orgName: action.org.name,
|
||||
actionTitle: action.title,
|
||||
assigneeName: action.ownerUser?.name ?? email,
|
||||
dueDate: formatDueDate(action.dueDate),
|
||||
actionUrl: buildActionUrl(baseUrl, action),
|
||||
priority: action.priority,
|
||||
status: action.status,
|
||||
});
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: content.subject,
|
||||
text: content.text,
|
||||
html: content.html,
|
||||
});
|
||||
sentIds.push(action.id);
|
||||
await prisma.downtimeAction.update({
|
||||
where: { id: action.id },
|
||||
data: { reminderStage: stage, lastReminderAt: now },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
failures.push({
|
||||
id: action.id,
|
||||
error: err instanceof Error ? err.message : "Failed to send reminder email",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
sent: sentIds.length,
|
||||
failed: failures.length,
|
||||
failures,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user