Rekapitulace (accountant notes, confirmed by owner 2026-07):
- Odpracováno = all worked hours EXCEPT hours worked on holidays
(was floor(worked/8)×8 — a 3h day printed as 0h)
- Vč. svátků a přesčasů = ALL worked hours; line2 − line1 = Svátek
(was worked + vacation − 8h×worked-holidays — could print less than
actually worked, and vacation inflated it)
- Přesčas = max(0, worked − fond), month-level, never negative
(was the sub-8h remainder + vacation — printed 3h overtime for a
3h month, 8h overtime for pure vacation, and −6h when a holiday
was worked)
- Svátek = hours actually WORKED on holiday dates
(was 8h per UNworked holiday — opposite meaning)
- So/Ne, Práce v noci, Dovolená, Fond unchanged (verified correct)
Single shared computeMzdaSummary now feeds BOTH the print and the
admin screen (they had drifted: capped vs uncapped holiday adjustment).
Print day table: one row PER RECORD — days with two shifts (real in
prod data) silently dropped all but the last record from the table
while the totals counted them all.
+7 spec tests (accountant's own examples); print snapshot updated to
the new spec values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs in the attendance admin print (Tisk on Správa docházky):
1. Viewer-timezone date shift: day rows are built from date-only strings
('2026-06-01'), which new Date() parses as UTC midnight per spec; local
formatting then rendered the PREVIOUS day for any viewer west of UTC —
a June print opened with '31. 5. Neděle' (a May row holding June 1's
punches) and every weekday name + weekend tint shifted by one. Czech
viewers were unaffected (UTC+1/+2), US viewers always hit it.
formatDate / getCzechWeekday / isWeekendDate now parse the calendar day
from the string parts — identical output in every timezone.
2. Every time cell carried a bogus date prefix ('1.6. 08:00'): the
overnight-only guard compared the extracted date part against the raw
wire shift_date ('2026-06-01T02:00:00') — never equal. Now compares
normalized date parts; the prefix fires only for true overnight punches.
Verified end-to-end against the production June payload; +8 regression
tests (timezone-proof by construction — parts-based, no UTC parse).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW shared StatusChipMenu: clickable status chip opens a dense menu of valid
next states; picks confirm via the shared ConfirmDialog (danger variant,
loading, stays open on failure); plain chip without edit permission;
row-click-safe (stopPropagation both directions).
- Offers list: draft -> Aktivovat (number assignment noted, PDF archived after
finalize like the detail); active -> 'Vytvořit objednávku…' (opens the
existing create-order modal — 'ordered' stays owned by that flow) +
Zneplatnit; ordered -> Zneplatnit.
- ReceivedOrders list: Zahájit realizaci / Dokončit / Stornovat / Obnovit with
cascade notes; Czech quote pairs fixed („…“); OrderDetail transition button
says 'Obnovit' when reopening.
- Projects list + ProjectDetail: status combobox replaced by transition
buttons (Dokončit/Zrušit/Obnovit projekt) rendered from valid_transitions;
cascade notes only when a linked order will actually change; save no longer
carries status; busy states symmetric.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- projects.service: VALID_TRANSITIONS (aktivni->dokonceny/zruseny,
terminal->aktivni reopen), tolerant of legacy statuses; getProject returns
valid_transitions; updateProject validates transitions (invalid_transition
token -> Czech 400 in the route).
- NEW project->order cascade, transactional with the project update: finishing/
cancelling a project completes/cancels its linked received order — only while
the order is still open (never resurrects storno, never cancels completed);
reopen never cascades. Direct tx write, no service recursion; cascaded order
change gets its own audit row.
- orders.service: reopen edges (dokoncena/stornovana -> v_realizaci);
syncProjectStatus is reopen-aware (reopen skips project sync) and returns
the cascaded projects for per-project audit rows in the route.
- orders.schema: fix phantom 'zrusena' enum token -> canonical 'stornovana'
(every storno PUT through the route 400ed with a raw English Zod message;
latent until now because no UI sent it) + route-level regression test.
- NEW project-order-status-sync.test.ts: 15 tests — both cascade directions,
guards, reopen-no-cascade, legacy tolerance, forward-sync regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Review panel: flex children no longer shrink (flexShrink 0) — with 2+
extracted invoices the cards kept compressing to fit 35vh, squashing the
field grids with nothing to scroll; panel now scrolls, maxHeight 40vh.
- Drag & drop onto the chat column: depth-counted dragenter/leave (no
flicker), Files-only, refused while busy, PDF/image filter with a Czech
hint toast when nothing usable was dropped, channel-alpha overlay with
dashed primary border ('Přetáhněte soubory sem'), staged through the same
path as the paperclip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /trips honored body.user_id for any trips.record holder — impersonation
on create (reads were already scoped). Now an explicit Czech 403 unless the
caller has the manager capability; manager on-behalf flow unchanged. +4 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PROJECT_STATUS added to documentStatus.ts (aktivni=info, dokonceny=success,
zruseny=error — app convention); Projects/ProjectDetail local maps deleted;
status Select derives its MenuItems from the shared map.
- ProjectDetail linked-order status now rendered via the received-order map
(was the project map with zero key overlap → raw DB token).
- Cancelled label unified across received/issued orders in documentStatus.ts.
- 'Chyba pripojeni' → 'Chyba připojení' (Dashboard, AuthContext).
- Dashboard lazy-loaded like every other route — its 7 subcomponents leave
the Login critical path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Marking an invoice paid (issued + received lists) now confirms via
ConfirmDialog with loading guard — was a single unguarded chip click on a
terminal transition; chips got affordance tooltips.
- Invoices list PDF icon hidden for drafts (no number → /file 404s),
matching Offers/IssuedOrders.
- Search debounced (300ms) on Invoices/ReceivedInvoices (list + totals).
- Orders tabs create buttons renamed 'Nová vydaná/přijatá objednávka'
(were identical for two document types); issued empty-state CTA aligned;
draft delete dialog uses documentNumberLabel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Shared Tabs: variant=scrollable + auto scroll buttons — the 5 status-filter
tabs no longer clip unreachable at ~360px (desktop rendering unchanged).
- TitleSync: browser tab shows the active page ('Faktury · BOHA'; ambiguous
labels qualified with their section, e.g. 'Záznam – Docházka · BOHA');
index.html fallback now 'BOHA Admin'.
- Skip-to-content link (visually hidden, visible on focus) + id/tabIndex on
<main> — keyboard users skip the 248px sidebar.
- MUI csCZ locale merged into the theme (kills built-in English strings like
'No options'); Czech noOptionsText on Customer/Supplier pickers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- OrderDetail/InvoiceDetail/LeaveRequests mutations now invalidate the same
domain sets as their list-page twins (orders+offers+projects+invoices /
+projects / +users+dashboard) — no more stale project/order labels after
a detail-page action.
- USER_INVALIDATE hoisted to lib/queries/users.ts; DashProfile self-edit now
refreshes trips/attendance/leave/projects like an admin edit.
- Global QueryCache.onError toast: failed first-loads no longer masquerade as
empty lists (data-present refetches stay silent; Unauthorized skipped; 4s
rate limit; meta.suppressGlobalErrorToast opt-out used by offer/issued-order
detail + the local TOTP QR query).
- Users/Vehicles active-toggle mutations toast on error (was silent revert);
clickable status chips got affordance tooltips (incl. warehouse parity).
- AuditLog: refetchOnMount always + staleTime 0 (mutations write audit rows
but nothing invalidates the key); search debounced; dead auditLogOptions
module removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Items are no longer required on issued orders — an order can be issued
purely from its rich-text sections (Obsah).
- DocumentItemsEditor: opt-in allowEmpty prop lets the list go to zero
rows (default false keeps the offers/invoices at-least-one-item rule).
- IssuedOrderDetail: allowEmpty on the editor; a saved order with no
items reopens empty (not a blank starter row); submit guard now
requires at least one item OR a non-empty section, not an item.
- PDF: with no items the billing heading, items table and total row are
omitted entirely — a sections-only order shows only its Obsah.
- Service: finalize (draft->sent) and create-as-non-draft reject a
completely blank order (no items AND no sections) with a Czech 400
(empty_document); drafts may still be saved empty as WIP.
- Tests: sections-only finalize + blank-order rejection + sections-only
PDF render; existing finalize fixtures given minimal content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review: add selected_custom_fields to offers/issued-orders
NON_STATUS_UPDATE_FIELDS so a non-editable document rejects selection
edits (status-only transitions still pass); tighten Zod cap 200->50 to
stay within VARCHAR(255).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dashboard content grids used alignItems:"start", so cards in a row sized to
their own content (ragged bottoms). Switch to alignItems:"stretch" so each row
shares one height, and make the wrapped cards actually fill the cell:
- DashProfile / DashSessions: motion-div + Card height:100% (the inner card was
shorter than its stretched cell).
- Right column (Aktivní projekty + Nabídky): cards flex:1 so the stacked pair
fills the column to match the neighbouring single cards.
CSS-only; no logic change. tsc clean, lint 0, 669 tests pass. Verified live via
Playwright (3-col row cells equal at 607; bottom 2-col cards equal at 257).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a new version is deployed, open tabs no longer silently run stale code
until a manual F5.
- Server stamps every response with X-App-Version (src/middleware/version.ts,
onSend hook in server.ts).
- Client bakes its build version via Vite define (__APP_VERSION__); apiFetch
compares the header to it (no polling — piggybacks existing traffic) and
latches a mismatch in a small store (appVersion.ts).
- UpdateBanner: a persistent bottom Snackbar "Je k dispozici nová verze
aplikace. — Obnovit" → location.reload(). User-initiated, so an in-progress
form is never interrupted.
- lazyWithReload: every React.lazy page reloads once if its hashed chunk 404s
after a deploy (sessionStorage-guarded against loops), fixing the "old chunk
gone" error screen.
Approach chosen after researching current SPA practice (version header + banner
beats polling / a service worker for a non-PWA admin app).
Tests: +4 (server header, client store latch/notify). Full suite 669 pass,
tsc -b clean, lint 0. Verified live via Playwright (header emitted; banner
appears on a simulated mismatch). Detection applies to deploys from the NEXT
release onward (current clients lack the detection code).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an optional per-line-item percentage discount ("Sleva") to offers and
issued invoices, on both the detail-page editor and the PDF.
- DB: discount Decimal(5,2) DEFAULT 0 on quotation_items + invoice_items
(migration 20260613195833_add_item_discount).
- Totals/VAT: new shared lineNet() (qty × price × (1 − discount/100)); offers'
NET total and invoices' subtotal AND VAT now compute on the discounted net
(every getBase/enrichQuotation/stats path). Backward compatible: discount 0
is identical to before.
- Schemas: discount = numberInRange(0,100), default 0; persisted on
create/update (+ offer duplicate).
- Detail editors: editable "Sleva %" column — offers via DocumentItemsEditor's
new opt-in showDiscount prop (issued orders unaffected); invoices in their own
editor. Spinners hidden + 7rem column so "100" is fully visible. Read-only
invoice view shows the column when any line has a discount.
- PDFs (offers + invoices): conditional "Sleva"/"Discount" column rendered only
when an item has a discount; line/total/VAT always use the discounted net.
Tests: +16 (money, totals incl. VAT-on-discounted-net, schema range, PDF
column present/absent). Full suite 665 pass, tsc -b clean, lint 0. Editor
verified live via Playwright. Design spec in docs/superpowers/specs/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approved design: percentage per line item, reduces net (VAT on discounted
net), conditional PDF column, offers + issued invoices only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI fix:
- Close-only modals showing a redundant second close button now use
hideCancel: ReceivedInvoices paid-detail modal (was two identical "Zavřít"
buttons) and PlanCellModal "Den je součástí rozsahu".
- Add modal-duplicate-close test enforcing close-only modals set hideCancel.
Lint: cleared all 68 warnings → 0.
- preserve-caught-error: attach { cause } in ai.service / exchange-rates.
- no-require-imports: package.json version read via fs (APP_VERSION) instead
of require(), avoiding a rootDir-expanding static JSON import.
- react-hooks/exhaustive-deps (11): ref-in-cleanup copies, derived-value
useMemo wrapping, PlanGrid field extraction, stable nextKey useCallback,
AuthContext documented cycle-break.
- no-explicit-any (53): precise route param/Prisma types, generic enrich*()
preserving payload shape, minimal vite module type, frontend body/query-key
types, SystemInfo for Settings.
Refactor (test enablement): shift-form types moved to dependency-free
shiftFormTypes.ts so the print-HTML builders are unit-testable without the
component graph; characterization test pins their output.
Gates: 649 tests pass, tsc -b clean, lint 0. Verified touched flows live
via Playwright (PlanWork CRUD + optimistic cache, warehouse form keys,
Settings system info, invoice detail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unused-vars (25): leftover imports/types across pages, routes and services;
write-only sickHours accumulator; unused holidayCount pair; test helpers
authPatch/authDelete; TOut dropped from ApiMutationOptions (the hook keeps
its own generic — no call-site changes); requireAuth no longer importable
in customers/warehouse routes (matches the no-bare-auth-reads convention).
useless-assignment (5): initializers proven overwritten on every path
(systemQty x2, hours, writtenSize -> let x: number) and the seed's dead
adminUser reassignment (create kept, assignment dropped).
useless-escape (4): [eE+\-] -> [eE+-] and [\/...] -> [/...] — identical
semantics.
Behavior-neutral; lint 102 -> 68 warnings (0 errors), suite 641 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migration 20260612234500: CHECK (quantity >= 0) on sklad_batches and
sklad_item_locations — the DB itself now rejects negative stock, so a
future C2-class bug (cumulative decrements driving a batch negative and
silently vanishing from totals) becomes a loud error instead of hidden
corruption. All three databases verified clean of negative rows before
the migration.
audit_logs.created_at TIMESTAMP(0) -> DATETIME(0): TIMESTAMP dies in 2038
and converts through the session timezone; existing wall times preserved
by the MODIFY.
Pinned by db-constraints.test.ts (4 tests, each watched fail pre-migration:
negative insert/update/location all succeeded before, 2098 audit timestamp
was rejected before).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sidebar (user-approved design, docs/superpowers/specs/2026-06-12-sidebar-
icons-design.md): all 31 menu items get unique contextual stroke glyphs in
rounded-square tiles tinted per section (Docházka blue, Kniha jízd teal,
Administrativa amber, Sklad violet, Systém slate; new teal/violet/slate
palette entries with channel tokens in both schemes). Replaces the old set
where 13 items shared a glyph (3x people, 3x sliders, ...) and Faktury was a
dollar sign. Odin stays the only solid + animated tile. Uniqueness pinned by
navdata-icons.test.ts (tsconfig.test.json gains jsx for the .tsx import).
Headers: ProjectDetail and OrderDetail now match the Offer/Invoice shell —
number rendered in secondary color at regular weight next to the bold
entity word, and flexWrap on both left header boxes so the title drops
below Zpět on phones instead of overflowing the viewport (the reported
mobile bug on received orders).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical (data integrity):
- warehouse inventory confirm: throw (not return) inside $transaction so a
failed deficit line rolls back the surplus corrective receipt — retries
no longer accumulate phantom stock
- warehouse issue confirm: validate batches against the COMBINED quantity
of all lines (duplicate FIFO-resolved lines drove batches negative)
- attendance delete: restore vacation_used/sick_used for the deleted day
(in-transaction, clamped at 0)
High:
- auth refresh: terminated sessions (replaced_at only) get a plain 401 —
the theft branch (family revocation) now fires only on replaced_by_hash
- POST /users strips role_id for non-admin callers (mirrors PUT guard)
- issued-order transition flushes unsaved edits via the full save payload
when dirty; server contract (items+status in one PUT) pinned
- received-invoices list: usePaginatedQuery + pager (rows 26+ unreachable)
- received-invoice dates: nullableIsoDateString + NaN guard before NAS save
(Czech-format dates corrupted month/year, orphaned NAS files)
- leave approval skips Czech public holidays and books each calendar year's
hours against its own balance (mirrors createLeave)
Medium/Low (classes):
- 52 Zod caps aligned to DB column widths across 7 schemas (over-cap input
500ed at Prisma instead of a Czech 400)
- FK pre-validation: projects update + warehouse receipts/issues return
Czech 400s instead of P2003 500s
- invoice PDF degrades gracefully when the CNB rate is unavailable
(recap omitted instead of 500 + lost NAS archival)
- date boundaries: local-day filters (warehouse lists/reports, audit-log),
@db.Date coercion on invoice dates
- plan updateEntry re-checks the per-cell cap (self-excluding)
- {id} tiebreaks on customers/received-invoices/warehouse-items sorts;
/items honors the client sort param
- htmlToPdf relaunches once when the shared browser died mid-render
- offer number release parses the year from the document number (cross-year
finalize+delete left permanent sequence gaps)
- trips/vehicles km fields integer-coerced; AI budget regated to
settings.company|settings.system; Settings System tab no longer clobbers
Firma numbering patterns; draft invoices hide the dead PDF button;
dashboard quick-trip invalidates ["vehicles"]; TOTP secret cap 64;
audit-log + invoice month buckets day-shift fixes
Docs: corrected the stale "Chromium has no CSS margin-box footers" claim
(html-to-pdf.ts + CLAUDE.md — margin boxes render since Chrome 131); audit
report M3 withdrawn accordingly.
~65 new pinning tests; every finding reproduced RED against the real test
DB before its fix. Suite: 58 files / 634 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One 0.2s enter-only fade (opacity + 6px rise) in the kit TabPanel covers
every tab switch (M3 clean-fade pattern; CSS animation so the global
reduced-motion kill switch applies). Orders and Invoices view switchers
now render through TabPanel (also fixes their dangling aria-controls).
Removed the inconsistent bespoke framer entrances inside tab content
(CompanySettings cascade, ReceivedInvoices card). Filter-style status
tabs (Projects/Offers/Invoices) deliberately keep the refetch dim only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_document_totals gains type=received_invoices: whole-set gross sums per
currency + total_czk via the same toCzk logic as the stats route (unknown
currencies skipped + reported), month/year via the literal columns (bare
year supported), unpaid/paid filter, period echo. invoices.view joined the
coarse gate (the per-type re-check keeps other families locked); the tool
description routes 'kolik jsme utratili' questions here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>