Added edit mode

This commit is contained in:
2026-09-19 21:53:10 +02:00
parent 9bb9260c09
commit d46a5f10af
8 changed files with 458 additions and 64 deletions

105
src/lib/editing.js Normal file
View File

@@ -0,0 +1,105 @@
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')}`;
}