Check, format and test / build (push) Successful in 10m22s
Add a settings screen (gear button on the month-confirmation screen) to configure the expected shift times, the rounding step, the interface language, and the time clock address — replacing the old onboarding-only connection flow. Settings persist to the same preferences.toml as the reader address, under a single load_settings/ save_settings command pair. The review screen's "fix shifts" action now redistributes stacked punches by nearest configured shift time instead of blind position, and a stacked cell manually cleared down to one entry no longer leaves a dangling empty input. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
315 lines
7.4 KiB
Svelte
315 lines
7.4 KiB
Svelte
<script lang="ts">
|
|
import { _, locale } from 'svelte-i18n';
|
|
import { monthName } from '$lib/month';
|
|
import { dayWord, shortSummary } from './analysis';
|
|
import IconButton from '../IconButton.svelte';
|
|
|
|
let {
|
|
year,
|
|
month,
|
|
issuesAll,
|
|
infosAll,
|
|
onback,
|
|
menuOpen = $bindable(false),
|
|
employeeCount,
|
|
currentName,
|
|
currentIssues,
|
|
currentInfos,
|
|
onexportall,
|
|
onexportone
|
|
}: {
|
|
year: number;
|
|
month: number;
|
|
issuesAll: number;
|
|
infosAll: number;
|
|
onback?: () => void;
|
|
menuOpen?: boolean;
|
|
employeeCount: number;
|
|
currentName: string;
|
|
currentIssues: number;
|
|
currentInfos: number;
|
|
onexportall: () => void;
|
|
onexportone: () => void;
|
|
} = $props();
|
|
</script>
|
|
|
|
<header class="topbar">
|
|
<div class="tb-left">
|
|
{#if onback}
|
|
<IconButton size="lg" ariaLabel={$_('review.back_to_month')} onclick={onback}>
|
|
{#snippet icon()}
|
|
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style="stroke-width: 2"
|
|
><path d="M15 6l-6 6 6 6" /></svg
|
|
>
|
|
{/snippet}
|
|
</IconButton>
|
|
{/if}
|
|
<div class="tb-title">
|
|
<span>{$_('review.title')}</span>
|
|
<h1>{monthName($locale, month)} {year}</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tb-right">
|
|
{#if issuesAll}
|
|
<span class="tb-count red">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"
|
|
><path d="M10.3 3.9L2.4 17.5A2 2 0 0 0 4.1 20.5h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" /><path
|
|
d="M12 9.5v4M12 17h.01"
|
|
/></svg
|
|
>
|
|
{dayWord(issuesAll, $_)} {$_('review.to_check')}
|
|
</span>
|
|
{/if}
|
|
{#if infosAll}
|
|
<span class="tb-count blue">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"
|
|
><circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 8h.01" /></svg
|
|
>
|
|
{dayWord(infosAll, $_)} {$_('review.to_round')}
|
|
</span>
|
|
{/if}
|
|
|
|
<!-- Bottone diviso Esporta -->
|
|
<div class="split-wrap">
|
|
<div class="split">
|
|
<button type="button" class="split-main" onclick={onexportall}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"
|
|
><path d="M12 4v11M7 10l5 5 5-5M5 20h14" /></svg
|
|
>
|
|
<span>{$_('review.export')}</span>
|
|
</button>
|
|
<span class="split-sep" aria-hidden="true"></span>
|
|
<button
|
|
type="button"
|
|
class="split-arrow"
|
|
aria-haspopup="menu"
|
|
aria-expanded={menuOpen}
|
|
aria-label={$_('review.export_options_aria')}
|
|
onclick={() => (menuOpen = !menuOpen)}
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
|
|
</button>
|
|
</div>
|
|
|
|
{#if menuOpen}
|
|
<div class="dropdown" role="menu" aria-label={$_('review.export')}>
|
|
<button type="button" role="menuitem" class="menu-item" onclick={onexportall}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"
|
|
><circle cx="9" cy="8" r="3.5" /><path d="M2.5 20c.8-3.3 3.3-5 6.5-5s5.7 1.7 6.5 5" /><path
|
|
d="M16 4.5a3.5 3.5 0 0 1 0 7M18 15.2c1.8.7 3 2.3 3.5 4.8"
|
|
/></svg
|
|
>
|
|
<span class="mi-col">
|
|
<span class="mi-title">{$_('review.all_employees')}</span>
|
|
<span class="mi-sub" class:red={issuesAll > 0} class:blue={!issuesAll && infosAll > 0}>
|
|
{$_('review.employee_count', { values: { n: employeeCount } })} ·
|
|
{shortSummary(issuesAll, infosAll, $_) || $_('review.all_good')}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
<button type="button" role="menuitem" class="menu-item" onclick={onexportone}>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"
|
|
><circle cx="12" cy="8" r="3.5" /><path d="M5 20c.8-3.3 3.6-5 7-5s6.2 1.7 7 5" /></svg
|
|
>
|
|
<span class="mi-col">
|
|
<span class="mi-title">{$_('review.only_name', { values: { name: currentName } })}</span>
|
|
<span class="mi-sub" class:red={currentIssues > 0} class:blue={!currentIssues && currentInfos > 0}>
|
|
{shortSummary(currentIssues, currentInfos, $_) || $_('review.all_good_cap')}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<style>
|
|
.topbar {
|
|
flex: none;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid #222222;
|
|
}
|
|
|
|
.tb-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.tb-title {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.tb-title span {
|
|
font-size: 12.5px;
|
|
color: #8a8a8a;
|
|
}
|
|
|
|
.tb-title h1 {
|
|
margin: 0;
|
|
font-family: var(--ox-font-display);
|
|
font-weight: 700;
|
|
font-size: 26px;
|
|
line-height: 1.1;
|
|
letter-spacing: -0.025em;
|
|
color: var(--ox-text-strong);
|
|
}
|
|
|
|
.tb-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tb-count {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13.5px;
|
|
}
|
|
|
|
/* Bottone diviso */
|
|
.split-wrap {
|
|
position: relative;
|
|
z-index: 21;
|
|
}
|
|
|
|
.split {
|
|
display: flex;
|
|
align-items: stretch;
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
background: var(--ox-accent);
|
|
color: var(--ox-on-accent);
|
|
}
|
|
|
|
.split-main,
|
|
.split-arrow {
|
|
all: unset;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 14.5px;
|
|
font-weight: 600;
|
|
color: var(--ox-on-accent);
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.split-main {
|
|
gap: 10px;
|
|
padding: 0 16px 0 18px;
|
|
border-radius: 12px 0 0 12px;
|
|
}
|
|
|
|
.split-arrow {
|
|
width: 40px;
|
|
justify-content: center;
|
|
border-radius: 0 12px 12px 0;
|
|
}
|
|
|
|
.split-arrow svg {
|
|
stroke-width: 2.2;
|
|
}
|
|
|
|
.split-main:hover,
|
|
.split-arrow:hover {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
}
|
|
|
|
.split-main:active,
|
|
.split-arrow:active {
|
|
background: rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.split-main:focus-visible,
|
|
.split-arrow:focus-visible {
|
|
outline: 2px solid #ffffff;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.split-sep {
|
|
width: 1px;
|
|
margin: 10px 0;
|
|
background: rgba(26, 7, 0, 0.28);
|
|
}
|
|
|
|
.dropdown {
|
|
position: absolute;
|
|
top: calc(100% + 8px);
|
|
right: 0;
|
|
width: 300px;
|
|
box-sizing: border-box;
|
|
padding: 6px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333333;
|
|
border-radius: 14px;
|
|
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.55);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.menu-item {
|
|
all: unset;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
color: #bdbdbd;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background: #252525;
|
|
}
|
|
|
|
.menu-item:focus-visible {
|
|
outline: 2px solid var(--ox-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.menu-item > svg {
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.mi-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.mi-title {
|
|
font-weight: 500;
|
|
color: #f2f2f2;
|
|
}
|
|
|
|
.mi-sub {
|
|
font-size: 12.5px;
|
|
color: #8a8a8a;
|
|
}
|
|
|
|
.mi-sub.red {
|
|
color: #ff9ea3;
|
|
}
|
|
|
|
.mi-sub.blue {
|
|
color: #8db8ff;
|
|
}
|
|
</style>
|