149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
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<Record<string, unknown>>).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,
|
|
}
|
|
})
|
|
: [],
|
|
})),
|
|
}
|
|
}
|