Commit Graph

255 Commits

Author SHA1 Message Date
BOHA
87e644eef8 fix(freshness): converge invalidation sets; global query-error toast; silent-toggle feedback
- 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>
2026-07-04 03:21:50 +02:00
BOHA
237ebf3ef8 feat(issued-orders): make line items optional (issue by sections alone)
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>
2026-06-24 21:36:35 +02:00
BOHA
59f555059b fix(documents): include custom-field selection in offer/issued-order unsaved-changes guard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:22:36 +02:00
BOHA
3d784adf5d feat(documents): per-document custom-field print picker on offer/issued-order/invoice detail
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:20:51 +02:00
BOHA
4674ff3389 fix(dashboard): equal-height cards — stretch + fill instead of ragged
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>
2026-06-13 21:34:17 +02:00
BOHA
719d1f2659 feat(documents): per-item Sleva (% discount) on offers + invoices
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>
2026-06-13 20:46:44 +02:00
BOHA
1ee59b54bc chore(release): v2.4.35 — lint cleanup (68→0) + duplicate-Zavřít modal fix
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>
2026-06-13 18:35:31 +02:00
BOHA
907ee574e6 chore(lint): remove all 34 dead-code warnings (verified site by site)
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>
2026-06-13 00:13:52 +02:00
BOHA
fd2e613aa5 feat(ui): sidebar icon refresh + unified detail-page headers
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>
2026-06-12 23:37:44 +02:00
BOHA
e11765bf0e fix: resolve all 28 findings from the 2026-06-12 full audit (TDD-pinned)
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>
2026-06-12 23:00:19 +02:00
BOHA
b9103c59f0 feat(ui): unified tab-switch animation - enter-only fade in TabPanel
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>
2026-06-12 18:04:32 +02:00
BOHA
64bb7f9c37 feat(projects): editable notes with edit stamp + NoteCard bubble component
- author-only note editing (PUT /projects/:id/notes/:noteId, strict schema,
  audit with old/new content); edits stamped via new edited_at column
  (migration 20260612160000) and shown as '· upraveno <datum>'
- note deletion tightened to admin-only server-side (was any projects.edit
  holder via direct API)
- new NoteCard component: chat-bubble note row (initials avatar, wrapping
  header, inline editor, actions in the bubble corner so they don't squeeze
  content on mobile), table-matching edit/delete icons
- Odin project detail carries the edited_at stamp; route-level tests cover
  author-edit, foreign-edit 403, validation, admin-only delete

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:26:38 +02:00
BOHA
6f3f1c40e7 fix(ui): zmeneny text - text - v select boxu 2026-06-12 14:56:52 +02:00
BOHA
7e4469b29a fix(richtext): blank lines survive into PDFs and read-only previews
Quill 2's semantic HTML saves a blank line as a truly empty <p></p> (no
<br> placeholder) — verified in the DB (invoice_sections hold <p></p>,
zero <p><br>) — which collapses to nothing outside the editor.
cleanQuillHtml (PDF path) and the new restoreQuillBlankLines (order/
invoice read-only previews) restore the placeholder at render time, so
existing stored content is covered retroactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:35:46 +02:00
BOHA
e9b432bec6 feat(documents): autosize item descriptions (minRows 2, maxRows 16)
The 'Podrobny popis' textareas in the shared items editor and the invoice
page now grow with content like the rich-text editor (fixed rows={2} ->
minRows/maxRows autosize), capped at 16 rows; the manual vertical resize
handle stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:16:43 +02:00
BOHA
674b44d047 feat(odin): Phase 2a read-only agentic assistant + tool-trace UI
- agentic chat loop (max 6 round-trips, budget re-checked between iterations)
  over 10 read-only, permission-delegated tools in src/services/ai-tools.ts
- persist assistant tool trace in ai_chat_messages.content_json (+ migration)
- consulted-tools chips in OdinThread; header now shows month spend only
  (budget cap hidden from the UI, server-side 402 guard unchanged)
- seed: ai.use permission; Settings exposes the ai permission module
- docs: REVIEW consolidation; deployment-guide.md superseded by docs/release.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:31:50 +02:00
BOHA
c21f02e5d1 fix(mobile): plan landscape collapse + person picker, Odin page scroll, plan toolbar stacking
- PlanGrid maxHeight gets a 360px floor: calc(100dvh - 240px) left
  <160px in phone landscape and the rows vanished under the sticky
  header
