import "server-only" import { addMonths, addWeeks, isBefore } from "date-fns" import type { ReportExportRun, ReportExportSchedule } from "@/lib/prisma" import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions" type SerializableSchedule = ReportExportSchedule & { reportKeys: ReportExportKey[] | null recipients: string[] | null } export function sanitizeReportKeys(keys: string[]): ReportExportKey[] { return Array.from( new Set( keys .map((key) => key as ReportExportKey) .filter((key): key is ReportExportKey => Boolean(REPORT_EXPORT_DEFINITIONS[key])) ) ) } export function sanitizeRecipients(recipients: string[]): string[] { return Array.from( new Set( recipients .map((value) => value.trim().toLowerCase()) .filter((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) ) ) } type NextRunInput = { frequency: "daily" | "weekly" | "monthly" dayOfWeek?: number | null dayOfMonth?: number | null hour: number minute: number timezone: string } export function computeNextRunAt({ frequency, dayOfWeek, dayOfMonth, hour, minute, timezone }: NextRunInput, currentDate = new Date()): Date { const base = zonedDate(currentDate, timezone) let candidate = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate(), hour, minute, 0, 0)) if (frequency === "daily") { if (!isBefore(base, candidate)) { candidate.setUTCDate(candidate.getUTCDate() + 1) } return candidate } if (frequency === "weekly") { const targetDow = typeof dayOfWeek === "number" ? clamp(dayOfWeek, 0, 6) : 1 while (candidate.getUTCDay() !== targetDow || !isBefore(base, candidate)) { candidate = addWeeks(candidate, 1) } return candidate } // monthly const targetDay = typeof dayOfMonth === "number" ? clamp(dayOfMonth, 1, 28) : 1 candidate.setUTCDate(targetDay) if (!isBefore(base, candidate)) { candidate = addMonths(candidate, 1) candidate.setUTCDate(Math.min(targetDay, daysInMonth(candidate))) } return candidate } function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)) } function daysInMonth(date: Date) { const year = date.getUTCFullYear() const month = date.getUTCMonth() return new Date(Date.UTC(year, month + 1, 0)).getUTCDate() } function zonedDate(date: Date, timeZone: string) { const formatter = new Intl.DateTimeFormat("en-US", { timeZone, hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }) const parts = formatter.formatToParts(date) const get = (type: Intl.DateTimeFormatPartTypes) => Number(parts.find((part) => part.type === type)?.value ?? "0") const year = get("year") const month = get("month") const day = get("day") const hour = get("hour") const minute = get("minute") const second = get("second") return new Date(Date.UTC(year, month - 1, day, hour, minute, second)) } export function serializeSchedule( schedule: SerializableSchedule, runs: ReportExportRun[] = [] ) { return { id: schedule.id, name: schedule.name, tenantId: schedule.tenantId, reportKeys: schedule.reportKeys ?? [], range: schedule.range, companyId: schedule.companyId, companyName: schedule.companyName, format: schedule.format, frequency: schedule.frequency, dayOfWeek: schedule.dayOfWeek, dayOfMonth: schedule.dayOfMonth, hour: schedule.hour, minute: schedule.minute, timezone: schedule.timezone, recipients: schedule.recipients ?? [], status: schedule.status, lastRunAt: schedule.lastRunAt?.toISOString() ?? null, nextRunAt: schedule.nextRunAt?.toISOString() ?? null, createdAt: schedule.createdAt.toISOString(), updatedAt: schedule.updatedAt.toISOString(), runs: runs.map((run) => ({ id: run.id, status: run.status, startedAt: run.startedAt.toISOString(), completedAt: run.completedAt?.toISOString() ?? null, error: run.error, artifacts: Array.isArray(run.artifacts) ? (run.artifacts as Array>).map((artifact, index) => { const rawKey = artifact["key"] const rawName = artifact["fileName"] const rawMime = artifact["mimeType"] return { index, key: typeof rawKey === "string" ? rawKey : `arquivo-${index + 1}`, fileName: typeof rawName === "string" ? rawName : null, mimeType: typeof rawMime === "string" ? rawMime : null, } }) : [], })), } }