Edited wording
This commit is contained in:
58
README.md
58
README.md
@@ -23,7 +23,15 @@ npm run validate:all # include browser tests in full validation
|
|||||||
|
|
||||||
## Exporter et importer les données
|
## 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
|
`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
|
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.
|
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
|
dans son intégralité. Le récapitulatif, le thème et les suggestions sont ensuite
|
||||||
actualisés sans recharger la page.
|
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 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
|
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
|
## Corriger les postes et les journées terminées
|
||||||
|
|
||||||
Le **Total travaillé** exclut le repas, qui reste affiché séparément dans le
|
Le **Total des heures de présence** exclut les pauses et le repas. Le repas reste affiché
|
||||||
récapitulatif et le texte copié. Les totaux suivent le réglage d'arrondi existant.
|
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
|
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
|
son début, sa fin, sa tâche ou son client. Les boutons **Poste ordinaire**,
|
||||||
le repas du travail : il n'y a plus de menu « Type de poste ». Le type interne
|
**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
|
d'un transport déjà enregistré est conservé quand on corrige ses horaires ou
|
||||||
son libellé.
|
son libellé.
|
||||||
Les sélecteurs d'heure ne proposent pas de date : ils conservent la date du
|
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
|
Les portions avant et après sont
|
||||||
conservées avec leur tâche et leur client. Par exemple, un poste 08:00–12:00
|
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.
|
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
|
Le bouton **Repas** permet aussi d’ajouter un déjeuner oublié et de renseigner
|
||||||
panier ou restaurant. Pour ajouter juste après une frontière existante,
|
panier ou restaurant ; **Pause** ajoute une pause hors repas. Pour ajouter juste
|
||||||
choisissez le poste qui commence à cette frontière.
|
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
|
Dans **Récap**, choisissez la date puis **Voir les détails → Corriger cette
|
||||||
actions sont disponibles sans réouverture, même si une autre journée est en
|
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
|
cours. Le récapitulatif et son texte à copier sont recalculés après chaque
|
||||||
enregistrement. La suppression d'un poste demande confirmation ; le voisin
|
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é.
|
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
|
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
|
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
|
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 :
|
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.
|
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.
|
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.
|
- 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,
|
- 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 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.
|
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
|
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é
|
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.
|
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.
|
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 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
|
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.
|
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
|
**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.
|
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
|
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
|
préserver les modifications d’autres onglets. Les erreurs de démarrage et
|
||||||
actualisation sont affichées avec une action Réessayer.
|
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,
|
Les tests Playwright utilisent des contextes neufs et des données synthétiques,
|
||||||
à 320 px. Ils vérifient consultation/correction, focus, Retour, reprise à deux
|
à 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
|
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.
|
`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
|
La CI installe Chromium Headless Shell et ses dépendances avant de lancer ces
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<button type="button" onclick={close}>Annuler</button>
|
<button type="button" onclick={close}>Annuler</button>
|
||||||
{#if mode === 'edit' && original.endTs != null && entries.length > 1}
|
{#if mode === 'edit' && original.endTs != null && entries.length > 1}
|
||||||
{#if confirmingDelete}
|
{#if confirmingDelete}
|
||||||
<p class="hint">Le poste voisin absorbera cette durée. Supprimer un repas ou une pause peut modifier le total travaillé.</p>
|
<p class="hint">Le poste voisin absorbera cette durée. Supprimer un repas ou une pause peut modifier le total des heures de présence.</p>
|
||||||
<button type="button" class="btn-danger" onclick={() => commit(true)}>Confirmer la suppression</button>
|
<button type="button" class="btn-danger" onclick={() => commit(true)}>Confirmer la suppression</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button type="button" onclick={() => (confirmingDelete = true)}>Supprimer ce poste</button>
|
<button type="button" onclick={() => (confirmingDelete = true)}>Supprimer ce poste</button>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<label><input type="radio" bind:group={mode} value="break" /> Créer une pause jusqu’à maintenant</label>
|
<label><input type="radio" bind:group={mode} value="break" /> Créer une pause jusqu’à maintenant</label>
|
||||||
<p class="dim">Conserver la fin enregistrée, ajouter une pause et démarrer le travail à la reprise.</p>
|
<p class="dim">Conserver la fin enregistrée, ajouter une pause et démarrer le travail à la reprise.</p>
|
||||||
<label><input type="radio" bind:group={mode} value="continue" /> {last?.kind === 'lunch' ? 'Prolonger le repas depuis la fin enregistrée' : last?.kind === 'break' ? 'Prolonger la pause depuis la fin enregistrée' : 'Continuer depuis la fin enregistrée'}</label>
|
<label><input type="radio" bind:group={mode} value="continue" /> {last?.kind === 'lunch' ? 'Prolonger le repas depuis la fin enregistrée' : last?.kind === 'break' ? 'Prolonger la pause depuis la fin enregistrée' : 'Continuer depuis la fin enregistrée'}</label>
|
||||||
<p class="dim">{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.</p>
|
<p class="dim">{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.</p>
|
||||||
{:else}<p>Aucun intervalle à ajouter. Aucune pause vide ne sera créée.</p>{/if}
|
{:else}<p>Aucun intervalle à ajouter. Aucune pause vide ne sera créée.</p>{/if}
|
||||||
{#if error}<p role="alert">{error}</p>{/if}
|
{#if error}<p role="alert">{error}</p>{/if}
|
||||||
<button class="btn-primary" onclick={confirm}>Confirmer la reprise</button>
|
<button class="btn-primary" onclick={confirm}>Confirmer la reprise</button>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('forgotten entries', () => {
|
|||||||
expect(record.status).toBe('closed'); expect(entries[1].client).toBeNull();
|
expect(record.status).toBe('closed'); expect(entries[1].client).toBeNull();
|
||||||
expect(resume.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: HOUR });
|
expect(resume.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: HOUR });
|
||||||
expect(resume.workedMs).toBe(8 * 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();
|
const backup = await exportBackup();
|
||||||
expect(backup.entries).toHaveLength(3); expect(backup.days[0].status).toBe('closed');
|
expect(backup.entries).toHaveLength(3); expect(backup.days[0].status).toBe('closed');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function resumeToText(resume) {
|
|||||||
lines.push('');
|
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();
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ describe('resumeToText', () => {
|
|||||||
'',
|
'',
|
||||||
'Déjeuner : restaurant (Le Relais), 1h00',
|
'Déjeuner : restaurant (Le Relais), 1h00',
|
||||||
'',
|
'',
|
||||||
'Total travaillé : 9h00',
|
'Total des heures de présence : 9h00',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<div class="row-between total">
|
<div class="row-between total">
|
||||||
<strong>Total travaillé</strong>
|
<strong>Total des heures de présence</strong>
|
||||||
<strong class="tnum">{fmtDuration(resume.workedMs)}</strong>
|
<strong class="tnum">{fmtDuration(resume.workedMs)}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<section class="card stack" aria-labelledby="stats-overview">
|
<section class="card stack" aria-labelledby="stats-overview">
|
||||||
<h2 id="stats-overview">Vue d’ensemble</h2>
|
<h2 id="stats-overview">Vue d’ensemble</h2>
|
||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
<div><span>Temps travaillé</span><strong class="tnum" data-testid="stats-worked">{fmtDuration(stats.workedMs)}</strong></div>
|
<div><span>Heures de présence</span><strong class="tnum" data-testid="stats-worked">{fmtDuration(stats.workedMs)}</strong></div>
|
||||||
<div><span>Temps de pause</span><strong class="tnum">{fmtDuration(stats.breakMs)}</strong></div>
|
<div><span>Temps de pause</span><strong class="tnum">{fmtDuration(stats.breakMs)}</strong></div>
|
||||||
<div><span>Temps de repas</span><strong class="tnum" data-testid="stats-lunch">{fmtDuration(stats.lunchMs)}</strong></div>
|
<div><span>Temps de repas</span><strong class="tnum" data-testid="stats-lunch">{fmtDuration(stats.lunchMs)}</strong></div>
|
||||||
<div><span>Journées enregistrées</span><strong>{stats.days}</strong></div>
|
<div><span>Journées enregistrées</span><strong>{stats.days}</strong></div>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div><span>Clients renseignés</span><strong>{stats.clientCount}</strong></div>
|
<div><span>Clients renseignés</span><strong>{stats.clientCount}</strong></div>
|
||||||
<div><span>Tâches distinctes</span><strong>{stats.taskCount}</strong></div>
|
<div><span>Tâches distinctes</span><strong>{stats.taskCount}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="small">Transport identifié : <strong>{fmtDuration(stats.travelMs)}</strong> ({pct(percent(stats.travelMs, stats.workedMs))} du temps travaillé). Ce chiffre dépend des postes enregistrés comme transport.</p>
|
<p class="small">Transport identifié : <strong>{fmtDuration(stats.travelMs)}</strong> ({pct(percent(stats.travelMs, stats.workedMs))} des heures de présence). Ce chiffre dépend des postes enregistrés comme transport.</p>
|
||||||
{#if stats.activeDays}<p class="small" role="status">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.</p>{/if}
|
{#if stats.activeDays}<p class="small" role="status">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.</p>{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -77,26 +77,26 @@
|
|||||||
<h2>Repères et observations</h2>
|
<h2>Repères et observations</h2>
|
||||||
{#if stats.closedDays}
|
{#if stats.closedDays}
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>Moyenne travaillée / jour</dt><dd>{fmtDuration(stats.averageWorkMs)}</dd></div>
|
<div><dt>Heures de présence moyennes / jour</dt><dd>{fmtDuration(stats.averageWorkMs)}</dd></div>
|
||||||
<div><dt>Médiane travaillée / jour</dt><dd>{fmtDuration(stats.medianWorkMs)}</dd></div>
|
<div><dt>Médiane des heures de présence / jour</dt><dd>{fmtDuration(stats.medianWorkMs)}</dd></div>
|
||||||
<div><dt>Amplitude moyenne, repas et pauses compris</dt><dd>{fmtDuration(stats.averageSpanMs)}</dd></div>
|
<div><dt>Amplitude moyenne, repas et pauses compris</dt><dd>{fmtDuration(stats.averageSpanMs)}</dd></div>
|
||||||
<div><dt>Repas moyen, jours avec pause</dt><dd>{fmtDuration(stats.averageLunchMs)}</dd></div>
|
<div><dt>Repas moyen, jours avec pause</dt><dd>{fmtDuration(stats.averageLunchMs)}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<p class="small">Journée la plus longue en travail : <strong>{dateLabel(stats.longest.date)}, {fmtDuration(stats.longest.workedMs)}</strong>. La plus courte : {dateLabel(stats.shortest.date)}, {fmtDuration(stats.shortest.workedMs)}.</p>
|
<p class="small">Journée avec le plus d’heures de présence : <strong>{dateLabel(stats.longest.date)}, {fmtDuration(stats.longest.workedMs)}</strong>. Celle avec le moins : {dateLabel(stats.shortest.date)}, {fmtDuration(stats.shortest.workedMs)}.</p>
|
||||||
{:else}<p class="small">Les moyennes seront disponibles avec une journée terminée dans cette période.</p>{/if}
|
{:else}<p class="small">Les moyennes seront disponibles avec une journée terminée dans cette période.</p>{/if}
|
||||||
{#if stats.topClient && stats.workedMs}
|
{#if stats.topClient && stats.workedMs}
|
||||||
<p><strong>{stats.topClient.name}</strong> représente {pct(stats.topClientShare)} du temps travaillé enregistré.</p>
|
<p><strong>{stats.topClient.name}</strong> représente {pct(stats.topClientShare)} des heures de présence enregistrées.</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if stats.tasks.length && stats.workedMs}
|
{#if stats.tasks.length && stats.workedMs}
|
||||||
<p>La tâche la plus longue au total est <strong>{stats.tasks[0].name}</strong> : {fmtDuration(stats.tasks[0].ms)}, soit {pct(stats.topTaskShare)} du travail.</p>
|
<p>La tâche la plus longue au total est <strong>{stats.tasks[0].name}</strong> : {fmtDuration(stats.tasks[0].ms)}, soit {pct(stats.topTaskShare)} des heures de présence.</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="small dim">Ces observations décrivent les saisies de la période ; elles ne mesurent pas la productivité.</p>
|
<p class="small dim">Ces observations décrivent les saisies de la période ; elles ne mesurent pas la productivité.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card stack">
|
<section class="card stack">
|
||||||
<h2>Évolution des heures</h2>
|
<h2>Évolution des heures de présence</h2>
|
||||||
<label class="stack">Regrouper par<select bind:value={periodMode}><option value="month">Mois</option><option value="week">Semaine</option></select></label>
|
<label class="stack">Regrouper par<select bind:value={periodMode}><option value="month">Mois</option><option value="week">Semaine</option></select></label>
|
||||||
<p class="small dim">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.</p>
|
<p class="small dim">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.</p>
|
||||||
<ul class="periods">
|
<ul class="periods">
|
||||||
{#each periods as period (period.key)}
|
{#each periods as period (period.key)}
|
||||||
<li class="stack">
|
<li class="stack">
|
||||||
@@ -109,14 +109,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card stack">
|
<section class="card stack">
|
||||||
<h2>Heures par client</h2>
|
<h2>Heures de présence par client</h2>
|
||||||
<p class="small dim">Ouvrez un client pour voir ses tâches. La moyenne porte sur les journées où ce client apparaît.</p>
|
<p class="small dim">Ouvrez un client pour voir ses tâches. La moyenne porte sur les journées où ce client apparaît.</p>
|
||||||
{#if !stats.clients.length}<p>Aucun poste de travail enregistré.</p>{/if}
|
{#if !stats.clients.length}<p>Aucun poste de travail enregistré.</p>{/if}
|
||||||
{#each stats.clients as client (client.key)}
|
{#each stats.clients as client (client.key)}
|
||||||
<details class="breakdown">
|
<details class="breakdown">
|
||||||
<summary><span>{client.name}</span><strong class="tnum">{fmtDuration(client.ms)}</strong></summary>
|
<summary><span>{client.name}</span><strong class="tnum">{fmtDuration(client.ms)}</strong></summary>
|
||||||
<div class="stack detail-body">
|
<div class="stack detail-body">
|
||||||
<p class="small">{pct(percent(client.ms, stats.workedMs))} du travail · {client.days} journée(s) · {client.count} poste(s) · moyenne {fmtDuration(client.averageMs)} / jour concerné.</p>
|
<p class="small">{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é.</p>
|
||||||
<ul class="lines">
|
<ul class="lines">
|
||||||
{#each stats.pairs.filter((pair) => pair.client === client.key) as pair (pair.key)}
|
{#each stats.pairs.filter((pair) => pair.client === client.key) as pair (pair.key)}
|
||||||
<li class="row"><span>{pair.name}</span><strong class="tnum">{fmtDuration(pair.ms)}</strong></li>
|
<li class="row"><span>{pair.name}</span><strong class="tnum">{fmtDuration(pair.ms)}</strong></li>
|
||||||
@@ -128,13 +128,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card stack">
|
<section class="card stack">
|
||||||
<h2>Heures par tâche</h2>
|
<h2>Heures de présence par tâche</h2>
|
||||||
{#if !stats.tasks.length}<p>Aucun poste de travail enregistré.</p>{/if}
|
{#if !stats.tasks.length}<p>Aucun poste de travail enregistré.</p>{/if}
|
||||||
{#each stats.tasks as task (task.key)}
|
{#each stats.tasks as task (task.key)}
|
||||||
<details class="breakdown">
|
<details class="breakdown">
|
||||||
<summary><span>{task.name}</span><strong class="tnum">{fmtDuration(task.ms)}</strong></summary>
|
<summary><span>{task.name}</span><strong class="tnum">{fmtDuration(task.ms)}</strong></summary>
|
||||||
<div class="stack detail-body">
|
<div class="stack detail-body">
|
||||||
<p class="small">{pct(percent(task.ms, stats.workedMs))} du travail · {task.days} journée(s) · {task.count} poste(s) · moyenne {fmtDuration(task.averageMs)} / jour concerné.</p>
|
<p class="small">{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é.</p>
|
||||||
<ul class="lines">
|
<ul class="lines">
|
||||||
{#each stats.pairs.filter((pair) => pair.name === task.key) as pair (pair.key)}
|
{#each stats.pairs.filter((pair) => pair.name === task.key) as pair (pair.key)}
|
||||||
<li class="row"><span>{pair.clientName}</span><strong class="tnum">{fmtDuration(pair.ms)}</strong></li>
|
<li class="row"><span>{pair.clientName}</span><strong class="tnum">{fmtDuration(pair.ms)}</strong></li>
|
||||||
@@ -180,8 +180,8 @@
|
|||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard focus enables horizontal scrolling of this labelled table region.) -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard focus enables horizontal scrolling of this labelled table region.) -->
|
||||||
<div class="table-wrap" role="region" aria-label="Détail quotidien" tabindex="0">
|
<div class="table-wrap" role="region" aria-label="Détail quotidien" tabindex="0">
|
||||||
<table>
|
<table>
|
||||||
<caption>Travail hors repas et pauses par journée</caption>
|
<caption>Heures de présence hors repas et pauses par journée</caption>
|
||||||
<thead><tr><th scope="col">Date</th><th scope="col">Travail</th><th scope="col">Repas</th><th scope="col">Amplitude</th><th scope="col">Postes</th></tr></thead>
|
<thead><tr><th scope="col">Date</th><th scope="col">Présence</th><th scope="col">Repas</th><th scope="col">Amplitude</th><th scope="col">Postes</th></tr></thead>
|
||||||
<tbody>{#each stats.daily as day (day.date)}
|
<tbody>{#each stats.daily as day (day.date)}
|
||||||
<tr><th scope="row">{dateLabel(day.date)}{day.status === 'active' ? ' (en cours)' : ''}</th><td>{fmtDuration(day.workedMs)}</td><td>{fmtDuration(day.lunchMs)}</td><td>{fmtDuration(day.spanMs)}</td><td>{day.entries}</td></tr>
|
<tr><th scope="row">{dateLabel(day.date)}{day.status === 'active' ? ' (en cours)' : ''}</th><td>{fmtDuration(day.workedMs)}</td><td>{fmtDuration(day.lunchMs)}</td><td>{fmtDuration(day.spanMs)}</td><td>{day.entries}</td></tr>
|
||||||
{/each}</tbody>
|
{/each}</tbody>
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ test('consultation, correction, recalcul, copie et statistiques à 320 px', asyn
|
|||||||
await context.grantPermissions(['clipboard-read','clipboard-write']);
|
await context.grantPermissions(['clipboard-read','clipboard-write']);
|
||||||
await importFixture(page);
|
await importFixture(page);
|
||||||
await expect(page.locator('.total')).toContainText('8h30');
|
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 expect(page.locator('.screen')).not.toContainText('0h30');
|
||||||
await page.getByRole('button',{name:'Copier le récapitulatif'}).click();
|
await page.getByRole('button',{name:'Copier le récapitulatif'}).click();
|
||||||
const copied = await page.evaluate(() => navigator.clipboard.readText());
|
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 page.getByRole('button',{name:'Voir les détails'}).click();
|
||||||
await expect(page.locator('.timeline button')).toHaveCount(3);
|
await expect(page.locator('.timeline button')).toHaveCount(3);
|
||||||
await expect(page.locator('.timeline button').nth(1)).toBeDisabled();
|
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 expect(page.locator('.total')).toContainText('9h00');
|
||||||
await page.getByRole('button',{name:'Réglages',exact:true}).click();
|
await page.getByRole('button',{name:'Réglages',exact:true}).click();
|
||||||
await page.getByRole('button',{name:'Consulter les statistiques'}).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');
|
await expect(page.getByTestId('stats-worked')).toHaveText('9h00');
|
||||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||||
// Wider or larger fonts must also fit the backup reminder on CI and mobile.
|
// Wider or larger fonts must also fit the backup reminder on CI and mobile.
|
||||||
|
|||||||
Reference in New Issue
Block a user