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.
295 lines
4.5 KiB
JavaScript
295 lines
4.5 KiB
JavaScript
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'
|
|
]
|
|
|
|
}); |