feat(z-task-modal): implement draft handling for timeblocks and improve employee-specific timeblock management

This commit is contained in:
Matej Glasnák committed 2026-01-08 02:10:46 +01:00
1 parent 1bca6cbfae
commit 81aaa293dd
3 files changed
+188 -25

No files matched your search

@@ -33,9 +33,9 @@
icon="mdi-minus"
size="medium"
@click.stop="
subtask.id
timeblock.id
? deleteTimeblock(timeblock)
: deleteTimeblockWithoutId(index)
: deleteTimeblockByIndex(index)
"
/>
<p
@@ -48,11 +48,17 @@
<v-icon
v-if="!isOffice365Task"
v-show="subtask.id"
class="circle-border cursor-pointer ml-2"
:class="{ completed: timeblock.is_finished }"
class="circle-border ml-2"
:class="{
completed: timeblock.is_finished,
'cursor-pointer': !subtask.is_draft,
'disabled-check': subtask.is_draft,
}"
icon="mdi-check"
size="medium"
@click.stop="toggleTimeblockIsFinished(timeblock)"
@click.stop="
!subtask.is_draft && toggleTimeblockIsFinished(timeblock, index)
"
/>
</div>
@@ -551,14 +557,29 @@ export default {
subtask.is_finished = 0;
}
// Add timeblock in internal format from newTimeblock
subtask.timeblocks.push({
const newTimeblockData = {
id: null,
date: this.newTimeblock.date,
starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0,
});
};
// For drafts with employees array, add to the correct employee's timeblocks
if (subtask.is_draft && Array.isArray(subtask.employees)) {
const employeeIndex = subtask.employees.findIndex(
(emp) => emp.id == this.subtask.employee_id,
);
if (employeeIndex !== -1) {
if (!subtask.employees[employeeIndex].timeblocks) {
subtask.employees[employeeIndex].timeblocks = [];
}
subtask.employees[employeeIndex].timeblocks.push(newTimeblockData);
}
} else {
// For published subtasks, add to root timeblocks
subtask.timeblocks.push(newTimeblockData);
}
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
@@ -592,14 +613,29 @@ export default {
subtask.is_finished = 0;
}
// Add timeblock in internal format from newTimeblock
subtask.timeblocks.push({
const newTimeblockData = {
id: null,
date: this.newTimeblock.date,
starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0,
});
};
// For drafts with employees array, add to the correct employee's timeblocks
if (subtask.is_draft && Array.isArray(subtask.employees)) {
const employeeIndex = subtask.employees.findIndex(
(emp) => emp.id == this.subtask.employee_id,
);
if (employeeIndex !== -1) {
if (!subtask.employees[employeeIndex].timeblocks) {
subtask.employees[employeeIndex].timeblocks = [];
}
subtask.employees[employeeIndex].timeblocks.push(newTimeblockData);
}
} else {
// For published subtasks, add to root timeblocks
subtask.timeblocks.push(newTimeblockData);
}
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
@@ -616,8 +652,12 @@ export default {
this._update(subtask);
},
async toggleTimeblockIsFinished(originalTimeblock) {
// TODO: Pridat odpracovany cas modal?
async toggleTimeblockIsFinished(originalTimeblock, index) {
// Only works for published subtasks with saved timeblocks
if (!originalTimeblock.id) {
return;
}
const subtask = JSON.parse(JSON.stringify(this.subtask));
const timeblock = JSON.parse(JSON.stringify(originalTimeblock));
@@ -657,12 +697,25 @@ export default {
is_finished: newTimeblock.is_finished,
};
subtask.timeblocks = subtask.timeblocks.map((originalTimeblock) => {
if (originalTimeblock.id == convertedTimeblock.id)
return convertedTimeblock;
return originalTimeblock;
// Update timeblock in correct location (employee's array for drafts, root for published)
if (subtask.is_draft && Array.isArray(subtask.employees)) {
const employeeIndex = subtask.employees.findIndex(
(emp) => emp.id == this.subtask.employee_id,
);
if (employeeIndex !== -1) {
subtask.employees[employeeIndex].timeblocks = subtask.employees[
employeeIndex
].timeblocks.map((tb) => {
if (tb.id == convertedTimeblock.id) return convertedTimeblock;
return tb;
});
}
} else {
subtask.timeblocks = subtask.timeblocks.map((tb) => {
if (tb.id == convertedTimeblock.id) return convertedTimeblock;
return tb;
});
}
this.$events.emit('on-subtask-edited', subtask);
this.$emit('update-subtask', subtask);
@@ -677,9 +730,34 @@ export default {
async deleteTimeblock({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask));
// Remove timeblock from correct location (employee's array for drafts, root for published)
if (subtask.is_draft && Array.isArray(subtask.employees)) {
// Find which employee this timeblock belongs to by searching through all employees
let found = false;
subtask.employees = subtask.employees.map((emp) => {
if (!emp.timeblocks) return emp;
// Check if this employee has the timeblock
const hasTimeblock = emp.timeblocks.some((tb) => tb.id == id);
if (hasTimeblock) {
found = true;
return {
...emp,
timeblocks: emp.timeblocks.filter((tb) => tb.id != id),
};
}
return emp;
});
if (!found) {
console.warn('Timeblock not found in any employee', id);
}
} else {
subtask.timeblocks = subtask.timeblocks.filter(
(timeblock) => timeblock.id != id,
);
}
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
@@ -688,8 +766,34 @@ export default {
this._update(subtask);
},
deleteTimeblockWithoutId(_index) {
this.$emit('delete-timeblock');
async deleteTimeblockByIndex(index) {
const subtask = JSON.parse(JSON.stringify(this.subtask));
// Remove timeblock by index from correct location
if (subtask.is_draft && Array.isArray(subtask.employees)) {
const employeeIndex = subtask.employees.findIndex(
(emp) => emp.id == this.subtask.employee_id,
);
if (
employeeIndex !== -1 &&
subtask.employees[employeeIndex].timeblocks
) {
subtask.employees[employeeIndex].timeblocks.splice(index, 1);
}
} else {
subtask.timeblocks.splice(index, 1);
}
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
if (!subtask.id) {
this.$emit('update-unfinished-subtask', subtask);
} else {
this._update(subtask);
}
},
async _update(data) {
@@ -699,7 +803,42 @@ export default {
return timesheet;
});
// Convert timeblocks from internal format to BE format
const payload = { ...data, timesheets };
// For drafts with employees array, properly structure the employees with their timeblocks
if (data.is_draft && Array.isArray(data.employees)) {
const validEmployees = data.employees
.filter((emp) => emp.id)
.map((emp) => ({
id: emp.id,
duration: emp.duration,
// Convert employee timeblocks to BE format
timeblocks: (emp.timeblocks || [])
.filter((tb) => tb.date || tb.start)
.map((tb) => {
if (tb.start && !tb.date) {
return {
id: tb.id || null,
start: tb.start,
end: tb.end,
is_finished: tb.is_finished || 0,
};
}
const dateStr = moment(tb.date).format('YYYY-MM-DD');
return {
id: tb.id || null,
start: `${dateStr} ${tb.starts_at_time}:00`,
end: `${dateStr} ${tb.ends_at_time}:00`,
is_finished: tb.is_finished || 0,
};
}),
}));
payload.employees = validEmployees;
// For drafts, don't send root-level timeblocks
delete payload.timeblocks;
} else {
// For published subtasks, convert and send root-level timeblocks
const timeblocks = (data.timeblocks || [])
.filter((timeblock) => {
// Filter timeblocks that have either old format (date) or new format (start)
@@ -726,9 +865,18 @@ export default {
};
});
payload.timeblocks = timeblocks;
}
// Remove UI-only enriched fields
delete payload._created_by;
delete payload._employee;
delete payload._master_task;
delete payload._deadline_time;
const { subtask } = await this.$axios.put_auth(
`v1/subtasks/${data.id}`,
{ ...data, timesheets, timeblocks },
payload,
);
this.closeModal();
@@ -792,6 +940,10 @@ export default {
.completed
background: #06dece
.disabled-check
opacity: 0.5
cursor: not-allowed
.existing-timeblock-timeline
margin-top: 8px
padding-left: 16px
@@ -87,8 +87,8 @@ export default {
...mapGetters('tm', ['enrichedEmployees']),
normalizedEmployees() {
// Draft with multiple employees
if (this.subtask.is_draft && Array.isArray(this.subtask.employees)) {
// Drafts always have employees array
if (this.subtask.is_draft) {
return this.subtask.employees
.filter((emp) => emp.id) // Filter out empty employee slots
.map((emp) => ({
@@ -99,7 +99,7 @@ export default {
}));
}
// Single employee (published subtask)
// Published subtask - single employee format
return [
{
id: this.subtask.employee_id,
@@ -111,6 +111,11 @@ export default {
},
hasMultipleEmployees() {
// For drafts, always use multi-employee styling (even with 1 employee)
if (this.subtask.is_draft) {
return true;
}
// For published subtasks, check actual count
return this.normalizedEmployees.length > 1;
},
},
@@ -86,6 +86,12 @@ export default {
methods: {
async showModal({ subtask }) {
// Prevent using this modal for drafts - they should be edited instead
if (subtask.is_draft) {
this.$toast.info('Pre koncepty použite režim úprav');
return;
}
this.subtask = subtask;
this.visible = true;
this.form.hours = null;