- Phones get a person picker above the plan grid (one column at a
  time, defaults to the logged-in user) - the multi-person grid never
  fit a portrait viewport; desktop unchanged
- Odin chat height uses 100svh instead of 100dvh: dvh grows live as
  Android Chrome collapses its address bar, so the page always allowed
  a chrome-height scroll; svh sizes for the bar-visible viewport
  (verified zero overflow at 390x844 via Playwright)
- Plan toolbar stacks into full-width rows on xs (headerActionsSx
  pattern from the detail pages); Dnes/arrows and Tyden/Mesic stay
  paired on one row each

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:53:52 +02:00
BOHA
bfd2c59ad3 fix(ui): detail-form headers no longer overflow on mobile
Two causes: theme h4 had no responsive size (desktop 2.125rem on
phones - now 1.5rem under 600px, applies to all page headlines), and
the Zpet+title header row had no flexWrap so long document titles
pushed past the viewport (issued orders, offers, both invoice views).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:21:29 +02:00
BOHA
db7a5c3d15 feat(suppliers)!: full customers model - structured address + custom fields
The dodavatele modal is now an exact mirror of the customers modal
(minus the PDF field-order picker): Nazev+ARES, Ulice, Mesto+PSC, Zeme,
ICO+ARES, DIC, and the same "Vlastni pole" editor (maxWidth md).

Two migrations on sklad_suppliers:
- structured street/city/postal_code/country replace the free-text
  address blob (best-effort split: street / PSC regex / city)
- custom_fields LONGTEXT replaces contact_person/email/phone/notes;
  existing values are preserved as labeled custom fields

The encode/decode of the custom_fields blob is shared with customers
via the new utils/custom-fields.ts. The PO PDF supplier block renders
the structured address + custom-field lines like the customer block;
the supplier picker/detail selects and types follow. Legacy clients
sending the dropped keys are silently stripped (tested). Suppliers
list shows Ulice/Mesto instead of the dropped contact columns; search
covers name/ico/city.

BREAKING CHANGE: sklad_suppliers.address, contact_person, email,
phone, notes columns dropped (data migrated); deploy must run
prisma migrate deploy + generate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:11:22 +02:00
BOHA
f1ce76d21d feat(ares): ARES company lookup with prefill in customer/supplier modals
New /api/admin/ares proxy (browser cannot reach ares.gov.cz - CORS+CSP):
GET /ico/:ico and GET /search?q= map the MFCR REST API to the form
shape (street incl. orientation numbers, "511 01" PSC format, DIC).
Guarded by customers.create/edit + warehouse.manage; token errors map
to Czech messages. Shared AresAdornment button sits in the Nazev and
ICO fields of both modals: ICO mode fills directly, name mode searches
with a result picker. Tested against live ARES (BOHA, ICO 22599851).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:11:08 +02:00
BOHA
4bf245d35c feat(projects): status filter tabs + per-status row tints on the list
Mirrors the offers page: centered status tabs (Vsechny/Aktivni/
Dokonceny/Zruseny) filtering server-side (?status= already supported),
filtered-vs-empty empty states, and row tints - dokonceny rows get the
info-channel low-alpha wash matching the badge, zruseny rows are faded
like invalidated offers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:30:09 +02:00
BOHA
79d026e756 fix(dnd): mobile item reorder works via long-press instead of scrolling
PointerSensor also receives touch-derived pointer events and its 5px
distance constraint fired before the TouchSensor's hold delay - the
browser claimed the gesture for scrolling (pointercancel) and the drag
died. Replaced with MouseSensor (desktop unchanged), TouchSensor now
activates on a 300ms hold (tolerance 8px), and all drag handles carry
touch-action: none so the browser never starts a scroll from them.
Applies to DocumentItemsEditor (offers/issued orders) and the invoice
items list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:30:01 +02:00
BOHA
40a859f5e1 feat(invoices)!: Obsah sections, internal-only notes, unified per-page PDF header+footer
Invoices now mirror the issued-orders document model:

- New invoice_sections table (CZ/EN rich-text "Obsah") edited via the
  shared SectionsEditor, printed inline right after the items on the
  PDF. Full-replace on update, same transaction as items.
- Printed notes dropped: the notes column is removed (migration merges
  existing content into internal_notes first); the form field is now
  "Interni poznamky", never printed. Legacy payloads sending notes are
  silently stripped.
