chore: sync staging
This commit is contained in:
parent
c5ddd54a3e
commit
561b19cf66
610 changed files with 105285 additions and 1206 deletions
149
src/server/report-schedule-service.ts
Normal file
149
src/server/report-schedule-service.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use server"
|
||||
|
||||
import { addMonths, addWeeks, isBefore } from "date-fns"
|
||||
import type { ReportExportRun, ReportExportSchedule } from "@prisma/client"
|
||||
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,
|
||||
}
|
||||
})
|
||||
: [],
|
||||
})),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue