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

@@ -115,6 +115,13 @@
<div class="fclk-planner__cell-error" t-if="cell.error">
<t t-esc="cell.error"/>
</div>
<span class="fclk-planner__cell-recur" t-if="cell.recurring"
title="Recurring shift">
<i class="fa fa-repeat"/>
</span>
<span class="fclk-planner__cell-role" t-if="cell.role_color"
t-att-style="'background-color: ' + cell.role_color + ';'"
t-att-title="cell.role_name"/>
</td>
<td class="fclk-planner__hours-cell">
<t t-esc="cell.hours_display || '0:00'"/>
@@ -182,12 +189,57 @@
<t t-esc="state.editor.error"/>
</div>
<div class="fclk-planner__repeat-panel" t-if="state.editor.showRepeat">
<div class="fclk-planner__repeat-row">
<span>Every</span>
<input type="number" min="1" class="fclk-planner__repeat-int"
t-att-value="state.editor.repeat.interval"
t-on-change="(ev) => this.onRepeatField('interval', ev)"/>
<select t-on-change="(ev) => this.onRepeatField('unit', ev)">
<option value="day" t-att-selected="state.editor.repeat.unit === 'day'">day(s)</option>
<option value="week" t-att-selected="state.editor.repeat.unit === 'week'">week(s)</option>
<option value="month" t-att-selected="state.editor.repeat.unit === 'month'">month(s)</option>
<option value="year" t-att-selected="state.editor.repeat.unit === 'year'">year(s)</option>
</select>
</div>
<div class="fclk-planner__repeat-row">
<select t-on-change="(ev) => this.onRepeatField('type', ev)">
<option value="forever" t-att-selected="state.editor.repeat.type === 'forever'">Forever</option>
<option value="until" t-att-selected="state.editor.repeat.type === 'until'">Until date</option>
<option value="x_times" t-att-selected="state.editor.repeat.type === 'x_times'"># of times</option>
</select>
<input type="date" t-if="state.editor.repeat.type === 'until'"
t-att-value="state.editor.repeat.until"
t-on-change="(ev) => this.onRepeatField('until', ev)"/>
<input type="number" min="1" t-if="state.editor.repeat.type === 'x_times'"
class="fclk-planner__repeat-int"
t-att-value="state.editor.repeat.number"
t-on-change="(ev) => this.onRepeatField('number', ev)"/>
</div>
<button type="button" class="btn btn-sm btn-primary w-100"
t-on-click="() => this.setRecurrence()">
<i class="fa fa-check me-1"/> Apply recurrence
</button>
</div>
<div class="fclk-planner__editor-actions">
<button type="button"
class="btn btn-sm btn-light"
t-on-click="() => this.clearActiveCell()">
<i class="fa fa-eraser me-1"/> Clear
</button>
<button type="button"
t-if="!state.editor.recurring"
class="btn btn-sm btn-light"
t-on-click="() => this.toggleRepeatPanel()">
<i class="fa fa-repeat me-1"/> Repeat…
</button>
<button type="button"
t-if="state.editor.recurring"
class="btn btn-sm btn-warning"
t-on-click="() => this.clearRecurrence()">
<i class="fa fa-ban me-1"/> Stop repeat
</button>
<button type="button"
class="btn btn-sm btn-primary"
t-on-click="() => this.applyEditorRange(true)">