RichEditor now supports readOnly prop — hides toolbar and disables
editing via ReactQuill's built-in readOnly. Content renders with
proper Quill CSS (list margins, indentation, fonts) instead of
broken browser defaults from dangerouslySetInnerHTML.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
admin-form-input has fixed height (36px) causing overflow. Replaced
with a styled div matching the editor appearance. No new CSS needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added section-content class with proper ul/ol/li/p margins and
overflow:hidden. Browser defaults for lists were causing content
to extend outside the form input box.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added isLockedByOther check to:
- All disabled={} on selects (currency, language) and checkboxes (apply_vat)
- All conditional renders that swap date pickers for read-only inputs
- Rich editor conditional that swaps editor for static HTML display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user A opens an offer, a lock is acquired (locked_by + locked_at).
User B opening the same offer sees a warning banner and the form is
read-only. Lock expires after 5 minutes without heartbeat.
Backend:
- POST /:id/lock — acquire lock (returns 423 if locked by another)
- POST /:id/heartbeat — refresh lock timestamp (every 2 min)
- POST /:id/unlock — release lock
- GET /:id — includes locked_by info
- PUT /:id — auto-releases lock on save
Frontend:
- Acquires lock on page load (edit mode only)
- Sends heartbeat every 2 minutes
- Releases lock on page unmount (navigate away)
- Shows warning banner with locker's name
- All inputs read-only + action buttons hidden when locked
Migration: adds locked_by (INT) and locked_at (DATETIME) to quotations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was using toISOString() which outputs UTC time. Now formats with
getHours/getMinutes (local time via TZ=Europe/Prague), outputting
"07:45" instead of "2026-03-24T06:45:00.000Z".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prisma reads MySQL DATETIME as UTC Date objects. JSON.stringify calls
Date.toJSON() which defaults to toISOString() — outputting UTC with Z
suffix. Frontend then shows times shifted by -1 hour.
Override Date.toJSON to format using local getters (getHours etc.)
instead of getUTCHours. Combined with TZ=Europe/Prague, all API
responses now contain Czech local times without Z suffix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server was using UTC — clock-in at 7:45 CET was stored as 6:45 UTC.
MySQL DATETIME columns store values without timezone, so the UTC
value was saved as-is, appearing 1 hour behind.
Now new Date() returns CET/CEST time, matching the PHP behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
clearCookie was missing httpOnly, secure, sameSite — browser ignored
the Set-Cookie header because the options didn't match the original
cookie attributes. Cookie persisted after logout, allowing F5 to
re-authenticate via silent refresh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On logout, finds all refresh tokens matching the same user + IP +
user-agent (same browser session) and deletes them all. This cleans
up zombie tokens from previous logins and token rotations that
were showing as stale sessions on the dashboard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Deletes current token AND tokens replaced by it
- Cleans up all expired tokens on logout
- Prevents stale sessions from showing on dashboard after re-login
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
loadAuthData() didn't include totp_enabled or require_2fa in the
AuthData response. The frontend always saw undefined → false.
Now includes both fields from the database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The TOTP verification endpoint always created refresh tokens with
remember_me=false and 1-hour expiry, regardless of what the user
selected at login.
Fix:
- Frontend now sends remember_me in the TOTP verify request body
- Backend reads remember_me and uses it for token expiry (30 days)
and cookie maxAge
Users with 2FA who checked "remember me" will now stay logged in
for 30 days instead of being kicked out after 1 hour.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added billing_text column to invoices table (VARCHAR 500)
- Prisma migration: 20260323_add_billing_text
- Form field on invoice create page with placeholder
- PDF uses billing_text, falls back to default translation
- Stored on create and editable on draft invoices
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed from offers-editor-section + offers-items-table to
admin-card + admin-card-body + admin-table-responsive, matching
the offer detail page structure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Item reordering: replaced placeholder with @dnd-kit drag-and-drop.
Each item row has a drag handle for reordering via vertical drag.
Uses SortableContext with verticalListSortingStrategy.
2. Scope template insertion: fixed template loading to use already-fetched
data instead of re-fetching from non-existent endpoint. Templates with
sections are now stored fully and inserted directly on selection.
Also copies template description to scope_description.
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>
- Added white-space: nowrap to .admin-btn (prevents text wrapping)
- Modal footer buttons get min-width: 100px for consistent sizing
- Spinner in buttons doesn't add extra vertical space
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed offers, orders, attendance, scope-templates schemas — quantity,
unit_price, position, hours, minutes now use z.union([z.number(), z.string()])
with transform instead of bare z.number().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Component used admin-pagination-btn/pages/dots but CSS has
admin-pagination-page/controls/ellipsis. Fixed to match existing CSS.
Added record count display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Times in the database are stored as local time (CET). JavaScript's
Date constructor treated them as UTC, then toLocaleTimeString added
+1 hour for CET timezone.
Fix: extract hours/minutes directly from the datetime string via regex
instead of going through Date object. No timezone conversion applied.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added GET /api/admin/received-invoices/suppliers endpoint (distinct names)
- Upload and edit forms use HTML datalist for browser-native autocomplete
- Suggestions loaded once on page mount
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vehicle list (GET) now requires only authentication, not trips.vehicles
permission. Users with trips.view can see available cars in the trip
modal. Create/update/delete still require trips.vehicles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mailer.ts: nodemailer transport via local sendmail
- leave-notification.ts: HTML email matching PHP template
- Sends notification to LEAVE_NOTIFY_EMAIL on new leave request
- Non-blocking: errors logged but don't fail the request
- Added LEAVE_NOTIFY_EMAIL and APP_URL env vars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>