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,
|
"when": 1779158400000,
|
||||||
"tag": "0038_events_state",
|
"tag": "0038_events_state",
|
||||||
"breakpoints": true
|
"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),
|
quick: boolean("quick").notNull().default(false),
|
||||||
state: text("state").notNull().default("Final"),
|
state: text("state").notNull().default("Final"),
|
||||||
color: text("color"),
|
color: text("color"),
|
||||||
|
repeatInterval: text("repeatInterval").notNull().default("Keine Wiederholung"),
|
||||||
|
|
||||||
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists
|
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 resourceTimelinePlugin from "@fullcalendar/resource-timeline";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { expandRecurringEvent } from "~/utils/eventRecurrence"
|
||||||
|
|
||||||
//TODO BACKEND CHANGE COLOR IN TENANT FOR RENDERING
|
//TODO BACKEND CHANGE COLOR IN TENANT FOR RENDERING
|
||||||
|
|
||||||
@@ -33,6 +34,124 @@ const selectedEvent = ref({})
|
|||||||
const selectedResources = ref([])
|
const selectedResources = ref([])
|
||||||
|
|
||||||
const events = 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(() => {
|
const calendarOptionsGrid = computed(() => {
|
||||||
return {
|
return {
|
||||||
locale: deLocale,
|
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}`)
|
router.push(`/standardEntity/events/show/${info.event.extendedProps.eventId}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
datesSet: function(info) {
|
||||||
|
expandEventsForRange(info.startStr, info.endStr)
|
||||||
|
},
|
||||||
resourceGroupField: "type",
|
resourceGroupField: "type",
|
||||||
resourceOrder: "-type",
|
resourceOrder: "-type",
|
||||||
resources: [],
|
resources: [],
|
||||||
@@ -127,75 +252,37 @@ const calendarOptionsTimeline = ref({
|
|||||||
|
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
let tempData = (await useEntities("events").select()).filter(i => !i.archived)
|
sourceEvents.value = (await useEntities("events").select()).filter(i => !i.archived)
|
||||||
let absencerequests = (await useEntities("absencerequests").select("*, profile(*)")).filter(i => !i.archived)
|
sourceAbsenceRequests.value = (await useEntities("absencerequests").select("*, profile(*)")).filter(i => !i.archived)
|
||||||
let projects = (await useEntities("projects").select( "*")).filter(i => !i.archived)
|
sourceProjects.value = (await useEntities("projects").select( "*")).filter(i => !i.archived)
|
||||||
let inventoryitems = (await useEntities("inventoryitems").select()).filter(i => !i.archived)
|
sourceInventoryItems.value = (await useEntities("inventoryitems").select()).filter(i => !i.archived)
|
||||||
let inventoryitemgroups = (await useEntities("inventoryitemgroups").select()).filter(i => !i.archived)
|
sourceInventoryItemGroups.value = (await useEntities("inventoryitemgroups").select()).filter(i => !i.archived)
|
||||||
let profiles = (await useEntities("profiles").select()).filter(i => !i.archived)
|
sourceProfiles.value = (await useEntities("profiles").select()).filter(i => !i.archived)
|
||||||
let vehicles = (await useEntities("vehicles").select()).filter(i => !i.archived)
|
sourceVehicles.value = (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",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
calendarOptionsTimeline.value.resources = [
|
calendarOptionsTimeline.value.resources = [
|
||||||
...profiles.filter(i => i.tenant === profileStore.currentTenant).map(profile => {
|
...sourceProfiles.value.filter(i => i.tenant === profileStore.currentTenant).map(profile => {
|
||||||
return {
|
return {
|
||||||
type: 'Mitarbeiter',
|
type: 'Mitarbeiter',
|
||||||
title: profile.fullName,
|
title: profile.fullName,
|
||||||
id: `P-${profile.id}`
|
id: `P-${profile.id}`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...vehicles.map(vehicle => {
|
...sourceVehicles.value.map(vehicle => {
|
||||||
return {
|
return {
|
||||||
type: 'Fahrzeug',
|
type: 'Fahrzeug',
|
||||||
title: vehicle.licensePlate,
|
title: vehicle.licensePlate,
|
||||||
id: `F-${vehicle.id}`
|
id: `F-${vehicle.id}`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...inventoryitems.filter(i=> i.usePlanning).map(item => {
|
...sourceInventoryItems.value.filter(i=> i.usePlanning).map(item => {
|
||||||
return {
|
return {
|
||||||
type: 'Inventar',
|
type: 'Inventar',
|
||||||
title: item.name,
|
title: item.name,
|
||||||
id: `I-${item.id}`
|
id: `I-${item.id}`
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...inventoryitemgroups.filter(i=> i.usePlanning).map(item => {
|
...sourceInventoryItemGroups.value.filter(i=> i.usePlanning).map(item => {
|
||||||
return {
|
return {
|
||||||
type: 'Inventargruppen',
|
type: 'Inventargruppen',
|
||||||
title: item.name,
|
title: item.name,
|
||||||
@@ -234,83 +321,7 @@ const setupPage = async () => {
|
|||||||
]
|
]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let tempEvents = []
|
expandEventsForRange(dayjs().startOf("month").toISOString(), dayjs().endOf("month").toISOString())
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
|
|
||||||
@@ -385,4 +396,4 @@ const convertResourceIds = () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import interactionPlugin from "@fullcalendar/interaction"
|
|||||||
import resourceTimelinePlugin from "@fullcalendar/resource-timeline"
|
import resourceTimelinePlugin from "@fullcalendar/resource-timeline"
|
||||||
import { parseDate } from "@internationalized/date"
|
import { parseDate } from "@internationalized/date"
|
||||||
import { useDraggable } from "@vueuse/core"
|
import { useDraggable } from "@vueuse/core"
|
||||||
|
import { expandRecurringEvent } from "~/utils/eventRecurrence"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -534,7 +535,7 @@ function buildResources({ profiles, inventoryitems }) {
|
|||||||
function buildEvents({ rawEvents, projectsById }) {
|
function buildEvents({ rawEvents, projectsById }) {
|
||||||
const mappedEvents = rawEvents
|
const mappedEvents = rawEvents
|
||||||
.filter((event) => !event.archived)
|
.filter((event) => !event.archived)
|
||||||
.map((event) => {
|
.flatMap((event) => {
|
||||||
const resourceIds = [
|
const resourceIds = [
|
||||||
...(profiles.value
|
...(profiles.value
|
||||||
.filter((profile) => (event.profiles || []).includes(profile.id))
|
.filter((profile) => (event.profiles || []).includes(profile.id))
|
||||||
@@ -542,20 +543,26 @@ function buildEvents({ rawEvents, projectsById }) {
|
|||||||
...(event.inventoryitems || []).map((itemId) => `I-${itemId}`)
|
...(event.inventoryitems || []).map((itemId) => `I-${itemId}`)
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return expandRecurringEvent(
|
||||||
title: resolveDisplayedEventTitle(event, projectsById),
|
event,
|
||||||
start: event.startDate,
|
`${visibleRange.value.from}T00:00:00`,
|
||||||
end: event.endDate,
|
`${visibleRange.value.to}T23:59:59`,
|
||||||
resourceIds,
|
(occurrenceStart, occurrenceEnd, occurrenceIndex) => ({
|
||||||
color: event.color || null,
|
title: resolveDisplayedEventTitle(event, projectsById),
|
||||||
state: event.state || "Final",
|
start: occurrenceStart.toISOString(),
|
||||||
backgroundColor: resolveRenderedEventColor(event),
|
end: occurrenceEnd ? occurrenceEnd.toISOString() : null,
|
||||||
borderColor: resolveRenderedEventColor(event),
|
resourceIds,
|
||||||
textColor: "#ffffff",
|
color: event.color || null,
|
||||||
classNames: event.state === "Entwurf" ? ["planning-board-draft-event"] : [],
|
state: event.state || "Final",
|
||||||
entrytype: "event",
|
backgroundColor: resolveRenderedEventColor(event),
|
||||||
eventId: event.id
|
borderColor: resolveRenderedEventColor(event),
|
||||||
}
|
textColor: "#ffffff",
|
||||||
|
classNames: event.state === "Entwurf" ? ["planning-board-draft-event"] : [],
|
||||||
|
entrytype: "event",
|
||||||
|
eventId: event.id,
|
||||||
|
occurrenceIndex
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.filter((event) => event.resourceIds.length > 0)
|
.filter((event) => event.resourceIds.length > 0)
|
||||||
|
|
||||||
|
|||||||
@@ -3096,6 +3096,13 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
selectManualOptions: ["Entwurf", "Final"],
|
selectManualOptions: ["Entwurf", "Final"],
|
||||||
sortable: true
|
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",
|
key: "color",
|
||||||
label: "Farbe",
|
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