diff --git a/app/src/plugins/tm@modal/task/_components/_components/a-task-timeblocks.vue b/app/src/plugins/tm@modal/task/_components/_components/a-task-timeblocks.vue index 5bd71f4..d149935 100644 --- a/app/src/plugins/tm@modal/task/_components/_components/a-task-timeblocks.vue +++ b/app/src/plugins/tm@modal/task/_components/_components/a-task-timeblocks.vue @@ -33,9 +33,9 @@ icon="mdi-minus" size="medium" @click.stop=" - subtask.id + timeblock.id ? deleteTimeblock(timeblock) - : deleteTimeblockWithoutId(index) + : deleteTimeblockByIndex(index) " />

@@ -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)); - subtask.timeblocks = subtask.timeblocks.filter( - (timeblock) => timeblock.id != id, - ); + // 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,36 +803,80 @@ export default { return timesheet; }); - // Convert timeblocks from internal format to BE format - 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) { + 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) + 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 { id: timeblock.id || null, - start: timeblock.start, - end: timeblock.end, + start: `${dateStr} ${timeblock.starts_at_time}:00`, + end: `${dateStr} ${timeblock.ends_at_time}:00`, is_finished: timeblock.is_finished || 0, }; - } + }); - // Convert from internal format to BE format - const dateStr = moment(timeblock.date).format('YYYY-MM-DD'); - return { - id: timeblock.id || null, - start: `${dateStr} ${timeblock.starts_at_time}:00`, - end: `${dateStr} ${timeblock.ends_at_time}:00`, - is_finished: timeblock.is_finished || 0, - }; - }); + 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 diff --git a/app/src/plugins/tm@modal/task/_components/z-task-readonly-info.vue b/app/src/plugins/tm@modal/task/_components/z-task-readonly-info.vue index a996220..da6e8c6 100644 --- a/app/src/plugins/tm@modal/task/_components/z-task-readonly-info.vue +++ b/app/src/plugins/tm@modal/task/_components/z-task-readonly-info.vue @@ -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; }, }, diff --git a/app/src/plugins/tm@modal/task/z-task-add-time-modal.vue b/app/src/plugins/tm@modal/task/z-task-add-time-modal.vue index 93041c2..9dc4560 100644 --- a/app/src/plugins/tm@modal/task/z-task-add-time-modal.vue +++ b/app/src/plugins/tm@modal/task/z-task-add-time-modal.vue @@ -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;