feat(fusion_clock): native recurring shifts engine [A4-A5]

fusion.clock.schedule.recurrence (repeat every N day/week/month/year;
forever/until/N-times) re-fit from planning.recurrency onto per-day rows;
daily generation cron; _fclk_on_leave skip; planner Repeat…/Stop-repeat
UI + endpoints; recurrence + role indicators on cells.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 20:49:26 -04:00
parent b4ca85e291
commit 734b3b94fd
12 changed files with 591 additions and 0 deletions

View File

@@ -45,6 +45,9 @@ export class FusionClockShiftPlanner extends Component {
error: "",
top: 0,
left: 0,
recurring: false,
showRepeat: false,
repeat: { interval: 1, unit: "week", type: "forever", until: "", number: 4 },
},
});
@@ -258,9 +261,72 @@ export class FusionClockShiftPlanner extends Component {
this.state.editor.breakMinutes = breakMinutes;
this.state.editor.hoursDisplay = cell.hours_display || this._formatHours(hours);
this.state.editor.error = cell.error || "";
this.state.editor.recurring = !!cell.recurring;
this.state.editor.showRepeat = false;
this._positionActiveEditor(anchor);
}
toggleRepeatPanel() {
this.state.editor.showRepeat = !this.state.editor.showRepeat;
}
onRepeatField(field, ev) {
const value = ev.target.value;
this.state.editor.repeat[field] =
field === "interval" || field === "number" ? Number(value) : value;
}
async setRecurrence() {
const editor = this.state.editor;
this.state.saving = true;
try {
const result = await rpc("/fusion_clock/shift_planner/set_recurrence", {
employee_id: editor.employeeId,
date: editor.date,
week_start: this.state.weekStart,
repeat: {
repeat_interval: editor.repeat.interval,
repeat_unit: editor.repeat.unit,
repeat_type: editor.repeat.type,
repeat_until: editor.repeat.until || false,
repeat_number: editor.repeat.number,
},
});
if (result.error || result.success === false) {
this.notification.add(result.error || result.message || "Could not repeat shift.", {
type: "danger",
});
} else {
this._applyData(result.data);
this.notification.add("Recurring shift created.", { type: "success" });
}
} catch (error) {
this.notification.add(error.message || "Could not repeat shift.", { type: "danger" });
}
this.state.saving = false;
}
async clearRecurrence() {
const editor = this.state.editor;
this.state.saving = true;
try {
const result = await rpc("/fusion_clock/shift_planner/clear_recurrence", {
employee_id: editor.employeeId,
date: editor.date,
week_start: this.state.weekStart,
});
if (result.error) {
this.notification.add(result.error, { type: "danger" });
} else {
this._applyData(result.data);
this.notification.add("Recurrence stopped.", { type: "success" });
}
} catch (error) {
this.notification.add(error.message || "Could not stop recurrence.", { type: "danger" });
}
this.state.saving = false;
}
closeCellEditor() {
this.state.editor.open = false;
this.activeCellAnchor = null;