Tofu/app/hooks/useWeekPlanning.js

122 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 nest enregistré, tout part au rechargement.',
})
return
}
const failure = result && result.failure ? result.failure.message : 'le serveur na 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 na pas répondu.'
setStatus({
state: 'error',
message: 'Semaine non chargée — ' + failure + ' Rien ne sera enregistré tant que la semaine na 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 denregistré pour linstant.',
})
// 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 }
}