Files
jandl-taskmanager-app/app/src/plugins/tm@modal/task/z-task-modal.vue
T

1165 lines
36 KiB
Vue

<template>
<z-remove-employee-modal
ref="removeEmployeeModal"
@confirm="confirmRemoveEmployee"
/>
<z-remove-timeblock-modal ref="removeTimeblockModal" />
<v-dialog
v-model="visible"
:width="modalWidth"
:height="modalHeight"
persistent
:retain-focus="false"
>
<W-loadable :content="subtask" class="h-100">
<v-card
v-if="editMode"
height="100%"
class="d-flex flex-column scrolling"
style="position: relative"
>
<div class="d-flex justify-space-between align-center modal-header">
<h1>{{ subtask?.id ? 'Upraviť sub task' : 'Vytvoriť sub task' }}</h1>
<div class="d-flex align-center">
<a-reset-size-button @reset="resetModalSize" />
<a-close-button @close="closeModal()" />
</div>
</div>
<v-card-actions class="d-flex flex-column modal-content">
<v-text-field
v-model="subtask.project_code"
label="Kód projektu"
:value="subtask.project_code"
class="modal-input"
variant="solo"
disabled
></v-text-field>
<v-autocomplete
v-model="subtask.master_task_id"
:custom-filter="customFilter"
:match-any="true"
label="Master task"
:class="{ error: v$.subtask.master_task_id.$errors.length }"
:items="enrichedMastertasks"
:item-title="(item) => item._fullName || item.name"
item-value="id"
variant="solo"
class="modal-input"
clearable
/>
<v-text-field
v-model="subtask.title"
class="modal-input"
label="Názov subtasku"
variant="solo"
></v-text-field>
<div class="splitted-inputs">
<!-- TODO: Bialko validacie -->
<div class="left">
<Z-datepicker
ref="datepicker-01"
v-model="subtask.deadline"
label="Deadline"
:auto-apply="true"
:min-date="new Date()"
class="modal-input"
format="dd.MM.yyyy"
locale="sk"
select-text="Vybrať"
cancel-text="Zrušiť"
placeholder="Deadline"
:class="{ error: v$.subtask.deadline.$errors?.length }"
:validations="v$.subtask.deadline"
/>
</div>
<v-text-field
v-model="subtask._deadline_time"
class="modal-input right"
variant="solo"
label="Čas"
append-inner-icon="mdi-clock-outline"
type="time"
:class="{ error: v$.subtask._deadline_time.$errors.length }"
:hint="
v$.subtask._deadline_time.$errors
? errorUtils.parseErrors(v$.subtask._deadline_time.$errors)
: null
"
/>
</div>
<!-- Employee Assignment with Timeblocks -->
<!-- Edit Mode: Single Employee -->
<A-employee
v-if="subtask.id"
v-model:employee-data="editModeEmployee"
:timeblocks="getEmployeeTimeblocks(subtask.employee_id)"
:all-timeblocks="timeblocks"
:deleted-timeblock-ids="deletedTimeblockIds"
:custom-filter="customFilter"
@update:timeblocks="
updateEmployeeTimeblocks($event, subtask.employee_id)
"
@clear-employee="handleRemoveEmployee"
@add-timeblock="
(afterIndex) => addTimeblock(subtask.employee_id, afterIndex)
"
/>
<!-- Create Mode: Multiple Employees -->
<template v-else>
<A-employee
v-for="(employee, index) in subtask.employees"
:key="`employee-${employee.id || index}`"
:ref="(el) => setEmployeeRef(el, index)"
v-model:employee-data="subtask.employees[index]"
:timeblocks="getEmployeeTimeblocks(employee.id)"
:all-timeblocks="timeblocks"
:deleted-timeblock-ids="deletedTimeblockIds"
:custom-filter="customFilter"
:required="index === 0"
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
@clear-employee="() => removeEmployeeAtIndex(index)"
@add-timeblock="
(afterIndex) => addTimeblock(employee.id, afterIndex)
"
/>
</template>
<div class="w-100 modal-input quill-editor-wrapper">
<QuillEditor
ref="description"
v-model="subtask.description"
placeholder="Popis"
theme="snow"
class="rich-text-editor"
@input="subtask.description = $event.srcElement.innerHTML"
/>
</div>
<Z-files-upload
class="mt-15 files"
:files="subtask.files"
:is-saving="isSaving"
@files-uploaded="filesUploaded"
/>
</v-card-actions>
<template v-if="!subtask.id">
<v-btn class="button primary" :disabled="isSaving" @click="save()"
>VYTVORIŤ</v-btn
>
<v-btn
class="button primary outline mt-2"
:disabled="isSaving"
@click="save(true)"
>ULOŽIŤ KONCEPT</v-btn
>
</template>
<template v-else>
<template v-if="subtask.is_draft">
<v-btn class="button primary" :disabled="isSaving" @click="save()"
>ULOŽIŤ A PUBLIKOVAŤ</v-btn
>
<v-btn
class="button primary outline mt-2"
:disabled="isSaving"
@click="save(true)"
>ULOŽIŤ KONCEPT</v-btn
>
</template>
<template v-else>
<v-btn class="button primary" :disabled="isSaving" @click="save()"
>UPRAVIŤ</v-btn
>
</template>
</template>
<a-resize-handle
:current-width="modalWidthPx"
:current-height="modalHeightPx"
@resize="handleModalResize"
@resize-end="handleResizeEnd"
/>
</v-card>
<v-card
v-else
height="100%"
class="d-flex flex-column scrolling"
style="position: relative"
>
<div class="d-flex justify-space-between align-center">
<div>
<p>{{ subtask._master_task.project_code }}</p>
<h1>{{ subtask._master_task.name }}</h1>
</div>
<div class="d-flex align-center">
<a-reset-size-button @reset="resetModalSize" />
<a-close-button @close="closeModal()" />
</div>
</div>
<div
class="d-flex justify-space-between align-center modal-header border-bottom pb-4"
>
<h1>{{ subtask.title }}</h1>
<div class="d-flex justify-start">
<img
class="icon"
:src="require('@/plugins/app/_img/icons/pencil.svg')"
alt="pencil-icon"
@click.stop="toggleEditMode"
/>
<img
class="icon trash"
:src="require('@/plugins/app/_img/icons/bin.svg')"
alt="trash-icon"
@click.stop="openDeleteTaskModal(subtask)"
/>
</div>
</div>
<v-card-actions class="d-flex flex-column modal-content">
<div class="w-100">
<Z-task-readonly-info
:subtask="subtask"
@close-modal="closeModal"
@update-subtask="updateSubtask"
/>
<div class="d-flex justify-start align-center mt-8">
<v-btn
v-if="!subtask.is_finished"
class="done-btn"
@click.stop="showCompleteTaskModal()"
>Dokončiť</v-btn
>
<v-btn
v-else
class="button secondary"
@click.stop="showCompleteTaskModal()"
>Znovu otvoriť</v-btn
>
<v-btn class="button secondary" @click="closeModal()"
>Uložiť zmeny</v-btn
>
</div>
</div>
</v-card-actions>
<a-resize-handle
:current-width="modalWidthPx"
:current-height="modalHeightPx"
@resize="handleModalResize"
@resize-end="handleResizeEnd"
/>
</v-card>
</W-loadable>
</v-dialog>
</template>
<script>
import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue';
import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import errorUtils from '@/plugins/lib@auth/_utils/error.utils';
import { QuillEditor } from '@vueup/vue-quill';
import SelectsMixin from '../master-task/_mixins/selects.mixin';
import tmUtils from '@/plugins/tm@_utils/tm.utils';
import moment from 'moment';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import AutocompleteMixin from '@/plugins/tm@_mixins/autocomplete.mixin';
import ZTaskReadonlyInfo from './_components/z-task-readonly-info.vue';
import AEmployee from './_components/employees/a-employee.vue';
import { mapGetters } from 'vuex';
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: {
ZTaskReadonlyInfo,
AEmployee,
ZFilesUpload,
ZDatepicker,
// ATaskTimeblocks,
QuillEditor,
AResizeHandle,
AResetSizeButton,
ACloseButton,
ZRemoveEmployeeModal,
ZRemoveTimeblockModal,
},
mixins: [SelectsMixin, AutocompleteMixin, modalMixin],
emits: ['close', 'refresh'],
data() {
return {
tmUtils,
editMode: false,
visible: false,
subtask: null,
isSaving: false,
errorUtils,
v$: useVuelidate(),
showDatePicker: false,
selectedNewDate: [],
timeblocks: [],
deletedTimeblockIds: [], // Track IDs of saved timeblocks that have been deleted
wasFormValidationTriggered: false,
modalName: 'z-task-modal',
employeeRefs: [],
pendingRemoveEmployeeId: undefined,
pendingRemoveEmployeeIndex: undefined,
};
},
computed: {
...mapGetters('tm', ['mastertasks', 'enrichedEmployees']),
enrichedMastertasks() {
return this.mastertasks.map((mastertask) => ({
...mastertask,
_fullName: `${mastertask.name} [${mastertask.project_code}]`,
}));
},
editModeEmployee: {
get() {
return {
id: this.subtask?.employee_id || null,
duration: this.subtask?.duration || null,
};
},
set(value) {
if (this.subtask) {
this.subtask.employee_id = value.id;
this.subtask.duration = value.duration;
}
},
},
},
validations() {
return {
subtask: {
master_task_id: { required },
deadline: { required },
_deadline_time: { required },
},
};
},
watch: {
'subtask.employee_id'(value, oldValue) {
if (!value || !oldValue) return;
// In edit mode: reassign timeblocks to new employee
if (this.subtask.id) {
this.timeblocks = this.timeblocks.map((tb) => {
if (tb.employee_id === oldValue) {
return { ...tb, employee_id: value };
}
return tb;
});
} else {
// In create mode: remove old employee's timeblocks
this.timeblocks = this.timeblocks.filter(
(tb) => tb.employee_id !== oldValue,
);
}
this.subtask._employee = this.enrichedEmployees.find(
(employee) => employee.id == this.subtask.employee_id,
);
},
'subtask.employees': {
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({});
},
deep: true,
},
'subtask.master_task_id'(value) {
if (!value) return;
this.subtask._master_task = this.mastertasks.find(
(mastertask) => mastertask.id == this.subtask.master_task_id,
);
this.subtask.project_code =
this.subtask._master_task?.project_code || null;
},
editMode() {
this.$nextTick(() => {
this.$refs['description']?.setHTML(this.subtask.description || '');
});
},
subtask: {
handler(val) {
if (this.editMode && !this.subtask?.id) {
const employees = val?.employees?.filter(
(employee) => employee?.id || employee?.duration,
) || [{}];
const subtaskData = {
...val,
employees: employees && employees.length > 0 ? employees : [{}],
};
localStorage.setItem('subtaskData', JSON.stringify(subtaskData));
}
},
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) return;
if (!this.subtask.timeblocks) {
this.subtask.timeblocks = [];
}
if (!this.subtask.timesheets) {
this.subtask.timesheets = [];
}
if (!this.subtask.is_finished) {
this.subtask.is_finished = 0;
}
this.subtask.timeblocks.push(timeblock);
});
this.$events.on('on-subtask-edited', (subtask) => {
if (subtask?.id !== this.subtask?.id) return;
const data = JSON.parse(JSON.stringify(subtask));
const splittedDeadline = data.deadline?.split(' ') || [];
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,
employee_id: data.employee_id, // Add employee_id
});
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,
employee_id: data.employee_id, // Add employee_id
});
}
});
}
// Store in component's timeblocks array
this.timeblocks = convertedTimeblocks;
this.subtask = this._enrichTask(data);
// TODO: Remove? riesil som neodstranovanie jedineho zanamu v trvani a odpracovanom case
this.$forceUpdate();
});
this.$events.on('on-subtask-deleted', (subtask) => {
if (subtask?.id == this.subtask?.id) this.closeModal();
});
},
methods: {
toggleEditMode() {
if (this.subtask.type == 'office365')
return this.$events.emit('show-unable-to-edit-office-task-modal', {
mode: 'edit',
});
this.editMode = !this.editMode;
},
// Set refs for employee components in create mode
setEmployeeRef(el, index) {
if (el) {
this.employeeRefs[index] = el;
}
},
openDatepicker(ref) {
this.$refs[ref].openMenu();
},
// Helper: Get employee by ID
getEmployeeById(employeeId) {
return this.enrichedEmployees.find((emp) => emp.id === employeeId);
},
// Helper: Get employee full name
getEmployeeFullName(employeeId) {
const employee = this.getEmployeeById(employeeId);
return employee ? `${employee.name} ${employee.surname}` : '';
},
// Helper: Get valid employees (with id and duration) in create mode
getValidEmployees() {
if (!this.subtask?.employees) return [];
return this.subtask.employees.filter((emp) => emp.id && emp.duration);
},
// Helper: Get timeblocks for specific employee
getEmployeeTimeblocks(employeeId) {
if (!this.timeblocks) return [];
return this.timeblocks.filter((tb) => tb.employee_id === employeeId);
},
updateEmployeeTimeblocks(updatedTimeblocks, employeeId) {
// Track deleted timeblocks (saved timeblocks that are no longer in the updated list)
const updatedIds = new Set(
updatedTimeblocks.filter((tb) => tb.id).map((tb) => tb.id),
);
const deletedIds = this.timeblocks
.filter(
(tb) =>
tb.employee_id === employeeId && tb.id && !updatedIds.has(tb.id),
)
.map((tb) => tb.id)
.filter((id) => !this.deletedTimeblockIds.includes(id));
this.deletedTimeblockIds.push(...deletedIds);
// Replace timeblocks for this employee
const otherEmployeeTimeblocks = this.timeblocks.filter(
(tb) => tb.employee_id !== employeeId,
);
const updatedWithEmployeeId = updatedTimeblocks.map((tb) => ({
...tb,
employee_id: employeeId,
}));
this.timeblocks = [...otherEmployeeTimeblocks, ...updatedWithEmployeeId];
},
updateTimeblock(val, index, employeeId) {
const employeeTimeblocks = this.getEmployeeTimeblocks(employeeId);
const timeblock = employeeTimeblocks[index];
if (!timeblock) return;
// Find the actual index in the full timeblocks array
const actualIndex = this.timeblocks.indexOf(timeblock);
if (actualIndex !== -1) {
this.timeblocks[actualIndex] = { ...val, employee_id: employeeId };
}
},
addTimeblock(employeeId, afterIndex = -1) {
const newTimeblock = {
date: new Date(), // Preset to current day
starts_at_time: null,
ends_at_time: null,
employee_id: employeeId,
};
// Get timeblocks for this employee
const employeeTimeblocks = this.timeblocks.filter(
(tb) => tb.employee_id === employeeId,
);
if (afterIndex === -1 || afterIndex >= employeeTimeblocks.length - 1) {
// Add at the end
this.timeblocks.push(newTimeblock);
} else {
// Find the position in the full timeblocks array
const timeblockToInsertAfter = employeeTimeblocks[afterIndex];
const insertPosition =
this.timeblocks.indexOf(timeblockToInsertAfter) + 1;
this.timeblocks.splice(insertPosition, 0, newTimeblock);
}
},
removeTimeblock(index, employeeId) {
const employeeTimeblocks = this.getEmployeeTimeblocks(employeeId);
const timeblock = employeeTimeblocks[index];
if (!timeblock) return;
// Find the actual index in the full timeblocks array
const actualIndex = this.timeblocks.indexOf(timeblock);
if (actualIndex !== -1) {
this.timeblocks.splice(actualIndex, 1);
}
},
// Handle remove employee - also removes their timeblocks
handleRemoveEmployee() {
this.pendingRemoveEmployeeId = this.subtask.employee_id;
this.$refs.removeEmployeeModal.showModal();
},
removeEmployeeAtIndex(index) {
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(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;
this.visible = true;
this.timeblocks = [];
this.deletedTimeblockIds = []; // Reset deleted timeblock IDs
this.employeeRefs = [];
this.$nextTick(() => {
this.v$.$reset();
// Reset after modal is fully rendered and v-dialog autofocus has been blocked
setTimeout(() => {
this.preventInputFocus = false;
}, 300);
});
try {
if (subtask?.id) {
const data = await this.$axios.get_auth_data(
`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,
employee_id: data.employee_id, // Add employee_id for edit mode
});
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,
employee_id: data.employee_id, // Add employee_id for edit mode
});
}
});
}
// Store timeblocks in the component's timeblocks array
this.timeblocks = convertedTimeblocks;
// Ensure at least one timeblock exists for edit mode
if (data.employee_id && this.timeblocks.length === 0) {
this.addTimeblock(data.employee_id);
}
if (!data.deadline) {
this.editMode = editMode || !data.id;
return (this.subtask = this._enrichTask(data));
}
const splittedDeadline = data.deadline.split(' ');
data.deadline = splittedDeadline[0];
data._deadline_time = splittedDeadline[1];
this.subtask = this._enrichTask(data);
} else {
const savedSubtask = localStorage.getItem('subtaskData');
const savedTimeblocks = localStorage.getItem('subtaskTimeblocks');
try {
if (savedSubtask) {
const parsedSubtask = JSON.parse(savedSubtask);
this.subtask = parsedSubtask;
// If the parsed result is not an object, reset to 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 || '',
);
});
} else {
throw new Error('subtaskData is null or undefined');
}
} catch (error) {
this.subtask = {
master_task: mastertask,
employees: [{}],
_deadline_time: '17:00',
};
}
if (
subtask?.deadline &&
subtask._deadline_time &&
subtask.employee_id
) {
const employee = this.enrichedEmployees.find(
(employee) => employee.id == subtask.employee_id,
);
this.subtask.employees = [employee];
this.subtask.deadline = subtask.deadline;
this.subtask._deadline_time = subtask._deadline_time;
}
if (subtask?.master_task_id && subtask?.project_code) {
this.subtask.master_task_id = subtask.master_task_id;
this.subtask.project_code = subtask.project_code;
}
}
this.editMode = editMode || !this.subtask.id;
} catch (error) {
this.$toast.error(this.$parseError(error));
}
},
deleteTimeblock(i) {
this.subtask.timeblocks.splice(i, 1);
},
closeModal() {
this.visible = false;
this.$router.replace({ name: this.$route.name });
},
async validate(concept = false) {
if (concept === false) {
const employeesValid = await this._validateEmployees();
const timeblocksValid = await this._validateTimeblocks();
if (!(await this.v$.$validate())) return;
if (!employeesValid || !timeblocksValid) return;
return true;
} else {
this.v$.subtask.master_task_id.$touch();
return this.v$.subtask.master_task_id.$errors.length === 0;
}
},
async save(concept = false) {
this.v$.$reset();
this.wasFormValidationTriggered = true;
if (!(await this.validate(concept))) return;
this.subtask.description = this.$refs['description'].getHTML();
this._mergeDeadlineTimeDate();
try {
this.isSaving = true;
// Concept tasks may not have a deadline time set
this.subtask.deadline = this.subtask.deadline
? moment(this.subtask.deadline).format('YYYY-MM-DD HH:mm:SS')
: null;
if (this.subtask?.id) {
const timesheets = this.subtask.timesheets.map((timesheet) => {
delete timesheet.employee_name;
return timesheet;
});
// 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 complete timeblocks for this employee
.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 payload = { ...this.subtask, timesheets, timeblocks };
if (concept) {
payload.is_draft = 1;
} else {
payload.is_draft = 0;
}
const { subtask } = await this.$axios.patch_auth_form(
`v1/subtasks/${this.subtask.id}`,
payload,
);
localStorage.removeItem('subtaskData');
localStorage.removeItem('subtaskTimeblocks');
this.$store.commit('tm/update_subtask', subtask);
this.$events.emit('on-subtask-edited', this.subtask);
} else {
// Filter out employees that don't have both id and duration
const validEmployees = this.subtask.employees.filter(
(employee) => employee.id && employee.duration,
);
// 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.starts_at_time &&
timeblock.ends_at_time &&
timeblock.employee_id === employee.id,
)
.map((timeblock) => {
return {
start: `${moment(timeblock.date).format('YYYY-MM-DD')} ${
timeblock.starts_at_time
}:00`,
end: `${moment(timeblock.date).format('YYYY-MM-DD')} ${
timeblock.ends_at_time
}:00`,
id: null,
};
});
});
const subtaskCopy = {
...this.subtask,
employees: validEmployees,
};
if (concept) {
subtaskCopy.is_draft = true;
subtaskCopy.employees =
subtaskCopy.employees?.length > 0
? subtaskCopy.employees
: [{ id: this.$store.getters['auth/user']?.id, duration: 0 }];
}
delete subtaskCopy._master_task;
const { subtasks } = await this.$axios.post_auth(
`v1/mastertasks/${this.subtask.master_task_id}/subtasks`,
subtaskCopy,
);
localStorage.removeItem('subtaskData');
localStorage.removeItem('subtaskTimeblocks');
this.subtask.id = subtasks[0].id;
subtasks.forEach((subtask) => {
this.$store.commit('tm/update_subtask', subtask);
this.$events.emit('on-subtask-edited', this.subtask);
});
this.$events.emit('task-created');
this.$events.emit('close-both-modals');
}
this.closeModal();
} catch (error) {
this.$toast.error(this.$parseError(error));
} finally {
this.isSaving = false;
}
},
openDeleteTaskModal(subtask) {
if (subtask.type == 'office365')
return this.$events.emit('show-unable-to-edit-office-task-modal', {
mode: 'delete',
});
this.$events.emit('show-delete-task-modal', { subtask });
},
showCompleteTaskModal() {
this.$events.emit('show-complete-task-modal', { subtask: this.subtask });
if (this.subtask.deadline)
this.subtask.deadline = tmUtils.formatDateTimeForBackend(
this.subtask.deadline,
);
},
filesUploaded(files) {
this.subtask.files = files;
},
updateSubtask(subtask) {
this.subtask = subtask;
},
_mergeDeadlineTimeDate() {
// Override for concept tasks which may not have a deadline time set (check for invalid date)
if (isNaN(new Date(this.subtask.deadline).getTime())) {
this.subtask.deadline = null;
} else {
const date = new Date(this.subtask.deadline);
const timeParts = this.subtask._deadline_time.split(':');
date.setHours(parseInt(timeParts[0]));
date.setMinutes(parseInt(timeParts[1]));
this.subtask.deadline = date;
}
},
async _validateEmployees() {
if (this.subtask.id) {
// For existing subtasks, at least one employee is required
const hasEmployee = !!(
this.subtask.employee_id && this.subtask.duration
);
return hasEmployee;
} else {
let valid = true;
// 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 employees that have any data (partial or complete)
if (employeeRef && typeof employeeRef.validate === 'function') {
if (!(await employeeRef.validate())) {
valid = false;
}
}
}
return valid;
}
},
async _validateTimeblocks() {
// 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) {
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;
}
// Validate that end time is after start time
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
const start = moment(
`${dateStr} ${timeblock.starts_at_time}`,
'YYYY-MM-DD HH:mm',
);
const end = moment(
`${dateStr} ${timeblock.ends_at_time}`,
'YYYY-MM-DD HH:mm',
);
if (end.isSameOrBefore(start)) {
valid = false;
break;
}
}
return valid;
},
_enrichTask(subtask) {
return {
...subtask,
_created_by: this.enrichedEmployees.find(
(employee) => employee.id == subtask.created_by,
),
_employee: this.enrichedEmployees.find(
(employee) => employee.id == subtask.employee_id,
),
_master_task: this.mastertasks.find(
(mastertask) => mastertask.id == subtask.master_task_id,
),
};
},
},
};
</script>
<style lang="sass" scoped>
.w-full
width: 100%
.date-wrap
display: flex
flex-flow: row
width: 100%
gap: 20px
margin-bottom: 20px
.dp__main
width: calc(50% - 10px) !important
.scrolling
overflow: hidden !important
.border-bottom
border-bottom: 1px solid rgba(0,0,0,.1)
.splitted-inputs
display: flex
justify-content: space-between
gap: 16px
.left
flex: 1
.right ::v-deep
.v-field__field
height: 56px !important
.done-btn
background: #7f39fb !important
border-radius: 4px !important
color: #fff !important
height: 50px
width: 203px
@media screen and (max-width: 522px)
.files
margin-top: 104px !important
.quill-editor-wrapper
:deep(.ql-container)
min-height: 200px
max-height: 500px
overflow-y: auto
:deep(.ql-editor)
min-height: 200px
max-height: 500px
// Mobile fullscreen fix - remove padding
:deep(.v-overlay__content)
@media (max-width: 768px)
max-height: 100% !important
width: 100% !important
max-width: 100% !important
margin: 0 !important
</style>