Files
r701/oxibadge/src/lib/components/review/TopBar.svelte
T
nicolabelluti b65e85f908
Check, format and test / build (push) Successful in 10m22s
feat(oxibadge): add settings screen for shifts, rounding and language
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>
2026-10-08 18:29:57 +02:00

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>