- Form cleanup: Cislo faktury and Vystavil fields removed (number lives
  in the header, issued_by auto-fills); page header title restyled to
  the orders/offers pattern (number span + status chip).
- Unified per-page PDF header for the red-accent family: shared
  buildPdfHeaderTemplate in pdf-shared (22mm logo, red heading, red
  rule) rendered by a Puppeteer headerTemplate on EVERY page of both
  invoices and issued orders (incl. the /file fallback render); body
  headers are print-hidden. htmlToPdf gained the headerTemplate option.
- Footer parity: invoices get the per-page "Vystavil + Strana X z Y"
  footer; the invoice bottom block (notice + QR/VAT recap + Prevzal)
  is break-inside: avoid so a page break can never split it.
- @page margins now match the template space (32mm top, 18mm bottom) -
  Chromium lays out by CSS @page margins, which also fixes issued
  orders' content running into the 18mm footer zone on full pages.

BREAKING CHANGE: invoices.notes column dropped (data merged into
internal_notes); deploy must run prisma migrate deploy + generate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:02:37 +02:00
BOHA
75dc97516e feat(issued-orders)!: sections replace flat terms/notes fields; Obsah title
Per user decision the rich-text sections now carry all free-form PDF content on issued orders: dropped columns delivery_terms, payment_terms, issued_by, notes (migration applied to dev+test) and their PDF blocks + form fields. Kept: order_text (moved from the bottom card into the Zakladni udaje grid, replacing the Vystavil field) and internal_notes (now the only field in the bottom card, never printed). Sections card is titled 'Obsah' on issued orders; offers keep 'Rozsah projektu' and are untouched. Legacy clients sending the dropped keys are silently stripped (tested). PDF footer 'Vystavil:' stays - it prints the logged-in user, not the dropped column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 17:27:08 +02:00
BOHA
d1533ffc4d feat(documents)!: unify offers and issued orders end to end
One coherent pass over the two sibling document models, driven by a 3-lens
1:1 scan (~60 divergences inventoried) + user decisions. Migration adds
issued_orders.locked_by/locked_at and the issued_order_sections table
(mirror of scope_sections; applied to dev + test DBs).

Issued orders gained (offers as reference implementation):
- rich-text SECTIONS with CZ/EN titles, rendered on their own PDF page in
  the PO template's red style (shared DocumentSectionSchema, one-transaction
  create/update incl. items - fixes a torn-write bug)
- edit LOCKING (lock/heartbeat/unlock routes, 423 + holder name, 30s TTL =
  3 missed 10s heartbeats; locked_by enrichment on detail)
- archived-PDF serving: GET /:id/file reads the NAS copy (new
  readIssuedByNumber sweep) with live-render fallback + re-archive; offers'
  /file got the same fallback (kills the 'ulozte nabidku' dead end)
- NAS cleanup on delete, in-tx po_number uniqueness (409), collision-advancing
  number previews (also invoice previews), PUT returns assigned po_number

Offers hardened (issued as reference):
- VALID_TRANSITIONS enforced (no more numberless 'ordered' offers; invalidate
  follows the table; order creation only from active offers) +
  valid_transitions on detail
- explicit 400 instead of silent edits outside draft/active (mirrored on
  issued: explicit 400 replaced silent drops)
- customer existence check + Number(null)->0 clear bug fixed; delete of a
  linked offer -> 409 instead of P2003 500; error-token convention; Zod caps
  DB-aligned (desc 500/unit 20/number 50/project_code 100, isoDateString
  dates, ints for positions); audit old/new values + koncept fallbacks;
  list id tiebreaks; stats include trimmed; NAS delete dedupe

Frontend unification (6 new shared modules):
- components/document/{DocumentItemsEditor,SectionsEditor,LockBanner}
- hooks/{useDocumentLock,useUnsavedChangesGuard,useDocumentPdf(+list variant)}
- OfferDetail 1940->1100 lines, IssuedOrderDetail 1400->980: headline-only
  document number (form field removed), one form layout/readonly convention,
  view-permission opens read-only everywhere (issued's editable-for-viewers
  hole closed), server-driven transition buttons, dirty guard + Enter submit
  on both, useApiMutation everywhere (new opt-in envelope mode), fixed
  infinite spinner on failed detail fetch, draft PDFs hidden (no number yet)
