Files
pardis/src/components/DayEditor.svelte
2026-09-19 21:53:10 +02:00

123 lines
6.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { insertRecordedEntry, localInput, removeRecordedEntry, saveRecordedEntry } from '../lib/editing.js';
import { dayState, refresh } from '../lib/day.svelte.js';
import { fmtTime, MINUTE } from '../lib/time.js';
import Sheet from './Sheet.svelte';
import Timeline from './Timeline.svelte';
let { day, entries = [], now = Date.now(), onchange = () => {} } = $props();
let mode = $state('');
let original = $state(null);
let draft = $state({ kind: 'work', task: '', client: '', start: '', end: '', lunchType: 'panier', restaurant: '' });
let busy = $state(false);
let error = $state('');
let confirmingDelete = $state(false);
const ordered = $derived([...entries].sort((a, b) => a.seq - b.seq));
function select(entry, operation = mode) {
original = { ...entry };
mode = operation;
error = '';
confirmingDelete = false;
const insertion = operation === 'insert';
draft = {
kind: insertion ? 'work' : entry.kind,
task: insertion ? '' : entry.task,
client: entry.client ?? '',
start: localInput(entry.startTs),
end: localInput(insertion ? Math.min(entry.startTs + 30 * MINUTE, entry.endTs ?? now) : entry.endTs ?? now),
lunchType: day.lunchType ?? 'panier', restaurant: day.restaurant ?? '',
};
}
function close() { if (!busy) { mode = ''; original = null; } }
async function commit(remove = false) {
if (busy || dayState.importing) return;
busy = true; error = '';
try {
// Preserve seconds when only text changes; typed times have minute precision.
const startTs = draft.start === localInput(original.startTs) ? original.startTs : new Date(draft.start).getTime();
const endTs = mode === 'edit' && original.endTs == null ? null :
mode === 'edit' && draft.end === localInput(original.endTs) ? original.endTs : new Date(draft.end).getTime();
const values = { ...draft, startTs, endTs };
if (remove) await removeRecordedEntry(original);
else if (mode === 'insert') await insertRecordedEntry(original, values);
else await saveRecordedEntry(original, values);
await refresh();
onchange();
mode = ''; original = null;
} catch (cause) { error = cause.message; }
finally { busy = false; }
}
</script>
<div class="card stack">
<h3>Postes de la journée</h3>
<p class="dim hint">Touchez un poste pour corriger ses horaires, sa tâche ou son client.</p>
<p class="dim hint">Horaires à la minute ; le récapitulatif applique l’arrondi choisi dans les réglages.</p>
<Timeline {entries} rounding={1} {now} onselect={(entry) => select(entry, 'edit')} />
<button type="button" disabled={!ordered.length || dayState.importing} onclick={() => select(ordered[0], 'insert')}>Ajouter un poste oublié</button>
</div>
<Sheet open={Boolean(mode)} title={mode === 'insert' ? 'Ajouter un poste' : 'Modifier le poste'} onclose={close}>
{#if original}
<form onsubmit={(event) => { event.preventDefault(); commit(); }}>
<fieldset disabled={busy || dayState.importing} class="stack">
{#if mode === 'insert'}
<label class="stack">Plage à corriger
<select value={original.id} onchange={(event) => select(ordered.find((e) => e.id === Number(event.currentTarget.value)), 'insert')}>
{#each ordered as entry (entry.id)}
<option value={entry.id}>{fmtTime(entry.startTs)}–{entry.endTs == null ? 'en cours' : fmtTime(entry.endTs)} · {entry.task}</option>
{/each}
</select>
</label>
<p class="dim hint">Le poste oublié sera inséré dans cette plage. Les portions avant et après seront conservées.</p>
{:else}
<p class="dim hint">Changer une limite ajuste aussi le poste voisin. Le premier début et la dernière fin définissent la durée de la journée.</p>
{/if}
<label class="stack">Type de poste
<select bind:value={draft.kind}>
<option value="work">Travail</option><option value="travel">Transport</option><option value="lunch">Repas</option>
</select>
</label>
<label class="stack">Début du poste<input type="datetime-local" required bind:value={draft.start} /></label>
{#if mode === 'edit' && original.endTs == null}
<p class="dim hint">Poste en cours : sa fin sera enregistrée au prochain changement de tâche.</p>
{:else}
<label class="stack">Fin du poste<input type="datetime-local" required bind:value={draft.end} /></label>
{/if}
{#if draft.kind === 'lunch'}
<label class="stack">Type de repas<select bind:value={draft.lunchType}><option value="panier">Panier</option><option value="restaurant">Restaurant</option></select></label>
{#if draft.lunchType === 'restaurant'}
<label class="stack">Restaurant<input type="text" bind:value={draft.restaurant} /></label>
{/if}
{:else}
<label class="stack">Tâche<input type="text" required bind:value={draft.task} /></label>
<label class="stack">Client<input type="text" bind:value={draft.client} /></label>
{/if}
{#if day.status === 'closed'}<p class="dim hint">La journée restera terminée après la correction.</p>{/if}
{#if error}<p role="alert">{error}</p>{/if}
<button type="submit" class="btn-primary">{busy ? 'Enregistrement…' : 'Enregistrer'}</button>
<button type="button" onclick={close}>Annuler</button>
{#if mode === 'edit' && original.endTs != null && entries.length > 1}
{#if confirmingDelete}
<p class="hint">Le poste voisin absorbera cette durée. Le total de la journée restera inchangé.</p>
<button type="button" class="btn-danger" onclick={() => commit(true)}>Confirmer la suppression</button>
{:else}
<button type="button" onclick={() => (confirmingDelete = true)}>Supprimer ce poste</button>
{/if}
{/if}
</fieldset>
</form>
{/if}
</Sheet>
<style>
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.hint { font-size: 0.85rem; }
input, select { width: 100%; min-width: 0; }
input[type='datetime-local'] { font: inherit; color: var(--fg); background: var(--bg); border: var(--border) solid var(--line); border-radius: var(--radius); min-height: var(--tap); padding: 8px; }
[role='alert'] { color: var(--danger); }
</style>