Commit Graph

349 Commits

Author SHA1 Message Date
BOHA
28fb399217 feat(odin): refine hero — Newsreader serif, red name, staggered entrance
- Swap Fraunces → Newsreader (literary serif, full Czech, optical sizing).
- Split the greeting so the user's first name renders in brand-red italic.
- Add a subtle staggered fade-in (framer-motion), skipped under
  prefers-reduced-motion.
- New description that names Odin and covers chat + invoice import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:03:16 +02:00
BOHA
c0ff7b4549 feat(odin): editorial serif hero — Fraunces greeting + italic subtitle
Drop the gradient "Odin" wordmark from the new-conversation hero and make the
personalized greeting the centerpiece in Fraunces (optical-sized serif), with
an italic Fraunces description — a refined, literary, claude-like treatment.
Loads Fraunces (upright + italic) via the existing Google Fonts link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:57:15 +02:00
BOHA
1e21c12f33 feat(odin): personalized greeting hero on the new-conversation screen
Time-based Czech greeting + the user's first name (e.g. "Dobrý večer, Admin")
above the big gradient "Odin" wordmark, with a stylish description line. Drop
the redundant mark from the hero (it lives on the nav item + thinking state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:51:00 +02:00
BOHA
ab12f9d791 feat(odin): sidebar brand is a big gradient "Odin" wordmark (drop the icon)
The mark already lives on the nav menu item; the conversation sidebar's brand
no longer duplicates it — just a larger gradient "Odin" wordmark above the new
chat button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:47:39 +02:00
BOHA
cf442b9a99 feat(odin): big claude.ai-style hero on the new-conversation screen
Replace the small empty-state with a large centred hero — the animated Odin
mark + a big gradient "Odin" wordmark (responsive clamp font) and the helper
line beneath.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:44:38 +02:00
BOHA
2e7caea5a6 feat(odin): use the Odin mark as the sidebar nav icon
Replace the chat-bubble glyph on the "Odin" nav item with the OdinMark, and
make the mark's SVG sizing !important so the nav's `& svg { width:18 }` rule
can't shrink it (its `size` prop stays authoritative everywhere).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:41:20 +02:00
BOHA
702a4984ee feat(odin): animated Odin brand mark (AI core orbiting business data)
A self-contained animated SVG mark in BOHA red: a 4-point AI spark (Odin) that
slow-rotates and breathes, a data node orbiting a faint ring (the assistant at
the centre of the business system). An "idle" and a "thinking" state (faster
orbit + red glow); honours prefers-reduced-motion. Used as the sidebar brand,
the empty-state hero, and the busy/thinking indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:31:48 +02:00
BOHA
769767c33d feat(odin): left sidebar + lazy conversation creation (claude.ai-style)
- Replace top tabs with a left sidebar (New chat + scrollable conversation
  list, per-row rename/delete menu, active highlight), chat on the right.
- Fix duplicate-empty-conversations bug: "Nová konverzace" now just opens an
  empty composer; a DB row is created only on the first successful message
  (and a failed AI call no longer leaves an orphan conversation). Repeated
  clicks never spawn empties, and nothing empty survives a refresh.
- Land on a fresh new chat on mount; deleting the open conversation returns
  to new chat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:07:07 +02:00
BOHA
d62abe81fd feat(odin): use the full page (full width + taller shell)
Drop the 1100px max-width cap and size the chat shell to fill the content
area (100dvh - 100px = top bar + main bottom padding), so Odin uses the whole
page now that it owns the route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:51:23 +02:00
BOHA
ac7c220bb1 fix(odin): gate Uložit on invoices.create + correct invoice invalidation key
Final review caught two minor issues:
- saveInvoice invalidated ["received-invoices"], which matches no query (the
  received list/stats are keyed ["invoices","received",…]); use ["invoices"]
  so the list/stats actually refresh after an import.
- The Save button hit /received-invoices (requires invoices.create) while Odin
  is gated only on ai.use; disable Uložit (with a hint) when the user lacks
  invoices.create so the UI never offers a 403 action. Dormant today (ai.use is
  admin-only and admin bypasses checks) but correct going forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:46:49 +02:00
BOHA
b7ebf04001 feat(odin): OdinChat orchestrator; render on /odin; remove DashAssistant
Wire OdinTabs/OdinThread/InvoiceReviewCard/OdinComposer into a single
OdinChat orchestrator; mount it on /odin (maxWidth 1100); delete the
old DashAssistant and its aiHistoryOptions query helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:39:15 +02:00
BOHA
441f46a471 feat(odin): tabs, thread, review-card, composer components
Add four presentational components + shared types under
src/admin/components/odin/ as props-driven building blocks for the
multi-conversation Odin page (Task 6 wires state/queries).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:34:22 +02:00
BOHA
f011a67ff3 feat(odin): conversation query options 2026-06-08 19:28:04 +02:00
BOHA
e4ec08a5ef feat(ai): move the assistant to its own "Odin" sidebar page
Promote the AI chat from a dashboard widget to a dedicated page:
- new sidebar item "Odin" under the Přehled section, gated on ai.use, → /odin
- new /odin route + Odin page (reuses the assistant component, with a light
  ai.use guard)
- remove the widget from the dashboard
- rebrand the assistant header "Asistent" → "Odin" and give the thread more
  height now that it owns the page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:38:35 +02:00
BOHA
f4bc038f8b fix(received-invoices): treat amount as gross (VAT-inclusive), back-calc VAT
The amount on a received invoice is the GROSS total the user pays (VAT
included), and VAT is the portion contained within it — not a net base with
VAT added on top. Switch the formula from amount*rate/100 to the
VAT-inclusive amount*rate/(100+rate) via a shared, tested vatFromGross()
helper used by both the create (manual upload + AI import share this
endpoint) and edit paths. Rate 0 → no VAT.

- AI extraction now asks Claude for the total INCLUDING VAT (was "základ bez
  DPH"), reinforced in the structured-output schema description.
- Label the amount field "Částka s DPH" in the AI review card and the manual
  upload/edit forms so the gross convention is explicit.
- Add unit tests with the user-confirmed figures (22 542,91 @ 21% → 3 912,41,
  base 18 630,50) plus other rates and the no-VAT case.

Existing rows keep their stored (gross) amounts; their vat_amount recomputes
on next edit. A one-shot backfill can correct historical vat_amount if wanted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:27:56 +02:00
BOHA
58e5fd2b1d feat(ai): server-side chat history + assistant UX fixes
Server-side, per-user chat history (the thread now survives reload):
- new ai_chat_messages table + migration; getChatHistory/appendChatMessages/
  clearChatHistory; GET/POST/DELETE /api/admin/ai/history (ai.use-guarded);
  service + HTTP tests (isolation, ordering, clear, perm, validation).
- DashAssistant loads the thread on mount and persists each turn; "Vymazat"
  clears it. gcTime:0 so each mount re-reads the DB (no stale-cache resurrection);
  composer gated until history settles + seed flag flipped on submit (no
  seed/submit race clobbering a freshly-sent turn).

UX + correctness fixes from review/feedback:
- review cards moved OUT of the scrollable thread into a "Faktury k potvrzení"
  section so Uložit is always reachable; chat auto-scroll no longer fires on
  field edits.
- chat bubble text colour set on the Typography (GlobalStyles pins `p` to
  text.secondary, which was overriding the inherited bubble colour → unreadable).
- never clear input/staged PDFs on a failed submit (rollback + keep), so a
  budget error can't wipe staged invoices; roll back the optimistic user turn.
- stable attachment ids (no crypto.randomUUID — undefined over plain HTTP).
- editable Datum splatnosti + Popis fields; client-side extraction summary.

Security: DashProfile clipboard copy now uses an execCommand fallback (works
over plain HTTP) and only shows the success toast when the copy actually
succeeded — previously it falsely claimed to copy 2FA codes / the TOTP secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:02:26 +02:00
BOHA
b665ea1d9b feat(ai): dashboard chat widget with invoice import + budget indicator 2026-06-08 15:01:52 +02:00
BOHA
8bed920de1 feat(dashboard): show every active employee in "Docházka dnes"
The attendance card listed only employees with a record today (present /
break / clocked-out / on-leave). Now every active employee appears;
those with no record today show as "absent" (Nepřítomen), so the card
reflects the whole team. Loads active users (was a bare count) and
appends the unrecorded ones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:00:09 +02:00
BOHA
1ddc0feccd fix(plan): always show notes for every record in a multi-record cell
Previously notes were hidden when a cell held 2-3 records (a density
choice). Show each record's note regardless of count — drop the
now-unused showNote prop on PlanRangeChips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:53:45 +02:00
BOHA
13d1fc2be3 feat(plan): bulk assignment modal on /plan-work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:11:20 +02:00
BOHA
23ac924472 fix(plan): edit/add the clicked record + layer-aware add in exception mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:29:52 +02:00
BOHA
9370423fb0 feat(plan): day panel — list/add/edit up to 3 records per cell
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:16:52 +02:00
BOHA
a146fc26a3 feat(plan): grid renders up to 3 stacked records per cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:08:48 +02:00
BOHA
72888bf9cd feat(plan): resolveCell/resolveGrid return arrays; dashboard shows up to 3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:53:26 +02:00
BOHA
67d62a6df0 fix(dashboard): rename today-plan card to "Vaše dnešní zařazení"
Clearer, formal (matches the app's Vy/Vaše tone) and category-agnostic
(reads fine for work and absence days) so the employee plainly sees what
they're assigned to. Was "Můj plán na dnešek".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:25:35 +02:00
BOHA
512f1fd92b feat(dashboard): show the user's work plan for today
Adds a "Můj plán na dnešek" card near the clock-in: for the logged-in
user it shows today's work-plan entry — category (coloured dot + label),
the project/site (the "where"), the note, and a "součást rozsahu …" badge
only for genuinely multi-day ranges. A muted "Pro dnešek nemáte
naplánováno." empty state when nothing is scheduled.

Server: GET /api/admin/dashboard now returns today_plan, reusing
plan.service resolveCell (override-beats-range precedence) for the current
user + today's local (Prague) date, enriched with the category label +
colour so the widget needs no extra query. Gated by attendance.record /
attendance.manage (the plan permission); omitted otherwise. No migration.

Verified in Chrome: renders today's entry (Práce · OBJ-2026-006 — koko)
with the category colour. tsc -b --noEmit, npm run build, vitest 152/152.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:22:07 +02:00
BOHA
b00d18d0b3 fix(plan): drop duplicate 'Zrušit' in the view-mode cell modal
The read-only plan-cell detail modal (view mode) already has a "Zavřít"
submit button, but the kit Modal also rendered its built-in "Zrušit"
cancel — two buttons that do the same thing. Pass hideCancel so only
"Zavřít" remains (same fix as the category-management modal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:54:42 +02:00
BOHA
decadd895e fix(ui): stack detail-page header action buttons one-per-row on mobile
Detail-page headers put their action buttons in a flex row with
flexWrap:"wrap", so on a phone the 2-4 buttons (Potvrzení / Zahájit
realizaci / Smazat, etc.) wrapped into a ragged grid.

New shared `headerActionsSx` (ui/PageHeader): on xs it's a full-width
column (each button its own full-width row); from sm up it's the usual
right-aligned wrapping row — desktop unchanged. A StatusChip kept in the
same group stays its natural size (alignSelf) instead of stretching.

Applied to PageHeader's own actions slot and every detail-page header
action group: Order/Offer/Invoice (both views)/Project + the Warehouse
Receipt/Issue/Item/Inventory detail headers.

Verified in Chrome at 430px on OrderDetail: the group is flexDirection
column with all three buttons at the same x, full-width, stacked. tsc -b
--noEmit, npm run build, vitest 152/152 clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:44:22 +02:00
BOHA
54f3c414f5 fix(ui): unify icon badges — solid tile + white glyph via shared helper
The labelled icon-badge tiles were inconsistent: some used a same-hue
light tile + colored glyph (e.g. the Settings 2FA row: success.light tile
+ success.main glyph — low contrast, the reported "badly visible" icon),
and the solid-tile ones used the bright .main fill in dark mode without
darkening, so a white glyph on bright green/amber was low-contrast too
(DashProfile's success badge was ~1.9:1).

Add a single `iconBadgeSx(color)` helper (theme.ts): solid `<color>.main`
fill + white glyph, darkened in dark mode (FILLED_DARK_BG) so white stays
legible; `default` = neutral grey. Applied to every badge so they can't
drift: StatCard, DashActivityFeed, DashProfile (2FA badge), Dashboard
(2FA banner icon), AttendanceHistory (month tile), and the Settings 2FA
row tile (now solid green when required / grey when optional + white lock,
instead of the low-contrast light-green tile). Also switched DashProfile's
backup-codes warning callout from warning.light to a subtle warning wash
so its amber text stays readable in dark mode.

tsc -b --noEmit, npm run build, vitest 152/152 all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:34:06 +02:00
BOHA
ca092c6166 fix(ui): unify colored-control text — white on all filled, both themes
The text color on filled colored controls was inconsistent: contained
primary resolved to white in both schemes, but error/success/warning/info
used a per-scheme contrastText (#fff light, #1a1a1a dark). So in DARK mode
a primary (red) button showed white text while an error (red) button —
e.g. every ConfirmDialog delete confirm, the contained "Smazat", and the
dashboard quick-action tiles — showed near-black text. "Black text on one
red button, white on another."

Unify on a single rule: every FILLED colored surface gets WHITE text/glyph
in BOTH themes. The palette .main stays bright in dark mode (it drives text,
outlined buttons and row tints), but a bright fill makes white illegible, so
filled surfaces drop to a darker fill in dark mode (new FILLED_DARK_BG, ≈ the
light-scheme shades; all ≥4.5:1 with white):
- theme MuiButton: contained error/success/warning/info -> white text +
  darker dark-mode fill (disabled state preserved).
- theme MuiChip: filled error/success/warning/info -> white label + darker
  dark-mode fill (covers StatusChip + status pills).
- StatCard icon badges: white glyph + darker dark-mode tile.

Plus two consistency cleanups:
- Detail-page "Smazat" unified to variant=outlined color=error (Offer +
  Invoice now match Order/Project/Warehouse; the contained->confirm pattern
  stays: subtle trigger, strong contained confirm dialog).
- "Zrušit filtry" reset buttons -> color=inherit (neutral), matching the
  rest of the secondary-button family (were default outlined-primary).

Neutral (color=inherit) text/outlined buttons were already correct (label =
ambient text color) and are untouched. tsc -b --noEmit, npm run build and
vitest 152/152 all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:41:50 +02:00
BOHA
2e4fe2a8fe fix(offers): stop heartbeat 401 spam on dead session + de-dup item keys
Three console issues observed while idle on an offer:

1. Heartbeat flooded the console with 401s. The lock-keepalive interval
   fired apiFetch(...).catch(() => {}) every 10s; apiFetch RESOLVES with a
   401 (it doesn't throw), so .catch never saw it and the interval never
   stopped. Once the refresh token had also expired, every tick 401'd
   forever. Now we inspect the response and clearInterval on a 401 (which
   only occurs after a failed refresh = session truly gone); the healthy
   path still returns 200 and keeps ticking (verified in Chrome).

2. Duplicate React key "item-1" in the line-items table. The form/items/
   sections useState initializers restored the create-mode draft
   UNCONDITIONALLY — even in edit mode — and reused the draft's stale
   item _key values while itemKeyCounter restarts at 0 each load, so an
   added row collided. Now the draft is only restored when !isEdit, and
   restored items are re-keyed with fresh unique _keys.

3. framer-motion "motion() is deprecated" warning — PageEnter used the
   deprecated motion(Box) factory; switched to motion.create(Box).

tsc -b --noEmit, npm run build, vitest 152/152 clean. Console verified
clean in Chrome (only benign dev notices remain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:28:49 +02:00
BOHA
27c690285a fix(theme): white icon glyphs on solid tiles + single theme storage key
Two issues:

1. Icon badges read as a coloured glyph on a same-hue LIGHT tile
   (bgcolor X.light + color X.main) — low contrast, "badly visible".
   The AttendanceHistory month tile and the Dashboard 2FA-banner icon now
   use a SOLID tile (X.main) + white glyph, matching the StatCard badge
   convention. Readable in both schemes (verified: info.main #1d4ed8 light
   / #3b82f6 dark, error.main, white glyph).

2. Theme reverted to light on F5 while the toggle still showed dark.
   Cause: two independent stores — our ThemeContext key (boha-theme) and
   MUI's own cssVariables mode key (mui-mode). The toggle only wrote
   boha-theme, so on mount MUI restored its stale mui-mode and overrode
   data-theme. Collapsed to a SINGLE source of truth: ThemeContext now
   reads/writes MUI's `mui-mode` key directly (with a one-time migration
   from the legacy boha-theme, which is then removed). Verified: toggle
   dark -> F5 -> stays dark; only `mui-mode` remains in localStorage.

tsc -b --noEmit, npm run build, vitest 152/152 clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:12:20 +02:00
BOHA
8e7ab9158c fix(ui): readable status row tints — subtle washes, not solid .light fills
The per-status row backgrounds used MUI's solid `.light` palette shades
(error.light/success.light/warning.light). Those are LIGHT colors in both
schemes, so in dark mode the row's white text sat on a light fill and was
effectively invisible; in light mode they read as garish saturated blocks.

Replaced with low-alpha washes via the palette channel vars
(rgba(var(--mui-palette-X-mainChannel) / 0.12), 0.18 on hover), which keep
the row's normal theme text colour fully readable in BOTH schemes:

- Offers: completed = subtle green wash; invalidated = faded/muted
  (opacity + secondary text, the original "voided" look) instead of a
  solid red block; expired = subtle amber wash.
- DataTable rowDanger (kit-wide — Warehouse, WarehouseReports): subtle
  error wash for both the desktop row and the mobile card (card keeps its
  error.main border as the danger cue).
- Invoices overdue rows: subtle amber wash.
- Dashboard 2FA banner Card: subtle error wash (keeps the error.main
  border) so the banner copy stays readable in dark mode.

Verified live in Chrome on Offers (completed + invalidated, light + dark):
faint tint + normal readable text, channel switches per scheme. tsc -b
--noEmit, npm run build, vitest 152/152 all clean. (Left untouched: small
icon tiles / callout boxes that pair a .light bg with an explicit dark or
coloured text/glyph — those are readable in both schemes by construction.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:57:58 +02:00
BOHA
03285c6ff6 refactor(css): migrate Login + file icons off legacy vars, delete variables.css
Phase 4 (final) of the "fully MUI" cleanup — no custom stylesheets remain
in src/admin (only the third-party quill.snow.css + leaflet.css imports).

- Login.tsx: every var(--glass-*/bg-*/text-*/accent-*/orb-*/border-*/
  font-*/transition) in its sx/inline styles is now an MUI token
  (bgcolor/color/borderColor: "background.paper" / "text.secondary" /
  "divider" / "primary.main"), a channel-alpha (rgba(var(--mui-palette-
  primary-mainChannel) / a)) for tints, or theme.applyStyles("dark", …)
  for the glass-card shadow. The card sx is now a (theme)=>({}) callback.
- Restores the orb drift animation via the MUI keyframes() helper — the
  @keyframes float had been dropped in Phase 1 (the dead-CSS scan missed
  its use inside an sx animation string); recovered verbatim from git.
- ProjectFileManager: the fallback file-icon color uses
  var(--mui-palette-text-secondary).
- variables.css deleted; its import removed from AdminApp (last one).

tsc -b --noEmit + npm run build clean. applyStyles dark-scheme output and
the channel-alpha pattern were both verified live in Chrome via the Quill
editor, which uses the identical constructs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:45:36 +02:00
BOHA
d424ef8c15 refactor(css): scope Quill editor into styled() and delete offers.css
Phase 3 of the "fully MUI" cleanup.

- New src/admin/ui/RichText.tsx exports two theme-aware styled() wrappers:
  RichEditorRoot (the .admin-rich-editor Quill overrides) and RichTextView
  (the .admin-rich-text-view read-only output). All colors resolve via
  theme.vars; picker/tooltip shadows use theme.applyStyles for the
  light/dark difference; the active-button tint uses the primary channel.
- Dropped the ~170 dead font/size picker rules from the old stylesheet —
  the editor toolbar exposes neither picker. Kept the .ql-font-* content
  classes (shared by editor + view) so legacy stored HTML still renders.
- RichEditor uses <RichEditorRoot> (min-height still via --re-min-height);
  InvoiceDetail + OrderDetail use <RichTextView> for the sanitized output.
- offers.css deleted; its import removed from AdminApp.

tsc -b --noEmit and npm run build clean. Verified the editor in Chrome
(light + dark): toolbar, editor surface, text, icon strokes and min-height
all correct and theme-reactive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:37:11 +02:00
BOHA
2a228ea659 refactor(css): migrate base.css to MUI GlobalStyles and Leaflet to styled()
Phase 2 of the "fully MUI" cleanup — eliminates two more stylesheets.

- base.css -> src/admin/GlobalStyles.tsx: the reset, typography,
  scrollbar/::selection, theme cross-fade timing and all utility
  classes now live in a single theme-aware MUI <GlobalStyles>, rendered
  inside MuiProvider so every rule resolves against theme.vars (verified
  reactive in both schemes). base.css deleted.
- attendance.css -> a styled("div") LocationMap in AttendanceLocation;
  attendance.css deleted.
- App.tsx bootstrap loader is now fully self-contained (inline spinner
  + keyframes, theme-aware background read from the data-theme attr),
  since it renders before MuiProvider/GlobalStyles mount — it no longer
  depends on the removed .admin-spinner / var(--bg-primary).

tsc -b --noEmit and npm run build clean; verified in Chrome (light +
dark): body bg, text, fonts, .text-warning/.link-accent all correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:29:16 +02:00
BOHA
96e35df535 refactor(css): delete dead pre-MUI CSS and eliminate plan.css
Phase 1 of the "fully MUI" CSS cleanup.

- base.css 429->317: drop dead .admin-sidebar/header/card/modal,
  .admin-drag-handle/-kbd/-mono/-error-stack/-loading and the unused
  float/pulse keyframes. Keep reset, typography, scrollbar,
  ::view-transition cross-fade, the live .admin-spinner (App.tsx
  pre-React loader) and all utility classes.
- attendance.css 456->24: keep only the Leaflet .attendance-location-map
  rules; the old hand-rolled Attendance page is MUI now.
- offers.css 642->417: keep only the Quill (.admin-rich-editor/.ql-*)
  and .admin-rich-text-view rules; drop the dead offers table / filter /
  lang-badge / row-state rules.
- plan.css deleted: the grid already lives in PlanGrid's styled()
  wrapper; the last consumer (.plan-cat-color) is now an sx-styled
  Box input in PlanCategoriesModal.

tsc -b --noEmit and npm run build both clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:20:17 +02:00
BOHA
fa90bcc93b fix(plan): drop the redundant 'Zrušit' button in the category-management modal
The Správa kategorií modal footer had two buttons that both just close it (Zrušit + Zavřít). Added an opt-in hideCancel prop to the kit Modal (default false → every other modal unchanged) and set it on PlanCategoriesModal, leaving only 'Zavřít'. Verified: footer shows only Zavřít; close/Escape/backdrop intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:09:59 +02:00
BOHA
3117e80027 feat(plan): migrate the planner grid to MUI (theme-driven, off plan.css)
PlanGrid's bespoke plan.css surface is replaced by a styled(Box) wrapper that scopes all the grid + chip rules and pulls every color from the theme palette via theme.vars (auto light/dark — no more [data-theme] CSS-var blocks). Sticky header + date column, marker-tape cells, category color via the per-cell --cat-color var, weekend/today treatment, hover affordances, the '+' empty hint, and the one-shot mutation pulse are all preserved; the paper-grain/ruled-paper texture was dropped for a clean MUI surface. plan.css trimmed from ~1150 lines to just the category-manager modal rows (the grid is MUI now; the toolbar/banner/day-choice modal had already moved to MUI components). Verified on /plan-work in both light and dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:05:27 +02:00
BOHA
2f7ca0f5a8 fix(theme): uniform contained-button text — white in light, black in dark
success/info/warning/error palettes had no explicit contrastText, so MUI auto-picked per color: in dark mode the brighter green/amber got black text while red/blue got white (inconsistent). Set contrastText explicitly per scheme (#fff in light, #1a1a1a in dark) so all contained color buttons (and filled chips) read uniformly — white on light, dark on dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:54:06 +02:00
BOHA
2367e8a0ff feat(ui): mobile card layout for the remaining item tables
OrderConfirmationModal (the order→confirmation editable items dialog) and InvoiceDetail's read-only paid-invoice items table now render stacked label:value cards on phones (< sm) instead of a wide horizontally-scrolling table; desktop keeps the table. Same pattern as the Offer/Invoice editors. Gates: tsc=0, build=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:48:47 +02:00
BOHA
b168c68264 feat(invoice-detail): mobile card layout for the editable line-items grid
Same treatment as OfferDetail: on phones SortableInvoiceRow renders a stacked card (labeled Popis/Množství/Jednotka/Jedn.cena inputs + DPH select when apply_vat, Celkem footer, drag+remove header) and the wrapper renders a card stack instead of the table (DndContext/SortableContext kept). Verified at 430px on invoices/new: no inner table, no page horizontal scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:40:47 +02:00
BOHA
aeec0b703c fix(ui): wrap detail-page header action buttons on mobile
OfferDetail/OrderDetail/InvoiceDetail/ProjectDetail headers each have a custom action-button row that was flexShrink:0, so (like the alert/PageHeader cases) it stayed at content width and its buttons overflowed/clipped on phones. Dropped flexShrink:0 (and added flexWrap on ProjectDetail which lacked it) so the action row conforms to the available width and the buttons wrap onto multiple lines. Verified on OfferDetail at 430px: buttons span two rows, no container overflow, no page scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:36:24 +02:00
BOHA
2ff26241d7 feat(offer-detail): mobile card layout for the line-items editor
On phones (< sm) the editable items grid was a wide horizontally-scrolling table of inputs — unusable. SortableItemRow now renders each item as a stacked card (labeled Popis / Množství / Jednotka / Cena/ks inputs, V ceně checkbox + Celkem, drag handle + remove in the header), and the wrapper renders a card stack instead of TableContainer/Table on mobile (DndContext/SortableContext preserved so drag still works via TouchSensor). Desktop keeps the table. Verified: no inner table on mobile, no page horizontal scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:32:08 +02:00
BOHA
f0b5fb8a21 feat(ui): DataTable renders a card layout on phones instead of a scrolling table
On screens below the sm breakpoint, a wide multi-column table forced horizontal scrolling and truncated every cell (e.g. 'NA-202…'). DataTable now renders each row as a stacked label:value card on phones: every column becomes a 'HEADER: value' line with full, wrapping content; the actions column (key 'actions') drops to a right-aligned button row at the bottom; rowDanger/rowInactive/rowSx/onRowClick are all honored (action buttons stopPropagation so they don't trigger the row click). Tablets/desktop (>= sm) keep the normal table. Applies to every list table app-wide. Verified on Offers (actions) and Warehouse items (onRowClick): no truncation, no page horizontal scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:20:08 +02:00
BOHA
1e155f6ffb fix(ui): wrap action-button groups so they don't clip on mobile
Since kit buttons now keep their natural width (flexShrink:0 + nowrap), a row of them can't shrink — so button groups that were flexShrink:0 with no wrap got clipped on phones (the page has overflow-x:hidden). The draft-concept Alert actions (Offers + Invoices) and the shared PageHeader actions now use flexWrap:wrap and drop flexShrink:0, so the group conforms to the available width and its buttons wrap onto stacked lines. Verified at 430px: the Offers draft alert no longer overflows (buttons stack), page has no horizontal scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:05:32 +02:00
BOHA
8cee828212 fix(attendance): stop the page + leave modal overflowing (grid minmax + minWidth:0)
Two overflow fixes on Attendance.tsx: (1) the leave-request modal's Od/Do date row used a bare gridTemplateColumns '1fr 1fr', whose minmax(auto,1fr) tracks couldn't shrink below the two DatePickers' min-content, overflowing the narrow modal (horizontal scroll, 'Do' clipped) — now minmax(0,1fr). (2) On phones the whole page overflowed: the main two-column grid used { xs: '1fr' }, and the left column's 5-col 'Dnešní dokončené směny' table forced a 477px min-content (the TableContainer's overflow-x:auto never engaged because the grid item had min-width:auto). Now the grid uses minmax(0,1fr) and both columns get minWidth:0, so the table scrolls inside its container and the page fits the viewport. Verified at 390px width via DOM measurement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:48:39 +02:00
BOHA
b23d15453d fix(audit-log): stop replaying the page entrance animation on every filter
AuditLog uses a raw useQuery with the filters in the queryKey and a page-level early return (if isPending && no rows → <LoadingState/>), which sits ABOVE <PageEnter>. On any filter change the new queryKey had no cached data, so isPending flipped true with empty rows, the early return unmounted the whole PageEnter subtree, and it remounted (replaying the staggered entrance) when data arrived. Add placeholderData: keepPreviousData so rows persist through the refetch (isPending stays false, subtree never unmounts), and dim the table via isFetching instead of isPending. Verified by DOM node-identity: the PageEnter root survives a filter (no remount). The paginated list pages were already immune (usePaginatedQuery bakes in keepPreviousData).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:34:06 +02:00
BOHA
c4e71475ce fix(ui): unify checkbox size — drop size="small" on raw checkboxes
The kit CheckboxField (used on most pages) renders MUI's default medium checkbox, but four raw <Checkbox> usages forced size="small": OfferDetail's 'V ceně' table cell and 'Účtovat DPH' form checkbox, and the Settings permission-matrix module + per-permission checkboxes. They looked undersized next to the rest of the page. Removing size="small" makes every checkbox the same standard size app-wide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:26:41 +02:00
BOHA
bf5d38df14 fix(ui): stop kit buttons from shrinking below their label (flexShrink: 0)
Follow-up to the nowrap fix: MUI's default flexShrink:1 was still squeezing buttons in flex rows below their content width, so with nowrap the label overflowed its padding and touched the button edges (measured: '+ Přidat položku' was 96px wide for 98px of content). Adding flexShrink:0 keeps each button at its natural width (now 131px, full padding restored). Verified via DOM measurement + visual on the offer detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:10:31 +02:00