- lists: supplier filter + count line on issued, Mena column dropped + mono
  numbers on offers, shared hardened per-row PDF flow (spinner, double-click
  guard, 401 close, blob cleanup), proper Czech quotes, real CTA empty
  states, query-lib cleanups (["offers","customers"] key, typed list rows,
  shared CurrencyAmount, retry:false on details)
- pdf-shared.ts: one escapeHtml/cleanQuillHtml(strict)/formatNum(NBSP)/
  formatCurrency/formatDate for all four PDF routes; offer PDF keeps its
  monochrome look (fractional qty fix: 1.5 no longer prints as 2); issued
  PDF language now comes from the document column

+49 tests (suite 364 -> 413). Each stage passed an independent review; final
cross-stage integration review verified the FE<->BE contracts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:22:39 +02:00
BOHA
7398cad466 feat(vat)!: remove VAT entirely from offers, orders and issued orders
Accounting rule: nabidky, objednavky (incl. confirmation PDF) and objednavky
vydane are NOT tax documents - VAT belongs only on invoices. Per user
decision this is a FULL removal including DB columns.

- migration: DROP orders.vat_rate/apply_vat, issued_orders.vat_rate/apply_vat,
  issued_order_items.vat_rate, quotations.vat_rate/apply_vat (applied to
  dev + test DBs)
- services: net-only totals everywhere (computeIssuedOrderTotals(items) ->
  {total}; enrichOrder/enrichQuotation net; per-currency list totals net)
- PDFs (offers, order confirmation, issued order): no VAT columns/summary,
  single 'Celkem bez DPH' / 'Total excl. VAT' total, note under totals:
  'Ceny jsou uvedeny bez DPH. DPH bude uctovano dle platnych predpisu.';
  duplicate Cena/Celkem column merged (desc width 56%); orders-pdf render
  extracted as exported renderOrderConfirmationHtml for testability
- frontend: Uplatnit DPH checkboxes, VAT selects and per-item VAT columns
  removed from OfferDetail/OrderDetail/IssuedOrderDetail/
  OrderConfirmationModal/ReceivedOrders manual-create; list footers read
  'Celkem bez DPH'; invoice-from-order prefill now takes the company default
  VAT (invoice decides its own VAT)
- tests: suites reworked to net math; new pdf-vat-note.test.ts pins the
  exact cs+en note text and VAT-free layout on all three PDFs

Invoices and received invoices keep their VAT handling unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:13:16 +02:00
BOHA
ca1f07671f feat(issued-orders): editable PDF heading 'Text objednavky (na PDF)' (order_text)
New nullable issued_orders.order_text column (migration applied to dev+test). The heading above the PDF items table is now editable per order, mirroring invoices' billing_text: empty falls back to the default, which is now 'Objednavame si u Vas:' per user wording. Field sits above Dodaci podminky in the detail form; persisted via create + update strFields (both schemas - the invoices Update-schema gap is not repeated). Tests: persistence round-trip incl. null-clears, PDF custom heading + default fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:16:40 +02:00
BOHA
c2746d78c9 fix(dashboard): stale punch button/KPIs after attendance changes on other pages
Verified flow: clock in on dashboard -> delete the record in /attendance/admin
-> navigate back: 'Zaznamenat odchod', the Pritomni KPI and the presence card
still showed the pre-delete state until F5. Root cause: every attendance
mutation outside the dashboard invalidated only ["attendance"], never
["dashboard"], so within the dashboard query's 60s staleTime the remount was
served from cache.

- useAttendanceAdmin create/bulk/edit/delete, /attendance punch +
  leave-request, AttendanceCreate, LeaveApproval approve/reject now also
  invalidate ["dashboard"] (CLAUDE.md rule: mutations invalidate every domain
  that embeds their data)
- systemic backstop: dashboardOptions uses refetchOnMount "always" - the
  dashboard aggregates attendance/offers/invoices/orders/projects/leave, and
  domain pages can't all be expected to invalidate it; returning to the
  dashboard must never show pre-mutation data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:40:06 +02:00
BOHA
6a22195c7d feat(issued-orders): counterparty is now a supplier (dodavatel), not a customer
Issued orders are purchase orders WE send - they must pick from suppliers
(sklad_suppliers), not from customers. Per user decision customer_id was
REPLACED (not kept alongside): migration drops issued_orders.customer_id and
adds supplier_id FK -> sklad_suppliers (existing rows lose their counterparty
- the feature is days old; re-point them in the UI).

