diff --git a/app/src/plugins/tm@modal/task/_components/employees/a-employee-select.vue b/app/src/plugins/tm@modal/task/_components/employees/a-employee-select.vue
index d927193..61a350d 100644
--- a/app/src/plugins/tm@modal/task/_components/employees/a-employee-select.vue
+++ b/app/src/plugins/tm@modal/task/_components/employees/a-employee-select.vue
@@ -10,7 +10,8 @@
item-value="id"
variant="solo"
class="employee-select__select-field"
- :class="{ error: v$.$errors.length && v$.id.$errors.length }"
+ :class="{ error: v$.id.$error }"
+ :error-messages="v$.id.$errors.map((e: any) => String(e.$message))"
@update:model-value="handleUpdateEmployee"
/>
@@ -20,7 +21,8 @@
variant="solo"
type="number"
label="Časový odhad"
- :class="{ error: v$.$errors.length && v$.duration.$errors.length }"
+ :class="{ error: v$.duration.$error }"
+ :error-messages="v$.duration.$errors.map((e: any) => String(e.$message))"
append-inner-icon="mdi-clock-outline"
@update:model-value="handleUpdateDuration"
/>
@@ -36,15 +38,16 @@
+
diff --git a/app/src/plugins/tm@modal/task/z-remove-timeblock-modal.vue b/app/src/plugins/tm@modal/task/z-remove-timeblock-modal.vue
new file mode 100644
index 0000000..52e279b
--- /dev/null
+++ b/app/src/plugins/tm@modal/task/z-remove-timeblock-modal.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
Naozaj chcete odstrániť tento timeblock?
+
+
+
+
+ ÁNO
+ ZRUŠIŤ
+
+
+
+
+
+
+
+
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 88d1eef..bce24d1 100644
--- a/app/src/plugins/tm@modal/task/z-task-modal.vue
+++ b/app/src/plugins/tm@modal/task/z-task-modal.vue
@@ -1,4 +1,10 @@
+
+
+
removeEmployeeAtIndex(index)"
@add-timeblock="
@@ -276,6 +283,8 @@ import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin';
import AResizeHandle from '@/plugins/tm@modal/_components/a-resize-handle.vue';
import AResetSizeButton from '@/plugins/tm@modal/_components/a-reset-size-button.vue';
import ACloseButton from '@/plugins/tm@modal/_components/a-close-button.vue';
+import ZRemoveEmployeeModal from './z-remove-employee-modal.vue';
+import ZRemoveTimeblockModal from './z-remove-timeblock-modal.vue';
export default {
components: {
@@ -288,6 +297,8 @@ export default {
AResizeHandle,
AResetSizeButton,
ACloseButton,
+ ZRemoveEmployeeModal,
+ ZRemoveTimeblockModal,
},
mixins: [SelectsMixin, AutocompleteMixin, modalMixin],
emits: ['close', 'refresh'],
@@ -307,6 +318,8 @@ export default {
wasFormValidationTriggered: false,
modalName: 'z-task-modal',
employeeRefs: [],
+ pendingRemoveEmployeeId: undefined,
+ pendingRemoveEmployeeIndex: undefined,
};
},
@@ -355,34 +368,13 @@ export default {
},
'subtask.employees': {
- handler(newEmployees, oldEmployees) {
+ handler() {
if (!this.subtask?.employees) return;
const lastEmployee =
this.subtask.employees[this.subtask.employees.length - 1];
if (lastEmployee?.id || lastEmployee?.duration)
this.subtask.employees.push({});
-
- // Auto-create first timeblock when employee gets both id and duration
- if (newEmployees && oldEmployees) {
- newEmployees.forEach((newEmp, index) => {
- const oldEmp = oldEmployees[index] || {};
- // Check if employee just became "complete" (has both id and duration)
- const wasComplete = oldEmp.id && oldEmp.duration;
- const isComplete = newEmp.id && newEmp.duration;
-
- if (!wasComplete && isComplete) {
- // Check if this employee already has timeblocks
- const hasTimeblocks = this.timeblocks.some(
- (tb) => tb.employee_id === newEmp.id,
- );
- if (!hasTimeblocks) {
- // Add first timeblock for this employee
- this.addTimeblock(newEmp.id);
- }
- }
- });
- }
},
deep: true,
},
@@ -418,9 +410,24 @@ export default {
},
deep: true,
},
+
+ timeblocks: {
+ handler(val) {
+ if (this.editMode && !this.subtask?.id) {
+ // Save timeblocks to localStorage for new tasks
+ localStorage.setItem('subtaskTimeblocks', JSON.stringify(val));
+ }
+ },
+ deep: true,
+ },
},
mounted() {
+ // Handle remove timeblock modal
+ this.$events.on('show-remove-timeblock-modal', ({ onConfirm }) => {
+ this.$refs.removeTimeblockModal.showModal({ onConfirm });
+ });
+
this.$events.on('add-timeblock', (timeblock) => {
if (!this.subtask?.timeblocks) {
this.subtask.timeblocks = [];
@@ -597,46 +604,51 @@ export default {
// Handle remove employee - also removes their timeblocks
handleRemoveEmployee() {
- if (this.subtask.employee_id) {
- // Remove all timeblocks for this employee
- this.timeblocks = this.timeblocks.filter(
- (tb) => tb.employee_id !== this.subtask.employee_id,
- );
- }
-
- // Clear employee data in edit mode
- this.subtask.employee_id = null;
- this.subtask.duration = null;
-
- if (this.wasFormValidationTriggered) {
- setTimeout(async () => {
- this.validateAllFields();
- }, 100);
- }
+ this.pendingRemoveEmployeeId = this.subtask.employee_id;
+ this.$refs.removeEmployeeModal.showModal();
},
removeEmployeeAtIndex(index) {
- const employee = this.subtask.employees[index];
- if (employee?.id) {
- // Remove all timeblocks for this employee
- this.timeblocks = this.timeblocks.filter(
- (tb) => tb.employee_id !== employee.id,
- );
+ this.pendingRemoveEmployeeIndex = index;
+ this.$refs.removeEmployeeModal.showModal();
+ },
+
+ confirmRemoveEmployee() {
+ // Handle edit mode (single employee removal)
+ if (this.pendingRemoveEmployeeId !== undefined) {
+ if (this.subtask.employee_id) {
+ // Remove all timeblocks for this employee
+ this.timeblocks = this.timeblocks.filter(
+ (tb) => tb.employee_id !== this.subtask.employee_id,
+ );
+ }
+
+ // Clear employee data in edit mode
+ this.subtask.employee_id = null;
+ this.subtask.duration = null;
+ this.pendingRemoveEmployeeId = undefined;
}
+ // Handle create mode (multiple employees removal)
+ else if (this.pendingRemoveEmployeeIndex !== undefined) {
+ const employee =
+ this.subtask.employees[this.pendingRemoveEmployeeIndex];
+ if (employee?.id) {
+ // Remove all timeblocks for this employee
+ this.timeblocks = this.timeblocks.filter(
+ (tb) => tb.employee_id !== employee.id,
+ );
+ }
- // Remove employee from array
- this.subtask.employees.splice(index, 1);
-
- if (this.wasFormValidationTriggered) {
- setTimeout(async () => {
- this.validateAllFields();
- }, 100);
+ // Remove employee from array
+ this.subtask.employees.splice(this.pendingRemoveEmployeeIndex, 1);
+ this.pendingRemoveEmployeeIndex = undefined;
}
},
async showModal({ subtask, editMode = false, mastertask = {} }) {
this.isSaving = false;
this.subtask = null;
+ this.wasFormValidationTriggered = false; // Reset validation trigger
// CRITICAL: Prevent v-dialog autofocus BEFORE modal opens
this.preventInputFocus = true;
@@ -710,6 +722,8 @@ export default {
this.subtask = this._enrichTask(data);
} else {
const savedSubtask = localStorage.getItem('subtaskData');
+ const savedTimeblocks = localStorage.getItem('subtaskTimeblocks');
+
try {
if (savedSubtask) {
const parsedSubtask = JSON.parse(savedSubtask);
@@ -718,6 +732,23 @@ export default {
if (typeof this.subtask !== 'object' || this.subtask === null) {
throw new Error('Parsed subtask is not an object');
}
+
+ // Load timeblocks from localStorage if they exist
+ if (savedTimeblocks) {
+ try {
+ const parsedTimeblocks = JSON.parse(savedTimeblocks);
+ if (Array.isArray(parsedTimeblocks)) {
+ this.timeblocks = parsedTimeblocks.map((tb) => ({
+ ...tb,
+ date: tb.date ? new Date(tb.date) : null,
+ }));
+ }
+ } catch (e) {
+ console.error('Error parsing saved timeblocks:', e);
+ this.timeblocks = [];
+ }
+ }
+
this.$nextTick(() => {
this.$refs['description']?.setHTML(
this.subtask.description || '',
@@ -767,7 +798,7 @@ export default {
this.$router.replace({ name: this.$route.name });
},
- async validateAllFields(concept = false) {
+ async validate(concept = false) {
if (concept === false) {
const employeesValid = await this._validateEmployees();
const timeblocksValid = await this._validateTimeblocks();
@@ -786,7 +817,7 @@ export default {
async save(concept = false) {
this.wasFormValidationTriggered = true;
- if (!(await this.validateAllFields(concept))) return;
+ if (!(await this.validate(concept))) return;
this.subtask.description = this.$refs['description'].getHTML();
this._mergeDeadlineTimeDate();
@@ -806,12 +837,15 @@ export default {
});
// Convert timeblocks from internal format to BE format for update
+ // Filter out completely empty timeblocks AND timeblocks without all required fields
const timeblocks = this.timeblocks
.filter(
(timeblock) =>
timeblock.date &&
+ timeblock.starts_at_time &&
+ timeblock.ends_at_time &&
timeblock.employee_id === this.subtask.employee_id,
- ) // Only send timeblocks for this employee
+ ) // Only send complete timeblocks for this employee
.map((timeblock) => {
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
return {
@@ -839,6 +873,7 @@ export default {
payload,
);
localStorage.removeItem('subtaskData');
+ localStorage.removeItem('subtaskTimeblocks');
this.$store.commit('tm/update_subtask', subtask);
this.$events.emit('on-subtask-edited', this.subtask);
@@ -849,11 +884,15 @@ export default {
);
// Assign timeblocks to each employee based on employee_id
+ // Filter out empty/incomplete timeblocks - only send fully filled timeblocks
validEmployees.forEach((employee) => {
employee.timeblocks = this.timeblocks
.filter(
(timeblock) =>
- timeblock.date && timeblock.employee_id === employee.id,
+ timeblock.date &&
+ timeblock.starts_at_time &&
+ timeblock.ends_at_time &&
+ timeblock.employee_id === employee.id,
)
.map((timeblock) => {
return {
@@ -881,6 +920,7 @@ export default {
subtaskCopy,
);
localStorage.removeItem('subtaskData');
+ localStorage.removeItem('subtaskTimeblocks');
this.subtask.id = subtasks[0].id;
subtasks.forEach((subtask) => {
@@ -941,51 +981,47 @@ export default {
async _validateEmployees() {
if (this.subtask.id) {
// For existing subtasks, at least one employee is required
- // Component-level validation will handle the conditional validation
const hasEmployee = !!(
this.subtask.employee_id && this.subtask.duration
);
- if (!hasEmployee) {
- this.$toast.error('Je potrebné vybrať zamestnanca a trvanie');
- return false;
- }
+ return hasEmployee;
} else {
- // For new subtasks, require at least one complete employee (id + duration)
- const validEmployees = this.subtask.employees.filter(
- (employee) => employee.id && employee.duration,
- );
+ let valid = true;
- if (validEmployees.length === 0) {
- this.$toast.error(
- 'Je potrebné pridať aspoň jedného zamestnanca s trvaním',
- );
- return false;
- }
+ // Validate ALL employee component refs (this will show errors on inputs)
+ for (let i = 0; i < this.employeeRefs.length; i++) {
+ const employeeRef = this.employeeRefs[i];
- // Validate all employee component refs (includes employee select and timeblocks validation)
- for (const employeeRef of this.employeeRefs) {
+ // Validate employees that have any data (partial or complete)
if (employeeRef && typeof employeeRef.validate === 'function') {
- const isValid = await employeeRef.validate();
- if (!isValid) {
- return false;
+ if (!(await employeeRef.validate())) {
+ valid = false;
}
}
}
- }
- return true;
+ return valid;
+ }
},
async _validateTimeblocks() {
- // Validate that all timeblocks have required fields and valid time ranges
+ // Date alone is valid. Validation only applies if start OR end time is filled.
+ // If start OR end is filled, then ALL fields (date, start, end) are required.
let valid = true;
for (const timeblock of this.timeblocks) {
- if (
- !timeblock.date ||
- !timeblock.starts_at_time ||
- !timeblock.ends_at_time
- ) {
+ const hasStart = !!timeblock.starts_at_time;
+ const hasEnd = !!timeblock.ends_at_time;
+ const hasAnyTime = hasStart || hasEnd;
+
+ // If no time data (start/end), timeblock is valid (date alone is OK)
+ if (!hasAnyTime) {
+ continue;
+ }
+
+ // If start OR end is filled, ALL fields are required
+ const hasDate = !!timeblock.date;
+ if (!hasDate || !hasStart || !hasEnd) {
valid = false;
break;
}
@@ -1002,7 +1038,6 @@ export default {
);
if (end.isSameOrBefore(start)) {
- this.$toast.error('Koniec času musí byť po začiatku');
valid = false;
break;
}