Check, format and test / build (push) Successful in 10m25s
The review screen now tracks whether there are edits not yet exported and asks for confirmation before discarding them: clicking back, or closing the app window entirely (intercepted via onCloseRequested, which needed a core:window:allow-destroy capability that wasn't granted by core:default). The confirmation dialog (ConfirmDialog.svelte) is generic, built on a shared ModalFrame extracted from ExportModal so the two don't duplicate the backdrop/box styling. Also adds undo: a per-row button (icon-only, left of the note, shown when that day has edits) reverts just that day instantly, and a per-employee "undo all" (icon in a red box, last among the header's tools) reverts every day for the current employee after a confirmation dialog, since that one can't be undone itself. ActionButton moves out of review/ next to IconButton/PillButton, since ConfirmDialog (outside the review feature) now uses it too. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
294 lines
7.6 KiB
Svelte
294 lines
7.6 KiB
Svelte
<script lang="ts">
|
|
import { _ } from 'svelte-i18n';
|
|
import { autofocus } from '$lib/autofocus';
|
|
import type { Shifts } from '$lib/shifts';
|
|
import { dayWord } from './analysis';
|
|
import FixButton from './FixButton.svelte';
|
|
import IconButton from '../IconButton.svelte';
|
|
|
|
let {
|
|
name,
|
|
editing = $bindable(false),
|
|
draft = $bindable(''),
|
|
onsave,
|
|
worked,
|
|
totalLabel,
|
|
issues,
|
|
infos,
|
|
edits,
|
|
stackedCount,
|
|
lunchCount,
|
|
roundCount,
|
|
shift,
|
|
onfixall,
|
|
onaddlunchall,
|
|
onroundall,
|
|
onundoall,
|
|
onlyIssues = $bindable(false)
|
|
}: {
|
|
name: string;
|
|
editing?: boolean;
|
|
draft?: string;
|
|
onsave: (name: string) => void;
|
|
worked: number;
|
|
totalLabel: string;
|
|
issues: number;
|
|
infos: number;
|
|
edits: number;
|
|
stackedCount: number;
|
|
lunchCount: number;
|
|
roundCount: number;
|
|
shift: Shifts;
|
|
onfixall: () => void;
|
|
onaddlunchall: () => void;
|
|
onroundall: () => void;
|
|
onundoall: () => void;
|
|
onlyIssues?: boolean;
|
|
} = $props();
|
|
|
|
function startEdit() {
|
|
draft = name;
|
|
editing = true;
|
|
}
|
|
|
|
function saveName() {
|
|
const n = draft.trim();
|
|
if (n && n !== name) onsave(n);
|
|
editing = false;
|
|
}
|
|
|
|
</script>
|
|
|
|
<div class="main-head">
|
|
<div class="who">
|
|
{#if editing}
|
|
<div class="name-row">
|
|
<input
|
|
class="name-input"
|
|
type="text"
|
|
aria-label={$_('review.employee_name_aria')}
|
|
bind:value={draft}
|
|
use:autofocus
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter') saveName();
|
|
}}
|
|
/>
|
|
<IconButton accent ariaLabel={$_('review.save_name')} title={$_('review.save')} onclick={saveName}>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2.4"
|
|
><path d="M5 12.5l4.5 4.5L19 7.5" /></svg
|
|
>
|
|
{/snippet}
|
|
</IconButton>
|
|
<IconButton ariaLabel={$_('review.cancel')} title={$_('review.cancel')} onclick={() => (editing = false)}>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M6 6l12 12M18 6L6 18" /></svg
|
|
>
|
|
{/snippet}
|
|
</IconButton>
|
|
</div>
|
|
{:else}
|
|
<div class="name-row">
|
|
<h2>{name}</h2>
|
|
<IconButton ariaLabel={$_('review.edit_name')} title={$_('review.edit_name')} onclick={startEdit}>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4z" /><path d="M13.5 6.5l4 4" /></svg
|
|
>
|
|
{/snippet}
|
|
</IconButton>
|
|
</div>
|
|
{/if}
|
|
<div class="stats">
|
|
<span><b>{worked}</b> {$_('review.days_worked')}</span>
|
|
<span><b>{totalLabel}</b> {$_('review.total_hours')}</span>
|
|
<span><b class:red={issues > 0}>{issues}</b> {$_('review.to_check')}</span>
|
|
<span><b class:blue={infos > 0}>{infos}</b> {$_('review.to_round')}</span>
|
|
<span><b class:orange={edits > 0}>{edits}</b> {$_('review.edited')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tools">
|
|
{#if stackedCount}
|
|
<FixButton size="lg" title={$_('review.fix_shifts_title')} onclick={onfixall}>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M7 4v16M3 8l4-4 4 4M17 20V4M13 16l4 4 4-4" /></svg
|
|
>
|
|
{/snippet}
|
|
{$_('review.fix_shifts', { values: { days: dayWord(stackedCount, $_) } })}
|
|
</FixButton>
|
|
{/if}
|
|
{#if lunchCount}
|
|
<FixButton
|
|
size="lg"
|
|
title={$_('review.add_lunch_title', { values: { start: shift.out1, end: shift.in2 } })}
|
|
onclick={onaddlunchall}
|
|
>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M7 3v8M5 3v4a2 2 0 0 0 4 0V3M7 11v10M17 21V3c-2 1-3 3.5-3 7h3" /></svg
|
|
>
|
|
{/snippet}
|
|
{$_('review.add_lunch', { values: { days: dayWord(lunchCount, $_) } })}
|
|
</FixButton>
|
|
{/if}
|
|
{#if roundCount}
|
|
<FixButton size="lg" blue title={$_('review.round_title')} onclick={onroundall}>
|
|
{#snippet icon()}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><circle cx="12" cy="13" r="8" /><path d="M12 9v4l2.5 2.5M10 2h4" /></svg
|
|
>
|
|
{/snippet}
|
|
{$_('review.round_all', { values: { step: shift.round_step_min, days: dayWord(roundCount, $_) } })}
|
|
</FixButton>
|
|
{/if}
|
|
<div class="seg" role="group" aria-label={$_('review.day_filter_aria')}>
|
|
<button type="button" class:on={!onlyIssues} aria-pressed={!onlyIssues} onclick={() => (onlyIssues = false)}
|
|
>{$_('review.all_days')}</button
|
|
>
|
|
<button type="button" class:on={onlyIssues} aria-pressed={onlyIssues} onclick={() => (onlyIssues = true)}
|
|
>{$_('review.to_check_cap')}</button
|
|
>
|
|
</div>
|
|
{#if edits}
|
|
<IconButton
|
|
size="lg"
|
|
dangerFilled
|
|
ariaLabel={$_('review.undo_all')}
|
|
title={$_('review.undo_all_title')}
|
|
onclick={onundoall}
|
|
>
|
|
{#snippet icon()}
|
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M9 14l-5-5 5-5" /><path d="M20 20v-7a4 4 0 0 0-4-4H4" /></svg
|
|
>
|
|
{/snippet}
|
|
</IconButton>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.main-head {
|
|
flex: none;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 12px 24px;
|
|
}
|
|
|
|
.who {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.name-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-height: 38px;
|
|
}
|
|
|
|
.name-row h2 {
|
|
margin: 0;
|
|
font-family: var(--ox-font-display);
|
|
font-weight: 600;
|
|
font-size: 22px;
|
|
letter-spacing: -0.02em;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.name-input {
|
|
all: unset;
|
|
box-sizing: border-box;
|
|
height: 38px;
|
|
width: 300px;
|
|
max-width: 100%;
|
|
padding: 0 10px;
|
|
border: 1px solid var(--ox-accent);
|
|
box-shadow: 0 0 0 4px var(--ox-accent-ring);
|
|
border-radius: 10px;
|
|
font-family: var(--ox-font-display);
|
|
font-weight: 600;
|
|
font-size: 22px;
|
|
letter-spacing: -0.02em;
|
|
color: #ffffff;
|
|
caret-color: var(--ox-accent);
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px 18px;
|
|
font-size: 13px;
|
|
color: #8a8a8a;
|
|
}
|
|
|
|
.stats b {
|
|
font-family: var(--ox-font-mono);
|
|
font-weight: 400;
|
|
color: #e6e6e6;
|
|
}
|
|
|
|
/* .stats b's own color above otherwise beats the global .red/.blue/.orange
|
|
utility on specificity, since it combines a class with a tag. */
|
|
.stats b.red {
|
|
color: #ff9ea3;
|
|
}
|
|
|
|
.stats b.blue {
|
|
color: #8db8ff;
|
|
}
|
|
|
|
.stats b.orange {
|
|
color: #ff9a66;
|
|
}
|
|
|
|
.tools {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.seg {
|
|
display: flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.seg button {
|
|
all: unset;
|
|
box-sizing: border-box;
|
|
height: 32px;
|
|
padding: 0 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #a3a3a3;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.seg button:hover {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.seg button:focus-visible {
|
|
outline: 2px solid var(--ox-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.seg button.on {
|
|
background: #2a2a2a;
|
|
color: #ffffff;
|
|
}
|
|
</style>
|