mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 06:20:21 +00:00
refactor(task-modal): enhance employee timeblock management with custom filtering and improved rendering logic
This commit is contained in:
1 parent
7daf559f1c
commit
8e9e586276
5 files changed
+742
-153
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import axios from '@/plugins/w@custom/axios/models/axios'
|
||||
import store from '@/store'
|
||||
import axios from '@/plugins/w@custom/axios/models/axios';
|
||||
import store from '@/store';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
@@ -13,16 +13,16 @@ export default {
|
||||
ongoingSubtasks: null,
|
||||
initialized: false,
|
||||
ongoingSubtasksEmployeeId: null,
|
||||
unscheduledSubtasksEmployeeId: null
|
||||
unscheduledSubtasksEmployeeId: null,
|
||||
},
|
||||
|
||||
mutations: {
|
||||
set_mastertasks(state: any, mastertasks: any) {
|
||||
state.mastertasks = mastertasks
|
||||
state.mastertasks = mastertasks;
|
||||
},
|
||||
|
||||
set_employees(state: any, employees: any) {
|
||||
state.employees = employees
|
||||
state.employees = employees;
|
||||
|
||||
// TODO: Remove
|
||||
// const employee = employees.find(employee => employee.email == 'zachariah.vonrueden@jandl.sk')
|
||||
@@ -32,45 +32,54 @@ export default {
|
||||
},
|
||||
|
||||
set_teams(state: any, teams: any) {
|
||||
state.teams = teams
|
||||
state.teams = teams;
|
||||
},
|
||||
|
||||
set_projects(state: any, projects: any) {
|
||||
state.projects = projects
|
||||
state.projects = projects;
|
||||
},
|
||||
|
||||
set_unscheduled_subtasks(state: any, subtasks: any) {
|
||||
state.unscheduledSubtasks = subtasks
|
||||
state.unscheduledSubtasks = subtasks;
|
||||
},
|
||||
|
||||
set_ongoing_subtasks(state: any, subtasks: any) {
|
||||
state.ongoingSubtasks = subtasks
|
||||
state.ongoingSubtasks = subtasks;
|
||||
},
|
||||
|
||||
remove_ongoing_subtask(state: any, subtaskId: any) {
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.filter(st => st.id != subtaskId)
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.filter(
|
||||
(st) => st.id != subtaskId,
|
||||
);
|
||||
},
|
||||
|
||||
update_team(state: any, team: any) {
|
||||
state.teams = state.teams.map(t => {
|
||||
if (t.id == team.id)
|
||||
return team
|
||||
state.teams = state.teams.map((t) => {
|
||||
if (t.id == team.id) return team;
|
||||
|
||||
return t
|
||||
})
|
||||
return t;
|
||||
});
|
||||
},
|
||||
|
||||
save_team(state: any, team: any) {
|
||||
state.teams.push(team)
|
||||
state.teams.push(team);
|
||||
},
|
||||
|
||||
delete_team(state: any, team: any) {
|
||||
state.teams = state.teams.filter(t => t.id != team.id)
|
||||
state.teams = state.teams.filter((t) => t.id != team.id);
|
||||
},
|
||||
|
||||
update_subtask(state: any, subtask: any) {
|
||||
state.unscheduledSubtasks = state.unscheduledSubtasks.map(st => st.id == subtask.id ? subtask : st)
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.map(st => st.id == subtask.id ? subtask : st)
|
||||
if (state.unscheduledSubtasks) {
|
||||
state.unscheduledSubtasks = state.unscheduledSubtasks.map((st) =>
|
||||
st.id == subtask.id ? subtask : st,
|
||||
);
|
||||
}
|
||||
if (state.ongoingSubtasks) {
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.map((st) =>
|
||||
st.id == subtask.id ? subtask : st,
|
||||
);
|
||||
}
|
||||
|
||||
// state.mastertasks = state.mastertasks.map(mastertask => {
|
||||
// mastertask.subtasks = mastertask.subtasks.map(st => st.id == subtask.id ? subtask : st)
|
||||
@@ -79,8 +88,12 @@ export default {
|
||||
},
|
||||
|
||||
delete_subtask(state: any, subtask: any) {
|
||||
state.unscheduledSubtasks = state.unscheduledSubtasks.filter(st => st.id != subtask.id)
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.filter(st => st.id != subtask.id)
|
||||
state.unscheduledSubtasks = state.unscheduledSubtasks.filter(
|
||||
(st) => st.id != subtask.id,
|
||||
);
|
||||
state.ongoingSubtasks = state.ongoingSubtasks.filter(
|
||||
(st) => st.id != subtask.id,
|
||||
);
|
||||
|
||||
// state.mastertasks = state.mastertasks.map(mastertask => {
|
||||
// mastertask.subtasks = mastertask.subtasks.filter(st => st.id != subtask.id)
|
||||
@@ -89,198 +102,209 @@ export default {
|
||||
},
|
||||
|
||||
set_ongoing_subtasks_employee_id(state: any, employeeId: any) {
|
||||
state.ongoingSubtasksEmployeeId = employeeId
|
||||
state.ongoingSubtasksEmployeeId = employeeId;
|
||||
},
|
||||
|
||||
set_unscheduled_subtasks_employee_id(state: any, employeeId: any) {
|
||||
state.unscheduledSubtasksEmployeeId = employeeId
|
||||
state.unscheduledSubtasksEmployeeId = employeeId;
|
||||
},
|
||||
|
||||
initialized(state: any) {
|
||||
state.initialized = true
|
||||
}
|
||||
state.initialized = true;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
initialize({dispatch, commit}: {dispatch: any, commit: any}) {
|
||||
initialize({ dispatch, commit }: { dispatch: any; commit: any }) {
|
||||
return Promise.all([
|
||||
dispatch('loadMastertasks'),
|
||||
dispatch('loadEmployees'),
|
||||
dispatch('loadTeams'),
|
||||
dispatch('loadProjects'),
|
||||
dispatch('loadUnscheduledSubtasks'),
|
||||
dispatch('loadOngoingSubtasks')
|
||||
]).then(() => commit('initialized'))
|
||||
dispatch('loadOngoingSubtasks'),
|
||||
]).then(() => commit('initialized'));
|
||||
},
|
||||
|
||||
reloadSubtasks({dispatch}: {dispatch: any}) {
|
||||
reloadSubtasks({ dispatch }: { dispatch: any }) {
|
||||
return Promise.all([
|
||||
dispatch('loadMastertasks'),
|
||||
dispatch('loadProjects'),
|
||||
dispatch('loadUnscheduledSubtasks'),
|
||||
dispatch('loadOngoingSubtasks')
|
||||
])
|
||||
dispatch('loadOngoingSubtasks'),
|
||||
]);
|
||||
},
|
||||
|
||||
reloadMastertasks({dispatch}: {dispatch: any}) {
|
||||
return Promise.all([
|
||||
dispatch('loadMastertasks')
|
||||
])
|
||||
reloadMastertasks({ dispatch }: { dispatch: any }) {
|
||||
return Promise.all([dispatch('loadMastertasks')]);
|
||||
},
|
||||
|
||||
loadMastertasks({commit}: {commit: any}) {
|
||||
loadMastertasks({ commit }: { commit: any }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// axios.get_auth('v1/mastertasks')
|
||||
axios.get_auth('v1/mastertasks/?simple=true')
|
||||
axios
|
||||
.get_auth('v1/mastertasks/?simple=true')
|
||||
.then(({ mastertasks }) => {
|
||||
commit('set_mastertasks', mastertasks)
|
||||
resolve(mastertasks)
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_mastertasks', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
commit('set_mastertasks', mastertasks);
|
||||
resolve(mastertasks);
|
||||
})
|
||||
.catch((error) => {
|
||||
commit('set_mastertasks', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadEmployees({commit}: {commit: any}) {
|
||||
loadEmployees({ commit }: { commit: any }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get_auth_data('v1/employees')
|
||||
.then(employees => {
|
||||
commit('set_employees', employees)
|
||||
resolve(employees)
|
||||
axios
|
||||
.get_auth_data('v1/employees')
|
||||
.then((employees) => {
|
||||
commit('set_employees', employees);
|
||||
resolve(employees);
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_employees', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
commit('set_employees', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadTeams({commit}: {commit: any}) {
|
||||
loadTeams({ commit }: { commit: any }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get_auth_data('v1/teams')
|
||||
.then(teams => {
|
||||
commit('set_teams', teams)
|
||||
resolve(teams)
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_teams', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
axios
|
||||
.get_auth_data('v1/teams')
|
||||
.then((teams) => {
|
||||
commit('set_teams', teams);
|
||||
resolve(teams);
|
||||
})
|
||||
.catch((error) => {
|
||||
commit('set_teams', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadProjects({commit}: {commit: any}) {
|
||||
loadProjects({ commit }: { commit: any }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get_auth_data('v1/projects')
|
||||
.then(project => {
|
||||
commit('set_projects', project)
|
||||
resolve(project)
|
||||
axios
|
||||
.get_auth_data('v1/projects')
|
||||
.then((project) => {
|
||||
commit('set_projects', project);
|
||||
resolve(project);
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_projects', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
commit('set_projects', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadUnscheduledSubtasks({commit, state}: {commit: any, state: any}, employeeId = undefined) {
|
||||
let employeeIdToUse = undefined
|
||||
loadUnscheduledSubtasks(
|
||||
{ commit, state }: { commit: any; state: any },
|
||||
employeeId = undefined,
|
||||
) {
|
||||
let employeeIdToUse = undefined;
|
||||
|
||||
if (employeeId) {
|
||||
employeeIdToUse = employeeId
|
||||
commit('set_unscheduled_subtasks_employee_id', employeeId)
|
||||
employeeIdToUse = employeeId;
|
||||
commit('set_unscheduled_subtasks_employee_id', employeeId);
|
||||
} else if (state.unscheduledSubtasksEmployeeId) {
|
||||
employeeIdToUse = state.unscheduledSubtasksEmployeeId
|
||||
employeeIdToUse = state.unscheduledSubtasksEmployeeId;
|
||||
} else {
|
||||
employeeIdToUse = store.getters['auth/user']?.id
|
||||
employeeIdToUse = store.getters['auth/user']?.id;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get_auth(`v1/subtasks?type=unscheduled&employee_id=${employeeIdToUse}`)
|
||||
.then(({subtasks}) => {
|
||||
commit('set_unscheduled_subtasks', subtasks)
|
||||
resolve(subtasks)
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_unscheduled_subtasks', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
axios
|
||||
.get_auth(
|
||||
`v1/subtasks?type=unscheduled&employee_id=${employeeIdToUse}`,
|
||||
)
|
||||
.then(({ subtasks }) => {
|
||||
commit('set_unscheduled_subtasks', subtasks);
|
||||
resolve(subtasks);
|
||||
})
|
||||
.catch((error) => {
|
||||
commit('set_unscheduled_subtasks', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadOngoingSubtasks({commit, state}: {commit: any, state: any}, employeeId = undefined) {
|
||||
let employeeIdToUse = undefined
|
||||
loadOngoingSubtasks(
|
||||
{ commit, state }: { commit: any; state: any },
|
||||
employeeId = undefined,
|
||||
) {
|
||||
let employeeIdToUse = undefined;
|
||||
|
||||
if (employeeId) {
|
||||
employeeIdToUse = employeeId
|
||||
commit('set_ongoing_subtasks_employee_id', employeeId)
|
||||
employeeIdToUse = employeeId;
|
||||
commit('set_ongoing_subtasks_employee_id', employeeId);
|
||||
} else if (state.ongoingSubtasksEmployeeId) {
|
||||
employeeIdToUse = state.ongoingSubtasksEmployeeId
|
||||
employeeIdToUse = state.ongoingSubtasksEmployeeId;
|
||||
} else {
|
||||
employeeIdToUse = store.getters['auth/user']?.id
|
||||
commit('set_ongoing_subtasks_employee_id', employeeIdToUse)
|
||||
employeeIdToUse = store.getters['auth/user']?.id;
|
||||
commit('set_ongoing_subtasks_employee_id', employeeIdToUse);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get_auth(`v1/subtasks?type=ongoing&employee_id=${employeeIdToUse}`)
|
||||
.then(({subtasks}) => {
|
||||
commit('set_ongoing_subtasks', subtasks)
|
||||
resolve(subtasks)
|
||||
})
|
||||
.catch(error => {
|
||||
commit('set_ongoing_subtasks', {error: error})
|
||||
reject(error)
|
||||
})
|
||||
axios
|
||||
.get_auth(`v1/subtasks?type=ongoing&employee_id=${employeeIdToUse}`)
|
||||
.then(({ subtasks }) => {
|
||||
commit('set_ongoing_subtasks', subtasks);
|
||||
resolve(subtasks);
|
||||
})
|
||||
.catch((error) => {
|
||||
commit('set_ongoing_subtasks', { error: error });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeOngoingSubtask({commit}: {commit: any}, subtaskId: any) {
|
||||
commit('remove_ongoing_subtask', subtaskId)
|
||||
}
|
||||
|
||||
removeOngoingSubtask({ commit }: { commit: any }, subtaskId: any) {
|
||||
commit('remove_ongoing_subtask', subtaskId);
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
mastertasks: (state: any) => {
|
||||
return state.mastertasks
|
||||
return state.mastertasks;
|
||||
},
|
||||
|
||||
employees: (state: any) => {
|
||||
return state.employees
|
||||
return state.employees;
|
||||
},
|
||||
|
||||
enrichedEmployees: (state: any) => {
|
||||
if (!state.employees) return []
|
||||
if (!state.employees) return [];
|
||||
|
||||
return state.employees.map(employee => ({
|
||||
return state.employees
|
||||
.map((employee) => ({
|
||||
...employee,
|
||||
_fullName: `${ employee.name } ${ employee.surname }`,
|
||||
_initials: employee.name.charAt(0) + employee.surname.charAt(0)
|
||||
})).sort((a, b) => a.name < b.name ? -1 : 1)
|
||||
_fullName: `${employee.name} ${employee.surname}`,
|
||||
_initials: employee.name.charAt(0) + employee.surname.charAt(0),
|
||||
}))
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1));
|
||||
},
|
||||
|
||||
teams: (state: any) => {
|
||||
return state.teams
|
||||
return state.teams;
|
||||
},
|
||||
|
||||
projects: (state: any) => {
|
||||
return state.projects
|
||||
return state.projects;
|
||||
},
|
||||
|
||||
unscheduledSubtasks: (state: any) => {
|
||||
return state.unscheduledSubtasks
|
||||
return state.unscheduledSubtasks;
|
||||
},
|
||||
|
||||
ongoingSubtasks: (state: any) => {
|
||||
return state.ongoingSubtasks
|
||||
return state.ongoingSubtasks;
|
||||
},
|
||||
|
||||
isInitialized: (state: any) => {
|
||||
return state.initialized
|
||||
}
|
||||
}
|
||||
}
|
||||
return state.initialized;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="employee-select">
|
||||
<v-autocomplete
|
||||
:key="`employee-select-${componentKey}`"
|
||||
ref="employeeSelectRef"
|
||||
:model-value="modelValue.id"
|
||||
:custom-filter="customFilter"
|
||||
:match-any="true"
|
||||
label="Priradený"
|
||||
:items="enrichedEmployees"
|
||||
item-title="_fullName"
|
||||
:item-title="getItemTitle"
|
||||
item-value="id"
|
||||
variant="solo"
|
||||
class="employee-select__select-field"
|
||||
@@ -40,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs } from 'vue';
|
||||
import { computed, toRefs, watch, nextTick, ref, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { helpers, required } from '@vuelidate/validators';
|
||||
@@ -63,7 +65,65 @@ const emit = defineEmits<{
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const enrichedEmployees = computed(() => store.getters['tm/enrichedEmployees']);
|
||||
const employeeSelectRef = ref<any>(null);
|
||||
|
||||
// Force re-render key
|
||||
const componentKey = ref(0);
|
||||
|
||||
// Ensure all employees have a displayable name
|
||||
const enrichedEmployees = computed(() => {
|
||||
const employees = store.getters['tm/enrichedEmployees'] || [];
|
||||
return employees.map((emp: any) => ({
|
||||
...emp,
|
||||
_fullName:
|
||||
emp._fullName ||
|
||||
`${emp.name || ''} ${emp.surname || ''}`.trim() ||
|
||||
`Employee ${emp.id}`,
|
||||
}));
|
||||
});
|
||||
|
||||
// Function to get item title - handles both objects and IDs
|
||||
const getItemTitle = (item: any): string => {
|
||||
// If item is an object with _fullName, return it
|
||||
if (item && typeof item === 'object' && item._fullName) {
|
||||
return item._fullName;
|
||||
}
|
||||
|
||||
// If item is just an ID (string or number), find the employee
|
||||
if (item && (typeof item === 'string' || typeof item === 'number')) {
|
||||
const employee = enrichedEmployees.value.find((emp) => emp.id == item);
|
||||
return employee?._fullName || `Employee ${item}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// Watch for when employees list changes to force re-render
|
||||
watch(
|
||||
() => enrichedEmployees.value.length,
|
||||
(newLength, oldLength) => {
|
||||
// Force component re-render when employees are loaded
|
||||
if (newLength > 0 && newLength !== oldLength) {
|
||||
nextTick(() => {
|
||||
componentKey.value++;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Watch for when the selected employee ID changes to ensure display updates
|
||||
watch(
|
||||
() => props.modelValue.id,
|
||||
(newId) => {
|
||||
// When ID is set and we have employees, force re-render to ensure proper display
|
||||
if (newId && enrichedEmployees.value.length > 0) {
|
||||
nextTick(() => {
|
||||
componentKey.value++;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Make modelValue reactive for Vuelidate
|
||||
const { modelValue } = toRefs(props);
|
||||
@@ -150,6 +210,12 @@ const validate = async (required = false) => {
|
||||
return isValid;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
console.log('Employee select ref:', employeeSelectRef.value);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
});
|
||||
|
||||
+459
-5
@@ -11,7 +11,13 @@
|
||||
:hour-width="hourWidth"
|
||||
/>
|
||||
|
||||
<div class="timeline-bar">
|
||||
<div
|
||||
ref="timelineBar"
|
||||
class="timeline-bar"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<!-- Non-working hours - before work -->
|
||||
<div
|
||||
class="non-working-hours"
|
||||
@@ -47,7 +53,14 @@
|
||||
:key="`task-${index}`"
|
||||
class="scheduled-block"
|
||||
:class="{
|
||||
'scheduled-block--blocker': ['blocker', 'adhoc', 'home_office', 'vacation', 'free_day', 'office365_blocker'].includes(task.type)
|
||||
'scheduled-block--blocker': [
|
||||
'blocker',
|
||||
'adhoc',
|
||||
'home_office',
|
||||
'vacation',
|
||||
'free_day',
|
||||
'office365_blocker',
|
||||
].includes(task.type),
|
||||
}"
|
||||
:style="{
|
||||
left: `${getTimePosition(task._real_start)}px`,
|
||||
@@ -68,11 +81,34 @@
|
||||
:title="`Other timeblock`"
|
||||
></div>
|
||||
|
||||
<!-- Hover preview block -->
|
||||
<div
|
||||
v-if="showPreview && previewBlock"
|
||||
class="preview-block"
|
||||
:class="{ 'is-stretching': dragState.isStretching }"
|
||||
:style="{
|
||||
left: `${getTimePosition(previewBlock.start)}px`,
|
||||
width: `${getTimeWidth(previewBlock.start, previewBlock.end)}px`,
|
||||
}"
|
||||
@mousedown.stop="handlePreviewMouseDown"
|
||||
>
|
||||
<div class="preview-time-label">
|
||||
{{ formatTime(previewBlock.start) }} -
|
||||
{{ formatTime(previewBlock.end) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current timeblock (if valid) -->
|
||||
<div
|
||||
v-if="currentTimeblockRange.start && currentTimeblockRange.end"
|
||||
v-if="
|
||||
currentTimeblockRange.start &&
|
||||
currentTimeblockRange.end &&
|
||||
!dragState.isResizingCurrent
|
||||
"
|
||||
class="current-timeblock"
|
||||
:class="{ 'has-collision': collision.hasCollision }"
|
||||
:class="{
|
||||
'has-collision': collision.hasCollision,
|
||||
}"
|
||||
:style="{
|
||||
left: `${getTimePosition(currentTimeblockRange.start)}px`,
|
||||
width: `${getTimeWidth(
|
||||
@@ -85,6 +121,35 @@
|
||||
<div v-if="timeblockDuration > 0" class="timeblock-duration">
|
||||
{{ timeblockDuration.toFixed(2) }}h
|
||||
</div>
|
||||
|
||||
<!-- Resize handles -->
|
||||
<div
|
||||
class="resize-handle resize-handle--left"
|
||||
@mousedown.stop="handleResizeMouseDown($event, 'left')"
|
||||
></div>
|
||||
<div
|
||||
class="resize-handle resize-handle--right"
|
||||
@mousedown.stop="handleResizeMouseDown($event, 'right')"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Resizing current timeblock preview -->
|
||||
<div
|
||||
v-if="dragState.isResizingCurrent && resizedCurrentBlock"
|
||||
class="current-timeblock is-resizing"
|
||||
:class="{ 'has-collision': collision.hasCollision }"
|
||||
:style="{
|
||||
left: `${getTimePosition(resizedCurrentBlock.start)}px`,
|
||||
width: `${getTimeWidth(
|
||||
resizedCurrentBlock.start,
|
||||
resizedCurrentBlock.end,
|
||||
)}px`,
|
||||
}"
|
||||
>
|
||||
<div class="preview-time-label">
|
||||
{{ formatTime(resizedCurrentBlock.start) }} -
|
||||
{{ formatTime(resizedCurrentBlock.end) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collision segments -->
|
||||
@@ -104,7 +169,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import moment from 'moment';
|
||||
import AEmployeeTimeblockTimelineHeader from './a-employee-timeblock-timeline-header.vue';
|
||||
import type { EmployeeFullData, TimeblockData } from './types';
|
||||
@@ -114,6 +179,7 @@ interface TimelineTask {
|
||||
_real_end: Date;
|
||||
subtask?: { title: string };
|
||||
blocker?: { type: string };
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface CollisionSegment {
|
||||
@@ -146,8 +212,32 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
otherTimeblocks: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'timeblockUpdated',
|
||||
payload: { starts_at_time: string; ends_at_time: string },
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const timelineContainer = ref<HTMLElement | null>(null);
|
||||
const timelineScroll = ref<HTMLElement | null>(null);
|
||||
const timelineBar = ref<HTMLElement | null>(null);
|
||||
|
||||
// Check if mobile device
|
||||
const isMobile = ref(false);
|
||||
|
||||
// Interactive state
|
||||
const showPreview = ref(false);
|
||||
const previewBlock = ref<{ start: Date; end: Date } | null>(null);
|
||||
const dragState = ref({
|
||||
isStretching: false,
|
||||
isResizingCurrent: false,
|
||||
resizeEdge: null as 'left' | 'right' | null,
|
||||
startX: 0,
|
||||
initialStart: null as Date | null,
|
||||
initialEnd: null as Date | null,
|
||||
});
|
||||
const resizedCurrentBlock = ref<{ start: Date; end: Date } | null>(null);
|
||||
|
||||
// Convert other timeblocks to renderable format
|
||||
const otherTimeblocksRendered = computed(() => {
|
||||
@@ -192,6 +282,303 @@ const getTimeWidth = (start: Date, end: Date): number => {
|
||||
return (endHour - startHour) * props.hourWidth - 2;
|
||||
};
|
||||
|
||||
// Time snapping utility - snap to 15 minute intervals
|
||||
const snapToInterval = (date: Date, intervalMinutes = 15): Date => {
|
||||
const minutes = date.getMinutes();
|
||||
const snappedMinutes =
|
||||
Math.round(minutes / intervalMinutes) * intervalMinutes;
|
||||
return moment(date)
|
||||
.set({ minute: snappedMinutes, second: 0, millisecond: 0 })
|
||||
.toDate();
|
||||
};
|
||||
|
||||
// Convert pixel position to time
|
||||
const positionToTime = (pixelX: number): Date => {
|
||||
const hoursFromStart = pixelX / props.hourWidth;
|
||||
const totalHours = props.startHour + hoursFromStart;
|
||||
const hour = Math.floor(totalHours);
|
||||
const minute = (totalHours % 1) * 60;
|
||||
|
||||
// Use current date from currentTimeblockRange or today
|
||||
const baseDate = props.currentTimeblockRange.start
|
||||
? moment(props.currentTimeblockRange.start)
|
||||
: moment();
|
||||
|
||||
return baseDate.set({ hour, minute, second: 0, millisecond: 0 }).toDate();
|
||||
};
|
||||
|
||||
// Format time as HH:mm
|
||||
const formatTime = (date: Date): string => {
|
||||
return moment(date).format('HH:mm');
|
||||
};
|
||||
|
||||
// Check if mouse is over an existing block (x is already relative position with scroll)
|
||||
const isOverExistingBlock = (relativeX: number): boolean => {
|
||||
// Check if over any scheduled block
|
||||
for (const task of props.scheduledBlocks) {
|
||||
const blockLeft = getTimePosition(task._real_start);
|
||||
const blockWidth = getTimeWidth(task._real_start, task._real_end);
|
||||
if (relativeX >= blockLeft && relativeX <= blockLeft + blockWidth) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over any other timeblock
|
||||
for (const tb of otherTimeblocksRendered.value) {
|
||||
const blockLeft = getTimePosition(tb._start);
|
||||
const blockWidth = getTimeWidth(tb._start, tb._end);
|
||||
if (relativeX >= blockLeft && relativeX <= blockLeft + blockWidth) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over current timeblock
|
||||
if (props.currentTimeblockRange.start && props.currentTimeblockRange.end) {
|
||||
const blockLeft = getTimePosition(props.currentTimeblockRange.start);
|
||||
const blockWidth = getTimeWidth(
|
||||
props.currentTimeblockRange.start,
|
||||
props.currentTimeblockRange.end,
|
||||
);
|
||||
if (relativeX >= blockLeft && relativeX <= blockLeft + blockWidth) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Mouse move handler - show preview
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
// Disable hover preview on mobile
|
||||
if (isMobile.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragState.value.isStretching) {
|
||||
handleStretchMove(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragState.value.isResizingCurrent) {
|
||||
handleResizeCurrentMove(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timelineScroll.value) return;
|
||||
|
||||
// Get position relative to scroll container, accounting for scroll
|
||||
const rect = timelineScroll.value.getBoundingClientRect();
|
||||
const scrollLeft = timelineScroll.value.scrollLeft;
|
||||
const x = event.clientX - rect.left + scrollLeft;
|
||||
|
||||
// Don't show preview if over existing block (using relative position)
|
||||
if (isOverExistingBlock(x)) {
|
||||
showPreview.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to time and snap
|
||||
const rawTime = positionToTime(x);
|
||||
const snappedStart = snapToInterval(rawTime);
|
||||
const snappedEnd = moment(snappedStart).add(1, 'hour').toDate();
|
||||
|
||||
// Only show preview within work hours
|
||||
const startHour = snappedStart.getHours() + snappedStart.getMinutes() / 60;
|
||||
const endHour = snappedEnd.getHours() + snappedEnd.getMinutes() / 60;
|
||||
|
||||
if (
|
||||
startHour >= props.employee.work_start &&
|
||||
endHour <= props.employee.work_end
|
||||
) {
|
||||
showPreview.value = true;
|
||||
previewBlock.value = {
|
||||
start: snappedStart,
|
||||
end: snappedEnd,
|
||||
};
|
||||
} else {
|
||||
showPreview.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse leave handler
|
||||
const handleMouseLeave = () => {
|
||||
if (!dragState.value.isStretching && !dragState.value.isResizingCurrent) {
|
||||
showPreview.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse down on timeline bar (not on preview or current block)
|
||||
const handleMouseDown = (_event: MouseEvent) => {
|
||||
// This will only trigger if not clicking on preview or current block
|
||||
// (those have @mousedown.stop)
|
||||
};
|
||||
|
||||
// Preview block mouse down - start creating/stretching
|
||||
const handlePreviewMouseDown = (event: MouseEvent) => {
|
||||
if (!previewBlock.value) return;
|
||||
|
||||
dragState.value.isStretching = true;
|
||||
dragState.value.startX = event.clientX;
|
||||
dragState.value.initialStart = previewBlock.value.start;
|
||||
dragState.value.initialEnd = previewBlock.value.end;
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
// Handle stretch move
|
||||
const handleStretchMove = (event: MouseEvent) => {
|
||||
if (!dragState.value.initialStart || !timelineScroll.value) return;
|
||||
|
||||
const rect = timelineScroll.value.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left + timelineScroll.value.scrollLeft;
|
||||
|
||||
const currentTime = positionToTime(x);
|
||||
const snappedTime = snapToInterval(currentTime);
|
||||
|
||||
const initialStartTime = dragState.value.initialStart.getTime();
|
||||
const snappedTimeValue = snappedTime.getTime();
|
||||
|
||||
if (snappedTimeValue > initialStartTime) {
|
||||
// Stretching right (extending end)
|
||||
previewBlock.value = {
|
||||
start: dragState.value.initialStart,
|
||||
end: snappedTime,
|
||||
};
|
||||
} else {
|
||||
// Stretching left (extending start)
|
||||
previewBlock.value = {
|
||||
start: snappedTime,
|
||||
end: dragState.value.initialStart,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle stretch end
|
||||
const handleStretchEnd = () => {
|
||||
if (dragState.value.isStretching && previewBlock.value) {
|
||||
// Emit the new timeblock
|
||||
emit('timeblockUpdated', {
|
||||
starts_at_time: formatTime(previewBlock.value.start),
|
||||
ends_at_time: formatTime(previewBlock.value.end),
|
||||
});
|
||||
}
|
||||
|
||||
dragState.value.isStretching = false;
|
||||
dragState.value.initialStart = null;
|
||||
dragState.value.initialEnd = null;
|
||||
showPreview.value = false;
|
||||
};
|
||||
|
||||
// Resize handle mouse down - start resizing
|
||||
const handleResizeMouseDown = (event: MouseEvent, edge: 'left' | 'right') => {
|
||||
if (!props.currentTimeblockRange.start || !props.currentTimeblockRange.end)
|
||||
return;
|
||||
|
||||
dragState.value.isResizingCurrent = true;
|
||||
dragState.value.resizeEdge = edge;
|
||||
dragState.value.startX = event.clientX;
|
||||
dragState.value.initialStart = props.currentTimeblockRange.start;
|
||||
dragState.value.initialEnd = props.currentTimeblockRange.end;
|
||||
|
||||
resizedCurrentBlock.value = {
|
||||
start: props.currentTimeblockRange.start,
|
||||
end: props.currentTimeblockRange.end,
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
// Handle resize current block move
|
||||
const handleResizeCurrentMove = (event: MouseEvent) => {
|
||||
if (
|
||||
!dragState.value.initialStart ||
|
||||
!dragState.value.initialEnd ||
|
||||
!timelineScroll.value
|
||||
)
|
||||
return;
|
||||
|
||||
const rect = timelineScroll.value.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left + timelineScroll.value.scrollLeft;
|
||||
|
||||
const currentTime = positionToTime(x);
|
||||
const snappedTime = snapToInterval(currentTime);
|
||||
|
||||
if (dragState.value.resizeEdge === 'left') {
|
||||
// Resizing left edge (changing start time)
|
||||
// Make sure new start is before end
|
||||
if (snappedTime < dragState.value.initialEnd) {
|
||||
resizedCurrentBlock.value = {
|
||||
start: snappedTime,
|
||||
end: dragState.value.initialEnd,
|
||||
};
|
||||
}
|
||||
} else if (dragState.value.resizeEdge === 'right') {
|
||||
// Resizing right edge (changing end time)
|
||||
// Make sure new end is after start
|
||||
if (snappedTime > dragState.value.initialStart) {
|
||||
resizedCurrentBlock.value = {
|
||||
start: dragState.value.initialStart,
|
||||
end: snappedTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle resize current block end
|
||||
const handleResizeCurrentEnd = () => {
|
||||
if (dragState.value.isResizingCurrent && resizedCurrentBlock.value) {
|
||||
// Emit the updated timeblock
|
||||
emit('timeblockUpdated', {
|
||||
starts_at_time: formatTime(resizedCurrentBlock.value.start),
|
||||
ends_at_time: formatTime(resizedCurrentBlock.value.end),
|
||||
});
|
||||
}
|
||||
|
||||
dragState.value.isResizingCurrent = false;
|
||||
dragState.value.resizeEdge = null;
|
||||
dragState.value.initialStart = null;
|
||||
dragState.value.initialEnd = null;
|
||||
resizedCurrentBlock.value = null;
|
||||
};
|
||||
|
||||
// Global mouse up handler
|
||||
const handleGlobalMouseUp = () => {
|
||||
if (dragState.value.isStretching) {
|
||||
handleStretchEnd();
|
||||
} else if (dragState.value.isResizingCurrent) {
|
||||
handleResizeCurrentEnd();
|
||||
}
|
||||
};
|
||||
|
||||
// Global mouse move handler
|
||||
const handleGlobalMouseMove = (event: MouseEvent) => {
|
||||
if (dragState.value.isStretching) {
|
||||
handleStretchMove(event);
|
||||
} else if (dragState.value.isResizingCurrent) {
|
||||
handleResizeCurrentMove(event);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup global listeners
|
||||
onMounted(() => {
|
||||
// Detect mobile device
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
|
||||
// Listen for resize to update mobile status
|
||||
const handleResize = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
document.addEventListener('mousemove', handleGlobalMouseMove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
document.removeEventListener('mousemove', handleGlobalMouseMove);
|
||||
});
|
||||
|
||||
// Expose refs for parent component
|
||||
defineExpose({
|
||||
timelineScroll,
|
||||
@@ -230,6 +617,7 @@ defineExpose({
|
||||
width: calc(24 * 60px)
|
||||
background: #fafafa
|
||||
border-radius: 4px
|
||||
cursor: crosshair
|
||||
|
||||
@media (max-width: 768px)
|
||||
width: calc(24 * 50px)
|
||||
@@ -240,6 +628,7 @@ defineExpose({
|
||||
height: 100%
|
||||
background: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(224, 224, 224, 0.3) 10px, rgba(224, 224, 224, 0.3) 20px)
|
||||
z-index: 1
|
||||
pointer-events: none
|
||||
|
||||
.work-hours-bar
|
||||
position: absolute
|
||||
@@ -247,6 +636,7 @@ defineExpose({
|
||||
height: 100%
|
||||
background: transparent
|
||||
z-index: 1
|
||||
pointer-events: none
|
||||
|
||||
.scheduled-block
|
||||
position: absolute
|
||||
@@ -257,6 +647,7 @@ defineExpose({
|
||||
opacity: 0.6
|
||||
border-radius: 2px
|
||||
z-index: 2
|
||||
pointer-events: none
|
||||
|
||||
&--blocker
|
||||
background: rgb(176, 0, 32)
|
||||
@@ -272,6 +663,40 @@ defineExpose({
|
||||
border: 2px dashed #FF9800
|
||||
border-radius: 2px
|
||||
z-index: 2
|
||||
pointer-events: none
|
||||
|
||||
.preview-block
|
||||
position: absolute
|
||||
top: 5px
|
||||
height: 22px
|
||||
background: #4CAF50
|
||||
opacity: 0.6
|
||||
border: 2px dashed #388E3C
|
||||
border-radius: 2px
|
||||
z-index: 5
|
||||
cursor: crosshair
|
||||
transition: opacity 0.2s, box-shadow 0.2s
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
&:hover
|
||||
opacity: 0.8
|
||||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4)
|
||||
|
||||
&.is-stretching
|
||||
opacity: 0.8
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.6)
|
||||
cursor: ew-resize
|
||||
|
||||
.preview-time-label
|
||||
font-size: 11px
|
||||
font-weight: 600
|
||||
color: white
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5)
|
||||
white-space: nowrap
|
||||
pointer-events: none
|
||||
user-select: none
|
||||
|
||||
.current-timeblock
|
||||
position: absolute
|
||||
@@ -282,10 +707,37 @@ defineExpose({
|
||||
border: 2px solid #1976D2
|
||||
border-radius: 2px
|
||||
z-index: 3
|
||||
transition: box-shadow 0.2s
|
||||
|
||||
&.has-collision
|
||||
border-color: #ff5252
|
||||
|
||||
&.is-resizing
|
||||
opacity: 0.8
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.6)
|
||||
z-index: 6
|
||||
|
||||
.resize-handle
|
||||
position: absolute
|
||||
top: 0
|
||||
width: 8px
|
||||
height: 100%
|
||||
cursor: ew-resize
|
||||
z-index: 4
|
||||
|
||||
&:hover
|
||||
background: rgba(255, 255, 255, 0.3)
|
||||
|
||||
&--left
|
||||
left: 0
|
||||
border-top-left-radius: 2px
|
||||
border-bottom-left-radius: 2px
|
||||
|
||||
&--right
|
||||
right: 0
|
||||
border-top-right-radius: 2px
|
||||
border-bottom-right-radius: 2px
|
||||
|
||||
.collision-segment
|
||||
position: absolute
|
||||
top: 5px
|
||||
@@ -294,6 +746,7 @@ defineExpose({
|
||||
opacity: 0.6
|
||||
border-radius: 2px
|
||||
z-index: 4
|
||||
pointer-events: none
|
||||
|
||||
.timeblock-duration
|
||||
position: absolute
|
||||
@@ -305,6 +758,7 @@ defineExpose({
|
||||
color: white
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3)
|
||||
pointer-events: none
|
||||
user-select: none
|
||||
|
||||
.timeline-loading
|
||||
height: 60px
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
:start-hour="START_HOUR"
|
||||
:end-hour="END_HOUR"
|
||||
:hour-width="hourWidth"
|
||||
@timeblock-updated="handleTimeblockUpdated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -269,24 +270,12 @@ const otherTimeblocksForSameDate = computed(() => {
|
||||
if (!props.modelValue.date) return [];
|
||||
|
||||
const currentDate = moment(props.modelValue.date).format('YYYY-MM-DD');
|
||||
const currentTimeblock = props.modelValue;
|
||||
|
||||
// Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only
|
||||
const timeblocksToCheck =
|
||||
props.allTimeblocks.length > 0
|
||||
? props.allTimeblocks
|
||||
: props.allEmployeeTimeblocks;
|
||||
|
||||
return timeblocksToCheck.filter((tb) => {
|
||||
// Skip current timeblock - compare by reference or by matching all key properties
|
||||
if (
|
||||
tb === currentTimeblock ||
|
||||
(tb.employee_id == currentTimeblock.employee_id &&
|
||||
tb.date === currentTimeblock.date &&
|
||||
tb.starts_at_time === currentTimeblock.starts_at_time &&
|
||||
tb.ends_at_time === currentTimeblock.ends_at_time &&
|
||||
tb.id === currentTimeblock.id)
|
||||
) {
|
||||
// Use allEmployeeTimeblocks which contains timeblocks for this specific employee
|
||||
// Filter out the current timeblock by comparing index directly
|
||||
return props.allEmployeeTimeblocks.filter((tb, index) => {
|
||||
// Skip current timeblock by index
|
||||
if (index === props.currentIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -405,6 +394,23 @@ const updateEndTime = (event: Event) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Handle timeblock updated from timeline (drag/drop or quick add)
|
||||
const handleTimeblockUpdated = (payload: {
|
||||
starts_at_time: string;
|
||||
ends_at_time: string;
|
||||
}) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
starts_at_time: payload.starts_at_time,
|
||||
ends_at_time: payload.ends_at_time,
|
||||
});
|
||||
|
||||
// Trigger validation after timeline update
|
||||
nextTick(() => {
|
||||
v$.value.$touch();
|
||||
});
|
||||
};
|
||||
|
||||
// Expose validate method
|
||||
const validate = async () => {
|
||||
v$.value.$touch();
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
:timeblocks="getEmployeeTimeblocks(subtask.employee_id)"
|
||||
:all-timeblocks="timeblocks"
|
||||
:deleted-timeblock-ids="deletedTimeblockIds"
|
||||
:custom-filter="customFilter"
|
||||
:custom-filter="getEmployeeCustomFilter(subtask.employee_id)"
|
||||
@update:timeblocks="
|
||||
updateEmployeeTimeblocks($event, subtask.employee_id)
|
||||
"
|
||||
@@ -122,7 +122,7 @@
|
||||
:timeblocks="getEmployeeTimeblocks(employee.id)"
|
||||
:all-timeblocks="timeblocks"
|
||||
:deleted-timeblock-ids="deletedTimeblockIds"
|
||||
:custom-filter="customFilter"
|
||||
:custom-filter="getEmployeeCustomFilter(employee.id)"
|
||||
:required="index === 0"
|
||||
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
|
||||
@clear-employee="() => removeEmployeeAtIndex(index)"
|
||||
@@ -564,6 +564,45 @@ export default {
|
||||
return this.timeblocks.filter((tb) => tb.employee_id == employeeId);
|
||||
},
|
||||
|
||||
// Custom filter for employee select to prevent selecting the same employee twice
|
||||
// currentEmployeeId: the ID of the employee currently selected in this dropdown (can be null)
|
||||
getEmployeeCustomFilter(currentEmployeeId) {
|
||||
return (itemText, queryText, item) => {
|
||||
// Get all selected employee IDs except the current one
|
||||
const selectedEmployeeIds = new Set();
|
||||
|
||||
if (this.isDraftWithMultipleEmployees && this.subtask?.employees) {
|
||||
// In multi-employee mode, collect all selected employee IDs except current
|
||||
this.subtask.employees.forEach((emp) => {
|
||||
if (emp.id && emp.id !== currentEmployeeId) {
|
||||
selectedEmployeeIds.add(emp.id);
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
this.subtask?.employee_id &&
|
||||
this.subtask.employee_id !== currentEmployeeId
|
||||
) {
|
||||
// In single employee edit mode
|
||||
selectedEmployeeIds.add(this.subtask.employee_id);
|
||||
}
|
||||
|
||||
// Filter out already selected employees (but allow current selection)
|
||||
if (selectedEmployeeIds.has(item.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply default text filtering (from AutocompleteMixin)
|
||||
if (this.customFilter) {
|
||||
return this.customFilter(itemText, queryText, item);
|
||||
}
|
||||
|
||||
// Default case-insensitive text match
|
||||
const searchText = queryText.toString().toLowerCase();
|
||||
const text = itemText.toString().toLowerCase();
|
||||
return text.indexOf(searchText) > -1;
|
||||
};
|
||||
},
|
||||
|
||||
updateEmployeeTimeblocks(updatedTimeblocks, employeeId) {
|
||||
// Track deleted timeblocks (saved timeblocks that are no longer in the updated list)
|
||||
const updatedIds = new Set(
|
||||
|
||||
Reference in new issue
Block a user