KI-AGENT: Ergänze wiederholende Termine für Kalender und Plantafel
This commit is contained in:
1
backend/db/migrations/0039_events_repeat_interval.sql
Normal file
1
backend/db/migrations/0039_events_repeat_interval.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "events" ADD COLUMN "repeatInterval" text DEFAULT 'Keine Wiederholung' NOT NULL;
|
||||
@@ -274,6 +274,13 @@
|
||||
"when": 1779158400000,
|
||||
"tag": "0038_events_state",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "7",
|
||||
"when": 1779840000000,
|
||||
"tag": "0039_events_repeat_interval",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export const events = pgTable(
|
||||
quick: boolean("quick").notNull().default(false),
|
||||
state: text("state").notNull().default("Final"),
|
||||
color: text("color"),
|
||||
repeatInterval: text("repeatInterval").notNull().default("Keine Wiederholung"),
|
||||
|
||||
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import timeGridPlugin from "@fullcalendar/timegrid"
|
||||
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import dayjs from "dayjs";
|
||||
import { expandRecurringEvent } from "~/utils/eventRecurrence"
|
||||
|
||||
//TODO BACKEND CHANGE COLOR IN TENANT FOR RENDERING
|
||||
|
||||
@@ -33,6 +34,124 @@ const selectedEvent = ref({})
|
||||
const selectedResources = ref([])
|
||||
|
||||
const events = ref([])
|
||||
const sourceEvents = ref([])
|
||||
const sourceAbsenceRequests = ref([])
|
||||
const sourceProjects = ref([])
|
||||
const sourceProfiles = ref([])
|
||||
const sourceVehicles = ref([])
|
||||
const sourceInventoryItems = ref([])
|
||||
const sourceInventoryItemGroups = ref([])
|
||||
|
||||
const buildEventTitle = (event) => {
|
||||
if (event.name) return event.name
|
||||
if (event.project) {
|
||||
const project = sourceProjects.value.find((item) => item.id === event.project)
|
||||
return project?.name || ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const buildEventColor = (event) =>
|
||||
profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype)?.color || "black"
|
||||
|
||||
const expandEventsForRange = (rangeStart, rangeEnd) => {
|
||||
const gridEvents = sourceEvents.value.flatMap((event) =>
|
||||
expandRecurringEvent(event, rangeStart, rangeEnd, (occurrenceStart, occurrenceEnd, occurrenceIndex) => {
|
||||
const eventColor = buildEventColor(event)
|
||||
|
||||
return {
|
||||
...event,
|
||||
start: occurrenceStart.toISOString(),
|
||||
end: occurrenceEnd ? occurrenceEnd.toISOString() : null,
|
||||
title: buildEventTitle(event),
|
||||
borderColor: eventColor,
|
||||
textColor: eventColor,
|
||||
backgroundColor: "black",
|
||||
entrytype: "event",
|
||||
eventId: event.id,
|
||||
occurrenceIndex
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const absenceEvents = sourceAbsenceRequests.value.map(absence => ({
|
||||
id: absence.id,
|
||||
resourceId: absence.user,
|
||||
resourceType: "person",
|
||||
title: `${absence.reason} - ${absence.profile.fullName}`,
|
||||
start: dayjs(absence.startDate).toDate(),
|
||||
end: dayjs(absence.endDate).add(1, 'day').toDate(),
|
||||
allDay: true,
|
||||
absencerequestId: absence.id,
|
||||
entrytype: "absencerequest",
|
||||
}))
|
||||
|
||||
calendarOptionsGrid.value.initialEvents = [
|
||||
...gridEvents,
|
||||
...absenceEvents
|
||||
]
|
||||
|
||||
const timelineEvents = sourceEvents.value.flatMap((event) => {
|
||||
const eventColor = buildEventColor(event)
|
||||
const title = buildEventTitle(event)
|
||||
|
||||
return expandRecurringEvent(event, rangeStart, rangeEnd, (occurrenceStart, occurrenceEnd) => {
|
||||
const returnData = {
|
||||
title,
|
||||
borderColor: eventColor,
|
||||
textColor: "white",
|
||||
backgroundColor: eventColor,
|
||||
start: occurrenceStart.toISOString(),
|
||||
end: occurrenceEnd ? occurrenceEnd.toISOString() : null,
|
||||
resourceIds: [],
|
||||
entrytype: "event",
|
||||
eventId: event.id
|
||||
}
|
||||
|
||||
if(event.profiles.length > 0) {
|
||||
event.profiles.forEach(profile => {
|
||||
returnData.resourceIds.push(`P-${profile}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.vehicles.length > 0) {
|
||||
event.vehicles.forEach(vehicle => {
|
||||
returnData.resourceIds.push(`F-${vehicle}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.inventoryitems.length > 0) {
|
||||
event.inventoryitems.forEach(inventoryitem => {
|
||||
returnData.resourceIds.push(`I-${inventoryitem}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.inventoryitemgroups.length > 0) {
|
||||
event.inventoryitemgroups.forEach(inventoryitemgroup => {
|
||||
returnData.resourceIds.push(`G-${inventoryitemgroup}`)
|
||||
})
|
||||
}
|
||||
|
||||
return returnData
|
||||
})
|
||||
})
|
||||
|
||||
sourceAbsenceRequests.value.forEach(absencerequest => {
|
||||
timelineEvents.push({
|
||||
title: `${absencerequest.reason}`,
|
||||
backgroundColor: "red",
|
||||
borderColor: "red",
|
||||
start: absencerequest.startDate,
|
||||
end: absencerequest.endDate,
|
||||
resourceIds: [`P-${absencerequest.profile.id}`],
|
||||
entrytype: "absencerequest",
|
||||
allDay: true,
|
||||
absencerequestId: absencerequest.id
|
||||
})
|
||||
})
|
||||
|
||||
calendarOptionsTimeline.value.initialEvents = timelineEvents
|
||||
}
|
||||
const calendarOptionsGrid = computed(() => {
|
||||
return {
|
||||
locale: deLocale,
|
||||
@@ -62,6 +181,9 @@ const calendarOptionsGrid = computed(() => {
|
||||
}
|
||||
|
||||
},
|
||||
datesSet: function(info) {
|
||||
expandEventsForRange(info.startStr, info.endStr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,6 +215,9 @@ const calendarOptionsTimeline = ref({
|
||||
router.push(`/standardEntity/events/show/${info.event.extendedProps.eventId}`)
|
||||
}
|
||||
},
|
||||
datesSet: function(info) {
|
||||
expandEventsForRange(info.startStr, info.endStr)
|
||||
},
|
||||
resourceGroupField: "type",
|
||||
resourceOrder: "-type",
|
||||
resources: [],
|
||||
@@ -127,75 +252,37 @@ const calendarOptionsTimeline = ref({
|
||||
|
||||
const loaded = ref(false)
|
||||
const setupPage = async () => {
|
||||
let tempData = (await useEntities("events").select()).filter(i => !i.archived)
|
||||
let absencerequests = (await useEntities("absencerequests").select("*, profile(*)")).filter(i => !i.archived)
|
||||
let projects = (await useEntities("projects").select( "*")).filter(i => !i.archived)
|
||||
let inventoryitems = (await useEntities("inventoryitems").select()).filter(i => !i.archived)
|
||||
let inventoryitemgroups = (await useEntities("inventoryitemgroups").select()).filter(i => !i.archived)
|
||||
let profiles = (await useEntities("profiles").select()).filter(i => !i.archived)
|
||||
let vehicles = (await useEntities("vehicles").select()).filter(i => !i.archived)
|
||||
|
||||
calendarOptionsGrid.value.initialEvents = [
|
||||
...tempData.map(event => {
|
||||
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
|
||||
|
||||
let title = ""
|
||||
if (event.name) {
|
||||
title = event.name
|
||||
} else if (event.project) {
|
||||
title = projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
title: title,
|
||||
borderColor: eventColor,
|
||||
textColor: eventColor,
|
||||
backgroundColor: "black",
|
||||
entrytype: "event",
|
||||
eventId: event.id
|
||||
}
|
||||
}),
|
||||
...absencerequests.map(absence => {
|
||||
return {
|
||||
id: absence.id,
|
||||
resourceId: absence.user,
|
||||
resourceType: "person",
|
||||
title: `${absence.reason} - ${absence.profile.fullName}`,
|
||||
start: dayjs(absence.startDate).toDate(),
|
||||
end: dayjs(absence.endDate).add(1, 'day').toDate(),
|
||||
allDay: true,
|
||||
absencerequestId: absence.id,
|
||||
entrytype: "absencerequest",
|
||||
}
|
||||
})
|
||||
]
|
||||
sourceEvents.value = (await useEntities("events").select()).filter(i => !i.archived)
|
||||
sourceAbsenceRequests.value = (await useEntities("absencerequests").select("*, profile(*)")).filter(i => !i.archived)
|
||||
sourceProjects.value = (await useEntities("projects").select( "*")).filter(i => !i.archived)
|
||||
sourceInventoryItems.value = (await useEntities("inventoryitems").select()).filter(i => !i.archived)
|
||||
sourceInventoryItemGroups.value = (await useEntities("inventoryitemgroups").select()).filter(i => !i.archived)
|
||||
sourceProfiles.value = (await useEntities("profiles").select()).filter(i => !i.archived)
|
||||
sourceVehicles.value = (await useEntities("vehicles").select()).filter(i => !i.archived)
|
||||
|
||||
calendarOptionsTimeline.value.resources = [
|
||||
...profiles.filter(i => i.tenant === profileStore.currentTenant).map(profile => {
|
||||
...sourceProfiles.value.filter(i => i.tenant === profileStore.currentTenant).map(profile => {
|
||||
return {
|
||||
type: 'Mitarbeiter',
|
||||
title: profile.fullName,
|
||||
id: `P-${profile.id}`
|
||||
}
|
||||
}),
|
||||
...vehicles.map(vehicle => {
|
||||
...sourceVehicles.value.map(vehicle => {
|
||||
return {
|
||||
type: 'Fahrzeug',
|
||||
title: vehicle.licensePlate,
|
||||
id: `F-${vehicle.id}`
|
||||
}
|
||||
}),
|
||||
...inventoryitems.filter(i=> i.usePlanning).map(item => {
|
||||
...sourceInventoryItems.value.filter(i=> i.usePlanning).map(item => {
|
||||
return {
|
||||
type: 'Inventar',
|
||||
title: item.name,
|
||||
id: `I-${item.id}`
|
||||
}
|
||||
}),
|
||||
...inventoryitemgroups.filter(i=> i.usePlanning).map(item => {
|
||||
...sourceInventoryItemGroups.value.filter(i=> i.usePlanning).map(item => {
|
||||
return {
|
||||
type: 'Inventargruppen',
|
||||
title: item.name,
|
||||
@@ -234,83 +321,7 @@ const setupPage = async () => {
|
||||
]
|
||||
*/
|
||||
|
||||
let tempEvents = []
|
||||
|
||||
tempData.forEach(event => {
|
||||
console.log(event)
|
||||
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
|
||||
|
||||
let title = ""
|
||||
if (event.name) {
|
||||
title = event.name
|
||||
} else if (event.project) {
|
||||
projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
|
||||
}
|
||||
|
||||
let returnData = {
|
||||
title: title,
|
||||
borderColor: eventColor,
|
||||
textColor: "white",
|
||||
backgroundColor: eventColor,
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
resourceIds: [],
|
||||
entrytype: "event",
|
||||
eventId: event.id
|
||||
}
|
||||
|
||||
|
||||
if(event.profiles.length > 0) {
|
||||
event.profiles.forEach(profile => {
|
||||
returnData.resourceIds.push(`P-${profile}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.vehicles.length > 0) {
|
||||
event.vehicles.forEach(vehicle => {
|
||||
returnData.resourceIds.push(`F-${vehicle}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.inventoryitems.length > 0) {
|
||||
event.inventoryitems.forEach(inventoryitem => {
|
||||
returnData.resourceIds.push(`I-${inventoryitem}`)
|
||||
})
|
||||
}
|
||||
|
||||
if(event.inventoryitemgroups.length > 0) {
|
||||
event.inventoryitemgroups.forEach(inventoryitemgroup => {
|
||||
returnData.resourceIds.push(`G-${inventoryitemgroup}`)
|
||||
})
|
||||
}
|
||||
|
||||
console.log(returnData)
|
||||
|
||||
tempEvents.push(returnData)
|
||||
|
||||
})
|
||||
|
||||
absencerequests.forEach(absencerequest => {
|
||||
let returnData = {
|
||||
title: `${absencerequest.reason}`,
|
||||
backgroundColor: "red",
|
||||
borderColor: "red",
|
||||
start: absencerequest.startDate,
|
||||
end: absencerequest.endDate,
|
||||
resourceIds: [`P-${absencerequest.profile.id}`],
|
||||
entrytype: "absencerequest",
|
||||
allDay: true,
|
||||
absencerequestId: absencerequest.id
|
||||
}
|
||||
|
||||
tempEvents.push(returnData)
|
||||
})
|
||||
|
||||
console.log(tempEvents)
|
||||
|
||||
calendarOptionsTimeline.value.initialEvents = tempEvents
|
||||
|
||||
console.log(calendarOptionsTimeline.value)
|
||||
expandEventsForRange(dayjs().startOf("month").toISOString(), dayjs().endOf("month").toISOString())
|
||||
|
||||
loaded.value = true
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import interactionPlugin from "@fullcalendar/interaction"
|
||||
import resourceTimelinePlugin from "@fullcalendar/resource-timeline"
|
||||
import { parseDate } from "@internationalized/date"
|
||||
import { useDraggable } from "@vueuse/core"
|
||||
import { expandRecurringEvent } from "~/utils/eventRecurrence"
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
@@ -534,7 +535,7 @@ function buildResources({ profiles, inventoryitems }) {
|
||||
function buildEvents({ rawEvents, projectsById }) {
|
||||
const mappedEvents = rawEvents
|
||||
.filter((event) => !event.archived)
|
||||
.map((event) => {
|
||||
.flatMap((event) => {
|
||||
const resourceIds = [
|
||||
...(profiles.value
|
||||
.filter((profile) => (event.profiles || []).includes(profile.id))
|
||||
@@ -542,10 +543,14 @@ function buildEvents({ rawEvents, projectsById }) {
|
||||
...(event.inventoryitems || []).map((itemId) => `I-${itemId}`)
|
||||
]
|
||||
|
||||
return {
|
||||
return expandRecurringEvent(
|
||||
event,
|
||||
`${visibleRange.value.from}T00:00:00`,
|
||||
`${visibleRange.value.to}T23:59:59`,
|
||||
(occurrenceStart, occurrenceEnd, occurrenceIndex) => ({
|
||||
title: resolveDisplayedEventTitle(event, projectsById),
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
start: occurrenceStart.toISOString(),
|
||||
end: occurrenceEnd ? occurrenceEnd.toISOString() : null,
|
||||
resourceIds,
|
||||
color: event.color || null,
|
||||
state: event.state || "Final",
|
||||
@@ -554,8 +559,10 @@ function buildEvents({ rawEvents, projectsById }) {
|
||||
textColor: "#ffffff",
|
||||
classNames: event.state === "Entwurf" ? ["planning-board-draft-event"] : [],
|
||||
entrytype: "event",
|
||||
eventId: event.id
|
||||
}
|
||||
eventId: event.id,
|
||||
occurrenceIndex
|
||||
})
|
||||
)
|
||||
})
|
||||
.filter((event) => event.resourceIds.length > 0)
|
||||
|
||||
|
||||
@@ -3096,6 +3096,13 @@ export const useDataStore = defineStore('data', () => {
|
||||
selectManualOptions: ["Entwurf", "Final"],
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "repeatInterval",
|
||||
label: "Wiederholung",
|
||||
inputType: "select",
|
||||
selectManualOptions: ["Keine Wiederholung", "Täglich", "Wöchentlich", "2-wöchentlich", "Monatlich", "Jährlich"],
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "color",
|
||||
label: "Farbe",
|
||||
|
||||
78
frontend/utils/eventRecurrence.ts
Normal file
78
frontend/utils/eventRecurrence.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import dayjs from "dayjs"
|
||||
|
||||
export const EVENT_REPEAT_INTERVALS = [
|
||||
"Keine Wiederholung",
|
||||
"Täglich",
|
||||
"Wöchentlich",
|
||||
"2-wöchentlich",
|
||||
"Monatlich",
|
||||
"Jährlich"
|
||||
] as const
|
||||
|
||||
const DEFAULT_REPEAT_INTERVAL = "Keine Wiederholung"
|
||||
const MAX_OCCURRENCES = 500
|
||||
|
||||
const addInterval = (date: dayjs.Dayjs, repeatInterval: string) => {
|
||||
switch (repeatInterval) {
|
||||
case "Täglich":
|
||||
return date.add(1, "day")
|
||||
case "Wöchentlich":
|
||||
return date.add(1, "week")
|
||||
case "2-wöchentlich":
|
||||
return date.add(2, "week")
|
||||
case "Monatlich":
|
||||
return date.add(1, "month")
|
||||
case "Jährlich":
|
||||
return date.add(1, "year")
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeRepeatInterval = (value?: string | null) =>
|
||||
EVENT_REPEAT_INTERVALS.includes(value as typeof EVENT_REPEAT_INTERVALS[number])
|
||||
? value as typeof EVENT_REPEAT_INTERVALS[number]
|
||||
: DEFAULT_REPEAT_INTERVAL
|
||||
|
||||
export const expandRecurringEvent = <T>(
|
||||
event: {
|
||||
startDate: string | Date
|
||||
endDate?: string | Date | null
|
||||
repeatInterval?: string | null
|
||||
},
|
||||
rangeStart: string | Date,
|
||||
rangeEnd: string | Date,
|
||||
buildOccurrence: (occurrenceStart: dayjs.Dayjs, occurrenceEnd: dayjs.Dayjs | null, occurrenceIndex: number) => T
|
||||
) => {
|
||||
const repeatInterval = normalizeRepeatInterval(event.repeatInterval)
|
||||
const baseStart = dayjs(event.startDate)
|
||||
const baseEnd = event.endDate ? dayjs(event.endDate) : null
|
||||
const visibleStart = dayjs(rangeStart)
|
||||
const visibleEnd = dayjs(rangeEnd)
|
||||
const durationMs = baseEnd ? Math.max(baseEnd.diff(baseStart), 0) : 0
|
||||
|
||||
if (repeatInterval === DEFAULT_REPEAT_INTERVAL) {
|
||||
const occurrenceEnd = baseEnd ? baseStart.add(durationMs, "millisecond") : null
|
||||
if (baseStart.isAfter(visibleEnd) || (occurrenceEnd && occurrenceEnd.isBefore(visibleStart))) return []
|
||||
return [buildOccurrence(baseStart, occurrenceEnd, 0)]
|
||||
}
|
||||
|
||||
const occurrences: T[] = []
|
||||
let occurrenceStart = baseStart
|
||||
let occurrenceIndex = 0
|
||||
|
||||
while (occurrenceIndex < MAX_OCCURRENCES && occurrenceStart.isBefore(visibleEnd.add(1, "day"))) {
|
||||
const occurrenceEnd = baseEnd ? occurrenceStart.add(durationMs, "millisecond") : null
|
||||
|
||||
if (!occurrenceStart.isAfter(visibleEnd) && !(occurrenceEnd && occurrenceEnd.isBefore(visibleStart))) {
|
||||
occurrences.push(buildOccurrence(occurrenceStart, occurrenceEnd, occurrenceIndex))
|
||||
}
|
||||
|
||||
const nextStart = addInterval(occurrenceStart, repeatInterval)
|
||||
if (!nextStart || nextStart.isSame(occurrenceStart)) break
|
||||
occurrenceStart = nextStart
|
||||
occurrenceIndex += 1
|
||||
}
|
||||
|
||||
return occurrences
|
||||
}
|
||||
Reference in New Issue
Block a user