- service: input/filters/search (suppliers.name + ico)/enrichment/detail all
  supplier-based; create validates the supplier inside the transaction and
  update before write -> Czech 400 'Dodavatel nenalezen' instead of P2003 500;
  detail returns a minimal supplier field set (no internal notes leak)
- routes: supplier_id on list + stats; new GET /issued-orders/suppliers
  lookup (orders.view/create/edit guard - orders users lack warehouse.manage
  which guards the warehouse suppliers CRUD), active suppliers only,
  name+id ordering
- PDF: Dodavatel block now renders the supplier (name, newline-split address,
  IC/DIC), layout and both language label sets unchanged
- frontend: new SupplierPicker kit component (CustomerPicker untouched),
  IssuedOrderDetail/IssuedOrders switched to supplier_id/supplier_name; the
  picker keeps a fallback option for orders whose supplier was later
  deactivated; WarehouseSuppliers CRUD now also invalidates issued-orders so
  the picker can't go stale
- tests: issued-orders suite switched to supplier fixtures + new coverage
  (lookup shape + 403, nonexistent supplier 400, PDF supplier block)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:29:06 +02:00
BOHA
1826fc7976 fix: audit fix pass #1 — all 19 verified HIGH findings + critical dep cleanup
Fixes every CRITICAL/HIGH finding from the 2026-06-09 full-codebase audit
(REVIEW_FINDINGS.md); each fix went through independent spec + code-quality
review. Plan and per-task log: docs/superpowers/plans/2026-06-10-audit-high-fix-pass.md

- attendance: schemas accept the combined local datetimes the forms/service
  use (new dateTimeString helpers in schemas/common.ts), breaks persist on
  create, AttendanceCreate submit rebuilt — every submit 400'd since 519edce
- 2fa: backup codes wired to /totp/backup-verify (+ remember-me parity),
  enrollment QR generated locally via qrcode (CSP-blocked external service
  also leaked the secret), dashboard shows per-user enrollment, not policy
- invoices/orders: per-line VAT survives re-saves (numberOr 0-respecting
  coercion in formatters.ts), billing_text persists on update, issued-order
  status transitions update UI gates
- trips: real pagination on all 3 pages, GET /trips/stats server aggregate
  (shared buildTripsWhere + legacy distance coalesce), vehicle_id applies on
  PUT with both-vehicle odometer recompute, print rebuilt (sync window.open,
  escaped template, server totals)
- orders api: attachment_data PDF blob excluded from all non-binary reads
- warehouse: unit field is a Select over UnitEnum, receipt attachments
  downloadable via new authenticated GET route
- downloads: shared RFC 5987 contentDisposition helper — Czech filenames no
  longer 500 (warehouse, received-invoices, orders endpoints)
- misc: block-env hook actually blocks (exit 2 + stderr), project create
  works with empty dates, NaN filter guards on trips endpoints
- deps: remove unused concurrently (clears both critical advisories), pin
  @hono/node-server >=1.19.13 via overrides (clears the 3 moderates without
  the Prisma 6 downgrade), drop deprecated @types stubs

Gates: tsc -b clean - vitest 30 files / 342 tests (31 new) - eslint 0 errors
- build OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:59:47 +02:00
BOHA
97101c4db7 feat(offers,invoices): per-currency 'Celkem' totals beneath the lists (consistent with orders)
Mirrors the orders totals exactly: /stats (offers) + /list-totals (issued +
received invoices) endpoints sum each doc's total per currency across the active
filter (reusing extracted where-builders so they stay in sync with the lists),
rendered as the identical 'Celkem: …' block via the shared formatMultiCurrency.
Existing invoice/received KPI stats untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:12:24 +02:00
BOHA
5462fcf944 feat(orders): PDF footer = issuer name+email, 'Zobrazit objednávku' button, per-currency list totals
- Issued-order PDF: drop the Vystavil/Schválil signature row; footer now shows
  'Vystavil: <name>' + 'E-mail: <email>' from the logged-in user (authData).
