From 321892bfac28661213b71b3b463cbd6126f2fd9a Mon Sep 17 00:00:00 2001 From: Klemek Date: Wed, 29 Jul 2026 14:41:06 +0200 Subject: [PATCH] feat: add rrule start date --- resources/lang/en.json | 3 ++- resources/lang/fr.json | 3 ++- resources/ts/components/CronInput.vue | 22 +++++++++++++++-- resources/ts/lib/recurrence.ts | 35 +++++++++++++++++++++------ resources/ts/lib/tasks.ts | 9 ++++--- 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 5adff8d..a1cd7d5 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -44,7 +44,8 @@ "October", "November", "December" - ] + ], + "rrule_start": "Start:" }, "checked": "Checked", "resets": "Resets", diff --git a/resources/lang/fr.json b/resources/lang/fr.json index f7dc53b..ef1af2c 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -44,7 +44,8 @@ "Octobre", "Novembre", "Décembre" - ] + ], + "rrule_start": "Début :" }, "checked": "Complété", "resets": "Reviens", diff --git a/resources/ts/components/CronInput.vue b/resources/ts/components/CronInput.vue index fe65c7a..209b379 100644 --- a/resources/ts/components/CronInput.vue +++ b/resources/ts/components/CronInput.vue @@ -12,16 +12,18 @@ const props = defineProps<{ editMode: boolean }>(); const cronType = ref(CronType.MANUAL); const cronValue = ref('0'); +const cronDate = ref(new Date()); function onCronChanged() { const data = parseCron(cron.value); rawCron.value = cron.value ?? DEFAULT_CRON; cronType.value = data.type; cronValue.value = data.value.toFixed(0); + cronDate.value = data.date ?? new Date(); } function updateCron() { - cron.value = getCron(cronType.value, parseInt(cronValue.value), rawCron.value); + cron.value = getCron(cronType.value, parseInt(cronValue.value), cronDate.value, rawCron.value); } function onChangeCronType() { @@ -48,6 +50,11 @@ function onChangeCronValue() { updateCron(); } +function onChangeCronDate(event: InputEvent) { + cronDate.value = new Date((event.target as HTMLInputElement).value); + updateCron(); +} + function onInputRawCron() { valid.value = validCron(rawCron.value); if (valid.value) { @@ -65,7 +72,7 @@ watch(cron, () => { diff --git a/resources/ts/lib/recurrence.ts b/resources/ts/lib/recurrence.ts index a4549a7..59aa2a6 100644 --- a/resources/ts/lib/recurrence.ts +++ b/resources/ts/lib/recurrence.ts @@ -7,17 +7,20 @@ const ONE_TIME_VALUE = "@one-time"; export function parseCron(cron: string | null): { type: CronType; value: number; + date: Date|null; } { let value: RegExpMatchArray | null; if (cron === null) { return { type: CronType.MANUAL, value: 0, + date: null, }; } else if (cron === ONE_TIME_VALUE) { return { type: CronType.ONE_TIME, value: 0, + date: null, }; } else if (cron.startsWith("RRULE:")) { return parseRrule(cron); @@ -25,41 +28,49 @@ export function parseCron(cron: string | null): { return { type: CronType.EVERY_DAY, value: parseInt(value[1] ?? ""), + date: null, }; } else if ((value = /^0 0 \* \* (\d+)$/i.exec(cron)) !== null) { return { type: CronType.EVERY_WEEK, value: parseInt(value[1] ?? ""), + date: null, }; } else if ((value = /^0 0 (\d+) \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: parseInt(value[1] ?? ""), + date: null, }; } else if ((value = /^0 0 L \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: 32, + date: null, }; } else if ((value = /^0 0 \* \* (\d+)#1$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: -parseInt(value[1] ?? ""), + date: null, }; } else if ((value = /^0 0 \* \* (\d+)L$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: -parseInt(value[1] ?? "") - 7, + date: null, }; } else if ((value = /^0 0 1 (\d+) \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_YEAR, value: parseInt(value[1] ?? ""), + date: null, }; } else { return { type: CronType.CUSTOM, value: 0, + date: null, }; } } @@ -67,6 +78,7 @@ export function parseCron(cron: string | null): { export function parseRrule(rule: string): { type: CronType; value: number; + date: Date | null; } { try { const rrule = RRule.parseString(rule); @@ -75,21 +87,25 @@ export function parseRrule(rule: string): { return { type: CronType.EVERY_N_MONTHS, value: (rrule.interval ?? 1) * 12, + date: rrule.dtstart ?? null, }; case RRule.MONTHLY: return { type: CronType.EVERY_N_MONTHS, value: rrule.interval ?? 1, + date: rrule.dtstart ?? null, }; case RRule.WEEKLY: return { type: CronType.EVERY_N_WEEKS, value: rrule.interval ?? 1, + date: rrule.dtstart ?? null, }; case RRule.DAILY: return { type: CronType.EVERY_N_DAYS, value: rrule.interval ?? 1, + date: rrule.dtstart ?? null, }; } } catch { @@ -98,12 +114,14 @@ export function parseRrule(rule: string): { return { type: CronType.MANUAL, value: 0, + date: null, }; } export function getCron( type: CronType, value: number, + date: Date, fallback: string, ): string | null { switch (type) { @@ -127,11 +145,11 @@ export function getCron( case CronType.EVERY_YEAR: return `0 0 1 ${value.toFixed(0)} *`; case CronType.EVERY_N_MONTHS: - return `RRULE:FREQ=MONTHLY;INTERVAL=${value.toFixed(0)}`; + return `RRULE:FREQ=MONTHLY;INTERVAL=${value.toFixed(0)}\nDTSTART:${dtStartFormat(date)}`; case CronType.EVERY_N_WEEKS: - return `RRULE:FREQ=WEEKLY;INTERVAL=${value.toFixed(0)}`; + return `RRULE:FREQ=WEEKLY;INTERVAL=${value.toFixed(0)}\nDTSTART:${dtStartFormat(date)}`; case CronType.EVERY_N_DAYS: - return `RRULE:FREQ=DAILY;INTERVAL=${value.toFixed(0)}`; + return `RRULE:FREQ=DAILY;INTERVAL=${value.toFixed(0)}\nDTSTART:${dtStartFormat(date)}`; case CronType.CUSTOM: if (validCron(fallback)) { return fallback; @@ -141,6 +159,10 @@ export function getCron( return null; } +function dtStartFormat(date: Date): string { + return `${date.getFullYear().toFixed(0)}${(date.getMonth() + 1).toFixed(0).padStart(2, '0')}${date.getDate().toFixed(0).padStart(2, '0')}T000000Z` +} + export function isOneTime(cron: string | null): boolean { return cron === ONE_TIME_VALUE; } @@ -155,11 +177,11 @@ export function validCron(cron: string): boolean { } export function nextCronReset( - date: Date | null, + date: Date, cron: string | null, hash: string, ): Date | null { - if (date === null || cron === null || isOneTime(cron)) { + if (cron === null || isOneTime(cron)) { return null; } try { @@ -167,11 +189,10 @@ export function nextCronReset( try { const rule = new RRule({ ...RRule.parseString(cron), - dtstart: date, count: 2, }); - return rule.all()[1] ?? null; + return rule.after(date) ?? null; } catch { // ignore } diff --git a/resources/ts/lib/tasks.ts b/resources/ts/lib/tasks.ts index c5004b6..490dd95 100644 --- a/resources/ts/lib/tasks.ts +++ b/resources/ts/lib/tasks.ts @@ -35,17 +35,20 @@ export function isTemporary(task: Task): boolean { } export function taskNextReset(task: Task): Date | null { - return nextCronReset(task.check_date, task.reset_cron, task.id); + return nextCronReset(task.check_date ?? new Date(), task.reset_cron, task.id); } export function taskFutureReset(task: Task): Date | null { - return nextCronReset(taskNextReset(task), task.reset_cron, task.id); + return nextCronReset(taskNextReset(task) ?? new Date(), task.reset_cron, task.id); } export function taskCompleted(task: Task): boolean { + if (task.check_date === null) { + return false; + } const nextReset = taskNextReset(task); if (nextReset === null) { - return task.check_date !== null; + return true; } return nextReset.getTime() > new Date().getTime(); }