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>
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>
Adopt the PageEnter wrapper as every page's outermost render element and remove the ad-hoc per-page entrance motion.div wrappers. Every page now enters the same way — all top-level sections rise+fade in, staggered — so nothing appears instantly and the motion is identical app-wide. Presentation-only: no data/logic/hooks/invalidate/permissions touched. Embedded sub-tabs (CompanySettings, ReceivedInvoices), Login (auth shell) and the dev UiKit are intentionally excluded. Gates: tsc -b --noEmit=0, build=0, vitest 152/152.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day)
- Reset migrations to single baseline init migration
- Add seed script with admin user (admin/admin)
- Update CLAUDE.md with migration workflow documentation
- Various frontend fixes (queries, pages, hooks)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones
- Remove skeleton.css and @keyframes shimmer from base.css
- Add <Skeleton> wrappers with fixtures to all 25+ page components
- Generate 20 bone captures via boneyard CLI (CDP auth-gated capture)
- Refactor data fetching from useEffect+useState to TanStack Query
- Extract query hooks into src/admin/lib/queries/ and apiAdapter
- Add usePaginatedQuery hook replacing useApiCall/useListData
- Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs
- Fix customer_id mandatory validation on offer creation
- Fix leave-requests comma-separated status filter (Prisma enum in: [])
- Add cross-entity cache invalidation for orders/offers/invoices/projects
- Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.)
- Add boneyard.config.json with routes and breakpoints
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Queried status "converted"/"expired" but actual DB values are
"ordered"/"invalidated". Updated label "Prošlé" → "Zneplatněné".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route handlers: add exhaustive return after error checks so TypeScript
narrows the union and result properties are accessible without casts
- attendance.service: use Prisma.attendanceGetPayload for included relations
- projects.service: remove unnecessary cast on orders relation
- Dashboard.tsx: replace Record<string,any> with proper DashData interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API now only returns data sections the user has permission to see:
- my_shift: attendance.record
- attendance: attendance.admin
- offers: offers.view
- projects: projects.view
- invoices: invoices.view
- orders: orders.view
- leave_pending: attendance.approve
- recent_activity: settings.audit
Frontend hides KPI cards, activity feed, and attendance sections
for users without the matching permissions.
Regular employees now only see their shift status, quick actions,
profile, and sessions — not company KPIs or admin data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>