mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
feat(012): enhance timeblock management with deleted tracking and improved rendering
This commit is contained in:
1 parent
fd74a0fe29
commit
33174b6bb1
4 files changed
+95
-45
No files matched your search
+11
-11
@@ -53,16 +53,16 @@
|
|||||||
:title="`${task.subtask?.title || task.blocker?.type || 'Task'}`"
|
:title="`${task.subtask?.title || task.blocker?.type || 'Task'}`"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Unsaved timeblocks for same date -->
|
<!-- Other timeblocks for same date -->
|
||||||
<div
|
<div
|
||||||
v-for="(timeblock, index) in unsavedTimeblocksRendered"
|
v-for="(timeblock, index) in otherTimeblocksRendered"
|
||||||
:key="`unsaved-${index}`"
|
:key="`other-${index}`"
|
||||||
class="unsaved-timeblock"
|
class="other-timeblock"
|
||||||
:style="{
|
:style="{
|
||||||
left: `${getTimePosition(timeblock._start)}px`,
|
left: `${getTimePosition(timeblock._start)}px`,
|
||||||
width: `${getTimeWidth(timeblock._start, timeblock._end)}px`,
|
width: `${getTimeWidth(timeblock._start, timeblock._end)}px`,
|
||||||
}"
|
}"
|
||||||
:title="`Unsaved timeblock`"
|
:title="`Other timeblock`"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Current timeblock (if valid) -->
|
<!-- Current timeblock (if valid) -->
|
||||||
@@ -122,7 +122,7 @@ interface Props {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
employee: EmployeeFullData;
|
employee: EmployeeFullData;
|
||||||
scheduledBlocks: TimelineTask[];
|
scheduledBlocks: TimelineTask[];
|
||||||
unsavedTimeblocks?: TimeblockData[];
|
otherTimeblocks?: TimeblockData[];
|
||||||
currentTimeblockRange: {
|
currentTimeblockRange: {
|
||||||
start: Date | null;
|
start: Date | null;
|
||||||
end: Date | null;
|
end: Date | null;
|
||||||
@@ -140,15 +140,15 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
unsavedTimeblocks: () => [],
|
otherTimeblocks: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const timelineContainer = ref<HTMLElement | null>(null);
|
const timelineContainer = ref<HTMLElement | null>(null);
|
||||||
const timelineScroll = ref<HTMLElement | null>(null);
|
const timelineScroll = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// Convert unsaved timeblocks to renderable format
|
// Convert other timeblocks to renderable format
|
||||||
const unsavedTimeblocksRendered = computed(() => {
|
const otherTimeblocksRendered = computed(() => {
|
||||||
return props.unsavedTimeblocks
|
return props.otherTimeblocks
|
||||||
.map((tb) => {
|
.map((tb) => {
|
||||||
if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return null;
|
if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return null;
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ defineExpose({
|
|||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
z-index: 2
|
z-index: 2
|
||||||
|
|
||||||
.unsaved-timeblock
|
.other-timeblock
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 5px
|
top: 5px
|
||||||
height: 22px
|
height: 22px
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
:employee="employee"
|
:employee="employee"
|
||||||
:scheduled-blocks="scheduledBlocks"
|
:scheduled-blocks="scheduledBlocks"
|
||||||
:unsaved-timeblocks="unsavedTimeblocksForSameDate"
|
:other-timeblocks="otherTimeblocksForSameDate"
|
||||||
:current-timeblock-range="currentTimeblockRange"
|
:current-timeblock-range="currentTimeblockRange"
|
||||||
:collision="collision"
|
:collision="collision"
|
||||||
:collision-segments="collisionSegments"
|
:collision-segments="collisionSegments"
|
||||||
@@ -48,19 +48,25 @@ import { computed, ref, watch, onMounted, nextTick } from 'vue';
|
|||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { helpers } from '@vuelidate/validators';
|
import { helpers } from '@vuelidate/validators';
|
||||||
import { useEmployeeTimeline } from '@/plugins/tm@_utils/useEmployeeTimeline';
|
import { useEmployeeTimeline } from '@/plugins/tm@_utils/useEmployeeTimeline';
|
||||||
import { timeblockToDateRange } from '@/plugins/tm@_utils/timeblock.utils';
|
import {
|
||||||
|
timeblockToDateRange,
|
||||||
|
detectCollision,
|
||||||
|
getCollisionSegments as getCollisionSegmentsUtil,
|
||||||
|
} from '@/plugins/tm@_utils/timeblock.utils';
|
||||||
import AEmployeeTimeblockHeader from './a-employee-timeblock-header.vue';
|
import AEmployeeTimeblockHeader from './a-employee-timeblock-header.vue';
|
||||||
import AEmployeeTimeblockTimeline from './a-employee-timeblock-timeline.vue';
|
import AEmployeeTimeblockTimeline from './a-employee-timeblock-timeline.vue';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import type { TimeblockData, EmployeeFullData } from './types';
|
import type { TimeblockData, EmployeeFullData } from './types';
|
||||||
|
|
||||||
// Configuration: Set to false to allow editing timeblocks with existing IDs
|
// Configuration: Set to false to allow editing timeblocks with existing IDs
|
||||||
const ALLOW_EDITING_EXISTING_TIMEBLOCKS = false;
|
const ALLOW_EDITING_EXISTING_TIMEBLOCKS = true;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: TimeblockData;
|
modelValue: TimeblockData;
|
||||||
employee: EmployeeFullData | null;
|
employee: EmployeeFullData | null;
|
||||||
allEmployeeTimeblocks?: TimeblockData[];
|
allEmployeeTimeblocks?: TimeblockData[]; // Timeblocks for current employee
|
||||||
|
allTimeblocks?: TimeblockData[]; // All timeblocks from all employees
|
||||||
|
deletedTimeblockIds?: number[]; // IDs of saved timeblocks that have been deleted
|
||||||
currentIndex?: number;
|
currentIndex?: number;
|
||||||
deletable?: boolean;
|
deletable?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
@@ -68,6 +74,8 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
allEmployeeTimeblocks: () => [],
|
allEmployeeTimeblocks: () => [],
|
||||||
|
allTimeblocks: () => [],
|
||||||
|
deletedTimeblockIds: () => [],
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -212,8 +220,6 @@ const {
|
|||||||
timelineTasks,
|
timelineTasks,
|
||||||
scheduledTime: _scheduledTime,
|
scheduledTime: _scheduledTime,
|
||||||
freeTime: _freeTime,
|
freeTime: _freeTime,
|
||||||
collision,
|
|
||||||
collisionSegments,
|
|
||||||
timeblockDuration,
|
timeblockDuration,
|
||||||
refetch,
|
refetch,
|
||||||
} = useEmployeeTimeline(employeeRef, dateRef, timeblockRef);
|
} = useEmployeeTimeline(employeeRef, dateRef, timeblockRef);
|
||||||
@@ -229,34 +235,62 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// All tasks from timelineTasks are already filtered for the selected date
|
// All tasks from timelineTasks are already filtered for the selected date
|
||||||
|
// Filter out timeblocks that are being edited or deleted locally
|
||||||
const scheduledBlocks = computed(() => {
|
const scheduledBlocks = computed(() => {
|
||||||
return timelineTasks.value;
|
// Combine IDs that should be filtered: edited locally or deleted
|
||||||
|
const hiddenTimeblockIds = new Set([
|
||||||
|
// Timeblock IDs being edited locally
|
||||||
|
...props.allTimeblocks.filter(tb => tb.id).map(tb => tb.id),
|
||||||
|
// Timeblock IDs that have been deleted
|
||||||
|
...(props.deletedTimeblockIds || [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filter out tasks whose timeblock IDs should be hidden
|
||||||
|
return timelineTasks.value.filter(task =>
|
||||||
|
!task._timeblock_id || !hiddenTimeblockIds.has(task._timeblock_id)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get other unsaved timeblocks for the same date (excluding current one)
|
// Recalculate collision using filtered scheduled blocks (not raw timeline tasks)
|
||||||
const unsavedTimeblocksForSameDate = computed(() => {
|
// This prevents false collisions with timeblocks being edited
|
||||||
if (!props.modelValue.date || !props.allEmployeeTimeblocks) return [];
|
const collision = computed(() => {
|
||||||
|
return detectCollision(props.modelValue, scheduledBlocks.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate collision segments using filtered scheduled blocks
|
||||||
|
const collisionSegments = computed(() => {
|
||||||
|
return getCollisionSegmentsUtil(props.modelValue, scheduledBlocks.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get other timeblocks for the same date (excluding current one)
|
||||||
|
// This includes timeblocks from both the current employee and other employees
|
||||||
|
const otherTimeblocksForSameDate = computed(() => {
|
||||||
|
if (!props.modelValue.date) return [];
|
||||||
|
|
||||||
const currentDate = moment(props.modelValue.date).format('YYYY-MM-DD');
|
const currentDate = moment(props.modelValue.date).format('YYYY-MM-DD');
|
||||||
|
const currentTimeblock = props.modelValue;
|
||||||
|
|
||||||
return props.allEmployeeTimeblocks
|
// Use allTimeblocks if available (includes all employees), otherwise fall back to current employee only
|
||||||
.filter((tb, index) => {
|
const timeblocksToCheck = props.allTimeblocks.length > 0
|
||||||
// Exclude the current timeblock
|
? props.allTimeblocks
|
||||||
if (index === props.currentIndex) return false;
|
: props.allEmployeeTimeblocks;
|
||||||
|
|
||||||
// Only include timeblocks WITHOUT IDs (unsaved)
|
return timeblocksToCheck.filter(tb => {
|
||||||
if (tb.id) return false;
|
// 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)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Must have a date
|
// Must have required fields
|
||||||
if (!tb.date) return false;
|
if (!tb.date || !tb.starts_at_time || !tb.ends_at_time) return false;
|
||||||
|
|
||||||
// Must be the same date
|
// Must be the same date
|
||||||
const tbDate = moment(tb.date).format('YYYY-MM-DD');
|
return moment(tb.date).format('YYYY-MM-DD') === currentDate;
|
||||||
return tbDate === currentDate;
|
|
||||||
})
|
|
||||||
.filter((tb) => {
|
|
||||||
// Only include if it has valid time range
|
|
||||||
return tb.starts_at_time && tb.ends_at_time;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
:model-value="timeblock"
|
:model-value="timeblock"
|
||||||
:employee="selectedEmployee"
|
:employee="selectedEmployee"
|
||||||
:all-employee-timeblocks="timeblocks"
|
:all-employee-timeblocks="timeblocks"
|
||||||
|
:all-timeblocks="allTimeblocks"
|
||||||
|
:deleted-timeblock-ids="deletedTimeblockIds"
|
||||||
:current-index="index"
|
:current-index="index"
|
||||||
:deletable="timeblocks.length > 1"
|
:deletable="timeblocks.length > 1"
|
||||||
:index="index"
|
:index="index"
|
||||||
@@ -42,6 +44,8 @@ import type {
|
|||||||
interface Props {
|
interface Props {
|
||||||
employeeData: EmployeeData;
|
employeeData: EmployeeData;
|
||||||
timeblocks?: TimeblockData[];
|
timeblocks?: TimeblockData[];
|
||||||
|
allTimeblocks?: TimeblockData[]; // All timeblocks from all employees
|
||||||
|
deletedTimeblockIds?: number[]; // IDs of saved timeblocks that have been deleted
|
||||||
customFilter?: CustomFilterFunction;
|
customFilter?: CustomFilterFunction;
|
||||||
hasBackground?: boolean;
|
hasBackground?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -49,6 +53,8 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
timeblocks: () => [],
|
timeblocks: () => [],
|
||||||
|
allTimeblocks: () => [],
|
||||||
|
deletedTimeblockIds: () => [],
|
||||||
customFilter: undefined,
|
customFilter: undefined,
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
v-if="subtask.id"
|
v-if="subtask.id"
|
||||||
v-model:employee-data="editModeEmployee"
|
v-model:employee-data="editModeEmployee"
|
||||||
:timeblocks="getEmployeeTimeblocks(subtask.employee_id)"
|
:timeblocks="getEmployeeTimeblocks(subtask.employee_id)"
|
||||||
|
:all-timeblocks="timeblocks"
|
||||||
|
:deleted-timeblock-ids="deletedTimeblockIds"
|
||||||
:custom-filter="customFilter"
|
:custom-filter="customFilter"
|
||||||
@update:timeblocks="
|
@update:timeblocks="
|
||||||
updateEmployeeTimeblocks($event, subtask.employee_id)
|
updateEmployeeTimeblocks($event, subtask.employee_id)
|
||||||
@@ -118,6 +120,8 @@
|
|||||||
:ref="(el) => setEmployeeRef(el, index)"
|
:ref="(el) => setEmployeeRef(el, index)"
|
||||||
v-model:employee-data="subtask.employees[index]"
|
v-model:employee-data="subtask.employees[index]"
|
||||||
:timeblocks="getEmployeeTimeblocks(employee.id)"
|
:timeblocks="getEmployeeTimeblocks(employee.id)"
|
||||||
|
:all-timeblocks="timeblocks"
|
||||||
|
:deleted-timeblock-ids="deletedTimeblockIds"
|
||||||
:custom-filter="customFilter"
|
:custom-filter="customFilter"
|
||||||
:required="index === 0"
|
:required="index === 0"
|
||||||
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
|
@update:timeblocks="updateEmployeeTimeblocks($event, employee.id)"
|
||||||
@@ -315,6 +319,7 @@ export default {
|
|||||||
showDatePicker: false,
|
showDatePicker: false,
|
||||||
selectedNewDate: [],
|
selectedNewDate: [],
|
||||||
timeblocks: [],
|
timeblocks: [],
|
||||||
|
deletedTimeblockIds: [], // Track IDs of saved timeblocks that have been deleted
|
||||||
wasFormValidationTriggered: false,
|
wasFormValidationTriggered: false,
|
||||||
modalName: 'z-task-modal',
|
modalName: 'z-task-modal',
|
||||||
employeeRefs: [],
|
employeeRefs: [],
|
||||||
@@ -537,16 +542,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateEmployeeTimeblocks(updatedTimeblocks, employeeId) {
|
updateEmployeeTimeblocks(updatedTimeblocks, employeeId) {
|
||||||
// Remove old timeblocks for this employee
|
// Track deleted timeblocks (saved timeblocks that are no longer in the updated list)
|
||||||
const otherTimeblocks = this.timeblocks.filter(
|
const updatedIds = new Set(updatedTimeblocks.filter(tb => tb.id).map(tb => tb.id));
|
||||||
(tb) => tb.employee_id !== employeeId,
|
const deletedIds = this.timeblocks
|
||||||
);
|
.filter(tb => tb.employee_id === employeeId && tb.id && !updatedIds.has(tb.id))
|
||||||
// Add updated timeblocks with employee_id
|
.map(tb => tb.id)
|
||||||
const timeblocksWithEmployeeId = updatedTimeblocks.map((tb) => ({
|
.filter(id => !this.deletedTimeblockIds.includes(id));
|
||||||
...tb,
|
|
||||||
employee_id: employeeId,
|
this.deletedTimeblockIds.push(...deletedIds);
|
||||||
}));
|
|
||||||
this.timeblocks = [...otherTimeblocks, ...timeblocksWithEmployeeId];
|
// 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) {
|
updateTimeblock(val, index, employeeId) {
|
||||||
@@ -655,6 +664,7 @@ export default {
|
|||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.timeblocks = [];
|
this.timeblocks = [];
|
||||||
|
this.deletedTimeblockIds = []; // Reset deleted timeblock IDs
|
||||||
this.employeeRefs = [];
|
this.employeeRefs = [];
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.v$.$reset();
|
this.v$.$reset();
|
||||||
|
|||||||
Reference in new issue
Block a user