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>
307 lines
20 KiB
XML
307 lines
20 KiB
XML
<?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 & 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>
|