mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
539 lines
23 KiB
TypeScript
539 lines
23 KiB
TypeScript
import { manualSchoolRegionId } from '@trek/shared'
|
|
import { create } from 'zustand'
|
|
import apiClient from '../api/client'
|
|
import { useAuthStore } from './authStore'
|
|
import type { AxiosResponse } from 'axios'
|
|
import type {
|
|
VacayPlan, VacayUser, VacayEntry, VacayStat, HolidaysMap, HolidayInfo, VacayHolidayCalendar,
|
|
VacayShareOutgoing, VacayShareIncoming, SharedVacayCalendar, VacayYearSettings,
|
|
} from '../types'
|
|
import { isSchoolHolidayCountrySupported } from '../vacay/schoolHolidayCountries'
|
|
import { DEFAULT_YEAR_SETTINGS, defaultPeriodYear, inGridWindow, windowCalendarYears } from '../vacay/yearWindow'
|
|
import type {
|
|
VacaySetColorRequest, VacayInviteRequest, VacayInviteActionRequest,
|
|
VacayAddYearRequest, VacayToggleEntryRequest, VacayCompanyHolidayRequest,
|
|
VacayUpdateStatsRequest, VacayShareRequest, VacayShareUpdateRequest,
|
|
VacayYearSettingsRequest,
|
|
} from '@trek/shared'
|
|
|
|
const ax = apiClient
|
|
|
|
interface PendingInvite {
|
|
user_id: number
|
|
username: string
|
|
}
|
|
|
|
interface IncomingInvite {
|
|
plan_id: number
|
|
owner_username: string
|
|
}
|
|
|
|
interface VacayPlanResponse {
|
|
plan: VacayPlan
|
|
users: VacayUser[]
|
|
pendingInvites: PendingInvite[]
|
|
incomingInvites: IncomingInvite[]
|
|
isOwner: boolean
|
|
isFused: boolean
|
|
}
|
|
|
|
interface VacayYearsResponse {
|
|
years: number[]
|
|
}
|
|
|
|
interface VacayEntriesResponse {
|
|
entries: VacayEntry[]
|
|
companyHolidays: { date: string; note?: string }[]
|
|
}
|
|
|
|
interface VacayStatsResponse {
|
|
stats: VacayStat[]
|
|
}
|
|
|
|
interface VacayHolidayRaw {
|
|
date: string
|
|
name: string
|
|
localName: string
|
|
global: boolean
|
|
counties: string[] | null
|
|
}
|
|
|
|
interface VacaySchoolHolidayRaw {
|
|
startDate?: string
|
|
endDate?: string
|
|
name?: string | { language?: string; text?: string }[]
|
|
}
|
|
|
|
interface VacayApi {
|
|
getPlan: () => Promise<VacayPlanResponse>
|
|
updatePlan: (data: Partial<VacayPlan>) => Promise<{ plan: VacayPlan }>
|
|
updateColor: (color: string, targetUserId?: number) => Promise<unknown>
|
|
invite: (userId: number) => Promise<unknown>
|
|
acceptInvite: (planId: number) => Promise<unknown>
|
|
declineInvite: (planId: number) => Promise<unknown>
|
|
cancelInvite: (userId: number) => Promise<unknown>
|
|
dissolve: () => Promise<unknown>
|
|
getYears: () => Promise<VacayYearsResponse>
|
|
addYear: (year: number) => Promise<VacayYearsResponse>
|
|
removeYear: (year: number) => Promise<VacayYearsResponse>
|
|
getEntries: (year: number) => Promise<VacayEntriesResponse>
|
|
toggleEntry: (date: string, targetUserId?: number, fraction?: 0.5 | 1, kind?: 'vacation' | 'comp') => Promise<unknown>
|
|
toggleCompanyHoliday: (date: string) => Promise<unknown>
|
|
getStats: (year: number) => Promise<VacayStatsResponse>
|
|
updateStats: (year: number, days: number, targetUserId?: number) => Promise<unknown>
|
|
getHolidays: (year: number, country: string) => Promise<VacayHolidayRaw[]>
|
|
getSchoolHolidays: (year: number, country: string, subdivision?: string | null, group?: string | null) => Promise<VacaySchoolHolidayRaw[]>
|
|
addHolidayCalendar: (data: { region: string; color?: string; label?: string | null; type?: 'public_holiday' | 'school_holiday' }) => Promise<{ calendar: VacayHolidayCalendar }>
|
|
updateHolidayCalendar: (id: number, data: { region?: string; color?: string; label?: string | null; type?: 'public_holiday' | 'school_holiday' }) => Promise<{ calendar: VacayHolidayCalendar }>
|
|
deleteHolidayCalendar: (id: number) => Promise<unknown>
|
|
getShares: () => Promise<{ outgoing: VacayShareOutgoing[]; incoming: VacayShareIncoming[] }>
|
|
share: (userId: number) => Promise<unknown>
|
|
removeShare: (shareId: number) => Promise<unknown>
|
|
updateShare: (shareId: number, hidden: boolean) => Promise<unknown>
|
|
getSharedCalendars: (year: number) => Promise<{ calendars: SharedVacayCalendar[] }>
|
|
getYearSettings: () => Promise<{ settings: VacayYearSettings }>
|
|
updateYearSettings: (data: VacayYearSettingsRequest) => Promise<{ settings: VacayYearSettings }>
|
|
}
|
|
|
|
const api: VacayApi = {
|
|
getPlan: () => ax.get('/addons/vacay/plan').then((r: AxiosResponse) => r.data),
|
|
updatePlan: (data) => ax.put('/addons/vacay/plan', data).then((r: AxiosResponse) => r.data),
|
|
updateColor: (color, targetUserId) => ax.put('/addons/vacay/color', { color, target_user_id: targetUserId } satisfies VacaySetColorRequest).then((r: AxiosResponse) => r.data),
|
|
invite: (userId) => ax.post('/addons/vacay/invite', { user_id: userId } satisfies VacayInviteRequest).then((r: AxiosResponse) => r.data),
|
|
acceptInvite: (planId) => ax.post('/addons/vacay/invite/accept', { plan_id: planId } satisfies VacayInviteActionRequest).then((r: AxiosResponse) => r.data),
|
|
declineInvite: (planId) => ax.post('/addons/vacay/invite/decline', { plan_id: planId } satisfies VacayInviteActionRequest).then((r: AxiosResponse) => r.data),
|
|
cancelInvite: (userId) => ax.post('/addons/vacay/invite/cancel', { user_id: userId }).then((r: AxiosResponse) => r.data),
|
|
dissolve: () => ax.post('/addons/vacay/dissolve').then((r: AxiosResponse) => r.data),
|
|
getYears: () => ax.get('/addons/vacay/years').then((r: AxiosResponse) => r.data),
|
|
addYear: (year) => ax.post('/addons/vacay/years', { year } satisfies VacayAddYearRequest).then((r: AxiosResponse) => r.data),
|
|
removeYear: (year) => ax.delete(`/addons/vacay/years/${year}`).then((r: AxiosResponse) => r.data),
|
|
getEntries: (year) => ax.get(`/addons/vacay/entries/${year}`).then((r: AxiosResponse) => r.data),
|
|
toggleEntry: (date, targetUserId, fraction, kind) => ax.post('/addons/vacay/entries/toggle', { date, target_user_id: targetUserId, fraction, kind } satisfies VacayToggleEntryRequest).then((r: AxiosResponse) => r.data),
|
|
toggleCompanyHoliday: (date) => ax.post('/addons/vacay/entries/company-holiday', { date } satisfies VacayCompanyHolidayRequest).then((r: AxiosResponse) => r.data),
|
|
getStats: (year) => ax.get(`/addons/vacay/stats/${year}`).then((r: AxiosResponse) => r.data),
|
|
updateStats: (year, days, targetUserId) => ax.put(`/addons/vacay/stats/${year}`, { vacation_days: days, target_user_id: targetUserId } satisfies VacayUpdateStatsRequest).then((r: AxiosResponse) => r.data),
|
|
getHolidays: (year, country) => ax.get(`/addons/vacay/holidays/${year}/${country}`).then((r: AxiosResponse) => r.data),
|
|
getSchoolHolidays: (year, country, subdivision, group) => {
|
|
const manualId = subdivision ? manualSchoolRegionId(subdivision) : null
|
|
if (manualId) return ax.get(`/school-holiday-catalog/regions/${manualId}/holidays/${year}`).then((r: AxiosResponse) => r.data)
|
|
const params = new URLSearchParams()
|
|
if (group) params.set('group', group)
|
|
const qs = params.toString()
|
|
return ax.get(`/addons/vacay/school-holidays/${year}/${country}${subdivision ? `/${subdivision}` : ''}${qs ? `?${qs}` : ''}`).then((r: AxiosResponse) => r.data)
|
|
},
|
|
addHolidayCalendar: (data) => ax.post('/addons/vacay/plan/holiday-calendars', data).then((r: AxiosResponse) => r.data),
|
|
updateHolidayCalendar: (id, data) => ax.put(`/addons/vacay/plan/holiday-calendars/${id}`, data).then((r: AxiosResponse) => r.data),
|
|
deleteHolidayCalendar: (id) => ax.delete(`/addons/vacay/plan/holiday-calendars/${id}`).then((r: AxiosResponse) => r.data),
|
|
getShares: () => ax.get('/addons/vacay/shares').then((r: AxiosResponse) => r.data),
|
|
share: (userId) => ax.post('/addons/vacay/shares', { user_id: userId } satisfies VacayShareRequest).then((r: AxiosResponse) => r.data),
|
|
removeShare: (shareId) => ax.delete(`/addons/vacay/shares/${shareId}`).then((r: AxiosResponse) => r.data),
|
|
updateShare: (shareId, hidden) => ax.put(`/addons/vacay/shares/${shareId}`, { hidden } satisfies VacayShareUpdateRequest).then((r: AxiosResponse) => r.data),
|
|
getSharedCalendars: (year) => ax.get(`/addons/vacay/shares/calendars/${year}`).then((r: AxiosResponse) => r.data),
|
|
getYearSettings: () => ax.get('/addons/vacay/year-settings').then((r: AxiosResponse) => r.data),
|
|
updateYearSettings: (data) => ax.put('/addons/vacay/year-settings', data satisfies VacayYearSettingsRequest).then((r: AxiosResponse) => r.data),
|
|
}
|
|
|
|
function pushHolidayMarker(map: HolidaysMap, date: string, marker: HolidayInfo) {
|
|
const existing = map[date]
|
|
if (!existing) {
|
|
map[date] = [marker]
|
|
return
|
|
}
|
|
const markers = Array.isArray(existing) ? existing : [existing]
|
|
// A shifted leave year (#737) loads two calendar years, and a school break that
|
|
// straddles New Year comes back in both — keep one marker per day. The label is
|
|
// part of the identity: two calendars left on the same colour are still two
|
|
// calendars, and collapsing them would hide one from the tooltip.
|
|
if (markers.some(m => m.name === marker.name && m.color === marker.color && m.type === marker.type && m.label === marker.label)) return
|
|
map[date] = [...markers, marker]
|
|
}
|
|
|
|
function parseCalendarRegion(region: string): { country: string; subdivision: string | null; group: string | null } {
|
|
const [base, meta] = region.split('|')
|
|
const group = meta?.startsWith('group:') ? meta.slice('group:'.length) : null
|
|
return {
|
|
country: base.split('-')[0],
|
|
subdivision: group ? null : base.includes('-') ? base : null,
|
|
group,
|
|
}
|
|
}
|
|
|
|
function schoolHolidayName(h: VacaySchoolHolidayRaw): string {
|
|
if (typeof h.name === 'string') return h.name
|
|
const names = Array.isArray(h.name) ? h.name : []
|
|
return names.find(n => n.language?.toUpperCase() === 'DE')?.text || names[0]?.text || 'School holidays'
|
|
}
|
|
|
|
function datesBetween(startStr: string, endStr: string): string[] {
|
|
const dates: string[] = []
|
|
const start = new Date(startStr + 'T00:00:00')
|
|
const end = new Date(endStr + 'T00:00:00')
|
|
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return dates
|
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
dates.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`)
|
|
}
|
|
return dates
|
|
}
|
|
|
|
interface VacayState {
|
|
plan: VacayPlan | null
|
|
users: VacayUser[]
|
|
pendingInvites: PendingInvite[]
|
|
incomingInvites: IncomingInvite[]
|
|
isOwner: boolean
|
|
isFused: boolean
|
|
years: number[]
|
|
entries: VacayEntry[]
|
|
companyHolidays: { date: string; note?: string }[]
|
|
stats: VacayStat[]
|
|
selectedYear: number
|
|
selectedUserId: number | null
|
|
holidays: HolidaysMap
|
|
loading: boolean
|
|
outgoingShares: VacayShareOutgoing[]
|
|
incomingShares: VacayShareIncoming[]
|
|
sharedCalendars: SharedVacayCalendar[]
|
|
// The viewer's leave-year configuration (#737). Everything the grid renders and
|
|
// filters runs through this, so it loads before the first year is picked.
|
|
yearSettings: VacayYearSettings
|
|
|
|
setSelectedYear: (year: number) => void
|
|
setSelectedUserId: (id: number | null) => void
|
|
loadPlan: () => Promise<void>
|
|
updatePlan: (updates: Partial<VacayPlan>) => Promise<void>
|
|
updateColor: (color: string, targetUserId?: number) => Promise<void>
|
|
invite: (userId: number) => Promise<void>
|
|
acceptInvite: (planId: number) => Promise<void>
|
|
declineInvite: (planId: number) => Promise<void>
|
|
cancelInvite: (userId: number) => Promise<void>
|
|
dissolve: () => Promise<void>
|
|
loadYears: () => Promise<void>
|
|
addYear: (year: number) => Promise<void>
|
|
removeYear: (year: number) => Promise<void>
|
|
loadEntries: (year?: number) => Promise<void>
|
|
toggleEntry: (date: string, targetUserId?: number, fraction?: 0.5 | 1, kind?: 'vacation' | 'comp') => Promise<void>
|
|
toggleCompanyHoliday: (date: string) => Promise<void>
|
|
loadStats: (year?: number) => Promise<void>
|
|
updateVacationDays: (year: number, days: number, targetUserId?: number) => Promise<void>
|
|
loadHolidays: (year?: number) => Promise<void>
|
|
addHolidayCalendar: (data: { region: string; color?: string; label?: string | null; type?: 'public_holiday' | 'school_holiday' }) => Promise<void>
|
|
updateHolidayCalendar: (id: number, data: { region?: string; color?: string; label?: string | null; type?: 'public_holiday' | 'school_holiday' }) => Promise<void>
|
|
deleteHolidayCalendar: (id: number) => Promise<void>
|
|
loadShares: () => Promise<void>
|
|
loadSharedCalendars: (year?: number) => Promise<void>
|
|
shareWith: (userId: number) => Promise<void>
|
|
removeShare: (shareId: number) => Promise<void>
|
|
setShareHidden: (shareId: number, hidden: boolean) => Promise<void>
|
|
loadYearSettings: () => Promise<void>
|
|
updateYearSettings: (data: VacayYearSettingsRequest) => Promise<void>
|
|
loadAll: () => Promise<void>
|
|
}
|
|
|
|
export const useVacayStore = create<VacayState>((set, get) => ({
|
|
plan: null,
|
|
users: [],
|
|
pendingInvites: [],
|
|
incomingInvites: [],
|
|
isOwner: true,
|
|
isFused: false,
|
|
years: [],
|
|
entries: [],
|
|
companyHolidays: [],
|
|
stats: [],
|
|
selectedYear: new Date().getFullYear(),
|
|
selectedUserId: null,
|
|
holidays: {},
|
|
loading: false,
|
|
outgoingShares: [],
|
|
incomingShares: [],
|
|
sharedCalendars: [],
|
|
yearSettings: DEFAULT_YEAR_SETTINGS,
|
|
|
|
setSelectedYear: (year: number) => set({ selectedYear: year }),
|
|
setSelectedUserId: (id: number | null) => set({ selectedUserId: id }),
|
|
|
|
loadPlan: async () => {
|
|
const data = await api.getPlan()
|
|
set({
|
|
plan: data.plan,
|
|
users: data.users,
|
|
pendingInvites: data.pendingInvites,
|
|
incomingInvites: data.incomingInvites,
|
|
isOwner: data.isOwner,
|
|
isFused: data.isFused,
|
|
})
|
|
},
|
|
|
|
updatePlan: async (updates: Partial<VacayPlan>) => {
|
|
const data = await api.updatePlan(updates)
|
|
set({ plan: data.plan })
|
|
await get().loadEntries()
|
|
await get().loadStats()
|
|
await get().loadHolidays()
|
|
},
|
|
|
|
updateColor: async (color: string, targetUserId?: number) => {
|
|
await api.updateColor(color, targetUserId)
|
|
await get().loadPlan()
|
|
await get().loadEntries()
|
|
},
|
|
|
|
invite: async (userId: number) => {
|
|
await api.invite(userId)
|
|
await get().loadPlan()
|
|
},
|
|
|
|
acceptInvite: async (planId: number) => {
|
|
await api.acceptInvite(planId)
|
|
await get().loadAll()
|
|
},
|
|
|
|
declineInvite: async (planId: number) => {
|
|
await api.declineInvite(planId)
|
|
await get().loadPlan()
|
|
},
|
|
|
|
cancelInvite: async (userId: number) => {
|
|
await api.cancelInvite(userId)
|
|
await get().loadPlan()
|
|
},
|
|
|
|
dissolve: async () => {
|
|
await api.dissolve()
|
|
await get().loadAll()
|
|
},
|
|
|
|
loadYears: async () => {
|
|
const data = await api.getYears()
|
|
// Opening Vacay lands on the period we are in, not on whichever year sorts
|
|
// last. A reload (invite, live settings update) keeps the year the viewer is
|
|
// looking at as long as it still exists — only the first load picks one.
|
|
const previous = get().selectedYear
|
|
const keepSelection = get().years.length > 0 && data.years.includes(previous)
|
|
set({
|
|
years: data.years,
|
|
selectedYear: keepSelection ? previous : defaultPeriodYear(data.years, get().yearSettings),
|
|
})
|
|
},
|
|
|
|
addYear: async (year: number) => {
|
|
const data = await api.addYear(year)
|
|
set({ years: data.years })
|
|
await get().loadStats(year)
|
|
},
|
|
|
|
removeYear: async (year: number) => {
|
|
const data = await api.removeYear(year)
|
|
const updates: Partial<VacayState> = { years: data.years }
|
|
if (get().selectedYear === year) {
|
|
updates.selectedYear = defaultPeriodYear(data.years, get().yearSettings)
|
|
}
|
|
set(updates)
|
|
await get().loadStats()
|
|
},
|
|
|
|
loadEntries: async (year?: number) => {
|
|
const y = year || get().selectedYear
|
|
const data = await api.getEntries(y)
|
|
set({ entries: data.entries, companyHolidays: data.companyHolidays })
|
|
},
|
|
|
|
toggleEntry: async (date: string, targetUserId?: number, fraction: 0.5 | 1 = 1, kind: 'vacation' | 'comp' = 'vacation') => {
|
|
// Optimistic: mirror the server's toggle locally so the cell reacts instantly,
|
|
// then reconcile (stats are server-computed). Same type AND fraction clears the
|
|
// day, a different type or fraction converts it (#552/#1074); an empty day gets
|
|
// the new entry.
|
|
const userId = targetUserId ?? useAuthStore.getState().user?.id
|
|
const prevEntries = get().entries
|
|
const prevStats = get().stats
|
|
if (userId != null) {
|
|
const existing = prevEntries.find(e => e.date === date && e.user_id === userId)
|
|
if (existing && (existing.fraction ?? 1) === fraction && (existing.kind ?? 'vacation') === kind) {
|
|
set({ entries: prevEntries.filter(e => !(e.date === date && e.user_id === userId)) })
|
|
} else if (existing) {
|
|
set({ entries: prevEntries.map(e => (e.date === date && e.user_id === userId ? { ...e, fraction, kind } : e)) })
|
|
} else {
|
|
const u = get().users.find(x => x.id === userId)
|
|
set({ entries: [...prevEntries, { date, user_id: userId, fraction, kind, person_color: u?.color || undefined, person_name: u?.username }] })
|
|
}
|
|
}
|
|
try {
|
|
await api.toggleEntry(date, targetUserId, fraction, kind)
|
|
await get().loadEntries()
|
|
await get().loadStats()
|
|
} catch (e) {
|
|
set({ entries: prevEntries, stats: prevStats })
|
|
throw e
|
|
}
|
|
},
|
|
|
|
toggleCompanyHoliday: async (date: string) => {
|
|
// Optimistic like toggleEntry; a company holiday also overrides personal
|
|
// entries on that day, so refetch entries afterwards to reconcile.
|
|
const prevEntries = get().entries
|
|
const prevCompany = get().companyHolidays
|
|
const prevStats = get().stats
|
|
const has = prevCompany.some(h => h.date === date)
|
|
set({ companyHolidays: has ? prevCompany.filter(h => h.date !== date) : [...prevCompany, { date }] })
|
|
try {
|
|
await api.toggleCompanyHoliday(date)
|
|
await get().loadEntries()
|
|
await get().loadStats()
|
|
} catch (e) {
|
|
set({ entries: prevEntries, companyHolidays: prevCompany, stats: prevStats })
|
|
throw e
|
|
}
|
|
},
|
|
|
|
loadStats: async (year?: number) => {
|
|
const y = year || get().selectedYear
|
|
const data = await api.getStats(y)
|
|
set({ stats: data.stats })
|
|
},
|
|
|
|
updateVacationDays: async (year: number, days: number, targetUserId?: number) => {
|
|
await api.updateStats(year, days, targetUserId)
|
|
await get().loadStats(year)
|
|
},
|
|
|
|
loadHolidays: async (year?: number) => {
|
|
const y = year || get().selectedYear
|
|
const plan = get().plan
|
|
const calendars = plan?.holiday_calendars ?? []
|
|
const enabledCalendars = calendars.filter(cal =>
|
|
(cal.type === 'school_holiday' && plan?.school_holidays_enabled) ||
|
|
((cal.type ?? 'public_holiday') === 'public_holiday' && plan?.holidays_enabled)
|
|
)
|
|
if (enabledCalendars.length === 0) {
|
|
set({ holidays: {} })
|
|
return
|
|
}
|
|
const map: HolidaysMap = {}
|
|
const settings = get().yearSettings
|
|
// A shifted leave year (#737) spans two calendar years and the holiday APIs are
|
|
// per calendar year, so each one the window touches is fetched and the result
|
|
// clipped back to what the grid actually renders.
|
|
const calendarYears = windowCalendarYears(y, settings)
|
|
for (const cal of enabledCalendars) {
|
|
const { country, subdivision, group } = parseCalendarRegion(cal.region)
|
|
for (const cy of calendarYears) {
|
|
try {
|
|
if ((cal.type ?? 'public_holiday') === 'school_holiday') {
|
|
if (!isSchoolHolidayCountrySupported(country) && !manualSchoolRegionId(cal.region)) continue
|
|
const data = await api.getSchoolHolidays(cy, country, subdivision, group)
|
|
data.forEach((h: VacaySchoolHolidayRaw) => {
|
|
if (!h.startDate) return
|
|
const name = schoolHolidayName(h)
|
|
datesBetween(h.startDate, h.endDate || h.startDate).filter(date => inGridWindow(date, y, settings)).forEach(date => {
|
|
pushHolidayMarker(map, date, { name, localName: name, color: cal.color, label: cal.label, type: 'school_holiday' })
|
|
})
|
|
})
|
|
} else {
|
|
const data = await api.getHolidays(cy, country)
|
|
const hasRegions = data.some((h: VacayHolidayRaw) => h.counties && h.counties.length > 0)
|
|
if (hasRegions && !subdivision) continue
|
|
data.forEach((h: VacayHolidayRaw) => {
|
|
if (!inGridWindow(h.date, y, settings)) return
|
|
if (h.global || !h.counties || (subdivision && h.counties.includes(subdivision))) {
|
|
pushHolidayMarker(map, h.date, { name: h.name, localName: h.localName, color: cal.color, label: cal.label, type: 'public_holiday' })
|
|
}
|
|
})
|
|
}
|
|
} catch { /* API error, skip */ }
|
|
}
|
|
}
|
|
set({ holidays: map })
|
|
},
|
|
|
|
addHolidayCalendar: async (data) => {
|
|
await api.addHolidayCalendar(data)
|
|
await get().loadPlan()
|
|
await get().loadHolidays()
|
|
},
|
|
|
|
updateHolidayCalendar: async (id, data) => {
|
|
await api.updateHolidayCalendar(id, data)
|
|
await get().loadPlan()
|
|
await get().loadHolidays()
|
|
},
|
|
|
|
deleteHolidayCalendar: async (id) => {
|
|
await api.deleteHolidayCalendar(id)
|
|
await get().loadPlan()
|
|
await get().loadHolidays()
|
|
},
|
|
|
|
loadShares: async () => {
|
|
const data = await api.getShares()
|
|
set({ outgoingShares: data.outgoing, incomingShares: data.incoming })
|
|
},
|
|
|
|
loadSharedCalendars: async (year?: number) => {
|
|
const y = year || get().selectedYear
|
|
const data = await api.getSharedCalendars(y)
|
|
set({ sharedCalendars: data.calendars })
|
|
},
|
|
|
|
shareWith: async (userId: number) => {
|
|
await api.share(userId)
|
|
await get().loadShares()
|
|
},
|
|
|
|
removeShare: async (shareId: number) => {
|
|
await api.removeShare(shareId)
|
|
await get().loadShares()
|
|
await get().loadSharedCalendars()
|
|
},
|
|
|
|
setShareHidden: async (shareId: number, hidden: boolean) => {
|
|
// Optimistic: the eye toggle should feel instant; reconcile via refetch.
|
|
const prevIncoming = get().incomingShares
|
|
const prevCalendars = get().sharedCalendars
|
|
set({
|
|
incomingShares: prevIncoming.map(s => (s.id === shareId ? { ...s, hidden } : s)),
|
|
sharedCalendars: prevCalendars.map(c => (c.share_id === shareId ? { ...c, hidden } : c)),
|
|
})
|
|
try {
|
|
await api.updateShare(shareId, hidden)
|
|
} catch (e) {
|
|
set({ incomingShares: prevIncoming, sharedCalendars: prevCalendars })
|
|
throw e
|
|
}
|
|
},
|
|
|
|
loadYearSettings: async () => {
|
|
const data = await api.getYearSettings()
|
|
set({ yearSettings: data.settings ?? DEFAULT_YEAR_SETTINGS })
|
|
},
|
|
|
|
updateYearSettings: async (data: VacayYearSettingsRequest) => {
|
|
const res = await api.updateYearSettings(data)
|
|
set({ yearSettings: res.settings ?? DEFAULT_YEAR_SETTINGS })
|
|
// The window moved, so entries, holidays and the counted usage all shift with it.
|
|
const year = get().selectedYear
|
|
await get().loadEntries(year)
|
|
await get().loadStats(year)
|
|
await get().loadHolidays(year)
|
|
await get().loadSharedCalendars(year)
|
|
},
|
|
|
|
loadAll: async () => {
|
|
set({ loading: true })
|
|
try {
|
|
await get().loadPlan()
|
|
// The leave-year window decides which period is current, so it is resolved
|
|
// before the year list picks a default (#737).
|
|
await get().loadYearSettings()
|
|
await get().loadYears()
|
|
const year = get().selectedYear
|
|
await get().loadEntries(year)
|
|
await get().loadStats(year)
|
|
await get().loadHolidays(year)
|
|
await get().loadShares()
|
|
await get().loadSharedCalendars(year)
|
|
} finally {
|
|
set({ loading: false })
|
|
}
|
|
},
|
|
}))
|