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