190 Commits

Author SHA1 Message Date
BOHA
3bc7fb6800 fix: lock heartbeat every 10s, timeout after 30s (was 2min/5min) 2026-03-24 11:24:51 +01:00
BOHA
9e6ce4359a fix: use RichEditor with readOnly prop instead of raw HTML for locked/invalidated offers
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>
2026-03-24 11:23:34 +01:00
BOHA
b1aaec4fb6 fix: read-only rich text — add word-break to prevent overflow from nbsp 2026-03-24 11:19:19 +01:00
BOHA
f9cb28afa0 fix: read-only rich text — use plain div instead of admin-form-input
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>
2026-03-24 11:15:51 +01:00
BOHA
5593c2a229 fix: read-only rich text content overflowing container
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>
2026-03-24 11:14:24 +01:00
BOHA
f8210d667f fix: locked offers — selects, checkboxes, date pickers, rich editor all read-only
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>
2026-03-24 11:12:39 +01:00
BOHA
0ad0e88853 feat: pessimistic locking for offer editing
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>
2026-03-24 11:08:41 +01:00
BOHA
17da3b17c3 fix: scope page — logo inline with title in same flex row, matching page 1 2026-03-24 10:52:29 +01:00
BOHA
cb5c26c4b0 fix: scope page header — logo above header like page 1, not inside flex 2026-03-24 10:48:04 +01:00
BOHA
a8195d7d49 fix: move quotationNumber declaration before scope HTML usage 2026-03-24 10:46:24 +01:00
BOHA
ff05f98d14 fix: scope page header matches quotation page — title, number, project, valid until 2026-03-24 10:45:05 +01:00
BOHA
96cbaf3315 fix: stack item description fields vertically with flex column 2026-03-24 07:54:10 +01:00
BOHA
a866384f08 feat: add item_description field to offer items editor (matches PHP) 2026-03-24 07:53:08 +01:00
BOHA
34134c0e07 fix: dashboard attendance — format arrived_at as HH:MM matching PHP
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>
2026-03-24 07:51:48 +01:00
BOHA
2fefdaf36a fix: serialize dates as local time in JSON responses, not UTC
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>
2026-03-24 07:45:29 +01:00
BOHA
8120f0a45e fix: set TZ=Europe/Prague so new Date() returns local Czech time
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>
2026-03-24 07:41:58 +01:00
BOHA
d92c5c56ac fix: remove Content-Type header from logout request (no body to send) 2026-03-23 20:46:29 +01:00
BOHA
aec822adc2 fix: clearCookie must match setCookie options for browser to clear it
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>
2026-03-23 20:44:28 +01:00
BOHA
04828fefe2 fix: logout deletes all tokens from same browser/IP, not just current
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>
2026-03-23 20:38:09 +01:00
BOHA
f71ad6e2a8 fix: logout now properly cleans up session tokens
- 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>
2026-03-23 20:35:28 +01:00
BOHA
456232cd82 fix: dashboard TOTP status always showing inactive
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>
2026-03-23 20:31:06 +01:00
BOHA
33268b38ae fix: TOTP login flow loses remember_me — sessions expire after 1 hour
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>
2026-03-23 20:28:54 +01:00
BOHA
c4c4433561 feat: editable billing text on invoices
- 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>
2026-03-23 19:47:15 +01:00
BOHA
2540efbec2 refactor: merge InvoiceCreate into InvoiceDetail (single page for create + edit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:34:16 +01:00
BOHA
5285c3c7c9 fix: VAT select in invoices — use admin-form-select instead of admin-form-input 2026-03-23 19:27:46 +01:00
BOHA
d33c2b3416 fix: invoice numbering — use MAX from invoices table instead of sequence counter 2026-03-23 19:25:16 +01:00
BOHA
93ea9911f8 fix: invoice items table — match offer detail card style
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>
2026-03-23 19:22:01 +01:00
BOHA
892d83cd90 feat: add drag-and-drop item reordering to invoice create and edit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:18:01 +01:00
BOHA
2b4a98b958 fix: save item position after drag-and-drop reordering 2026-03-23 19:06:21 +01:00
BOHA
bfb3a975ea fix: restrict item drag to parent table bounds 2026-03-23 19:04:55 +01:00
BOHA
3bef879ff9 fix: move useSensors hook to component top level (React hooks rules) 2026-03-23 19:03:38 +01:00
BOHA
185157fe86 feat: offer items drag-and-drop reordering + fix scope template insertion
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>
2026-03-23 19:02:15 +01:00
BOHA
95065f54eb fix: offer scope sections — add blue EN / red CZ language badges on title labels
Matches PHP styling with offers-lang-badge and offers-lang-badge-cz classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:56:12 +01:00
BOHA
bcad377f92 fix: dashboard — gate all sections by user permissions
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>
2026-03-23 18:51:29 +01:00
BOHA
a1c70ba25f fix: force all spinners inside buttons to 16px (admin-spinner-sm size) 2026-03-23 18:48:06 +01:00
BOHA
98454edcf1 fix: prevent buttons from resizing during loading state
- 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>
2026-03-23 18:46:18 +01:00
BOHA
aeac76687b fix: all item/sub-object schemas accept string numbers from form inputs
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>
2026-03-23 18:42:41 +01:00
BOHA
a8a28d8472 fix: confirm modal — always use admin-btn-primary (admin-btn-danger has no CSS) 2026-03-23 14:17:31 +01:00
BOHA
040b41ed90 fix: pagination component — use correct CSS class names
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>
2026-03-23 13:59:49 +01:00
BOHA
0b4b6b24e1 fix: attendance times showing +1 hour due to UTC timezone conversion
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>
2026-03-23 13:50:30 +01:00
BOHA
bb0bf25ce0 fix: CSP and Permissions-Policy blocking GPS, geocoding, and map
- Permissions-Policy: geolocation=(self) instead of geolocation=()
  (was blocking all geolocation access)
- connect-src: added nominatim.openstreetmap.org for reverse geocoding
- script-src: added unpkg.com for Leaflet JS
- style-src: added unpkg.com for Leaflet CSS
- img-src: added *.tile.openstreetmap.org for map tiles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:44:50 +01:00
BOHA
2718a7b716 fix: attendance admin — add user_name to records, fix Czech diacritics in table headers
- listAttendance() now maps users.first_name + last_name to user_name
- Fixed escaped Unicode in table headers (Zaměstnanec, Příchod, Poznámka)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:41:55 +01:00
BOHA
c817e004b7 feat: supplier name autocomplete on received invoices
- 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>
2026-03-23 13:32:38 +01:00
BOHA
a4c4a377c9 fix: frontend VAT preview — extract VAT from inclusive amount, not add on top 2026-03-23 13:27:50 +01:00
BOHA
8b5f216960 fix: received invoices — amount field is VAT-inclusive, extract VAT from it
Previously: amount was treated as base, VAT added on top (amount * rate / 100)
Now: amount includes VAT, VAT is extracted (amount - amount / (1 + rate/100))

Example: 9798.58 at 21% → VAT = 9798.58 - 8098.00 = 1700.58

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:24:54 +01:00
BOHA
fe12fde9db fix: convert dates to yyyy-MM-dd when opening received invoice edit form
ISO datetime strings from API caused "Invalid time value" in date picker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:19:43 +01:00
BOHA
9a0acb8983 fix: allow any authenticated user to list vehicles
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>
2026-03-23 13:12:22 +01:00
BOHA
20c1aab14c fix: use nodemailer address object for proper UTF-8 encoding in from name 2026-03-23 12:55:32 +01:00
BOHA
9f36ad0985 feat: configurable SMTP_FROM_NAME via .env (defaults to BOHA Automation) 2026-03-23 12:52:21 +01:00
BOHA
8a453deaac feat: add email notification for new leave requests
- 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>
2026-03-23 11:54:29 +01:00