diff --git a/README.md b/README.md index 7ce1be9..5d14d07 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,15 @@ npm run validate:all # include browser tests in full validation ## Exporter et importer les données -Dans **Réglages → Données**, **Exporter les données** télécharge un fichier +Dans **Réglages → Gérer les données et sauvegardes**, l’écran **Données et +sauvegardes** regroupe l’export, l’import, la vérification des sauvegardes et la +durée de conservation. Le rappel **Sauvegarder** ouvre aussi cet écran. + +Les données restent dans le navigateur sur cet appareil, sans synchronisation. +Effacer les données du site ou perdre l’appareil peut faire perdre l’historique : +gardez une copie vérifiée du fichier exporté sur un autre support. + +**Exporter les données** télécharge un fichier `pardis-AAAA-MM-JJ.json`. Il contient toutes les journées conservées, leurs postes et les réglages enregistrés. L'export fonctionne hors ligne et ne modifie pas les données de l'appareil. Les identifiants internes des postes sont recréés à l'import. @@ -53,7 +61,17 @@ L'écriture est transactionnelle : un échec annule le remplacement ou la fusion dans son intégralité. Le récapitulatif, le thème et les suggestions sont ensuite actualisés sans recharger la page. -La purge automatique est désactivée. **Purger maintenant** présente les dates concernées et demande confirmation. Une sauvegarde locale est conservée avant la suppression ; **Récupérer la sauvegarde avant purge** permet de la fusionner ou de la télécharger après restauration. Elle reste sur le même appareil : un export externe demeure nécessaire. +Dans **Durée de conservation**, **Conserver (jours)** détermine les journées +anciennes proposées à la purge. Modifier cette durée ne supprime rien ; la purge +automatique est désactivée. L’historique conservé alimente les suggestions et les +statistiques. La dernière journée terminée et les journées actives sont conservées. + +**Purger maintenant** présente les dates concernées et demande confirmation. +Une sauvegarde locale est conservée avant la suppression ; **Récupérer la +sauvegarde avant purge** permet de la fusionner ou de remplacer les données, +puis de l’exporter après restauration. Cette copie est remplacée à la prochaine +purge et disparaît si les données du site sont effacées : un fichier externe +reste nécessaire. Les tests d'import/export couvrent le format, les réglages, la fidélité des durées, les fichiers invalides, la fusion, le remplacement et l'annulation transactionnelle @@ -147,12 +165,13 @@ sauvegarde sans cette liste la réinitialise à vide lors d'un remplacement. ## Corriger les postes et les journées terminées -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. +Le **Total des heures de présence** exclut les pauses et le repas. Le repas 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 ou son client. La case **Pause déjeuner** distingue -le repas du travail : il n'y a plus de menu « Type de poste ». Le type interne +son début, sa fin, sa tâche ou son client. Les boutons **Poste ordinaire**, +**Repas** et **Pause** permettent de choisir la nature du poste. Le type interne d'un transport déjà enregistré est conservé quand on corrige ses horaires ou son libellé. Les sélecteurs d'heure ne proposent pas de date : ils conservent la date du @@ -174,17 +193,17 @@ Il suffit de changer une seule borne pour corriger le début ou la fin de la pla 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. -La case **Pause déjeuner** permet aussi d'ajouter une pause oubliée et de renseigner -panier ou restaurant. Pour ajouter juste après une frontière existante, -choisissez le poste qui commence à cette frontière. +Le bouton **Repas** permet aussi d’ajouter un déjeuner oublié et de renseigner +panier ou restaurant ; **Pause** ajoute une pause hors repas. Pour ajouter juste +après une frontière existante, choisissez le poste qui commence à cette frontière. -Dans **Récap**, choisissez la date puis **Corriger cette journée** : les mêmes -actions sont disponibles sans réouverture, même si une autre journée est en +Dans **Récap**, choisissez la date puis **Voir les détails → Corriger cette +journée** : les mêmes 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é. +total des heures de présence. 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 @@ -204,9 +223,9 @@ de semaine restent calculés sur les seules journées terminées enregistrées. Les statistiques présentent : -- Le travail hors repas, les pauses, le transport identifié, les nombres de +- Les heures de présence hors repas et pauses, les pauses, le transport identifié, les nombres de journées, postes, tâches et clients. -- Les heures par client et par tâche, leurs parts du travail, les nombres de +- Les heures de présence par client et par tâche, leurs parts des heures de présence, les nombres de journées concernées et les détails croisés, accessibles en ouvrant une ligne. - L'évolution par mois ou semaine, sans inventer des heures pour les jours absents. - Les journées avec repas panier ou restaurant, les restaurants fréquentés, @@ -217,7 +236,7 @@ Les statistiques présentent : - Les clients ou restaurants non renseignés, les postes à zéro après arrondi, les journées vides, les plages non couvertes et les chevauchements éventuels. -Le calcul reprend l'arrondi du récapitulatif et exclut toujours le repas du travail. +Le calcul reprend l'arrondi du récapitulatif et exclut toujours le repas et les pauses des heures de présence. Seules les données encore conservées sont analysées : les jours absents ou purgés ne permettent pas de conclure à une absence de travail. Le transport identifié repose sur le type enregistré, pas sur une déduction à partir du nom de la tâche. @@ -310,11 +329,13 @@ propose la dernière tâche de travail et son client, immédiatement modifiables Une pause a ses propres horaires et son libellé, sans client ni type de repas. Le détail montre tous les postes ; la consultation reste séparée de la correction. Le récapitulatif et sa copie excluent les pauses. Le transport entre dans le total -travaillé, les repas et pauses en sont exclus. Seules les statistiques montrent le +des heures de présence, les repas et pauses en sont exclus. Seules les statistiques montrent le cumul des pauses, séparément des repas. Les intervalles vides importés sont préservés. **Réglages → Gérer les automatismes** et **Gérer les jours non travaillés** ouvrent des pages dédiées ; le bouton Retour du navigateur fonctionne via les routes hash. +**Gérer les données et sauvegardes** ouvre aussi une page dédiée, avec retour aux +réglages, conservation de la durée après rechargement et accès hors ligne. Les réglages sont écrits par transaction sur la dernière valeur persistée pour préserver les modifications d’autres onglets. Les erreurs de démarrage et actualisation sont affichées avec une action Réessayer. @@ -337,7 +358,10 @@ nouvelle consultation. Les autres journées récemment importées restent intact Les tests Playwright utilisent des contextes neufs et des données synthétiques, à 320 px. Ils vérifient consultation/correction, focus, Retour, reprise à deux -onglets, export vérifié, purge/restauration et rechargement hors ligne. Traces, +onglets, export vérifié, purge/restauration et rechargement hors ligne. Ils couvrent +aussi le nouvel écran Données, le retour du champ de conservation à sa valeur +persistée après un échec, le refus d’un export modifié et l’annulation complète +d’une purge en erreur, avec préservation de la sauvegarde précédente. Traces, vidéos et captures sont désactivées. Pour un Chrome déjà installé, définir `PARDIS_CHROME_PATH` avec son chemin ; sinon installer Chromium avec Playwright. La CI installe Chromium Headless Shell et ses dépendances avant de lancer ces diff --git a/src/components/DayEditor.svelte b/src/components/DayEditor.svelte index 9b40f11..2019a05 100644 --- a/src/components/DayEditor.svelte +++ b/src/components/DayEditor.svelte @@ -138,7 +138,7 @@ {#if mode === 'edit' && original.endTs != null && entries.length > 1} {#if confirmingDelete} -