- IssuedOrderDetail PDF button 'Export PDF' -> 'Zobrazit objednávku' (already
  opens inline like invoices' 'Zobrazit fakturu').
- New /orders/stats + /issued-orders/stats endpoints sum order total (incl VAT)
  per currency across the active filter; rendered as 'Celkem: …' beneath both
  the received and issued lists. formatMultiCurrency lifted to a shared util.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:50:05 +02:00
BOHA
cb82349c86 refactor(orders): rename OrdersReceived to ReceivedOrders; add warehouse module label
Renames the received-orders page for naming consistency, updates the Orders tab
import and the issued-order create button label, and adds the Sklad module
label to role management.
2026-06-09 21:20:50 +02:00
BOHA
d445384408 feat(perms): drop dead document export permissions; seed the 12 survivors via migration
offers.export/orders.export/invoices.export gated nothing on the backend (PDF
routes enforce *.view); they only toggled frontend buttons and showed dead
role checkboxes. Migration deletes them (+ role grants) and idempotently seeds
the 12 enforced doc perms (offers/orders/invoices x view/create/edit/delete) +
admin grant, so a fresh prod DB has them (closes the seed-only drift gap).
Route reverts to orders.view; the 8 frontend *.export checks now use *.view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:19:38 +02:00
BOHA
841ab676ec feat(nas): archive issued-order PDFs to NAS_ORDERS; split NAS_FINANCIALS_PATH into NAS_INVOICES + NAS_ORDERS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:52:06 +02:00
BOHA
313d9218c1 fix(offers,invoices): per-button save loading state (only the clicked button spins)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:41:25 +02:00
BOHA
c270cb22e9 fix(issued-orders): correct status display after create-redirect + per-button loading state
Bug 1: creating an issued order as 'sent' redirected to the detail which showed
'Koncept' until a refresh — the component is reused across create→edit, so the
one-shot hydration (gated by dataReady) never reloaded the real status. Now the
just-saved status is reflected into local form state on every save with a target.

Bug 2: both edit-draft buttons shared one 'saving' flag, so pressing 'Uložit
koncept' made 'Odeslat' show 'Ukládání...'. Track which action is in flight so
only the clicked button spins (all stay disabled to block double-submit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:36:55 +02:00
BOHA
113edd9a0d fix(numbering): release project number on order delete + harden split tests + accurate settings preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:44:54 +02:00
BOHA
753c16423c fix(settings): bank accounts saved blank — flatten payload + harden schema to strictObject
The bank form posted {id, bank:{...}} (nested) but the route/schema read fields
at the top level; lenient z.object silently stripped the wrapper, so every field
saved as null while returning success. Flatten the payload to top level and switch
the create/update schemas to strictObject so a future nesting 400s loudly instead
of saving a blank record. Affects both create and edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:39:43 +02:00
BOHA
5c03570f07 feat(settings): separate numbering config for received orders, issued orders, and projects
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:31:36 +02:00
BOHA
52ff0a6ffa feat(drafts): show Koncept for null numbers in lists + fix filtered empty-state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:52:34 +02:00
BOHA
a15fa68c3b fix(drafts): resync invoice number field after in-place draft→issued finalize
Mirrors the issued-order po_number resync so all three detail pages
surface the freshly-assigned number after finalize without a reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:50:18 +02:00
BOHA
e86c2e9a80 feat(drafts): two-button create (Vytvořit / Uložit koncept) + draft finalize UX across offers/invoices/issued-orders
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:44:10 +02:00
BOHA
ce636215dc feat(drafts): remove localStorage draft banner/hook; add Koncept status + filter (drafts are DB rows)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:25:24 +02:00
BOHA
28186397a0 fix(ui): line-item number inputs — allow clearing the 0 (store raw string, coerce at totals/save), unify value alignment, widen Množství column
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:38:28 +02:00
BOHA
a3948c7da1 fix(ui): hide invoice list delete button for paid invoices (consistent with detail guard)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:12:44 +02:00
BOHA
53c8359acc feat(ui): unify VAT rate+toggle layout (offers/invoices/orders); move offers secondary row actions into an overflow menu
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:04:44 +02:00
BOHA
c2d7a96718 feat(ui): invoice notes use RichEditor (sanitized render + PDF), matching offers/orders
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:59:43 +02:00
BOHA
61673247bf feat(ui): list parity — row tinting, received-orders status filter, more sortable cols, search-aware empties, skeleton suppression
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:55:50 +02:00
BOHA
9fe5113c5b fix(ui): cross-module mechanical unifications (line-item labels/align, back-tab targets, status-button colors, apply_vat checkbox, currency-from-settings, read-only issued_by, Celkem header, centered offer tabs)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:49:31 +02:00