176 lines
5.1 KiB
Svelte
176 lines
5.1 KiB
Svelte
<script>
|
||
import DayEditor from '../components/DayEditor.svelte';
|
||
import { dayState, reopenToday } from '../lib/day.svelte.js';
|
||
import { buildResume, resumeToText } from '../lib/resume.js';
|
||
import { go } from '../lib/router.svelte.js';
|
||
import { settings } from '../lib/settings.svelte.js';
|
||
import { fmtDayLabel, fmtDuration, weekday } from '../lib/time.js';
|
||
|
||
let copied = $state(false);
|
||
let selectedDate = $state('');
|
||
let reopening = $state(false);
|
||
let error = $state('');
|
||
let editing = $state(false);
|
||
const selectedDay = $derived(dayState.closedDays.find((d) => d.date === selectedDate) ?? dayState.lastClosed);
|
||
const selectedEntries = $derived(dayState.historyEntries.filter((e) => e.date === selectedDay?.date));
|
||
|
||
const resume = $derived(
|
||
buildResume(
|
||
selectedDay,
|
||
selectedEntries,
|
||
settings.roundingMinutes,
|
||
),
|
||
);
|
||
|
||
const text = $derived(resumeToText(resume));
|
||
|
||
/** Shown when today is a work day whose start time has not arrived yet. */
|
||
const startsAt = $derived(
|
||
settings.workdays.includes(weekday(dayState.today)) ? settings.dayStart : null,
|
||
);
|
||
|
||
const closedToday = $derived(selectedDay?.date === dayState.today && !dayState.day);
|
||
|
||
async function reopen() {
|
||
if (reopening) return;
|
||
reopening = true;
|
||
error = '';
|
||
try { await reopenToday(); go('today'); }
|
||
catch (cause) { error = `Reprise impossible : ${cause.message}`; }
|
||
finally { reopening = false; }
|
||
}
|
||
|
||
async function copy() {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
} catch {
|
||
// Clipboard API needs a secure context; fall back to a selectable dialog.
|
||
window.prompt('Copier le récapitulatif :', text);
|
||
}
|
||
copied = true;
|
||
setTimeout(() => (copied = false), 2500);
|
||
}
|
||
</script>
|
||
|
||
<div class="screen">
|
||
{#if error}<p role="alert">{error}</p>{/if}
|
||
{#if dayState.closedDays.length > 1}
|
||
<label class="stack">
|
||
<select value={selectedDay?.date} onchange={(event) => { selectedDate = event.currentTarget.value; copied = false; editing = false; }}>
|
||
{#each dayState.closedDays as day (day.date)}
|
||
<option value={day.date}>{fmtDayLabel(day.date)} {day.date.slice(0, 4)}</option>
|
||
{/each}
|
||
</select>
|
||
</label>
|
||
{/if}
|
||
{#if !resume}
|
||
<div class="card center stack">
|
||
<h2>Aucune journée enregistrée</h2>
|
||
<p class="dim">
|
||
Le récapitulatif de la dernière journée s'affichera ici, prêt à recopier.
|
||
</p>
|
||
</div>
|
||
{:else}
|
||
<div class="card stack">
|
||
{#each resume.clients as client (client.name)}
|
||
<div class="client">
|
||
<div class="row-between head">
|
||
<h2>{client.name}</h2>
|
||
<span class="tnum">{fmtDuration(client.totalMs)}</span>
|
||
</div>
|
||
<ul>
|
||
{#each client.tasks as task (task.name)}
|
||
<li class="row-between">
|
||
<span>{task.name}</span>
|
||
<span class="tnum">{fmtDuration(task.ms)}</span>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
</div>
|
||
{/each}
|
||
|
||
{#if resume.lunch}
|
||
<hr />
|
||
<div class="row-between">
|
||
<span
|
||
>Déjeuner : {resume.lunch.type === 'restaurant'
|
||
? `restaurant (${resume.lunch.restaurant || 'non précisé'})`
|
||
: 'panier'}</span
|
||
>
|
||
<span class="tnum">{fmtDuration(resume.lunch.ms)}</span>
|
||
</div>
|
||
{/if}
|
||
|
||
<hr />
|
||
<div class="row-between total">
|
||
<strong>Total travaillé</strong>
|
||
<strong class="tnum">{fmtDuration(resume.workedMs)}</strong>
|
||
</div>
|
||
|
||
{#if resume.omitted}
|
||
<p class="dim small">
|
||
{resume.omitted} poste{resume.omitted > 1 ? 's' : ''} de moins de {settings.roundingMinutes}
|
||
min non affiché{resume.omitted > 1 ? 's' : ''}.
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<button type="button" class="btn-primary" onclick={copy}>
|
||
{copied ? '✓ Copié' : 'Copier le récapitulatif'}
|
||
</button>
|
||
<button type="button" onclick={() => (editing = !editing)}>{editing ? 'Masquer les postes' : 'Corriger cette journée'}</button>
|
||
{#if editing}
|
||
{#key selectedDay.date}
|
||
<DayEditor day={selectedDay} entries={selectedEntries} onchange={() => (copied = false)} />
|
||
{/key}
|
||
{/if}
|
||
{/if}
|
||
|
||
{#if closedToday}
|
||
<button
|
||
type="button"
|
||
disabled={reopening}
|
||
onclick={reopen}>Reprendre la journée</button
|
||
>
|
||
<p class="dim small">Annule la clôture et continue la dernière tâche depuis l’arrêt.</p>
|
||
{:else if !dayState.day && !dayState.todayRecord && startsAt}
|
||
<p class="dim center small">
|
||
La journée démarrera automatiquement à {startsAt}.
|
||
</p>
|
||
{:else if !dayState.day && !dayState.todayRecord}
|
||
<p class="dim center small">Aujourd'hui n'est pas un jour travaillé.</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<style>
|
||
.client + .client {
|
||
border-top: 2px solid var(--line);
|
||
padding-top: var(--gap);
|
||
}
|
||
|
||
.head {
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0 0 0 var(--pad);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
li {
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.total {
|
||
font-size: 1.15rem;
|
||
}
|
||
|
||
.small {
|
||
font-size: 0.85rem;
|
||
}
|
||
</style>
|