- bubbles spring in anchored at their avatar corner; thinking state is a
typing bubble (three bouncing dots) that springs in/out where the reply
lands; reduced-motion falls back to opacity fades
- mobile /odin is immersive: AppShell hides its header and main padding
below md, the chat owns 100svh full-bleed with safe-area insets and a
back arrow (history back, home on deep links)
- desktop card border kept divider-colored via longhand border props (a
responsive border shorthand reset the color to black)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assistant bubbles use the animated OdinMark instead of the plain 'O'
circle; the 'Pracuji…' row drops its horizontal padding and matches the
28px mark size, so the thinking mark sits exactly in the avatar column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the sent message now leaves the input the moment the optimistic bubble
appears (restored along with attachments if the request fails)
- text selected in the primary-filled user bubble inverts to
white-on-primary instead of vanishing into the global ::selection style
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
Odin currently advertises general Q&A via the /chat path, which burns API
credits on open-ended questions. Guard it: a text-only message makes NO AI
call — it gets a canned reply telling the user to attach an invoice. The
invoice path (attachments → extract) is unchanged. Removing the guard
re-enables the general /chat path for a later phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ensureActive() created the conversation and invalidated the list before the
server-side auto-title was applied (that happens on the message append), so
the sidebar showed "Nová konverzace" until a refresh. Move the
["ai","conversations"] invalidation to persist()'s success path — after the
append (and thus the auto-title) lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Composer rebuilt claude-style: a rounded pill with a multiline auto-growing
textarea and attach/send ICON buttons (vertically centered), so there's room
to type on mobile (was squeezed between two text buttons).
- Conversation list collapses into a slide-in Drawer below md, with a menu
button in the chat header; chat is full-width on mobile.
- Each new message bubble animates in (fade + slide-up; fade-only under
prefers-reduced-motion) via framer-motion, so sent messages and Odin's
replies appear smoothly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Replace the three remaining hand-written admin-loading/admin-spinner
markup blocks with the kit <LoadingState /> component:
- AdminApp.tsx Suspense fallback
- AppShell.tsx loading branch
- PlanGrid.tsx no-data branch (plan-* classes untouched)
After this the only remaining admin-* classes in TSX are
admin-rich-editor (RichEditor/Quill) and admin-rich-text-view
(sanitized HTML in Order/Invoice detail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both components have zero importers after the phase-25 migrations
(verified: no `./FormModal` or `./ConfirmModal` imports remain; build
passes). Their MUI kit replacements are ui/Modal and ui/ConfirmDialog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw admin-form-select with the MUI kit Select (string-based
value, converted at the boundary). Same query (warehouseReservationList,
ACTIVE, perPage 100), same option labels, same onChange contract
(reservationId|null, remainingQty).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>