122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
import { useEffect, useRef, useState } from 'preact/hooks'
|
||
import { createSaveScheduler } from '#tofu/services/scheduling/createSaveScheduler.js'
|
||
import { getWeekId } from '#tofu/tools/dates/getWeekId.js'
|
||
import { listWeekDates } from '#tofu/tools/dates/listWeekDates.js'
|
||
import { createEmptyWeekPlanning } from '#tofu/tools/planning/createEmptyWeekPlanning.js'
|
||
import { mergeEditedDays } from '#tofu/tools/planning/mergeEditedDays.js'
|
||
import { setPlanningDay } from '#tofu/tools/planning/setPlanningDay.js'
|
||
|
||
// Owns the planning of the displayed week: updateDay is the only mutation.
|
||
// Loading and saving never throw, they turn into a readable French status.
|
||
export function useWeekPlanning({ store, weekStart }) {
|
||
const weekId = getWeekId(weekStart)
|
||
const [planning, setPlanning] = useState(function buildEmptyWeek() {
|
||
return createEmptyWeekPlanning(weekId, listWeekDates(weekStart))
|
||
})
|
||
const [status, setStatus] = useState({ state: 'idle', message: 'Semaine vide.' })
|
||
// The ref holds the authoritative planning: a scheduled save reads it back
|
||
// when the timer fires, long after the render that scheduled it.
|
||
const planningRef = useRef(planning)
|
||
// Cells touched since the current week started loading, as "<dateKey>:<field>".
|
||
// The sheet stays editable while the GET is in flight, so the response must
|
||
// not silently undo what was typed meanwhile.
|
||
const editedKeysRef = useRef(new Set())
|
||
// True while the displayed week is still being read from the store.
|
||
const loadPendingRef = useRef(true)
|
||
const schedulerRef = useRef(null)
|
||
if (schedulerRef.current === null) schedulerRef.current = createSaveScheduler({})
|
||
|
||
function applyPlanning(nextPlanning) {
|
||
planningRef.current = nextPlanning
|
||
setPlanning(nextPlanning)
|
||
}
|
||
|
||
async function savePlanning(planningToSave) {
|
||
// Writing before the week has arrived would push a sheet that is still
|
||
// mostly empty over the days already stored. The save is not lost: once the
|
||
// week is merged, the edits are scheduled again. The one case that drops an
|
||
// edit is leaving the week while its load is still in flight — the remote
|
||
// content is unknown there, so refusing to write is the only safe answer.
|
||
if (loadPendingRef.current) return
|
||
setStatus({ state: 'saving', message: 'Enregistrement…' })
|
||
const result = await store.saveWeek(weekId, planningToSave)
|
||
if (result && result.ok) {
|
||
// The memory store says out loud that nothing is kept. It does not repeat
|
||
// the session label shown at the foot of the sheet, it says what just
|
||
// happened to this week.
|
||
const stored = store.describeStorage().kind === 'nextcloud'
|
||
setStatus({
|
||
state: 'saved',
|
||
message: stored ? 'Enregistré.' : 'Noté — rien n’est enregistré, tout part au rechargement.',
|
||
})
|
||
return
|
||
}
|
||
const failure = result && result.failure ? result.failure.message : 'le serveur n’a pas répondu.'
|
||
setStatus({ state: 'error', message: 'Non enregistré — ' + failure })
|
||
}
|
||
|
||
function updateDay(dateKey, patch) {
|
||
for (const field of Object.keys(patch)) editedKeysRef.current.add(`${dateKey}:${field}`)
|
||
applyPlanning(setPlanningDay(planningRef.current, dateKey, patch))
|
||
schedulerRef.current.schedule(() => savePlanning(planningRef.current))
|
||
}
|
||
|
||
function flush() {
|
||
return schedulerRef.current.flush()
|
||
}
|
||
|
||
// weekId identifies the displayed week: weekStart only moves with it.
|
||
useEffect(function loadDisplayedWeek() {
|
||
let active = true
|
||
editedKeysRef.current = new Set()
|
||
loadPendingRef.current = true
|
||
applyPlanning(createEmptyWeekPlanning(weekId, listWeekDates(weekStart)))
|
||
setStatus({ state: 'loading', message: 'Chargement de la semaine…' })
|
||
|
||
async function loadFromStore() {
|
||
const result = await store.loadWeek(weekId)
|
||
if (!active) return
|
||
if (!result || !result.ok) {
|
||
// loadPendingRef stays true on purpose: the stored week is unknown, so
|
||
// writing over it would be a guess. The message says it out loud.
|
||
const failure = result && result.failure ? result.failure.message : 'le serveur n’a pas répondu.'
|
||
setStatus({
|
||
state: 'error',
|
||
message: 'Semaine non chargée — ' + failure + ' Rien ne sera enregistré tant que la semaine n’a pas été relue.',
|
||
})
|
||
return
|
||
}
|
||
// The loaded week is the base: keeping the local one instead would PUT a
|
||
// mostly empty week back over the days already stored.
|
||
if (result.planning) {
|
||
applyPlanning(mergeEditedDays(result.planning, planningRef.current, editedKeysRef.current))
|
||
}
|
||
loadPendingRef.current = false
|
||
setStatus({
|
||
state: 'idle',
|
||
message: result.planning ? 'Semaine chargée.' : 'Nouvelle semaine, rien d’enregistré pour l’instant.',
|
||
})
|
||
// What was typed during the load was refused a save above: schedule it now.
|
||
if (editedKeysRef.current.size > 0) {
|
||
schedulerRef.current.schedule(() => savePlanning(planningRef.current))
|
||
}
|
||
}
|
||
|
||
loadFromStore()
|
||
|
||
return function saveBeforeLeavingWeek() {
|
||
active = false
|
||
schedulerRef.current.flush()
|
||
}
|
||
}, [store, weekId])
|
||
|
||
// Declared after the loading effect so its cleanup runs last: a pending save
|
||
// is flushed before the scheduler is destroyed.
|
||
useEffect(function keepSchedulerUntilUnmount() {
|
||
return function destroyScheduler() {
|
||
schedulerRef.current.destroy()
|
||
}
|
||
}, [])
|
||
|
||
return { planning, status, updateDay, flush }
|
||
}
|