mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
72a82b3c44
214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out.
535 lines
22 KiB
TypeScript
535 lines
22 KiB
TypeScript
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, currentPeriodYear, 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 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()
|
|
set({ years: data.years })
|
|
if (data.years.length > 0) {
|
|
set({ selectedYear: data.years[data.years.length - 1] })
|
|
} else {
|
|
set({ selectedYear: currentPeriodYear(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 = data.years.length > 0
|
|
? data.years[data.years.length - 1]
|
|
: currentPeriodYear(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)) 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 })
|
|
}
|
|
},
|
|
}))
|