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>
This commit is contained in:
@@ -50,6 +50,8 @@ export class FusionClockShiftPlanner extends Component {
|
||||
repeat: { interval: 1, unit: "week", type: "forever", until: "", number: 4 },
|
||||
},
|
||||
publish: { open: false, from: "", to: "", message: "" },
|
||||
openShifts: {},
|
||||
openShift: { open: false, date: "", start: "09:00", end: "17:00", count: 1 },
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
@@ -92,6 +94,7 @@ export class FusionClockShiftPlanner extends Component {
|
||||
this.state.departments = data.departments || [];
|
||||
this.state.employees = data.employees || [];
|
||||
this.state.shifts = data.shifts || [];
|
||||
this.state.openShifts = data.open_shifts || {};
|
||||
this.state.dirtyCount = 0;
|
||||
this.state.invalidCount = 0;
|
||||
let draft = 0;
|
||||
@@ -368,6 +371,106 @@ export class FusionClockShiftPlanner extends Component {
|
||||
this.state.saving = false;
|
||||
}
|
||||
|
||||
_timeStrToFloat(str) {
|
||||
const [h, m] = (str || "0:0").split(":").map(Number);
|
||||
return (h || 0) + (m || 0) / 60;
|
||||
}
|
||||
|
||||
getOpenShiftsForDay(date) {
|
||||
return this.state.openShifts[date] || [];
|
||||
}
|
||||
|
||||
get hasOpenShifts() {
|
||||
return Object.keys(this.state.openShifts || {}).length > 0;
|
||||
}
|
||||
|
||||
toggleOpenShiftPanel() {
|
||||
this.state.openShift.open = !this.state.openShift.open;
|
||||
if (this.state.openShift.open && !this.state.openShift.date) {
|
||||
this.state.openShift.date = this.state.weekStart;
|
||||
}
|
||||
}
|
||||
|
||||
onOpenShiftField(field, ev) {
|
||||
this.state.openShift[field] = ev.target.value;
|
||||
}
|
||||
|
||||
async addOpenShift() {
|
||||
const os = this.state.openShift;
|
||||
this.state.saving = true;
|
||||
try {
|
||||
const result = await rpc("/fusion_clock/shift_planner/create_open_shift", {
|
||||
date: os.date || this.state.weekStart,
|
||||
start_time: this._timeStrToFloat(os.start),
|
||||
end_time: this._timeStrToFloat(os.end),
|
||||
count: Number(os.count) || 1,
|
||||
week_start: this.state.weekStart,
|
||||
});
|
||||
if (result.error || result.success === false) {
|
||||
this.notification.add(result.error || result.message || "Could not add open shift.", {
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
this._applyData(result.data);
|
||||
this.state.openShift.open = false;
|
||||
this.notification.add("Open shift added.", { type: "success" });
|
||||
}
|
||||
} catch (error) {
|
||||
this.notification.add(error.message || "Could not add open shift.", { type: "danger" });
|
||||
}
|
||||
this.state.saving = false;
|
||||
}
|
||||
|
||||
async deleteOpenShift(id) {
|
||||
this.state.saving = true;
|
||||
try {
|
||||
const result = await rpc("/fusion_clock/shift_planner/delete_open_shift", {
|
||||
schedule_id: id,
|
||||
week_start: this.state.weekStart,
|
||||
});
|
||||
if (!result.error) {
|
||||
this._applyData(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notification.add(error.message || "Could not remove open shift.", { type: "danger" });
|
||||
}
|
||||
this.state.saving = false;
|
||||
}
|
||||
|
||||
async bulkApplyDept() {
|
||||
const editor = this.state.editor;
|
||||
const employee = this.state.employees.find((e) => e.id === editor.employeeId);
|
||||
if (!employee) {
|
||||
return;
|
||||
}
|
||||
const department = this.state.departments.find((d) => d.id === employee.department_id);
|
||||
const ids = (department && department.employee_ids) || [employee.id];
|
||||
this.state.saving = true;
|
||||
try {
|
||||
const result = await rpc("/fusion_clock/shift_planner/bulk_apply", {
|
||||
employee_ids: ids,
|
||||
date: editor.date,
|
||||
week_start: this.state.weekStart,
|
||||
payload: {
|
||||
start_time: Number(editor.startValue),
|
||||
end_time: Number(editor.endValue),
|
||||
break_minutes: editor.breakMinutes || 0,
|
||||
},
|
||||
});
|
||||
if (result.error || result.success === false) {
|
||||
this.notification.add(result.error || result.message || "Could not apply.", {
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
this._applyData(result.data);
|
||||
this.notification.add(`Applied to ${ids.length} employee(s).`, { type: "success" });
|
||||
}
|
||||
} catch (error) {
|
||||
this.notification.add(error.message || "Could not apply.", { type: "danger" });
|
||||
}
|
||||
this.state.saving = false;
|
||||
}
|
||||
|
||||
closeCellEditor() {
|
||||
this.state.editor.open = false;
|
||||
this.activeCellAnchor = null;
|
||||
|
||||
Reference in New Issue
Block a user