975 lines
27 KiB
TypeScript
975 lines
27 KiB
TypeScript
import type {
|
|
RPGDate,
|
|
RPGDateOrder,
|
|
} from "@/models/Date"
|
|
import { useUrlSearchParams } from "@vueuse/core"
|
|
import { defineStore } from "pinia"
|
|
import { computed, ref, type ComputedRef } from "vue"
|
|
import type { Calendar } from "~/models/CalendarConfig"
|
|
import type { CalendarEvent } from "~/models/CalendarEvent"
|
|
import type { CalendarMonth } from "~/models/CalendarMonth"
|
|
import type { Category } from "~/models/Category"
|
|
|
|
type CalendarViewType = "month" | "year" | "decade" | "century"
|
|
|
|
type CalendarCurrentConfig = {
|
|
viewType: CalendarViewType
|
|
}
|
|
|
|
type CalendarCurrentDate = {
|
|
currentDay: ComputedRef<number>
|
|
currentMonth: ComputedRef<number>
|
|
currentMonthName: ComputedRef<string>
|
|
currentYear: ComputedRef<number>
|
|
currentDateTitle: ComputedRef<string>
|
|
}
|
|
|
|
type DateDirectionTranslationKeys = {
|
|
days: string
|
|
months: string
|
|
years: string
|
|
yearsAndMonths: string
|
|
}
|
|
|
|
export const useCalendar = defineStore("calendar", () => {
|
|
const { t } = useI18n()
|
|
const user = useSupabaseUser()
|
|
|
|
/**
|
|
* Static calendar config
|
|
*/
|
|
const currentConfig = ref<CalendarCurrentConfig>({
|
|
viewType: "month"
|
|
})
|
|
const viewTypeOptions = new Set<CalendarViewType>([
|
|
"month",
|
|
"year"
|
|
])
|
|
|
|
const activeCalendar = ref<{ id: number; name: string; today: RPGDate, gmId: string | undefined } | null>(null)
|
|
|
|
const isReadOnly = ref<boolean>(true)
|
|
|
|
function setReadStatus(gmId: string) {
|
|
// If the user is not logged in, or the calendar is not owned by the user, it's read-only
|
|
isReadOnly.value = !user.value || gmId !== user.value.id
|
|
}
|
|
|
|
/**
|
|
* Month list (queried from API)
|
|
*/
|
|
const months = ref<CalendarMonth[]>([])
|
|
|
|
function setActiveCalendar(calendarData: Calendar, categoryData: Category[]) {
|
|
try {
|
|
if (!calendarData.id) return
|
|
|
|
activeCalendar.value = {
|
|
id: calendarData.id,
|
|
name: calendarData.name,
|
|
today: calendarData.today,
|
|
gmId: calendarData.world?.gmId
|
|
}
|
|
|
|
setDefaultDate(activeCalendar.value.today)
|
|
selectDate(activeCalendar.value.today)
|
|
setReadStatus(activeCalendar.value.gmId!)
|
|
|
|
if (!params.day) {
|
|
params.day = defaultDate.value.day.toString()
|
|
}
|
|
if (!params.month) {
|
|
params.month = defaultDate.value.month.toString()
|
|
}
|
|
if (!params.year) {
|
|
params.year = defaultDate.value.year.toString()
|
|
}
|
|
|
|
months.value = calendarData.months
|
|
|
|
baseEvents.value = calendarData.events
|
|
categories.value = categoryData
|
|
} catch (err) {
|
|
console.log(err)
|
|
}
|
|
}
|
|
|
|
const params = useUrlSearchParams("history", {
|
|
write: false
|
|
})
|
|
|
|
/**
|
|
* Sorted month data using the raw months
|
|
*/
|
|
const sortedMonths = computed<CalendarMonth[]>(() => months.value.sort((a, b) => a.position - b.position))
|
|
const monthsPerYear = computed<number>(() => months.value.length)
|
|
const daysPerYear = computed<number>(() => months.value.reduce((acc, o) => acc + o.days, 0))
|
|
|
|
// Default date settings (current day in the world)
|
|
// The base setting is the first day / month of year 0
|
|
const defaultDay = ref<number>(1)
|
|
const defaultMonth = ref<number>(0)
|
|
const defaultYear = ref<number>(0)
|
|
|
|
// Object representation
|
|
const defaultDate = computed<RPGDate>(() => {
|
|
return {
|
|
day: defaultDay.value,
|
|
month: defaultMonth.value,
|
|
year: defaultYear.value
|
|
}
|
|
})
|
|
|
|
function setDefaultDate(date: RPGDate): void {
|
|
defaultDay.value = date.day
|
|
defaultMonth.value = date.month
|
|
defaultYear.value = date.year
|
|
}
|
|
|
|
const currentDay = computed<number>(() => {
|
|
return Number(params.day)
|
|
})
|
|
|
|
const currentMonth = computed<number>(() => {
|
|
return Number(params.month)
|
|
})
|
|
const currentMonthData = computed<CalendarMonth>(() => {
|
|
return sortedMonths.value[currentMonth.value]
|
|
})
|
|
// Gets the label from currentMonth index
|
|
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
|
|
|
|
const currentYear = computed<number>(() => {
|
|
return Number(params.year)
|
|
})
|
|
|
|
const currentDateTitle = computed<string>(() => {
|
|
switch (currentConfig.value.viewType) {
|
|
case "month":
|
|
return getFormattedDateTitle({
|
|
day: currentDate.currentDay.value,
|
|
month: currentDate.currentMonth.value,
|
|
year: currentDate.currentYear.value,
|
|
})
|
|
|
|
case "year":
|
|
return `Année ${currentYear.value}`
|
|
|
|
case "decade":
|
|
return `Années ${currentYear.value} - ${currentYear.value + 10}`
|
|
|
|
case "century":
|
|
return `Années ${currentYear.value} - ${currentYear.value + 100}`
|
|
|
|
default:
|
|
return "Date inconnue"
|
|
}
|
|
})
|
|
|
|
// Create base config
|
|
const currentDate: CalendarCurrentDate = {
|
|
currentDay,
|
|
currentMonth,
|
|
currentMonthName,
|
|
currentYear,
|
|
currentDateTitle
|
|
}
|
|
|
|
const currentRPGDate = computed<RPGDate>(() => {
|
|
return {
|
|
day: currentDay.value,
|
|
month: currentMonth.value,
|
|
year: currentYear.value,
|
|
}
|
|
})
|
|
|
|
const selectedDate = ref<RPGDate>({...defaultDate.value})
|
|
|
|
/**
|
|
* Check whether the current viewType is active
|
|
*/
|
|
const isCurrentScreenActive = computed<boolean>(() => {
|
|
switch (currentConfig.value.viewType) {
|
|
case "month":
|
|
return (
|
|
defaultDate.value.month === currentDate.currentMonth.value &&
|
|
defaultDate.value.year === currentDate.currentYear.value
|
|
)
|
|
|
|
case "year":
|
|
case "decade":
|
|
case "century":
|
|
return defaultDate.value.year === currentDate.currentYear.value
|
|
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Moves the current date forward one month
|
|
*/
|
|
function incrementViewMonth(): void {
|
|
let newValue = Number(params.month) + 1
|
|
|
|
// If the new value would exceed the max number of month per year
|
|
if (newValue >= monthsPerYear.value) {
|
|
newValue = 0
|
|
// Increment the year
|
|
incrementViewYear()
|
|
}
|
|
|
|
params.month = newValue.toString()
|
|
}
|
|
|
|
/**
|
|
* Moves the current date backward one month
|
|
*/
|
|
function decrementViewMonth(): void {
|
|
let newValue = Number(params.month) - 1
|
|
|
|
// If the new value would go below 0
|
|
if (newValue < 0) {
|
|
newValue = monthsPerYear.value - 1
|
|
// Decrement the year
|
|
decrementViewYear()
|
|
}
|
|
|
|
params.month = newValue.toString()
|
|
}
|
|
|
|
/**
|
|
* Get the previous month number of the calendar view
|
|
*
|
|
* @param monthNumber Initial month
|
|
* @returns The previous month number in the year
|
|
*/
|
|
function getPreviousViewMonth(monthNumber: number): number {
|
|
const target: number = monthNumber - 1
|
|
|
|
if (target < 0) {
|
|
return monthsPerYear.value - 1
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
/**
|
|
* Get the following month number of the calendar view
|
|
*
|
|
* @param monthNumber Initial month
|
|
* @returns The next month number in the year
|
|
*/
|
|
function getNextViewMonth(monthNumber: number): number {
|
|
const target: number = monthNumber + 1
|
|
|
|
if (target + 1 > monthsPerYear.value) {
|
|
return 0
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
/**
|
|
* Moves the current view to a particular month
|
|
*/
|
|
function setViewMonth(target: number): void {
|
|
// If the target is outside the month bounds
|
|
if (target < 0 || target >= monthsPerYear.value) {
|
|
return
|
|
}
|
|
|
|
params.month = target.toString()
|
|
}
|
|
|
|
/**
|
|
* Moves the current view forward one year
|
|
*/
|
|
function incrementViewYear(inc: number = 1): void {
|
|
const newValue = Number(params.year) + inc
|
|
|
|
params.year = newValue.toString()
|
|
}
|
|
|
|
/**
|
|
* Moves the current view backward one year
|
|
*/
|
|
function decrementViewYear(inc: number = 1): void {
|
|
const newValue = Number(params.year) - inc
|
|
|
|
params.year = newValue.toString()
|
|
}
|
|
|
|
/**
|
|
* Get the formatted month name given its index
|
|
*
|
|
* @param monthNumber Index of the month
|
|
* @returns Formatted month name
|
|
*/
|
|
function getMonthName(monthNumber: number): string {
|
|
const index = Number(monthNumber)
|
|
if (sortedMonths.value[index]) {
|
|
return sortedMonths.value[index].name
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
/**
|
|
* State for advanced search modal
|
|
*/
|
|
const isAdvancedSearchOpen = ref<boolean>(false)
|
|
|
|
function revealAdvancedSearch() {
|
|
isAdvancedSearchOpen.value = true
|
|
}
|
|
|
|
/**
|
|
* Switches the active viewType
|
|
*
|
|
* @param viewType Target viewType
|
|
*/
|
|
function setViewType(viewType: CalendarViewType): void {
|
|
currentConfig.value.viewType = viewType
|
|
}
|
|
|
|
/**
|
|
* FORMATTING
|
|
*/
|
|
/**
|
|
* Gets the formatted viewType title
|
|
*
|
|
* @param viewType Target viewType
|
|
* @returns Formatted mode title
|
|
*/
|
|
function getViewTypeTitle(viewType: CalendarViewType): string {
|
|
switch (viewType) {
|
|
case "year":
|
|
return t("entity.calendar.years.displayMode")
|
|
|
|
case "decade":
|
|
return t("entity.calendar.decades.displayMode")
|
|
|
|
case "century":
|
|
return t("entity.calendar.centuries.displayMode")
|
|
|
|
case "month":
|
|
default:
|
|
return t("entity.calendar.months.displayMode")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* From a RPGDate, returns the legible date title
|
|
*
|
|
* @param date Date target
|
|
* @param showNumber Should the date show the day number
|
|
* @returns Formatted date name
|
|
*/
|
|
function getFormattedDateTitle(date: RPGDate, showNumber?: boolean): string {
|
|
if (showNumber) {
|
|
return `${date.day} ${getMonthName(date.month)} ${date.year}`
|
|
}
|
|
|
|
return `${getMonthName(date.month)} ${date.year}`
|
|
}
|
|
|
|
/**
|
|
* DATE JUMPS & SELECTION
|
|
*/
|
|
/**
|
|
* Jumps the calendar to the given date
|
|
*
|
|
* @param date Target date
|
|
*/
|
|
function jumpToDate(date: RPGDate): void {
|
|
params.day = date.day.toString()
|
|
params.month = date.month.toString()
|
|
params.year = date.year.toString()
|
|
selectDate(date)
|
|
}
|
|
|
|
/**
|
|
* Jump the calendar to the default date
|
|
*/
|
|
function jumpToDefaultDate(): void {
|
|
jumpToDate(defaultDate.value)
|
|
}
|
|
|
|
/**
|
|
* Jump the calendar to the default date
|
|
*/
|
|
function selectDate(date: RPGDate): void {
|
|
selectedDate.value = date
|
|
}
|
|
|
|
/**
|
|
* DATE OPERATIONS
|
|
*
|
|
* Used to convert dates, sort them and compare them
|
|
*/
|
|
/**
|
|
* Converts a RPGDate to its equivalent in days
|
|
*
|
|
* @param dateToConvert The date object
|
|
* @returns How many days does it represent
|
|
*/
|
|
function convertDateToDays(dateToConvert: RPGDate): number {
|
|
let numberOfDays: number = dateToConvert.day
|
|
|
|
// Get only the remaining months on the year
|
|
const remainingMonths = sortedMonths.value.filter((m) => dateToConvert.month >= m.position)
|
|
// From remaining months, reduce their days value
|
|
const monthDaysToAdd = remainingMonths.reduce((a, b) => {
|
|
return a + b.days
|
|
}, 0)
|
|
|
|
numberOfDays = numberOfDays + monthDaysToAdd
|
|
numberOfDays = numberOfDays + dateToConvert.year * daysPerYear.value
|
|
|
|
return numberOfDays - 1
|
|
}
|
|
|
|
/**
|
|
* From two dates, get the difference in days between them
|
|
* @param baseDate The base date
|
|
* @param relativeDate The year to compare it to
|
|
* @returns The number of days separating the two dates (both positive and negative numbers)
|
|
*/
|
|
function getDifferenceInDays(baseDate: RPGDate, relativeDate: RPGDate): number {
|
|
return convertDateToDays(relativeDate) - convertDateToDays(baseDate)
|
|
}
|
|
|
|
/**
|
|
* Check whether two dates are identical
|
|
*
|
|
* @param date1 First date
|
|
* @param date2 Second date
|
|
* @returns True if the dates are identical
|
|
*/
|
|
function areDatesIdentical(date1: RPGDate, date2: RPGDate): boolean {
|
|
return getDifferenceInDays({ ...date1 }, { ...date2 }) === 0
|
|
}
|
|
|
|
/**
|
|
* Compare dates (used for array sorting)
|
|
*
|
|
* @param date1 First date
|
|
* @param date2 Second date
|
|
* @returns 1 means the first date comes before the second, -1 means the second comes before the first, and 0 if they're identical
|
|
*/
|
|
function compareDates(a: RPGDate, b: RPGDate, order: RPGDateOrder = "desc"): number {
|
|
// Reverses the order if specified
|
|
const orderFactor: number = order === "desc" ? 1 : -1
|
|
|
|
// If somehow one of the date isn't available
|
|
if (!b) {
|
|
return -1 * orderFactor
|
|
}
|
|
if (!a) {
|
|
return 1 * orderFactor
|
|
}
|
|
|
|
// Compare years
|
|
if (a.year < b.year) return -1 * orderFactor
|
|
if (a.year > b.year) return 1 * orderFactor
|
|
|
|
// Compare months
|
|
if (a.month < b.month) return -1 * orderFactor
|
|
if (a.month > b.month) return 1 * orderFactor
|
|
|
|
// Compare days
|
|
if (a.day < b.day) return -1 * orderFactor
|
|
if (a.day > b.day) return 1 * orderFactor
|
|
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* From two dates, gives information on how many years, months and days it has been / will be between them
|
|
*
|
|
* @param baseDate The base date
|
|
* @param relativeDate The year to compare it to
|
|
* @returns A string with info on how the relative date differs to the base date
|
|
*/
|
|
function getRelativeString(
|
|
baseDate: RPGDate,
|
|
relativeDate: RPGDate,
|
|
formatting: "compact" | "complex" = "complex"
|
|
): string {
|
|
const differenceInDays: number = getDifferenceInDays(baseDate, relativeDate)
|
|
let output: string = ""
|
|
let direction: "past" | "present" | "future" = "present"
|
|
const futureKeys: DateDirectionTranslationKeys = {
|
|
days: "xDaysNext",
|
|
months: "xMonthsNext",
|
|
years: "xYearsNext",
|
|
yearsAndMonths: "xYearsAndMonthsNext",
|
|
}
|
|
const futureKeysCompact: DateDirectionTranslationKeys = {
|
|
days: "xDaysNextCompact",
|
|
months: "xMonthsNextCompact",
|
|
years: "xYearsNextCompact",
|
|
yearsAndMonths: "xYearsAndMonthsNextCompact",
|
|
}
|
|
const pastKeys: DateDirectionTranslationKeys = {
|
|
days: "xDaysAgo",
|
|
months: "xMonthsAgo",
|
|
years: "xYearsAgo",
|
|
yearsAndMonths: "xYearsAndMonthsAgo",
|
|
}
|
|
const pastKeysCompact: DateDirectionTranslationKeys = {
|
|
days: "xDaysAgoCompact",
|
|
months: "xMonthsAgoCompact",
|
|
years: "xYearsAgoCompact",
|
|
yearsAndMonths: "xYearsAndMonthsAgoCompact",
|
|
}
|
|
let directionKeys: DateDirectionTranslationKeys = pastKeys
|
|
|
|
// Check whether it's a past or future date
|
|
if (differenceInDays > 0) {
|
|
direction = "future"
|
|
} else if (differenceInDays < 0) {
|
|
direction = "past"
|
|
}
|
|
|
|
if (formatting === "complex") {
|
|
// Handle if it's the same date
|
|
if (direction === "present") {
|
|
return t("entity.calendar.date.today")
|
|
}
|
|
if (differenceInDays === -2) {
|
|
return t("entity.calendar.date.beforeYesterday")
|
|
}
|
|
if (differenceInDays === -1) {
|
|
return t("entity.calendar.date.yesterday")
|
|
}
|
|
if (differenceInDays === 1) {
|
|
return t("entity.calendar.date.tomorrow")
|
|
}
|
|
if (differenceInDays === 2) {
|
|
return t("entity.calendar.date.afterTomorrow")
|
|
}
|
|
|
|
// Get relevant prefix for the string
|
|
if (direction === "future") {
|
|
directionKeys = futureKeys
|
|
} else {
|
|
directionKeys = pastKeys
|
|
}
|
|
} else {
|
|
// Get relevant prefix for the string (without the prefix)
|
|
if (direction === "future") {
|
|
directionKeys = futureKeysCompact
|
|
} else {
|
|
directionKeys = pastKeysCompact
|
|
}
|
|
}
|
|
|
|
const isSameMonth = baseDate.month === relativeDate.month
|
|
const isSameYear = baseDate.year === relativeDate.year
|
|
|
|
// At this point, we are beyond simple scenarios and must calculate the distance accurately
|
|
|
|
// Get which date should come first
|
|
let ancientDate: RPGDate
|
|
let futureDate: RPGDate
|
|
|
|
if (direction === "future") {
|
|
ancientDate = baseDate
|
|
futureDate = relativeDate
|
|
} else {
|
|
ancientDate = relativeDate
|
|
futureDate = baseDate
|
|
}
|
|
|
|
// The pivot point that will help proceed towards the relativeDate
|
|
const datePivot: RPGDate = { ...ancientDate }
|
|
// The accumulator that will hold the distance between the two dates
|
|
const dateAcc: RPGDate = { day: 0, month: 0, year: 0 }
|
|
|
|
// If we are on the same month / year during acceleration, just don't rev up
|
|
if (isSameMonth && isSameYear) {
|
|
dateAcc.day = futureDate.day - datePivot.day
|
|
|
|
output = t(`entity.calendar.date.${directionKeys.days}`, { days: dateAcc.day })
|
|
|
|
return output
|
|
}
|
|
|
|
// If we are on the same year during acceleration, just don't rev up the next month, it might overflow to the next year
|
|
else if (isSameYear) {
|
|
dateAcc.month = futureDate.month - datePivot.month
|
|
|
|
output = t(`entity.calendar.date.${directionKeys.months}`, { months: dateAcc.month })
|
|
|
|
return output
|
|
}
|
|
|
|
// Else, we need to accelerate and decelerate
|
|
else {
|
|
// First, get all the remaining days in the current month
|
|
const currentMonth = sortedMonths.value[datePivot.month]
|
|
dateAcc.day = currentMonth.days - datePivot.day
|
|
if (direction === "future") {
|
|
datePivot.month = getNextViewMonth(datePivot.month)
|
|
} else {
|
|
datePivot.month = getPreviousViewMonth(datePivot.month)
|
|
}
|
|
datePivot.day = 1
|
|
|
|
// Then, get the remaining months in the year
|
|
const remainingMonthsStart = sortedMonths.value.filter((m) => datePivot.month < m.position )
|
|
if (direction === "future") {
|
|
dateAcc.month = remainingMonthsStart.length + 1
|
|
} else {
|
|
dateAcc.month = remainingMonthsStart.length - 1
|
|
}
|
|
datePivot.month = 0
|
|
|
|
// Then, Accelerate through years
|
|
dateAcc.month = dateAcc.month + ((futureDate.year - datePivot.year) - 1) * monthsPerYear.value
|
|
datePivot.year = futureDate.year
|
|
|
|
// Once we reached the relativeYear, decelerate and get through the extra months
|
|
const remainingMonthsEnd = futureDate.month
|
|
dateAcc.month = dateAcc.month + remainingMonthsEnd
|
|
datePivot.month = futureDate.month
|
|
|
|
// Then at the destination month, decelerate to get the rest of the days
|
|
dateAcc.day = dateAcc.day + futureDate.day
|
|
datePivot.day = futureDate.day
|
|
|
|
const computedYear = Math.trunc(dateAcc.month / monthsPerYear.value)
|
|
const remainderMonths = dateAcc.month % monthsPerYear.value
|
|
|
|
if (computedYear >= 1 && remainderMonths) {
|
|
output = t(`entity.calendar.date.${directionKeys.yearsAndMonths}`, { years: computedYear, months: remainderMonths })
|
|
}
|
|
else if (computedYear >= 1 && !remainderMonths) {
|
|
output = t(`entity.calendar.date.${directionKeys.years}`, { years: computedYear })
|
|
}
|
|
else {
|
|
output = t(`entity.calendar.date.${directionKeys.months}`, { months: remainderMonths })
|
|
}
|
|
|
|
return output
|
|
}
|
|
}
|
|
|
|
const baseEvents = ref<CalendarEvent[]>([])
|
|
const categories = ref<Category[]>([])
|
|
|
|
// Order base events by dates
|
|
const allEvents = computed<CalendarEvent[]>(() => [...baseEvents.value].sort((a, b) => compareDates(a.startDate, b.startDate, "desc")))
|
|
|
|
// Gets all current event in its default state
|
|
const currentEvents = ref<CalendarEvent[]>([])
|
|
|
|
// Watch for currentDate or events' list changes
|
|
// This is deep because we're watching an array, and changes need to trigger and mutations like .push and .splice
|
|
watch([currentRPGDate, allEvents], () => {
|
|
currentEvents.value = computeCurrentEvents()
|
|
}, { deep: true, immediate: true })
|
|
|
|
/**
|
|
* Determines if the event can appear in the front end
|
|
*
|
|
* This function takes into consideration the viewType of the calendar config
|
|
*
|
|
* @param event The event to analyze
|
|
* @returns Whether the event should appear in the current view
|
|
*/
|
|
function shouldEventBeDisplayed(event: CalendarEvent): boolean {
|
|
const isEventOnCurrentScreen =
|
|
(event.startDate.year === currentRPGDate.value.year &&
|
|
event.startDate.month === currentRPGDate.value.month) ||
|
|
(event.endDate &&
|
|
event.endDate.year === currentRPGDate.value.year &&
|
|
event.endDate.month === currentRPGDate.value.month)
|
|
|
|
switch (currentConfig.value.viewType) {
|
|
case "month":
|
|
return !!isEventOnCurrentScreen
|
|
|
|
case "year":
|
|
return event.startDate.year === currentRPGDate.value.year
|
|
|
|
case "decade":
|
|
return (
|
|
event.startDate.year >= currentRPGDate.value.year &&
|
|
event.startDate.year <= currentRPGDate.value.year + 10
|
|
)
|
|
|
|
case "century":
|
|
return (
|
|
event.startDate.year >= currentRPGDate.value.year &&
|
|
event.startDate.year <= currentRPGDate.value.year + 100
|
|
)
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches all the current events for the current view
|
|
*
|
|
* @returns A list of events that can appear in the current view
|
|
*/
|
|
function computeCurrentEvents(): CalendarEvent[] {
|
|
return [...allEvents.value].filter((event) => shouldEventBeDisplayed(event))
|
|
}
|
|
|
|
/**
|
|
* From a base event, gets the next or previous one in the timeline
|
|
*
|
|
* @param event The event at a given position in the data
|
|
* @param position Whether we should get the next or previous event
|
|
* @returns The next event in chronological order
|
|
*/
|
|
function getRelativeEventFromEvent(
|
|
event: CalendarEvent,
|
|
position: "next" | "prev" = "next",
|
|
initialIsEnd: boolean = false
|
|
): { event: CalendarEvent; targetDate: RPGDate } {
|
|
let dateToParse: RPGDate // Day value of the date that the user interacted with
|
|
|
|
if (initialIsEnd && event.endDate) {
|
|
dateToParse = event.endDate
|
|
} else {
|
|
dateToParse = event.startDate
|
|
}
|
|
|
|
return getRelativeEventFromDate(dateToParse, position)
|
|
}
|
|
|
|
/**
|
|
* From a date, gets the next or previous event in the timeline
|
|
*
|
|
* @param date The starting date from which to get the next event
|
|
* @param position Whether we should get the next or previous event
|
|
* @returns The next event in chronological order
|
|
*/
|
|
function getRelativeEventFromDate(
|
|
date: RPGDate,
|
|
position: "next" | "prev" = "next"
|
|
): { event: CalendarEvent; targetDate: RPGDate } {
|
|
const pivotValue = convertDateToDays(date)
|
|
let t: { eventData: CalendarEvent; distance: number; targetKey: "startDate" | "endDate" }[] = []
|
|
|
|
// Loop over all event once to convert the structure to a usable one
|
|
for (let i = 0; i < allEvents.value.length; i++) {
|
|
const e: CalendarEvent = allEvents.value[i]
|
|
|
|
// Estimate distance from pivot
|
|
const startDateDays: number = convertDateToDays(e.startDate)
|
|
const startDistance: number = startDateDays - pivotValue
|
|
|
|
// Push startDate to comparator array
|
|
t.push({
|
|
eventData: e,
|
|
distance: startDistance,
|
|
targetKey: "startDate"
|
|
})
|
|
|
|
// Check the same things for endDate
|
|
if (e.endDate) {
|
|
const endDateDays: number = convertDateToDays(e.endDate)
|
|
const endDistance: number = endDateDays - pivotValue
|
|
|
|
// Push optional endDate to comparator array
|
|
t.push({
|
|
eventData: e,
|
|
distance: endDistance,
|
|
targetKey: "endDate"
|
|
})
|
|
}
|
|
}
|
|
|
|
// Based on the direction, either ignore negative distance (past) or positive distance (future)
|
|
t = t.filter((i) => {
|
|
return position === "next" ? i.distance > 0 : i.distance < 0
|
|
})
|
|
|
|
if (!t.length) {
|
|
throw new Error(
|
|
"Aucun évènement suivant ou précédent trouvé ; Peut-être l'évènement se situe au début ou à la fin du calendrier ?"
|
|
)
|
|
}
|
|
|
|
// Get event with remaining minimum distance
|
|
const closestEvent = t.reduce((a, b) => {
|
|
return Math.abs(b.distance) < Math.abs(a.distance) ? b : a
|
|
})
|
|
|
|
return {
|
|
event: closestEvent.eventData,
|
|
targetDate: closestEvent.eventData[closestEvent.targetKey]!
|
|
}
|
|
}
|
|
|
|
/**
|
|
* State for event modal edition
|
|
*/
|
|
const isEditEventModalOpen = ref<boolean>(false)
|
|
|
|
function revealEditEventModal() {
|
|
isEditEventModalOpen.value = true
|
|
}
|
|
|
|
/**
|
|
* State for event modal edition
|
|
*/
|
|
const isDeleteEventModalOpen = ref<boolean>(false)
|
|
|
|
function revealDeleteEventModal() {
|
|
isDeleteEventModalOpen.value = true
|
|
}
|
|
|
|
/**
|
|
* EVENT CREATION FUNCTIONS
|
|
*/
|
|
const lastActiveEvent = ref<CalendarEvent | null>()
|
|
const isCreatingEvent = ref<boolean>(false)
|
|
const isUpdatingEvent = ref<boolean>(false)
|
|
const isDeletingEvent = ref<boolean>(false)
|
|
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
|
let abortController: AbortController | null = null
|
|
|
|
/**
|
|
* Dummy event to hold creation data
|
|
*/
|
|
const eventSkeleton = ref<CalendarEvent>({ title: "", startDate: defaultDate.value })
|
|
|
|
/**
|
|
* Resets the dummy event data
|
|
*/
|
|
function resetSkeleton() {
|
|
eventSkeleton.value = { title: "", startDate: defaultDate.value }
|
|
}
|
|
|
|
/**
|
|
* Submits the skeleton event and creates a real event from its data
|
|
*
|
|
* We assume it's been sanitized by the caller
|
|
*/
|
|
async function submitSkeleton() {
|
|
abortController = new AbortController()
|
|
isCreatingEvent.value = true
|
|
|
|
try {
|
|
const res = await $fetch("/api/calendars/events/create", { method: "POST", body: { event : eventSkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
|
|
|
baseEvents.value.push(res)
|
|
} catch (err) {
|
|
console.log(err)
|
|
} finally {
|
|
abortController = null
|
|
isCreatingEvent.value = false
|
|
}
|
|
}
|
|
|
|
async function updateEventFromSkeleton() {
|
|
abortController = new AbortController()
|
|
isUpdatingEvent.value = true
|
|
|
|
try {
|
|
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "PATCH", body: { event : eventSkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
|
|
|
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
|
baseEvents.value[eventIndex] = res
|
|
|
|
return res
|
|
} finally {
|
|
abortController = null
|
|
isUpdatingEvent.value = false
|
|
}
|
|
}
|
|
|
|
async function deleteEventFromSkeleton() {
|
|
abortController = new AbortController()
|
|
isDeletingEvent.value = true
|
|
|
|
try {
|
|
await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "DELETE", signal: abortController.signal })
|
|
|
|
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
|
baseEvents.value.splice(eventIndex, 1)
|
|
} catch (err) {
|
|
console.log(err)
|
|
} finally {
|
|
abortController = null
|
|
isDeletingEvent.value = false
|
|
}
|
|
}
|
|
|
|
function cancelLatestRequest() {
|
|
if (abortController) {
|
|
abortController.abort()
|
|
}
|
|
}
|
|
|
|
return {
|
|
isReadOnly,
|
|
setReadStatus,
|
|
setActiveCalendar,
|
|
activeCalendar,
|
|
months,
|
|
sortedMonths,
|
|
daysPerYear,
|
|
monthsPerYear,
|
|
viewTypeOptions,
|
|
currentConfig,
|
|
currentDate,
|
|
currentRPGDate,
|
|
currentMonthData,
|
|
defaultDate: defaultDate,
|
|
selectedDate: selectedDate,
|
|
selectDate,
|
|
params,
|
|
incrementViewMonth,
|
|
decrementViewMonth,
|
|
setViewMonth,
|
|
getPreviousViewMonth,
|
|
getNextViewMonth,
|
|
incrementViewYear,
|
|
decrementViewYear,
|
|
jumpToDate,
|
|
jumpToDefaultDate,
|
|
getFormattedDateTitle,
|
|
getMonthName,
|
|
setViewType,
|
|
getViewTypeTitle,
|
|
isCurrentScreenActive,
|
|
isAdvancedSearchOpen,
|
|
revealAdvancedSearch,
|
|
convertDateToDays,
|
|
getDifferenceInDays,
|
|
areDatesIdentical,
|
|
compareDates,
|
|
getRelativeString,
|
|
baseEvents,
|
|
allEvents,
|
|
categories,
|
|
currentEvents,
|
|
getRelativeEventFromDate,
|
|
getRelativeEventFromEvent,
|
|
cancelLatestRequest,
|
|
isCreatingEvent,
|
|
isUpdatingEvent,
|
|
isDeletingEvent,
|
|
operationInProgress,
|
|
eventSkeleton,
|
|
resetSkeleton,
|
|
submitSkeleton,
|
|
lastActiveEvent,
|
|
updateEventFromSkeleton,
|
|
deleteEventFromSkeleton,
|
|
isEditEventModalOpen,
|
|
revealEditEventModal,
|
|
isDeleteEventModalOpen,
|
|
revealDeleteEventModal
|
|
}
|
|
})
|