Add shift planning, month closure + payroll, and a dark modern redesign

Ports two modules from the friend's Google Apps Script build (kept as
reference in gscript/) onto the TypeScript stack, rewritten cleanly against
this app's own data model rather than copied 1:1:

- Shift planning: weekly template (Sun-Thu evening, Fri/Sat two slots),
  lazy idempotent generation per period (no cron needed), employee
  signup/cancel with collision + capacity checks, admin calendar view with
  slot editing and manual assignment.
- Month closure + payroll: employee confirms the month (blocked while a
  shift is still open), admin locks and finalizes pay (base hours * rate +
  tips/bonus/other), reopen to undo a premature lock, mark paid. Pay rates
  are versioned by date, defaulting the first-ever rate to apply
  retroactively to the employee's whole history.
- A shift left open more than 12h (forgotten clock-out) is auto-closed at
  clock_in + 12h, checked lazily on read instead of a background job.
- Full dark, sharp-edged modern restyle (theme.css replaces tui.css) with
  an amber accent, keeping every existing class name so no component
  logic needed to change.

Backend test coverage (jest) for all three workflows: shift planning,
closure/payroll, and the forgotten-clock-out auto-close.
This commit is contained in:
Michal Pemcak
2026-08-16 18:27:50 +02:00
parent fffcb73ea4
commit f917ed06a8
64 changed files with 46554 additions and 553 deletions

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { EmployeeManager } from '../components/EmployeeManager';
import { AdminStats } from '../components/AdminStats';
import { ShiftPlanManager } from '../components/ShiftPlanManager';
import { PayrollManager } from '../components/PayrollManager';
type Tab = 'employees' | 'stats';
type Tab = 'employees' | 'stats' | 'shifts' | 'payroll';
export function AdminApp() {
const [tab, setTab] = useState<Tab>('employees');
@@ -13,6 +15,8 @@ export function AdminApp() {
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
if (e.key === '1') setTab('employees');
if (e.key === '2') setTab('stats');
if (e.key === '3') setTab('shifts');
if (e.key === '4') setTab('payroll');
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
@@ -30,10 +34,19 @@ export function AdminApp() {
<button className={`tab ${tab === 'stats' ? 'active' : ''}`} onClick={() => setTab('stats')}>
[2] Statistiky
</button>
<button className={`tab ${tab === 'shifts' ? 'active' : ''}`} onClick={() => setTab('shifts')}>
[3] Směny
</button>
<button className={`tab ${tab === 'payroll' ? 'active' : ''}`} onClick={() => setTab('payroll')}>
[4] Mzdy
</button>
</div>
<div style={{ marginTop: '1rem' }}>
{tab === 'employees' ? <EmployeeManager /> : <AdminStats />}
{tab === 'employees' && <EmployeeManager />}
{tab === 'stats' && <AdminStats />}
{tab === 'shifts' && <ShiftPlanManager />}
{tab === 'payroll' && <PayrollManager />}
</div>
</>
);