/** Режимы расписания (5-field cron, UTC) — совместимы с robfig/cron в scheduler. */ export type ScheduleMode = 'minutes' | 'hours' | 'daily' | 'weekly' | 'custom'; export type ScheduleEditor = { mode: ScheduleMode; /** Интервал в минутах (режим minutes). */ intervalMinutes: string; /** Интервал в часах (режим hours). */ intervalHours: string; minute: string; hour: string; /** 0 = воскресенье … 6 = суббота */ weekday: string; customCron: string; }; export const scheduleModeOptions: { value: ScheduleMode; label: string }[] = [ { value: 'minutes', label: 'Каждые N минут' }, { value: 'hours', label: 'Каждые N часов' }, { value: 'daily', label: 'Ежедневно в указанное время' }, { value: 'weekly', label: 'Еженедельно в указанный день' }, { value: 'custom', label: 'Cron вручную (расширенный)' } ]; export const weekdayOptions = [ { value: '0', label: 'Воскресенье' }, { value: '1', label: 'Понедельник' }, { value: '2', label: 'Вторник' }, { value: '3', label: 'Среда' }, { value: '4', label: 'Четверг' }, { value: '5', label: 'Пятница' }, { value: '6', label: 'Суббота' } ] as const; export function defaultScheduleEditor(cron = '0 3 * * *'): ScheduleEditor { return cronToEditor(cron); } function splitCron(cron: string): [string, string, string, string, string] | null { const parts = cron.trim().split(/\s+/); if (parts.length !== 5) return null; return [parts[0], parts[1], parts[2], parts[3], parts[4]]; } function clampInt(raw: string, min: number, max: number): number { const n = Math.floor(Number(String(raw).trim())); if (!Number.isFinite(n)) return min; return Math.min(max, Math.max(min, n)); } function pad2(n: number): string { return String(n).padStart(2, '0'); } /** Пытается разобрать cron в редактор; неизвестные выражения → custom. */ export function cronToEditor(cron: string): ScheduleEditor { const base: ScheduleEditor = { mode: 'custom', intervalMinutes: '30', intervalHours: '2', minute: '0', hour: '3', weekday: '0', customCron: cron.trim() || '0 3 * * *' }; const parts = splitCron(cron); if (!parts) return base; const [min, hour, dom, month, dow] = parts; const minEvery = min.match(/^\*\/(\d+)$/); if (minEvery && hour === '*' && dom === '*' && month === '*' && dow === '*') { return { ...base, mode: 'minutes', intervalMinutes: minEvery[1], customCron: cron.trim() }; } const hourEvery = hour.match(/^\*\/(\d+)$/); if (hourEvery && !min.includes('*') && dom === '*' && month === '*' && dow === '*') { return { ...base, mode: 'hours', intervalHours: hourEvery[1], minute: min, customCron: cron.trim() }; } if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow === '*') { return { ...base, mode: 'daily', minute: min, hour, customCron: cron.trim() }; } if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow !== '*') { return { ...base, mode: 'weekly', minute: min, hour, weekday: dow, customCron: cron.trim() }; } return base; } /** Собирает 5-field cron из редактора. */ export function editorToCron(editor: ScheduleEditor): string { switch (editor.mode) { case 'minutes': { const n = clampInt(editor.intervalMinutes, 1, 59); return `*/${n} * * * *`; } case 'hours': { const n = clampInt(editor.intervalHours, 1, 23); const m = clampInt(editor.minute, 0, 59); return `${m} */${n} * * *`; } case 'daily': { const m = clampInt(editor.minute, 0, 59); const h = clampInt(editor.hour, 0, 23); return `${m} ${h} * * *`; } case 'weekly': { const m = clampInt(editor.minute, 0, 59); const h = clampInt(editor.hour, 0, 23); const d = clampInt(editor.weekday, 0, 6); return `${m} ${h} * * ${d}`; } case 'custom': return editor.customCron.trim() || '0 3 * * *'; } } /** Человекочитаемое описание расписания для таблицы и подсказок. */ export function describeCron(cron: string): string { const ed = cronToEditor(cron); switch (ed.mode) { case 'minutes': { const n = clampInt(ed.intervalMinutes, 1, 59); return `Каждые ${n} ${minutesLabel(n)} (UTC)`; } case 'hours': { const n = clampInt(ed.intervalHours, 1, 23); const m = clampInt(ed.minute, 0, 59); return `Каждые ${n} ${hoursLabel(n)}, в :${pad2(m)} (UTC)`; } case 'daily': { const h = clampInt(ed.hour, 0, 23); const m = clampInt(ed.minute, 0, 59); return `Ежедневно в ${pad2(h)}:${pad2(m)} UTC`; } case 'weekly': { const wd = weekdayOptions.find((w) => w.value === String(clampInt(ed.weekday, 0, 6)))?.label ?? ed.weekday; const h = clampInt(ed.hour, 0, 23); const m = clampInt(ed.minute, 0, 59); return `Каждую ${wd.toLowerCase()} в ${pad2(h)}:${pad2(m)} UTC`; } case 'custom': return `Cron: ${cron.trim()}`; } } /** Синхронизирует form.schedule из редактора. */ export function applyScheduleEditor(editor: ScheduleEditor): { editor: ScheduleEditor; cron: string; } { if (editor.mode === 'custom') { const cron = editor.customCron.trim() || '0 3 * * *'; return { editor: { ...editor, customCron: cron }, cron }; } const cron = editorToCron(editor); return { editor: { ...editor, customCron: cron }, cron }; } function minutesLabel(n: number): string { if (n % 10 === 1 && n % 100 !== 11) return 'минуту'; if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'минуты'; return 'минут'; } function hoursLabel(n: number): string { if (n % 10 === 1 && n % 100 !== 11) return 'час'; if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'часа'; return 'часов'; }