From 80d0e5735136f7b1a58f711d007246441053fbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Glasn=C3=A1k?= Date: Wed, 8 Oct 2025 20:36:52 +0200 Subject: [PATCH 1/7] fix(): fixed master task not editable because of invalid employee duration --- .../master-task/z-master-task-modal.vue | 1002 +++++++++++------ .../plugins/tm@modal/task/z-task-modal.vue | 10 +- 2 files changed, 643 insertions(+), 369 deletions(-) diff --git a/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue b/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue index c58d4e0..5b64aea 100644 --- a/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue +++ b/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue @@ -1,460 +1,728 @@ -e diff --git a/app/src/plugins/tm@modal/task/z-task-modal.vue b/app/src/plugins/tm@modal/task/z-task-modal.vue index 0b8e247..8ac4b3a 100644 --- a/app/src/plugins/tm@modal/task/z-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-task-modal.vue @@ -234,70 +234,22 @@ - - -
- + -
- - +
+
@@ -459,6 +411,27 @@ export default { _fullName: `${mastertask.name} [${mastertask.project_code}]`, })); }, + + // Unified timeblocks for both create and update modes + editableTimeblocks: { + get() { + if (this.subtask?.id) { + // Update mode: use subtask.timeblocks (from BE) + return this.subtask.timeblocks || []; + } + // Create mode: use this.timeblocks (local state) + return this.timeblocks; + }, + set(value) { + if (this.subtask?.id) { + // Update mode: update subtask.timeblocks (Vue 3 reactivity) + this.subtask.timeblocks = value; + } else { + // Create mode: update this.timeblocks + this.timeblocks = value; + } + }, + }, }, validations() { @@ -520,11 +493,42 @@ export default { }, updateTimeblock(val, index) { - this.timeblocks[index] = val; + if (this.subtask?.id) { + // Update mode: direct assignment (Vue 3 reactivity) + this.subtask.timeblocks[index] = val; + } else { + // Create mode: direct assignment + this.timeblocks[index] = val; + } + }, + + addTimeblock() { + const newTimeblock = { + date: null, + starts_at_time: null, + ends_at_time: null, + }; + + if (this.subtask?.id) { + // Update mode + if (!this.subtask.timeblocks) { + this.subtask.timeblocks = []; + } + this.subtask.timeblocks.push(newTimeblock); + } else { + // Create mode + this.timeblocks.push(newTimeblock); + } }, removeTimeblock(index) { - this.timeblocks.splice(index, 1); + if (this.subtask?.id) { + // Update mode + this.subtask.timeblocks.splice(index, 1); + } else { + // Create mode + this.timeblocks.splice(index, 1); + } }, async showModal({ subtask, editMode = false, mastertask = {} }) { @@ -549,6 +553,35 @@ export default { `v1/subtasks/${subtask.id}`, ); + // Convert BE format timeblocks to internal format + const convertedTimeblocks = []; + if (data.timeblocks && data.timeblocks.length > 0) { + data.timeblocks.forEach((timeblock) => { + // If already in internal format, keep as is + if (timeblock.date) { + convertedTimeblocks.push(timeblock); + return; + } + + // Convert from BE format to internal format + if (timeblock.start && timeblock.end) { + const startMoment = moment( + timeblock.start, + 'YYYY-MM-DD HH:mm:ss', + ); + const endMoment = moment(timeblock.end, 'YYYY-MM-DD HH:mm:ss'); + convertedTimeblocks.push({ + id: timeblock.id || null, + date: startMoment.toDate(), + starts_at_time: startMoment.format('HH:mm'), + ends_at_time: endMoment.format('HH:mm'), + is_finished: timeblock.is_finished || 0, + }); + } + }); + } + data.timeblocks = convertedTimeblocks; + if (!data.deadline) { this.editMode = editMode || !data.id; return (this.subtask = this._enrichTask(data)); @@ -624,9 +657,7 @@ export default { async validateAllFields() { const employeesValid = await this._validateEmployees(); - const timeblocksValid = this.subtask?.id - ? true - : await this._validateTimeblocks(); + const timeblocksValid = await this._validateTimeblocks(); if (!(await this.v$.$validate())) return; if (!employeesValid || !timeblocksValid) return; @@ -661,9 +692,22 @@ export default { return timesheet; }); + // Convert timeblocks from internal format to BE format for update + const timeblocks = (this.subtask.timeblocks || []) + .filter((timeblock) => timeblock.date) // Only send timeblocks with dates + .map((timeblock) => { + 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, + }; + }); + const { subtask } = await this.$axios.patch_auth_form( `v1/subtasks/${this.subtask.id}`, - { ...this.subtask, timesheets }, + { ...this.subtask, timesheets, timeblocks }, ); localStorage.removeItem('subtaskData'); @@ -809,12 +853,19 @@ export default { }, async _validateTimeblocks() { - let invalid = false; - for (let i = 0; i < this.timeblocks.length; i++) { + let valid = true; + const timeblocks = this.editableTimeblocks; + + for (let i = 0; i < timeblocks.length; i++) { const ref = this.$refs[`timetable-${i}`]; - invalid = (await ref[0].v$.$validate()) || invalid; + if (ref && ref[0]) { + const isValid = await ref[0].v$.$validate(); + if (!isValid) { + valid = false; + } + } } - return invalid; + return valid; }, _enrichTask(subtask) { From 61fc55d3131a86e770a3e4eabe80fee975bce59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Glasn=C3=A1k?= Date: Tue, 14 Oct 2025 01:42:41 +0200 Subject: [PATCH 3/7] fix(datepicker): improve focus handling and scroll position restoration in modal --- .../tm@modal/_mixins/vuetify-fix-focus.ts | 39 ++++++++++++--- .../master-task/z-master-task-modal.vue | 49 +++++++++++++------ .../tm@modal/task/_components/a-timeblock.vue | 11 ++++- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts b/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts index ce566cc..a3b544d 100644 --- a/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts +++ b/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts @@ -3,6 +3,7 @@ export default { data() { return { preventInputFocus: false, + savedScrollPosition: null, }; }, @@ -20,18 +21,44 @@ export default { }, onDateOpen($event) { - if ($event == undefined) { + // $event should be the element reference (or any truthy value to trigger saving) + if ($event) { + // Find the scrollable modal container + const modalContent = document.querySelector('.v-dialog .modal-content'); + if (modalContent) { + this.savedScrollPosition = modalContent.scrollTop; + } + this.preventInputFocus = true; + + // Reset after datepicker is opened + setTimeout(() => { + this.preventInputFocus = false; + this.savedScrollPosition = null; + }, 500); } }, onInputFocus($event) { - setTimeout(() => { - if (this.preventInputFocus) { - $event.target.blur(); - this.preventInputFocus = false; + // If we have a saved scroll position and an unwanted focus happened + if (this.preventInputFocus && this.savedScrollPosition !== null) { + const target = $event.target; + + // Only intercept text input focus (autocomplete fields) + if (target.tagName === 'INPUT' && target.type === 'text') { + // Blur the focused element + setTimeout(() => { + target.blur(); + + const modalContent = document.querySelector( + '.v-dialog .modal-content', + ); + if (modalContent && this.savedScrollPosition !== null) { + modalContent.scrollTop = this.savedScrollPosition; + } + }, 0); } - }, 100); + } }, }, }; diff --git a/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue b/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue index 5b64aea..e0f4425 100644 --- a/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue +++ b/app/src/plugins/tm@modal/master-task/z-master-task-modal.vue @@ -57,20 +57,26 @@
- +
+ +
diff --git a/app/src/plugins/tm@modal/task/_components/a-timeblock.vue b/app/src/plugins/tm@modal/task/_components/a-timeblock.vue index 6ad2e14..5fdf058 100644 --- a/app/src/plugins/tm@modal/task/_components/a-timeblock.vue +++ b/app/src/plugins/tm@modal/task/_components/a-timeblock.vue @@ -4,14 +4,13 @@
Date: Tue, 14 Oct 2025 18:04:00 +0200 Subject: [PATCH 4/7] refactor(task-modal): enhance timeblock handling and improve date formatting utilities --- app/src/plugins/tm@_utils/tm.utils.ts | 63 +- .../_components/a-task-timeblocks.vue | 611 +++++++++++------- 2 files changed, 405 insertions(+), 269 deletions(-) diff --git a/app/src/plugins/tm@_utils/tm.utils.ts b/app/src/plugins/tm@_utils/tm.utils.ts index 021e3cc..93a4add 100644 --- a/app/src/plugins/tm@_utils/tm.utils.ts +++ b/app/src/plugins/tm@_utils/tm.utils.ts @@ -1,32 +1,45 @@ -import moment from 'moment' +import moment from 'moment'; export default { - enrichEmployees(employees) { - return employees.map(employee => ({ - ...employee, - _fullName: `${ employee.name } ${ employee.surname }`, - _initials: employee.name.charAt(0) + employee.surname.charAt(0) - })) - }, + enrichEmployees(employees) { + return employees.map((employee) => ({ + ...employee, + _fullName: `${employee.name} ${employee.surname}`, + _initials: employee.name.charAt(0) + employee.surname.charAt(0), + })); + }, - formatDateTime(date) { - moment(date) - return moment(date).format('DD.MM.yyyy, HH:mm') - }, + formatDateTime(date) { + moment(date); + return moment(date).format('DD.MM.yyyy, HH:mm'); + }, - formatDateTimeForBackend(date) { - return moment(date).format('YYYY-MM-DD HH:mm:ss') - }, + formatTimeblockDateTime(timeblock) { + // Handle new format with separate date and time fields + if (timeblock.date && timeblock.starts_at_time) { + const dateStr = moment(timeblock.date).format('DD.MM.yyyy'); + return `${dateStr}, ${timeblock.starts_at_time}`; + } + // Fallback to old format + if (timeblock.start) { + return this.formatDateTime(timeblock.start); + } + return ''; + }, - formatDate(date) { - return moment(date).format('DD.MM.yyyy') - }, + formatDateTimeForBackend(date) { + return moment(date).format('YYYY-MM-DD HH:mm:ss'); + }, - isBefore(date, compareTo = null) { - return moment(date).isBefore(moment(compareTo)) - }, + formatDate(date) { + return moment(date).format('DD.MM.yyyy'); + }, - isAfter(date, compareTo = moment()) { - return moment(date).isAfter(moment(compareTo)) - } -} \ No newline at end of file + isBefore(date, compareTo = null) { + return moment(date).isBefore(moment(compareTo)); + }, + + isAfter(date, compareTo = moment()) { + return moment(date).isAfter(moment(compareTo)); + }, +}; 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 b2d9a4a..c8553e8 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 @@ -1,309 +1,432 @@ - \ No newline at end of file + From 003525108033cc5fc63b05a5fb2eaa4dcf248bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Glasn=C3=A1k?= Date: Tue, 14 Oct 2025 18:42:07 +0200 Subject: [PATCH 5/7] refactor(task-modals): updated timeblocks to new local dat format --- .../_components/a-task-spent-time.vue | 342 +++++++++++------- .../_components/a-task-timeblocks.vue | 127 +++++-- .../tm@modal/task/z-complete-task-modal.vue | 153 +++++--- .../task/z-delete-timeblock-modal.vue | 179 +++++---- .../tm@modal/task/z-task-add-time-modal.vue | 210 +++++++---- .../plugins/tm@modal/task/z-task-modal.vue | 30 +- .../task/z-unable-to-complete-task-modal.vue | 303 ++++++++++------ .../_components/a-ongoing-table.vue | 312 ++++++++++------ 8 files changed, 1059 insertions(+), 597 deletions(-) diff --git a/app/src/plugins/tm@modal/task/_components/_components/a-task-spent-time.vue b/app/src/plugins/tm@modal/task/_components/_components/a-task-spent-time.vue index 2f94e47..34726a6 100644 --- a/app/src/plugins/tm@modal/task/_components/_components/a-task-spent-time.vue +++ b/app/src/plugins/tm@modal/task/_components/_components/a-task-spent-time.vue @@ -1,170 +1,244 @@ \ No newline at end of file + 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 c8553e8..f41da5e 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 @@ -41,10 +41,8 @@ class="task-badge rounded-pill" :class="{ 'strike-through': timeblock.is_finished }" > - {{ tmUtils.formatTimeblockDateTime(timeblock) }} - - {{ - timeblock.ends_at_time || tmUtils.formatDateTime(timeblock.end) - }} + {{ formatTimeblockStart(timeblock) }} - + {{ formatTimeblockEnd(timeblock) }}

{ - if (originalTimeblock.id == newTimeblock.id) return newTimeblock; + if (originalTimeblock.id == convertedTimeblock.id) + return convertedTimeblock; return originalTimeblock; }); @@ -365,9 +401,36 @@ 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) { + 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: `${dateStr} ${timeblock.starts_at_time}:00`, + end: `${dateStr} ${timeblock.ends_at_time}:00`, + is_finished: timeblock.is_finished || 0, + }; + }); + const { subtask } = await this.$axios.put_auth( `v1/subtasks/${data.id}`, - { ...data, timesheets }, + { ...data, timesheets, timeblocks }, ); this.closeModal(); diff --git a/app/src/plugins/tm@modal/task/z-complete-task-modal.vue b/app/src/plugins/tm@modal/task/z-complete-task-modal.vue index aeca918..b8fefa4 100644 --- a/app/src/plugins/tm@modal/task/z-complete-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-complete-task-modal.vue @@ -1,63 +1,114 @@ \ No newline at end of file + const tempSubtask = { + ...this.subtask, + is_finished: this.subtask.is_finished ? 0 : 1, + timesheets, + timeblocks, + }; + + const { subtask } = await this.$axios.put_auth( + `v1/subtasks/${tempSubtask.id}`, + tempSubtask, + ); + + this.visible = false; + this.$emit('on-subtask-edited', subtask); + } catch (error) { + this.$toast.error(this.$parseError(error)); + + if (process.env.NODE_ENV === 'development') + console.log('[z-complete-task-modal.vue] Error: ', error); + } + }, + + closeModal() { + this.visible = false; + }, + }, +}; + diff --git a/app/src/plugins/tm@modal/task/z-delete-timeblock-modal.vue b/app/src/plugins/tm@modal/task/z-delete-timeblock-modal.vue index 3e5e01f..32f7505 100644 --- a/app/src/plugins/tm@modal/task/z-delete-timeblock-modal.vue +++ b/app/src/plugins/tm@modal/task/z-delete-timeblock-modal.vue @@ -1,86 +1,139 @@ \ No newline at end of file + 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 3939985..c0e676a 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 @@ -1,95 +1,151 @@ \ No newline at end of file + try { + const { subtask } = await this.$axios.put_auth( + `v1/subtasks/${this.subtask.id}`, + payload, + ); + + this.closeModal(); + this.$events.emit('on-subtask-edited', subtask); + } catch (error) { + this.$toast.error(error.message || error); + + if (process.env.NODE_ENV === 'development') + console.log('[z-task-add-time-modal.vue] Error: ', error); + } + }, + + closeModal() { + this.visible = false; + this.v$.$reset(); + }, + }, +}; + diff --git a/app/src/plugins/tm@modal/task/z-task-modal.vue b/app/src/plugins/tm@modal/task/z-task-modal.vue index 8ac4b3a..6cac85c 100644 --- a/app/src/plugins/tm@modal/task/z-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-task-modal.vue @@ -248,7 +248,7 @@ :on-input-focus="onInputFocus" :on-date-open="onDateOpen" /> -
+
@@ -466,10 +466,36 @@ export default { data.deadline = splittedDeadline[0]; data._deadline_time = splittedDeadline[1]; + + // Convert BE format timeblocks to internal format + const convertedTimeblocks = []; + if (data.timeblocks && data.timeblocks.length > 0) { + data.timeblocks.forEach((timeblock) => { + // If already in internal format, keep as is + if (timeblock.date) { + convertedTimeblocks.push(timeblock); + return; + } + + // Convert from BE format to internal format + if (timeblock.start && timeblock.end) { + const startMoment = moment(timeblock.start, 'YYYY-MM-DD HH:mm:ss'); + const endMoment = moment(timeblock.end, 'YYYY-MM-DD HH:mm:ss'); + convertedTimeblocks.push({ + id: timeblock.id || null, + date: startMoment.toDate(), + starts_at_time: startMoment.format('HH:mm'), + ends_at_time: endMoment.format('HH:mm'), + is_finished: timeblock.is_finished || 0, + }); + } + }); + } + data.timeblocks = convertedTimeblocks; + this.subtask = this._enrichTask(data); // TODO: Remove? riesil som neodstranovanie jedineho zanamu v trvani a odpracovanom case - console.log('2', this.subtask); this.$forceUpdate(); }); diff --git a/app/src/plugins/tm@modal/task/z-unable-to-complete-task-modal.vue b/app/src/plugins/tm@modal/task/z-unable-to-complete-task-modal.vue index a8cbb0f..88b7a33 100644 --- a/app/src/plugins/tm@modal/task/z-unable-to-complete-task-modal.vue +++ b/app/src/plugins/tm@modal/task/z-unable-to-complete-task-modal.vue @@ -1,159 +1,226 @@ \ No newline at end of file + diff --git a/app/src/plugins/tm@overview/tasks-ongoing/_components/a-ongoing-table.vue b/app/src/plugins/tm@overview/tasks-ongoing/_components/a-ongoing-table.vue index 9206f52..94331eb 100644 --- a/app/src/plugins/tm@overview/tasks-ongoing/_components/a-ongoing-table.vue +++ b/app/src/plugins/tm@overview/tasks-ongoing/_components/a-ongoing-table.vue @@ -1,154 +1,226 @@