Files
Odoo-Modules/fusion_clock/static/src/xml/fusion_clock_shift_planner.xml
gsinghpal 2ad94070c7 feat(fusion_clock): open shifts + self-assign + bulk apply [B4-B5]
Model: fclk_create_open_shifts/claim_open_shift/release_shift (days-before
cutoff + role eligibility)/bulk_apply. Planner: Open Shift… panel, open-shifts
strip with delete, Apply-to-dept; load includes open shifts. Portal: claim
open shifts + release own upcoming shifts with feedback banners. Tests for
claim/role-gate/release/bulk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:12:10 -04:00

307 lines
20 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="fusion_clock.ShiftPlanner">
<div class="o_action fclk-planner" t-ref="root">
<div class="fclk-planner__toolbar">
<div>
<h2 class="fclk-planner__title">Shift Planner</h2>
<div class="fclk-planner__subtitle"><t t-esc="weekTitle"/></div>
</div>
<div class="fclk-planner__actions">
<button class="btn btn-light" t-on-click="() => this.previousWeek()" t-att-disabled="state.loading or state.saving">
<i class="fa fa-chevron-left"/>
</button>
<button class="btn btn-light" t-on-click="() => this.currentWeek()" t-att-disabled="state.loading or state.saving">This Week</button>
<button class="btn btn-light" t-on-click="() => this.nextWeek()" t-att-disabled="state.loading or state.saving">
<i class="fa fa-chevron-right"/>
</button>
<button class="btn btn-outline-secondary" t-on-click="() => this.copyPreviousWeek()" t-att-disabled="state.loading or state.saving">
<i class="fa fa-copy me-1"/> Copy Previous Week
</button>
<button class="btn btn-outline-secondary" t-on-click="() => this.exportXlsx()" t-att-disabled="state.loading or state.saving">
<i class="fa fa-file-excel-o me-1"/> Export XLSX
</button>
<button class="btn btn-primary" t-on-click="() => this.save()" t-att-disabled="state.loading or state.saving or !state.dirtyCount">
<t t-if="state.saving"><i class="fa fa-spinner fa-spin me-1"/></t>
<t t-else=""><i class="fa fa-save me-1"/></t>
Save
<t t-if="state.dirtyCount">(<t t-esc="state.dirtyCount"/>)</t>
</button>
<button class="btn btn-success" t-on-click="() => this.postWeek()" t-att-disabled="state.loading or state.saving or state.dirtyCount" t-att-title="state.dirtyCount ? 'Save your changes before posting' : 'Publish this week and email employees their shifts'">
<i class="fa fa-paper-plane me-1"/> Post Schedule
<t t-if="state.draftCount">(<t t-esc="state.draftCount"/> draft)</t>
</button>
<button class="btn btn-outline-success" t-on-click="() => this.togglePublishPanel()" t-att-disabled="state.loading or state.saving" title="Publish a custom date range and notify employees">
<i class="fa fa-calendar-check-o me-1"/> Publish…
</button>
<button class="btn btn-outline-secondary" t-on-click="() => this.toggleOpenShiftPanel()" t-att-disabled="state.loading or state.saving" title="Create an open shift employees can claim">
<i class="fa fa-plus me-1"/> Open Shift…
</button>
</div>
</div>
<div t-if="state.publish.open" class="fclk-planner__publish-panel">
<label>From <input type="date" t-att-value="state.publish.from" t-on-change="(ev) => this.onPublishField('from', ev)"/></label>
<label>To <input type="date" t-att-value="state.publish.to" t-on-change="(ev) => this.onPublishField('to', ev)"/></label>
<input type="text" class="fclk-planner__publish-msg" placeholder="Optional message to employees…"
t-att-value="state.publish.message" t-on-change="(ev) => this.onPublishField('message', ev)"/>
<button class="btn btn-success btn-sm" t-on-click="() => this.publishRange()" t-att-disabled="state.saving">
<i class="fa fa-paper-plane me-1"/> Publish &amp; Notify
</button>
<button class="btn btn-light btn-sm" t-on-click="() => this.togglePublishPanel()">Cancel</button>
</div>
<div t-if="state.openShift.open" class="fclk-planner__publish-panel">
<label>Date <input type="date" t-att-value="state.openShift.date" t-on-change="(ev) => this.onOpenShiftField('date', ev)"/></label>
<label>Start <input type="time" t-att-value="state.openShift.start" t-on-change="(ev) => this.onOpenShiftField('start', ev)"/></label>
<label>End <input type="time" t-att-value="state.openShift.end" t-on-change="(ev) => this.onOpenShiftField('end', ev)"/></label>
<label>Count <input type="number" min="1" class="fclk-planner__repeat-int" t-att-value="state.openShift.count" t-on-change="(ev) => this.onOpenShiftField('count', ev)"/></label>
<button class="btn btn-secondary btn-sm" t-on-click="() => this.addOpenShift()" t-att-disabled="state.saving">
<i class="fa fa-plus me-1"/> Add Open Shift
</button>
<button class="btn btn-light btn-sm" t-on-click="() => this.toggleOpenShiftPanel()">Cancel</button>
</div>
<div t-if="hasOpenShifts" class="fclk-planner__open-strip">
<div class="fclk-planner__open-strip-title"><i class="fa fa-bullhorn me-1"/> Open Shifts (employees can claim)</div>
<div class="fclk-planner__open-cols">
<t t-foreach="state.days" t-as="day" t-key="'open_' + day.date">
<div class="fclk-planner__open-col" t-if="getOpenShiftsForDay(day.date).length">
<div class="fclk-planner__open-day"><t t-esc="day.weekday"/> <t t-esc="day.label"/></div>
<t t-foreach="getOpenShiftsForDay(day.date)" t-as="op" t-key="op.id">
<div class="fclk-planner__open-chip">
<span><t t-esc="op.label"/></span>
<span t-if="op.role_name" class="fclk-planner__open-role"><t t-esc="op.role_name"/></span>
<button class="fclk-planner__open-del" t-on-click="() => this.deleteOpenShift(op.id)" title="Remove open shift">×</button>
</div>
</t>
</div>
</t>
</div>
</div>
<t t-if="state.error">
<div class="alert alert-danger mx-3 mt-3"><t t-esc="state.error"/></div>
</t>
<t t-if="state.invalidCount">
<div class="fclk-planner__warning">
<i class="fa fa-exclamation-triangle me-1"/>
<t t-esc="state.invalidCount"/> invalid cells need attention.
</div>
</t>
<t t-if="state.loading">
<div class="fclk-planner__loading">
<i class="fa fa-spinner fa-spin fa-2x"/>
<span>Loading shift planner...</span>
</div>
</t>
<t t-if="!state.loading and !state.error">
<div class="fclk-planner__table-wrap">
<table class="fclk-planner__table">
<colgroup>
<col class="fclk-planner__employee-col"/>
<t t-foreach="state.days" t-as="day" t-key="'col_' + day.date">
<col class="fclk-planner__shift-col"/>
<col class="fclk-planner__hours-col"/>
</t>
</colgroup>
<thead>
<tr>
<th class="fclk-planner__employee-head" rowspan="2">Employee</th>
<t t-foreach="state.days" t-as="day" t-key="day.date">
<th class="fclk-planner__day-head" colspan="2">
<div class="fclk-planner__weekday"><t t-esc="day.weekday"/></div>
<div class="fclk-planner__date"><t t-esc="day.label"/></div>
</th>
</t>
</tr>
<tr>
<t t-foreach="state.days" t-as="day" t-key="'sub_' + day.date">
<th class="fclk-planner__sub-head">Shift</th>
<th class="fclk-planner__sub-head fclk-planner__hours-head">Hours</th>
</t>
</tr>
</thead>
<tbody>
<t t-foreach="state.departments" t-as="department" t-key="department.id">
<tr class="fclk-planner__department-row">
<td t-att-colspan="1 + state.days.length * 2">
<button class="fclk-planner__department-toggle" t-on-click="() => this.toggleDepartment(department)">
<i t-att-class="isCollapsed(department) ? 'fa fa-chevron-right' : 'fa fa-chevron-down'"/>
<span><t t-esc="department.name"/></span>
<span class="fclk-planner__department-count">
<t t-esc="department.employee_ids.length"/> employees
</span>
</button>
</td>
</tr>
<t t-if="!isCollapsed(department)">
<t t-foreach="getDepartmentEmployees(department)" t-as="employee" t-key="employee.id">
<tr class="fclk-planner__employee-row">
<td class="fclk-planner__employee-cell">
<div class="fclk-planner__employee-name"><t t-esc="employee.name"/></div>
<div class="fclk-planner__employee-role" t-if="employee.job_title">
<t t-esc="employee.job_title"/>
</div>
</td>
<t t-foreach="state.days" t-as="day" t-key="employee.id + '_' + day.date">
<t t-set="cell" t-value="employee.cells[day.date]"/>
<td t-att-class="'fclk-planner__shift-cell ' + (cell.error ? 'fclk-planner__shift-cell--error ' : '') + (cell.source !== 'schedule' ? 'fclk-planner__shift-cell--fallback ' : '') + (this.isActiveCell(employee, day) ? 'fclk-planner__shift-cell--active' : '')"
t-on-click="(ev) => this.openCellEditor(employee, day, ev)">
<input class="fclk-planner__shift-input"
t-att-value="cell.input"
t-att-title="cell.error || cell.label"
t-on-focus="(ev) => this.openCellEditor(employee, day, ev)"
t-on-change="(ev) => this.onCellInput(employee, day, ev)"
t-on-keydown="(ev) => this.onCellKeydown(employee, day, ev)"/>
<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'"/>
</td>
</t>
</tr>
</t>
</t>
</t>
</tbody>
</table>
</div>
<div t-if="state.editor.open"
t-ref="shiftEditor"
class="fclk-planner__cell-editor"
t-att-style="'top: ' + state.editor.top + 'px; left: ' + state.editor.left + 'px;'">
<div class="fclk-planner__editor-head">
<div class="fclk-planner__editor-person">
<div class="fclk-planner__editor-name"><t t-esc="state.editor.employeeName"/></div>
<div class="fclk-planner__editor-day"><t t-esc="state.editor.dayLabel"/></div>
</div>
<div class="fclk-planner__editor-hours">
<span><t t-esc="state.editor.hoursDisplay"/></span>
</div>
</div>
<div class="fclk-planner__quick-grid">
<t t-foreach="quickShiftOptions" t-as="option" t-key="option.key">
<button type="button"
class="fclk-planner__quick-chip"
t-on-click="() => this.selectQuickShift(option)">
<span class="fclk-planner__quick-label"><t t-esc="option.label"/></span>
<span class="fclk-planner__quick-detail"><t t-esc="option.detail"/></span>
</button>
</t>
</div>
<div class="fclk-planner__time-row">
<label class="fclk-planner__time-field">
<span>Start</span>
<select t-on-change="(ev) => this.onEditorStartChange(ev)">
<t t-foreach="timeOptions" t-as="option" t-key="'start_' + option.value">
<option t-att-value="option.value"
t-att-selected="option.value === state.editor.startValue">
<t t-esc="option.label"/>
</option>
</t>
</select>
</label>
<label class="fclk-planner__time-field">
<span>End</span>
<select t-on-change="(ev) => this.onEditorEndChange(ev)">
<t t-foreach="timeOptions" t-as="option" t-key="'end_' + option.value">
<option t-att-value="option.value"
t-att-selected="option.value === state.editor.endValue">
<t t-esc="option.label"/>
</option>
</t>
</select>
</label>
</div>
<div class="fclk-planner__editor-error" t-if="state.editor.error">
<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-light"
t-on-click="() => this.bulkApplyDept()"
title="Apply this shift to everyone in the same department">
<i class="fa fa-users me-1"/> Apply to dept
</button>
<button type="button"
class="btn btn-sm btn-primary"
t-on-click="() => this.applyEditorRange(true)">
<i class="fa fa-check me-1"/> Done
</button>
</div>
</div>
</t>
</div>
</t>
</templates>