mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
feat(task-modal): added timeline to timeblocks in employee modal
This commit is contained in:
1 parent
00c4bf4d65
commit
c55c2ce3e9
7 files changed
+468
-142
No files matched your search
@@ -1,4 +1,10 @@
|
||||
<template>
|
||||
<z-remove-employee-modal
|
||||
ref="removeEmployeeModal"
|
||||
@confirm="confirmRemoveEmployee"
|
||||
/>
|
||||
<z-remove-timeblock-modal ref="removeTimeblockModal" />
|
||||
|
||||
<v-dialog
|
||||
v-model="visible"
|
||||
:width="modalWidth"
|
||||
@@ -113,6 +119,7 @@
|
||||
v-model:employee-data="subtask.employees[index]"
|
||||
:timeblocks="getEmployeeTimeblocks(employee.id)"
|
||||
:custom-filter="customFilter"
|
||||
:required="index === 0"
|
||||
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
|
||||
@clear-employee="() => 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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user