diff --git a/README.md b/README.md index 5fd7f3b..a7297bf 100644 --- a/README.md +++ b/README.md @@ -83,22 +83,28 @@ concurrents, les retours d'horloge, les échecs transactionnels et la conservati ## Corriger les postes et les journées terminées -Le **Total journée** mesure la durée entre le début et la fin, repas compris. -Le récapitulatif et le texte copié distinguent aussi le travail hors repas et le -déjeuner. Les totaux suivent le réglage d'arrondi existant ; les éventuels trous -importés restent inclus dans l'amplitude de la journée. +Le **Total travaillé** exclut le repas, qui reste affiché séparément dans le +récapitulatif et le texte copié. Les totaux suivent le réglage d'arrondi existant. Dans **Journée**, touchez un poste dans **Postes de la journée** pour modifier son début, sa fin, sa tâche, son client ou son type (travail, transport, repas). -Les horaires sont saisis à la minute, avec leur date, et enregistrés ensemble -avec le bouton **Enregistrer**. Annuler ne modifie rien. Le début du premier +Les sélecteurs d'heure ne proposent pas de date : ils conservent la date du +poste. Les minutes et les boutons d'ajustement suivent l'arrondi choisi dans +les réglages (1, 5, 15 ou 30 minutes). Les tâches, clients et restaurants utilisent +les mêmes suggestions et la même saisie libre que l'écran de la journée. +Les corrections sont enregistrées ensemble avec **Enregistrer**. Annuler ne +modifie rien ; ouvrir puis valider un horaire inchangé conserve sa valeur +enregistrée. Le début du premier poste et la fin du dernier définissent les bornes de la journée. Modifier une limite intermédiaire ajuste la limite du poste voisin, sans chevauchement. La fin d'un poste encore en cours reste pilotée par les boutons de changement de tâche ou de clôture. **Ajouter un poste oublié** permet de choisir une plage existante puis les -horaires du poste à insérer dans cette plage. Les portions avant et après sont +horaires du poste à insérer dans cette plage. Le début et la fin sont préremplis +avec les deux bornes connues (jusqu'à l'heure actuelle pour un poste en cours). +Il suffit de changer une seule borne pour corriger le début ou la fin de la plage. +Les portions avant et après sont conservées avec leur tâche et leur client. Par exemple, un poste 08:00–12:00 peut devenir 08:00–09:00, une tâche oubliée 09:00–10:00, puis 10:00–12:00. Le type **Repas** permet aussi d'ajouter une pause oubliée et de renseigner @@ -110,6 +116,8 @@ actions sont disponibles sans réouverture, même si une autre journée est en cours. Le récapitulatif et son texte à copier sont recalculés après chaque enregistrement. La suppression d'un poste demande confirmation ; le voisin absorbe sa durée, et le dernier poste d'une journée ne peut pas être supprimé. +Supprimer un repas réaffecte sa durée au poste voisin et peut donc changer le +total travaillé. Chaque correction est transactionnelle. Les horaires inversés, les insertions hors de la plage choisie et les formulaires devenus périmés après une autre @@ -172,8 +180,8 @@ Two decisions shape everything else. **Raw timestamps are stored; rounding is applied only at render.** Changing the rounding setting therefore corrects past days too. Adjacent entries share one stored instant — entry *N*'s end *is* entry *N+1*'s start — so rounding can never open a gap between -them. The quick time picker follows the rounding grid. The detailed entry editor -stores minute-level corrections; summaries apply the selected rounding at render. +them. Time pickers follow the selected rounding grid; opening an unchanged time +preserves its stored timestamp. Summaries apply the same rounding at render. **Suggestions are derived from history, not from counter tables.** There is nothing to keep in sync; the model is rebuilt in memory from the entries. The tradeoff is that a diff --git a/src/components/DayEditor.svelte b/src/components/DayEditor.svelte index 2b2767d..625652a 100644 --- a/src/components/DayEditor.svelte +++ b/src/components/DayEditor.svelte @@ -1,19 +1,41 @@ diff --git a/src/components/SmartField.svelte b/src/components/SmartField.svelte index e0569e3..e4e8dac 100644 --- a/src/components/SmartField.svelte +++ b/src/components/SmartField.svelte @@ -41,7 +41,7 @@
{label} - diff --git a/src/components/TimeField.svelte b/src/components/TimeField.svelte index a4f9dd8..46ebae4 100644 --- a/src/components/TimeField.svelte +++ b/src/components/TimeField.svelte @@ -1,13 +1,13 @@
{label} - @@ -80,12 +82,18 @@ {/each}
- +
+ + +
@@ -131,4 +139,7 @@ .confirm { min-height: var(--tap); } + .clock { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); } + .clock label { display: flex; flex-direction: column; gap: 4px; min-width: 0; } + .clock select { width: 100%; } diff --git a/src/lib/editing.js b/src/lib/editing.js index 9923684..eb7620d 100644 --- a/src/lib/editing.js +++ b/src/lib/editing.js @@ -98,8 +98,3 @@ export function removeRecordedEntry(original) { }); } -/** 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')}`; -} diff --git a/src/lib/editing.test.js b/src/lib/editing.test.js index b623e54..a11b5f2 100644 --- a/src/lib/editing.test.js +++ b/src/lib/editing.test.js @@ -1,7 +1,7 @@ import 'fake-indexeddb/auto'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { closeAndStart, closeDay, createDay, db, exportBackup, getDay, getEntries } from './db.js'; -import { insertRecordedEntry, localInput, removeRecordedEntry, saveRecordedEntry } from './editing.js'; +import { insertRecordedEntry, removeRecordedEntry, saveRecordedEntry } from './editing.js'; import { buildResume, resumeToText } from './resume.js'; import { atLocalTime, HOUR } from './time.js'; @@ -51,8 +51,8 @@ describe('forgotten entries', () => { const resume = buildResume(record, entries); expect(record.status).toBe('closed'); expect(entries[1].client).toBeNull(); expect(resume.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: HOUR }); - expect(resume.workedMs).toBe(8 * HOUR); expect(resume.totalMs).toBe(9 * HOUR); - expect(resumeToText(resume)).toContain('Total journée (repas compris) : 9h00'); + expect(resume.workedMs).toBe(8 * HOUR); + expect(resumeToText(resume)).toContain('Total travaillé : 8h00'); const backup = await exportBackup(); expect(backup.entries).toHaveLength(3); expect(backup.days[0].status).toBe('closed'); }); @@ -63,7 +63,7 @@ describe('forgotten entries', () => { const original = (await getEntries(date))[1]; await insertRecordedEntry(original, values('12:15', '12:30')); const result = buildResume(await getDay(date), await getEntries(date), 1); - expect(result.lunch.ms).toBe(0.75 * HOUR); expect(result.totalMs).toBe(9 * HOUR); + expect(result.lunch.ms).toBe(0.75 * HOUR); expect(result.workedMs).toBe(8.25 * HOUR); }); it('keeps one running entry when inserting up to the present', async () => { @@ -118,7 +118,7 @@ describe('recorded day corrections', () => { await saveRecordedEntry(list.at(-1), values('13:00', '18:00')); const record = await getDay(date); expect(record).toMatchObject({ startedTs: at('07:30'), endedTs: at('18:00'), status: 'closed' }); - expect(buildResume(record, await getEntries(date)).totalMs).toBe(10.5 * HOUR); + expect(buildResume(record, await getEntries(date)).workedMs).toBe(10.5 * HOUR); expect(await getDay(other.date)).toEqual(other); expect((await getEntries(other.date))[0].endTs).toBeNull(); }); @@ -127,7 +127,7 @@ describe('recorded day corrections', () => { const list = await seed([entry(0, '08:00', '10:00'), entry(1, '11:00', '17:00')]); await saveRecordedEntry(list[1], values('11:00', '17:00')); expect((await getEntries(date))[0].endTs).toBe(at('10:00')); - expect(buildResume(await getDay(date), await getEntries(date)).totalMs).toBe(9 * HOUR); + expect(buildResume(await getDay(date), await getEntries(date)).workedMs).toBe(8 * HOUR); }); it('rejects overlaps beyond neighbours and future timestamps without writes', async () => { @@ -167,7 +167,5 @@ describe('recorded day corrections', () => { expect(await getEntries(date)).toHaveLength(1); }); - it('formats local input without a UTC conversion', () => { - expect(localInput(at('07:42'))).toBe('2026-09-07T07:42'); - }); + }); diff --git a/src/lib/resume.js b/src/lib/resume.js index 8ae888c..cf02a94 100644 --- a/src/lib/resume.js +++ b/src/lib/resume.js @@ -67,8 +67,6 @@ export function buildResume(day, entries, roundingMinutes = 15) { clients, lunch, workedMs, - totalMs: Math.max(0, roundTs(fallbackEnd, roundingMinutes) - - roundTs(day.startedTs ?? Math.min(...entries.map((e) => e.startTs), fallbackEnd), roundingMinutes)), omitted, }; } @@ -101,8 +99,7 @@ export function resumeToText(resume) { lines.push(''); } - lines.push(`Travail hors repas : ${fmtDuration(resume.workedMs)}`); - lines.push(`Total journée (repas compris) : ${fmtDuration(resume.totalMs)}`); + lines.push(`Total travaillé : ${fmtDuration(resume.workedMs)}`); return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim(); } diff --git a/src/lib/resume.test.js b/src/lib/resume.test.js index cdc182c..e15aff9 100644 --- a/src/lib/resume.test.js +++ b/src/lib/resume.test.js @@ -40,7 +40,6 @@ describe('buildResume', () => { it('keeps lunch out of the worked total', () => { const r = buildResume(DAY, ENTRIES, 15); expect(r.workedMs).toBe(9 * 60 * MINUTE); // 09h00 on site, lunch excluded - expect(r.totalMs).toBe(10 * 60 * MINUTE); expect(r.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: 60 * MINUTE }); }); @@ -93,8 +92,7 @@ describe('resumeToText', () => { '', 'Déjeuner : restaurant (Le Relais), 1h00', '', - 'Travail hors repas : 9h00', - 'Total journée (repas compris) : 10h00', + 'Total travaillé : 9h00', ].join('\n'), ); }); diff --git a/src/routes/Settings.svelte b/src/routes/Settings.svelte index 08b0327..315037f 100644 --- a/src/routes/Settings.svelte +++ b/src/routes/Settings.svelte @@ -122,8 +122,8 @@ {/each}

- S'applique à l'affichage seulement : les heures réelles restent enregistrées, un - changement d'arrondi corrige aussi les journées passées. + Définit l'affichage des durées et le pas des corrections d'horaire. Changer + l'arrondi actualise aussi les récapitulatifs passés sans modifier les heures enregistrées.