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
+212 -49

No files matched your search

@@ -33,9 +33,9 @@
icon="mdi-minus" icon="mdi-minus"
size="medium" size="medium"
@click.stop=" @click.stop="
subtask.id timeblock.id
? deleteTimeblock(timeblock) ? deleteTimeblock(timeblock)
: deleteTimeblockWithoutId(index) : deleteTimeblockByIndex(index)
" "
/> />
<p <p
@@ -48,11 +48,17 @@
<v-icon <v-icon
v-if="!isOffice365Task" v-if="!isOffice365Task"
v-show="subtask.id" v-show="subtask.id"
class="circle-border cursor-pointer ml-2" class="circle-border ml-2"
:class="{ completed: timeblock.is_finished }" :class="{
completed: timeblock.is_finished,
'cursor-pointer': !subtask.is_draft,
'disabled-check': subtask.is_draft,
}"
icon="mdi-check" icon="mdi-check"
size="medium" size="medium"
@click.stop="toggleTimeblockIsFinished(timeblock)" @click.stop="
!subtask.is_draft && toggleTimeblockIsFinished(timeblock, index)
"
/> />
</div> </div>
@@ -551,14 +557,29 @@ export default {
subtask.is_finished = 0; subtask.is_finished = 0;
} }
// Add timeblock in internal format from newTimeblock const newTimeblockData = {
subtask.timeblocks.push({
id: null, id: null,
date: this.newTimeblock.date, date: this.newTimeblock.date,
starts_at_time: this.newTimeblock.starts_at_time, starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.newTimeblock.ends_at_time, ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0, 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) if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend( subtask.deadline = tmUtils.formatDateTimeForBackend(
@@ -592,14 +613,29 @@ export default {
subtask.is_finished = 0; subtask.is_finished = 0;
} }
// Add timeblock in internal format from newTimeblock const newTimeblockData = {
subtask.timeblocks.push({
id: null, id: null,
date: this.newTimeblock.date, date: this.newTimeblock.date,
starts_at_time: this.newTimeblock.starts_at_time, starts_at_time: this.newTimeblock.starts_at_time,
ends_at_time: this.newTimeblock.ends_at_time, ends_at_time: this.newTimeblock.ends_at_time,
is_finished: 0, 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) if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend( subtask.deadline = tmUtils.formatDateTimeForBackend(
@@ -616,8 +652,12 @@ export default {
this._update(subtask); this._update(subtask);
}, },
async toggleTimeblockIsFinished(originalTimeblock) { async toggleTimeblockIsFinished(originalTimeblock, index) {
// TODO: Pridat odpracovany cas modal? // Only works for published subtasks with saved timeblocks
if (!originalTimeblock.id) {
return;
}
const subtask = JSON.parse(JSON.stringify(this.subtask)); const subtask = JSON.parse(JSON.stringify(this.subtask));
const timeblock = JSON.parse(JSON.stringify(originalTimeblock)); const timeblock = JSON.parse(JSON.stringify(originalTimeblock));
@@ -657,12 +697,25 @@ export default {
is_finished: newTimeblock.is_finished, is_finished: newTimeblock.is_finished,
}; };
subtask.timeblocks = subtask.timeblocks.map((originalTimeblock) => { // Update timeblock in correct location (employee's array for drafts, root for published)
if (originalTimeblock.id == convertedTimeblock.id) if (subtask.is_draft && Array.isArray(subtask.employees)) {
return convertedTimeblock; const employeeIndex = subtask.employees.findIndex(
(emp) => emp.id == this.subtask.employee_id,
return originalTimeblock; );
}); 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.$events.emit('on-subtask-edited', subtask);
this.$emit('update-subtask', subtask); this.$emit('update-subtask', subtask);
@@ -677,9 +730,34 @@ export default {
async deleteTimeblock({ id }) { async deleteTimeblock({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask)); const subtask = JSON.parse(JSON.stringify(this.subtask));
subtask.timeblocks = subtask.timeblocks.filter( // Remove timeblock from correct location (employee's array for drafts, root for published)
(timeblock) => timeblock.id != id, 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) if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend( subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(), this._mergeDeadlineTimeDate(),
@@ -688,8 +766,34 @@ export default {
this._update(subtask); this._update(subtask);
}, },
deleteTimeblockWithoutId(_index) { async deleteTimeblockByIndex(index) {
this.$emit('delete-timeblock'); 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) { async _update(data) {
@@ -699,36 +803,80 @@ export default {
return timesheet; return timesheet;
}); });
// Convert timeblocks from internal format to BE format const payload = { ...data, timesheets };
const timeblocks = (data.timeblocks || [])
.filter((timeblock) => { // For drafts with employees array, properly structure the employees with their timeblocks
// Filter timeblocks that have either old format (date) or new format (start) if (data.is_draft && Array.isArray(data.employees)) {
return timeblock.date || timeblock.start; const validEmployees = data.employees
}) .filter((emp) => emp.id)
.map((timeblock) => { .map((emp) => ({
// If already in BE format (has start field), return as is id: emp.id,
if (timeblock.start && !timeblock.date) { 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)
return timeblock.date || timeblock.start;
})
.map((timeblock) => {
// If already in BE format (has start field), return as is
if (timeblock.start && !timeblock.date) {
return {
id: timeblock.id || null,
start: timeblock.start,
end: timeblock.end,
is_finished: timeblock.is_finished || 0,
};
}
// Convert from internal format to BE format
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
return { return {
id: timeblock.id || null, id: timeblock.id || null,
start: timeblock.start, start: `${dateStr} ${timeblock.starts_at_time}:00`,
end: timeblock.end, end: `${dateStr} ${timeblock.ends_at_time}:00`,
is_finished: timeblock.is_finished || 0, is_finished: timeblock.is_finished || 0,
}; };
} });
// Convert from internal format to BE format payload.timeblocks = timeblocks;
const dateStr = moment(timeblock.date).format('YYYY-MM-DD'); }
return {
id: timeblock.id || null, // Remove UI-only enriched fields
start: `${dateStr} ${timeblock.starts_at_time}:00`, delete payload._created_by;
end: `${dateStr} ${timeblock.ends_at_time}:00`, delete payload._employee;
is_finished: timeblock.is_finished || 0, delete payload._master_task;
}; delete payload._deadline_time;
});
const { subtask } = await this.$axios.put_auth( const { subtask } = await this.$axios.put_auth(
`v1/subtasks/${data.id}`, `v1/subtasks/${data.id}`,
{ ...data, timesheets, timeblocks }, payload,
); );
this.closeModal(); this.closeModal();
@@ -792,6 +940,10 @@ export default {
.completed .completed
background: #06dece background: #06dece
.disabled-check
opacity: 0.5
cursor: not-allowed
.existing-timeblock-timeline .existing-timeblock-timeline
margin-top: 8px margin-top: 8px
padding-left: 16px padding-left: 16px
@@ -87,8 +87,8 @@ export default {
...mapGetters('tm', ['enrichedEmployees']), ...mapGetters('tm', ['enrichedEmployees']),
normalizedEmployees() { normalizedEmployees() {
// Draft with multiple employees // Drafts always have employees array
if (this.subtask.is_draft && Array.isArray(this.subtask.employees)) { if (this.subtask.is_draft) {
return this.subtask.employees return this.subtask.employees
.filter((emp) => emp.id) // Filter out empty employee slots .filter((emp) => emp.id) // Filter out empty employee slots
.map((emp) => ({ .map((emp) => ({
@@ -99,7 +99,7 @@ export default {
})); }));
} }
// Single employee (published subtask) // Published subtask - single employee format
return [ return [
{ {
id: this.subtask.employee_id, id: this.subtask.employee_id,
@@ -111,6 +111,11 @@ export default {
}, },
hasMultipleEmployees() { 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; return this.normalizedEmployees.length > 1;
}, },
}, },
@@ -86,6 +86,12 @@ export default {
methods: { methods: {
async showModal({ subtask }) { 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.subtask = subtask;
this.visible = true; this.visible = true;
this.form.hours = null; this.form.hours = null;