106 lines
5.1 KiB
JavaScript
106 lines
5.1 KiB
JavaScript
import { db, deleteEntry, getDay, getEntries } from './db.js';
|
||
import { localDateKey } from './time.js';
|
||
|
||
const conflict = () => new Error('Ce poste a changé. Fermez puis ouvrez à nouveau la correction.');
|
||
const fields = ['id', 'date', 'seq', 'kind', 'task', 'client', 'startTs', 'endTs'];
|
||
|
||
async function readOriginal(original) {
|
||
const entry = await db.entries.get(original.id);
|
||
if (!entry || fields.some((key) => entry[key] !== original[key])) throw conflict();
|
||
const day = await getDay(entry.date);
|
||
if (!day) throw conflict();
|
||
return { entry, day, entries: await getEntries(entry.date) };
|
||
}
|
||
|
||
function content(values) {
|
||
if (!['work', 'travel', 'lunch'].includes(values.kind)) throw new Error('Choisissez un type de poste.');
|
||
const task = values.kind === 'lunch' ? 'Pause déjeuner' : String(values.task ?? '').trim();
|
||
if (!task) throw new Error('Renseignez le nom de la tâche.');
|
||
return { kind: values.kind, task, client: values.kind === 'lunch' ? null : String(values.client ?? '').trim() || null };
|
||
}
|
||
|
||
function validateTimes(start, end, now) {
|
||
if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) {
|
||
throw new Error('La fin doit être postérieure au début.');
|
||
}
|
||
if (end > now) throw new Error('Les horaires ne peuvent pas être dans le futur.');
|
||
}
|
||
|
||
async function saveLunch(date, values) {
|
||
if (values.kind !== 'lunch') return;
|
||
if (!['panier', 'restaurant'].includes(values.lunchType)) throw new Error('Choisissez le type de repas.');
|
||
await db.days.update(date, {
|
||
lunchType: values.lunchType,
|
||
restaurant: values.lunchType === 'restaurant' ? String(values.restaurant ?? '').trim() || null : null,
|
||
});
|
||
}
|
||
|
||
/** Correct both boundaries and their neighbours without changing day status. */
|
||
export function saveRecordedEntry(original, values, now = Date.now()) {
|
||
return db.transaction('rw', db.days, db.entries, async () => {
|
||
const { entry, day, entries } = await readOriginal(original);
|
||
const index = entries.findIndex((e) => e.id === entry.id);
|
||
const prev = entries[index - 1], next = entries[index + 1];
|
||
const { startTs, endTs } = values;
|
||
if ((entry.endTs == null) !== (endTs == null)) throw conflict();
|
||
validateTimes(startTs, endTs ?? now, now);
|
||
if (prev && startTs < prev.startTs || next && endTs > (next.endTs ?? now)) {
|
||
throw new Error('Ces horaires dépassent le poste voisin. Corrigez d’abord ce voisin.');
|
||
}
|
||
if (!prev && localDateKey(startTs) !== day.date) throw new Error('Le début doit rester à la date de cette journée.');
|
||
await db.entries.update(entry.id, { ...content(values), startTs, endTs });
|
||
if (prev && startTs !== entry.startTs) await db.entries.update(prev.id, { endTs: startTs });
|
||
if (next && endTs !== entry.endTs) await db.entries.update(next.id, { startTs: endTs });
|
||
if (!prev) await db.days.update(day.date, { startedTs: startTs });
|
||
if (!next && day.status === 'closed') await db.days.update(day.date, { endedTs: endTs });
|
||
await saveLunch(day.date, values);
|
||
});
|
||
}
|
||
|
||
/** Carve a forgotten interval out of a selected entry, preserving both remnants. */
|
||
export function insertRecordedEntry(original, values, now = Date.now()) {
|
||
return db.transaction('rw', db.days, db.entries, async () => {
|
||
const { entry, entries } = await readOriginal(original);
|
||
const { startTs, endTs } = values;
|
||
validateTimes(startTs, endTs, now);
|
||
if (startTs < entry.startTs || endTs > (entry.endTs ?? now)) {
|
||
throw new Error('Le poste ajouté doit rester dans la plage choisie.');
|
||
}
|
||
const { id, ...rest } = entry;
|
||
const pieces = [];
|
||
if (startTs > entry.startTs) pieces.push({ ...rest, endTs: startTs });
|
||
const insertedIndex = pieces.length;
|
||
pieces.push({ ...rest, ...content(values), startTs, endTs });
|
||
if (entry.endTs == null || endTs < entry.endTs) pieces.push({ ...rest, startTs: endTs });
|
||
// Keep the original id on a surviving portion, or on the replacement when full.
|
||
const keepIndex = startTs > entry.startTs ? 0 : pieces.length - 1;
|
||
const shift = pieces.length - 1;
|
||
for (const later of entries.filter((e) => e.seq > entry.seq)) {
|
||
await db.entries.update(later.id, { seq: later.seq + shift });
|
||
}
|
||
let insertedId;
|
||
for (let i = 0; i < pieces.length; i++) {
|
||
const piece = { ...pieces[i], seq: entry.seq + i };
|
||
const pieceId = i === keepIndex ? (await db.entries.put({ ...piece, id }), id) : await db.entries.add(piece);
|
||
if (i === insertedIndex) insertedId = pieceId;
|
||
}
|
||
await saveLunch(entry.date, values);
|
||
return insertedId;
|
||
});
|
||
}
|
||
|
||
export function removeRecordedEntry(original) {
|
||
return db.transaction('rw', db.days, db.entries, async () => {
|
||
const { entry, entries } = await readOriginal(original);
|
||
if (entries.length < 2) throw new Error('Le dernier poste de la journée ne peut pas être supprimé.');
|
||
if (entry.endTs == null) throw new Error('Le poste en cours ne peut pas être supprimé.');
|
||
await deleteEntry(entry.id);
|
||
});
|
||
}
|
||
|
||
/** Native datetime-local inputs use wall time, never UTC. */
|
||
export function localInput(ts) {
|
||
const d = new Date(ts);
|
||
return `${localDateKey(ts)}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||
}
|