Le poste voisin absorbera cette durée. Supprimer un repas ou une pause peut modifier le total travaillé.

+

Le poste voisin absorbera cette durée. Supprimer un repas ou une pause peut modifier le total des heures de présence.

{:else} diff --git a/src/components/ReopenDialog.svelte b/src/components/ReopenDialog.svelte index 718cd6b..e07cc1e 100644 --- a/src/components/ReopenDialog.svelte +++ b/src/components/ReopenDialog.svelte @@ -43,7 +43,7 @@

Conserver la fin enregistrée, ajouter une pause et démarrer le travail à la reprise.

-

{last?.kind === 'lunch' ? 'Prolonger le repas' : last?.kind === 'break' ? 'Prolonger la pause' : 'Prolonger le dernier poste et compter cet intervalle comme travaillé'}, puis reprendre le travail. La tâche et le client restent modifiables.

+

{last?.kind === 'lunch' ? 'Prolonger le repas' : last?.kind === 'break' ? 'Prolonger la pause' : 'Prolonger le dernier poste et compter cet intervalle dans les heures de présence'}, puis reprendre le travail. La tâche et le client restent modifiables.

{:else}

Aucun intervalle à ajouter. Aucune pause vide ne sera créée.

{/if} {#if error}

{error}

{/if} diff --git a/src/lib/editing.test.js b/src/lib/editing.test.js index a11b5f2..b330760 100644 --- a/src/lib/editing.test.js +++ b/src/lib/editing.test.js @@ -52,7 +52,7 @@ describe('forgotten 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(resumeToText(resume)).toContain('Total travaillé : 8h00'); + expect(resumeToText(resume)).toContain('Total des heures de présence : 8h00'); const backup = await exportBackup(); expect(backup.entries).toHaveLength(3); expect(backup.days[0].status).toBe('closed'); }); diff --git a/src/lib/resume.js b/src/lib/resume.js index c202169..895520b 100644 --- a/src/lib/resume.js +++ b/src/lib/resume.js @@ -100,7 +100,7 @@ export function resumeToText(resume) { lines.push(''); } - lines.push(`Total travaillé : ${fmtDuration(resume.workedMs)}`); + lines.push(`Total des heures de présence : ${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 e15aff9..d673dec 100644 --- a/src/lib/resume.test.js +++ b/src/lib/resume.test.js @@ -92,7 +92,7 @@ describe('resumeToText', () => { '', 'Déjeuner : restaurant (Le Relais), 1h00', '', - 'Total travaillé : 9h00', + 'Total des heures de présence : 9h00', ].join('\n'), ); }); diff --git a/src/routes/Resume.svelte b/src/routes/Resume.svelte index 71ec5ae..2cc8941 100644 --- a/src/routes/Resume.svelte +++ b/src/routes/Resume.svelte @@ -101,7 +101,7 @@
- Total travaillé + Total des heures de présence {fmtDuration(resume.workedMs)}
diff --git a/src/routes/Stats.svelte b/src/routes/Stats.svelte index 1785006..7c0ad10 100644 --- a/src/routes/Stats.svelte +++ b/src/routes/Stats.svelte @@ -61,7 +61,7 @@

Vue d’ensemble

-
Temps travaillé{fmtDuration(stats.workedMs)}
+
Heures de présence{fmtDuration(stats.workedMs)}
Temps de pause{fmtDuration(stats.breakMs)}
Temps de repas{fmtDuration(stats.lunchMs)}
Journées enregistrées{stats.days}
@@ -69,7 +69,7 @@
Clients renseignés{stats.clientCount}
Tâches distinctes{stats.taskCount}
-

Transport identifié : {fmtDuration(stats.travelMs)} ({pct(percent(stats.travelMs, stats.workedMs))} du temps travaillé). Ce chiffre dépend des postes enregistrés comme transport.

+

Transport identifié : {fmtDuration(stats.travelMs)} ({pct(percent(stats.travelMs, stats.workedMs))} des heures de présence). Ce chiffre dépend des postes enregistrés comme transport.

{#if stats.activeDays}

Les cumuls et les détails par client, tâche ou repas incluent la journée en cours jusqu’à maintenant. Les moyennes de « Repères et observations » restent limitées aux journées terminées.

{/if}
@@ -77,26 +77,26 @@

Repères et observations

{#if stats.closedDays}
-
Moyenne travaillée / jour
{fmtDuration(stats.averageWorkMs)}
-
Médiane travaillée / jour
{fmtDuration(stats.medianWorkMs)}
+
Heures de présence moyennes / jour
{fmtDuration(stats.averageWorkMs)}
+
Médiane des heures de présence / jour
{fmtDuration(stats.medianWorkMs)}
Amplitude moyenne, repas et pauses compris
{fmtDuration(stats.averageSpanMs)}
Repas moyen, jours avec pause
{fmtDuration(stats.averageLunchMs)}
-

Journée la plus longue en travail : {dateLabel(stats.longest.date)}, {fmtDuration(stats.longest.workedMs)}. La plus courte : {dateLabel(stats.shortest.date)}, {fmtDuration(stats.shortest.workedMs)}.

+

Journée avec le plus d’heures de présence : {dateLabel(stats.longest.date)}, {fmtDuration(stats.longest.workedMs)}. Celle avec le moins : {dateLabel(stats.shortest.date)}, {fmtDuration(stats.shortest.workedMs)}.

{:else}

Les moyennes seront disponibles avec une journée terminée dans cette période.

{/if} {#if stats.topClient && stats.workedMs} -

{stats.topClient.name} représente {pct(stats.topClientShare)} du temps travaillé enregistré.

+

{stats.topClient.name} représente {pct(stats.topClientShare)} des heures de présence enregistrées.

{/if} {#if stats.tasks.length && stats.workedMs} -

La tâche la plus longue au total est {stats.tasks[0].name} : {fmtDuration(stats.tasks[0].ms)}, soit {pct(stats.topTaskShare)} du travail.

+

La tâche la plus longue au total est {stats.tasks[0].name} : {fmtDuration(stats.tasks[0].ms)}, soit {pct(stats.topTaskShare)} des heures de présence.

{/if}

Ces observations décrivent les saisies de la période ; elles ne mesurent pas la productivité.

-

Évolution des heures

+

Évolution des heures de présence

-

Les barres montrent le travail hors repas. Les périodes sans données ne sont pas affichées ; les premières et dernières peuvent être partielles.

+

Les barres montrent les heures de présence hors repas et pauses. Les périodes sans données ne sont pas affichées ; les premières et dernières peuvent être partielles.

-

Heures par client

+

Heures de présence par client

Ouvrez un client pour voir ses tâches. La moyenne porte sur les journées où ce client apparaît.

{#if !stats.clients.length}

Aucun poste de travail enregistré.

{/if} {#each stats.clients as client (client.key)}
{client.name}{fmtDuration(client.ms)}
-

{pct(percent(client.ms, stats.workedMs))} du travail · {client.days} journée(s) · {client.count} poste(s) · moyenne {fmtDuration(client.averageMs)} / jour concerné.

+

{pct(percent(client.ms, stats.workedMs))} des heures de présence · {client.days} journée(s) · {client.count} poste(s) · moyenne {fmtDuration(client.averageMs)} / jour concerné.

    {#each stats.pairs.filter((pair) => pair.client === client.key) as pair (pair.key)}
  • {pair.name}{fmtDuration(pair.ms)}
  • @@ -128,13 +128,13 @@
-

Heures par tâche

+

Heures de présence par tâche

{#if !stats.tasks.length}

Aucun poste de travail enregistré.

{/if} {#each stats.tasks as task (task.key)}
{task.name}{fmtDuration(task.ms)}
-

{pct(percent(task.ms, stats.workedMs))} du travail · {task.days} journée(s) · {task.count} poste(s) · moyenne {fmtDuration(task.averageMs)} / jour concerné.

+

{pct(percent(task.ms, stats.workedMs))} des heures de présence · {task.days} journée(s) · {task.count} poste(s) · moyenne {fmtDuration(task.averageMs)} / jour concerné.

    {#each stats.pairs.filter((pair) => pair.name === task.key) as pair (pair.key)}
  • {pair.clientName}{fmtDuration(pair.ms)}
  • @@ -180,8 +180,8 @@
    - - + + {#each stats.daily as day (day.date)} {/each} diff --git a/tests/e2e/parcours.spec.js b/tests/e2e/parcours.spec.js index 75f125e..fabee34 100644 --- a/tests/e2e/parcours.spec.js +++ b/tests/e2e/parcours.spec.js @@ -57,10 +57,12 @@ test('consultation, correction, recalcul, copie et statistiques à 320 px', asyn await context.grantPermissions(['clipboard-read','clipboard-write']); await importFixture(page); await expect(page.locator('.total')).toContainText('8h30'); + await expect(page.locator('.total')).toContainText('Total des heures de présence'); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); await expect(page.locator('.screen')).not.toContainText('0h30'); await page.getByRole('button',{name:'Copier le récapitulatif'}).click(); const copied = await page.evaluate(() => navigator.clipboard.readText()); - expect(copied).toContain('8h30'); expect(copied).not.toContain('Pause'); + expect(copied).toContain('Total des heures de présence : 8h30'); expect(copied).not.toContain('Pause'); await page.getByRole('button',{name:'Voir les détails'}).click(); await expect(page.locator('.timeline button')).toHaveCount(3); await expect(page.locator('.timeline button').nth(1)).toBeDisabled(); @@ -83,6 +85,7 @@ test('consultation, correction, recalcul, copie et statistiques à 320 px', asyn await expect(page.locator('.total')).toContainText('9h00'); await page.getByRole('button',{name:'Réglages',exact:true}).click(); await page.getByRole('button',{name:'Consulter les statistiques'}).click(); + await expect(page.getByText('Heures de présence',{exact:true})).toBeVisible(); await expect(page.getByTestId('stats-worked')).toHaveText('9h00'); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); // Wider or larger fonts must also fit the backup reminder on CI and mobile.
    Travail hors repas et pauses par journée
    DateTravailRepasAmplitudePostes
    Heures de présence hors repas et pauses par journée
    DatePrésenceRepasAmplitudePostes
    {dateLabel(day.date)}{day.status === 'active' ? ' (en cours)' : ''}{fmtDuration(day.workedMs)}{fmtDuration(day.lunchMs)}{fmtDuration(day.spanMs)}{day.entries}