Commit Graph

154 Commits

Author SHA1 Message Date
BOHA
106606f3fa fix: code review — XSS, type safety, validation improvements
Critical:
- InvoiceDetail: sanitize notes HTML with DOMPurify
- OrderDetail: use proper DOMPurify import instead of window fallback

Important:
- AttendanceBalances: add fund_to_date to interface, remove as-any casts
- All schemas: replace z.any() with z.preprocess for boolean fields
- Routes: simplify boolean coercion (Zod handles it now)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:13:20 +01:00
BOHA
3c167cf5c4 style: run prettier on entire codebase 2026-03-24 19:59:14 +01:00
BOHA
872be42107 feat: Czech public holidays in work fund calculation
- Created czech-holidays.ts with 11 fixed + 2 Easter-based holidays
- Fund now automatically excludes public holidays (no manual records needed)
- covered = worked + vacation + sick (NOT holidays — already in fund)
- Renamed "Odpracováno" to "Pokryto" (worked + leave = what counts)
- Removed dependency on holiday attendance records per employee

Matches PHP CzechHolidays::getMonthlyWorkFund() logic exactly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:37:03 +01:00
BOHA
780a6db001 fix: Odpracováno column shows covered hours (worked + leave + holidays)
Was showing only worked hours, but +/- was calculated against covered.
Now both columns are consistent — Odpracováno includes vacation, sick,
and holiday hours so the numbers make sense with the +/- column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:21:05 +01:00
BOHA
bc2a14f637 fix: include holiday hours in covered time instead of subtracting from fund
Previously: holidays reduced the fund (fund = bizDays - holidays).
This caused a mismatch — frontend compared covered against full
month fund, but backend used reduced fund.

Now: holidays count as covered hours (like vacation/sick). Fund stays
at full working days. So worked + vacation + sick + holidays = covered,
and covered >= fund means fulfilled.

Example: Jan has 22 days (176h), 1 holiday. Haas worked 168h.
Before: fund=168, covered=168, OK but frontend saw fund=176, not OK.
After: fund=176, covered=168+8=176, OK everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:18:26 +01:00
BOHA
c3bb0a6782 fix: March card header shows prorated fund (136h/17 dnů) matching the +/- values
Card header, per-user +/-, progress bar, and yearly table all now
use the same prorated fund_to_date for the current month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:15:53 +01:00
BOHA
03e830f97b fix: monthly cards and table show same +/- using prorated fund for current month
Backend: per-user missing/overtime in current month now calculated
against bizDaysToDate (working days up to today), not full month.

Frontend: monthly card percentage and fulfilled check also use
fund_to_date for current month.

Now both the yearly summary table and the monthly cards agree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:14:28 +01:00
BOHA
9724a7b2e9 fix: separate full month fund from prorated fund
Monthly cards show full month fund (e.g., 168h for 21 days).
Yearly summary table uses fund_to_date (prorated to today for
current month) so the +/- column is accurate mid-month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:12:13 +01:00
BOHA
db9c2929a8 fix: work fund — prorate current month to today's date
Past months use full month working days. Current month counts
working days only up to today (e.g., March 24 = 16 working days
out of 21), so the +/- column shows an accurate difference
instead of always showing a deficit mid-month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:09:24 +01:00
BOHA
a0f86deedb fix: work fund overview — only show past and current month, not future
Matches PHP: past year shows all 12, current year shows up to current
month, future year shows nothing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:05:31 +01:00
BOHA
0ec2cde5e5 fix: scope template editor — use RichEditor instead of textarea for content 2026-03-24 19:01:30 +01:00
BOHA
19912ecbe6 fix: scope template edit — read scope_template_sections from API response
API returns scope_template_sections (Prisma relation name) but
frontend was reading sections. Now checks both field names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:59:21 +01:00
BOHA
6497933c3e fix: keep lock after save — user stays on the page 2026-03-24 18:56:42 +01:00
BOHA
1addd22a24 fix: lock timeout 10s to match heartbeat interval 2026-03-24 11:26:32 +01:00
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