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

295
gscript/Config.js Normal file
View File

@@ -0,0 +1,295 @@
const CFG = Object.freeze({
APP_NAME: 'EatMe Portál',
TZ: 'Europe/Prague',
SESSION_DAYS: 7,
LOGIN_CODE_MINUTES: 15,
PASSWORD_ROUNDS: 12000,
SHEETS: {
USERS: 'USERS',
EMPLOYEES: 'EMPLOYEES',
COMPANIES: 'COMPANIES',
LOCATIONS: 'LOCATIONS',
USER_COMPANIES: 'USER_COMPANIES',
PAY_RATES: 'PAY_RATES',
ATTENDANCE_EVENTS: 'ATTENDANCE_EVENTS',
SHIFTS: 'SHIFTS',
SHIFT_CHANGE_REQUESTS: 'SHIFT_CHANGE_REQUESTS',
SCHEDULES: 'SCHEDULES',
REQUESTS: 'REQUESTS',
MONTH_CLOSURES: 'MONTH_CLOSURES',
PAYROLL: 'PAYROLL',
PAYROLL_ITEMS: 'PAYROLL_ITEMS',
PAYSLIPS: 'PAYSLIPS',
NEWS: 'NEWS',
NEWS_READS: 'NEWS_READS',
DOCUMENTS: 'DOCUMENTS',
NOTIFICATIONS: 'NOTIFICATIONS',
SESSIONS: 'SESSIONS',
LOGIN_CODES: 'LOGIN_CODES',
AUDIT_LOG: 'AUDIT_LOG',
SETTINGS: 'SETTINGS'
}
});
const HEADERS = Object.freeze({
USERS: [
'user_id',
'email',
'role',
'employee_id',
'password_salt',
'password_hash',
'active',
'created_at',
'last_login'
],
EMPLOYEES: [
'employee_id',
'first_name',
'last_name',
'email',
'phone',
'company_id',
'location_id',
'position',
'employment_type',
'terminal_pin',
'start_date',
'end_date',
'active'
],
COMPANIES: [
'company_id',
'name',
'ico',
'active'
],
LOCATIONS: [
'location_id',
'company_id',
'name',
'active'
],
USER_COMPANIES: [
'user_id',
'company_id'
],
PAY_RATES: [
'pay_rate_id',
'employee_id',
'valid_from',
'valid_to',
'hourly_rate',
'created_by',
'created_at'
],
ATTENDANCE_EVENTS: [
'event_id',
'employee_id',
'timestamp',
'event_type',
'location_id',
'source',
'created_by'
],
SHIFTS: [
'shift_id',
'employee_id',
'work_date',
'clock_in',
'clock_out',
'break_minutes',
'worked_minutes',
'night_minutes',
'weekend_minutes',
'holiday_minutes',
'status',
'location_id',
'created_at',
'updated_at'
],
SHIFT_CHANGE_REQUESTS: [
'request_id',
'shift_id',
'employee_id',
'field',
'old_value',
'requested_value',
'reason',
'status',
'created_at',
'resolved_at',
'resolved_by'
],
SCHEDULES: [
'schedule_id',
'employee_id',
'location_id',
'start_at',
'end_at',
'status',
'note',
'created_by',
'created_at'
],
REQUESTS: [
'request_id',
'employee_id',
'type',
'date_from',
'date_to',
'reason',
'status',
'created_at',
'resolved_at',
'resolved_by',
'note'
],
MONTH_CLOSURES: [
'closure_id',
'employee_id',
'period',
'employee_confirmed_at',
'manager_approved_at',
'manager_approved_by',
'locked_at',
'status'
],
PAYROLL: [
'payroll_id',
'employee_id',
'period',
'approved_minutes',
'base_amount',
'bonus_amount',
'tips_amount',
'other_amount',
'final_amount',
'status',
'payment_date',
'updated_at'
],
PAYROLL_ITEMS: [
'item_id',
'payroll_id',
'employee_id',
'period',
'type',
'quantity',
'rate',
'amount',
'note',
'created_by',
'created_at'
],
PAYSLIPS: [
'payslip_id',
'employee_id',
'period',
'drive_file_id',
'file_name',
'uploaded_at',
'uploaded_by'
],
NEWS: [
'news_id',
'title',
'content',
'category',
'company_id',
'location_id',
'target_role',
'published_from',
'published_to',
'require_confirmation',
'active',
'created_by',
'created_at'
],
NEWS_READS: [
'news_read_id',
'news_id',
'user_id',
'read_at',
'confirmed_at'
],
DOCUMENTS: [
'document_id',
'employee_id',
'company_id',
'type',
'title',
'drive_file_id',
'visible_to_employee',
'uploaded_at',
'uploaded_by'
],
NOTIFICATIONS: [
'notification_id',
'user_id',
'type',
'title',
'message',
'url',
'read_at',
'created_at',
'email_sent_at'
],
SESSIONS: [
'session_id',
'user_id',
'token_hash',
'expires_at',
'created_at',
'last_seen_at'
],
LOGIN_CODES: [
'code_id',
'email',
'purpose',
'code_hash',
'expires_at',
'used_at',
'created_at'
],
AUDIT_LOG: [
'audit_id',
'user_id',
'action',
'entity_type',
'entity_id',
'old_value',
'new_value',
'created_at'
],
SETTINGS: [
'key',
'value'
]
});