maindrive

Platforma operacyjna dla MŚP — zarządzanie pracownikami, urlopami, sprzętem, marnotrawstwem i wynikami finansowymi w jednym miejscu.

Podwyżki i premie
Automatyczne obliczenia na podstawie wzrostu przychodów i zysku netto
Waste Radar
Zgłaszanie i śledzenie marnotrawstwa z systemem nagród
Urlopy
Wnioski urlopowe z przepływem zatwierdzenia przez managera
Działy i role
RBAC z hierarchią ról i izolacją danych po zakresie
Sprzęt
Ewidencja sprzętu z przypisaniem do pracownika
Kopie zapasowe
Automatyczne i ręczne snapshoty SQLite z możliwością przywrócenia

Szybki start

Jak wejść do aplikacji i wykonać pierwszą konfigurację.

Logowanie do panelu admina

Panel admina obsługuje dwa tryby logowania — przełącznik widoczny jest w oknie logowania.

1
Hasło admina (superadmin)
Pełny dostęp do wszystkich sekcji. Przy pierwszym logowaniu wpisane hasło staje się hasłem administratora (SHA-256). Dostęp: System motywacyjny, Urlopy, Organizacja, System.
2
Token pracownika (org_admin / manager)
Pracownik z rolą org_admin lub manager może zalogować się do panelu admina swoim osobistym tokenem. Widzi tylko sekcje do których ma uprawnienia.
3
Dodaj pracowników
W sekcji Organizacja → Pracownicy wpisz listę osób. Każda osoba dostaje unikalny token. Tutaj też ustawiasz role i % udziałów.
4
Skonfiguruj działy i role
W zakładce Organizacja → Działy i role utwórz działy, przypisz managerów i pracowników.
5
Pracownik loguje się tokenem
Pracownik wpisuje swój token na stronie głównej i widzi spersonalizowany widok z wynikami, urlopami i waste raderem.

Sekcje dostępne wg roli

Sekcjasuperadminorg_adminmanager
System motywacyjny
Urlopy✓ (cała org)✓ (team)
Wyposażenie
Waste Radar
Obiady (zarządzanie menu)
Moje obiady (wybór)
Organizacja
System
ℹ️
Token pracownika to jedyna forma jego uwierzytelnienia — nie ma loginów ani haseł. Token widoczny jest w panelu pracownika (ikona klucza) i można go zregenerować w sekcji Organizacja → Pracownicy.

Architektura

Backend
Node.js + Express.js

Synchroniczny serwer HTTP. Wszystkie operacje na bazie są synchroniczne dzięki better-sqlite3 — brak callbacków, brak race conditions. Serwer główny: server-sqlite.js (~3800 linii). Moduły wyciągnięte: server/lunch.js.

Baza danych
SQLite 3 + WAL mode

Pojedynczy plik db/kokpit.db. WAL mode zapewnia odczyty równoległe bez blokowania zapisów. Idealna dla MŚP do ~500k rekordów.

Frontend
Vanilla JS + Tailwind CDN

Pojedynczy plik public/index.html. Brak frameworków — szybkie ładowanie, zero zależności frontendowych.

Deployment
VPS OVH + PM2

PM2 zarządza procesem Node.js. Auto-restart po awarii. Dwie instancje: kokpit (port 3000) i kokpit-dev (port 3001). Pipeline deploy: syntax check + 253 testów + smoke test.

Test coverage
253 testów (node:test)

Pokrywają wszystkie moduły: financial (math pul, tenure-weighted bonus), voting, vacations, surveys, equipment, departments, waste, lunch, support, ownership-proposals, api-keys, anti-escalation, admin-scope, filters, reports, tenants, backups. Każdy deploy blokowany jeśli failure.

Bezpieczeństwo
Anti-escalation + Multi-sig

Middleware requireNotEscalating() blokuje org_admina przed modyfikacją superadmina (5 endpointów). Zmiany udziałów wymagają zatwierdzenia przez wszystkich pozostałych udziałowców (multi-sig).

Schemat bazy danych

-- Organizacje (multi-tenant)
tenants        id · name · slug · active · modules · created_at

-- Działy wewnątrz organizacji
departments    id · tenant_id · name · manager_id · created_at

-- Pracownicy z rolą i przypisaniem do działu
employees      id · name · start_date · eligible_for_raise · ownership_pct
               token · tenant_id · department_id · role

-- Moduł podwyżek i premii
financial_years   year · is_current · closed_at
financial_records year · period · navi_* · buildi_* · is_active · saved_at
votes             employee_id · count
used_tokens       token · voted_at
management        employee_id · amount

-- Moduły operacyjne
vacations      id · employee_id · type · start/end_date · status · tenant_id
equipment      id · employee_id · name · category · brand · model · serial_number · assigned_at · status · notes
equipment_requests id · employee_id · type · category · equipment_id · title · description · priority · status · admin_note · created_at · resolved_at · resolved_by
waste_items    id · employee_id · type · title · description · scale · estimated_cost · status · assigned_to · created_at · resolved_at · tenant_id

-- Obiady (template'owy: aktywne menu → resolveowane na konkretne daty)
lunch_menus    id · name · valid_from · valid_to · is_active · created_at · created_by · tenant_id
lunch_menu_items  id · menu_id · day_of_week (1..5) · name · description · price · position · is_vegetarian · is_vegan · is_gluten_free
lunch_orders   id · employee_id · date · menu_item_id · status · created_at · cancelled_at · cancellation_reason
lunch_restaurant_orders id · date · generated_at · total_amount · items_json · recipients · email_subject · email_body · status

-- Konfiguracja klucz-wartość (JSON)
settings       key · value
Migracja idempotentna — wszystkie ALTER TABLE ADD COLUMN są opakowane w sprawdzenie PRAGMA table_info, więc restart serwera na istniejącej bazie jest bezpieczny.

Role

System uprawnień oparty o RBAC (Role-Based Access Control) z hierarchią czterech ról.

superadmin
Właściciel platformy. Widzi wszystkie organizacje, może zarządzać tenantami i tworzyć nowe. Jedyna rola mogąca nadać innym rolę superadmin.
platform scope wszystkie moduły zarządzanie tenantami
org_admin
Administrator firmy-klienta. Zarządza pracownikami, działami i operacjami HR. Może zalogować się do panelu admina swoim tokenem pracowniczym. Nie widzi danych finansowych ani systemu motywacyjnego.
org scope panel admina (tokenem) zarządzanie działami urlopy całej org zmiana ról
manager
Kierownik działu. Może zalogować się do panelu admina tokenem i zarządzać urlopami swojego zespołu. Nie ma dostępu do danych finansowych, organizacji ani innych działów.
team scope panel admina (tokenem) urlopy zespołu zatwierdzanie waste
employee
Zwykły pracownik. Widzi wyłącznie swoje dane — własne urlopy, własne zgłoszenia waste, własny sprzęt i własne wyniki finansowe.
own scope podgląd wyników zgłaszanie waste wnioski urlopowe
⚠️
Rola jest przechowywana w kolumnie employees.role. Zmiana następuje przez dropdown w sekcji Organizacja → Pracownicy (tryb edycji) lub przez PATCH /api/v1/employees/:id/role.

Macierz uprawnień

Każde uprawnienie ma przypisany zakres danych (scope), który określa jakie rekordy są widoczne w odpowiedzi API.

own — tylko własne rekordy team — cały przypisany dział org — cała organizacja platform — wszystkie org — brak dostępu
Moduł / akcja superadmin org_admin manager employee
PRACOWNICY
readplatformorgteamown
create / delete
manage (role, dept)team
WASTE RADAR
readplatformorgteamown
create / update own
approve / resolveteam
delete
URLOPY
readplatformorgteamown
create (wniosek)
approve / rejectteam
FINANSE
readplatformorgteam*
create / update
DZIAŁY
readplatformorgteam
create / update / delete

* Manager widzi sumaryczne wyniki teamu, bez indywidualnych kwot (konfigurowalnie)


Działy

Działy to podstawowa jednostka organizacyjna określająca zakres danych dla managerów.

Jak działają zakresy

Kiedy manager wywołuje GET /api/v1/vacations, serwer wykonuje:

-- 1. Pobierz ID wszystkich pracowników z tego działu
SELECT id FROM employees
WHERE department_id = '[deptId managera]'
  AND tenant_id = '[tenantId]';

-- 2. Filtruj urlopy tylko tych pracowników
SELECT v.*, e.name FROM vacations v
LEFT JOIN employees e ON v.employee_id = e.id
WHERE v.tenant_id = '[tenantId]'
  AND v.employee_id IN (...);

Zarządzanie w panelu

Panel Organizacja → Działy i role umożliwia:

  • Tworzenie działów z opcjonalnym managerem
  • Przypisywanie pracowników przez modal „Członkowie"
  • Zmianę roli inline (dropdown przy każdym pracowniku)
  • Usuwanie działu — pracownicy zostają, tylko tracą przypisanie
💡
Ustawiając managera przy tworzeniu działu, jego rola jest automatycznie podnoszona z employee do manager.

Multi-tenant

Aplikacja obsługuje wiele niezależnych organizacji na jednej instalacji. Każda organizacja ma własny tenant_id — dane są izolowane na poziomie każdego zapytania SQL.

-- Każde zapytanie filtruje po tenant_id
WHERE tenant_id = '[tenantId z tokenu pracownika]'

Zarządzanie tenantami (API)

GET
/api/v1/tenants
Lista wszystkich organizacji — tylko superadmin (X-Admin-Token)
POST
/api/v1/tenants
Utwórz nową organizację
PATCH
/api/v1/tenants/:id/modules
Włącz lub wyłącz moduły dla organizacji (org_admin)

Dostępne moduły

Każda organizacja może włączyć lub wyłączyć poszczególne moduły:

POST /api/v1/tenants/me/modules
{
  "modules": ["dashboard", "employees", "waste", "vacations", "equipment"]
}

Wyłączony moduł zwraca 403 z komunikatem „Moduł X nie jest aktywny w Twojej organizacji".


Waste Radar

Pracownicy zgłaszają marnotrawstwo w firmie — drogie SaaS bez wartości, ad-hoc zakupy, nieefektywne procesy, marnowany czas. Za każde rozwiązane zgłoszenie — bonus dla całego zespołu (20% z odzyskanej kwoty).

4 kategorie

🔑
license · Subskrypcje i licencje
Nieużywane lub zdublowane SaaS, narzędzia za które płacimy bez wartości (np. nieaktywne konta Figma, dwa narzędzia robiące to samo)
🛒
purchase · Zakupy i koszty
Drogie zakupy, brak negocjacji, ad-hoc bez strategii zakupowej (np. drogi dostawca przy braku porównania, brak warunków hurtowych)
⚙️
process · Procesy
Nieefektywne workflow, ręczne kroki które dałoby się zautomatyzować, duplikacja prac między osobami / działami
time · Czas
Spotkania bez agendy, oczekiwanie na decyzje, długie procesy decyzyjne, kontekst-switching

Kwota straty (sprzężone inputy)

Pracownik wpisuje konkretną kwotę — albo miesięczną, albo roczną. Druga wylicza się dynamicznie (jak w Revolut/ING przy wymianie walut):

  • Wpiszesz 500 w polu mies. → roczne pokaże 6000, w stanie zapisuje się estimatedCost = 6000
  • Wpiszesz 25 000 w polu rocznym → mies. pokaże 2083, w stanie estimatedCost = 25000

Pole scale jest auto-derivowane z kwoty rocznej (etykieta agregatu w tabeli i raportach):

  • low — < 2 000 PLN/rok (Mała)
  • mid — 2 000–10 000 PLN/rok (Średnia)
  • high — 10 000–50 000 PLN/rok (Duża)
  • huge — ≥ 50 000 PLN/rok (Krytyczna)

Legacy fallback: jeśli klient wyśle wyłącznie scale bez estimatedCost, serwer wylicza koszt z domyślnej mapy (1k/5k/25k/75k). API od maja 2026 preferuje estimatedCost.

Przepływ statusów

active → in_progress → resolved
            ↑              ↑
            |              └── admin / manager / org_admin oznacza jako rozwiązane
            └── pracownik bierze się za naprawę (PATCH /waste/:id/assign)

Panel admina (zakładka Waste Radar)

  • Statystyki na górze: liczba otwartych, szacunkowa strata roczna, premia × pracownik (przy 100% rozwiązaniu), liczba rozwiązanych
  • Pasek strat per kategoria — kolorowy stack pokazujący w czym leży największy potencjał oszczędności
  • Status tabs z licznikami: Wszystkie / Aktywne / W trakcie / Rozwiązane
  • Filtr kategorii + checkbox "Tylko moje" (filtruje tabelę do zgłoszeń stworzonych przez zalogowanego użytkownika; agregaty na górze zostają firmowe)
  • Tabela: badge kategorii, tytuł + opis, autor, skala (auto-derived z kwoty), roczna strata, data, status (+ kto wziął), akcje
  • Akcje: → przypisz do mnie, ✓ rozwiąż (admin/manager dla swojego działu), × usuń (admin lub autor jeśli nie rozwiązane)

Mechanizm bonusów

Suma szacowanej rocznej straty z otwartych zgłoszeń × 20% = pula bonusów (gdyby udało się rozwiązać 100%). Pula podzielona równo na każdego pracownika to bonusPerPerson w GET /waste/stats.

3-krokowy formularz zgłoszenia

1
Kategoria
Wybór jednej z 4: subskrypcje / zakupy / procesy / czas
2
Tytuł i opis
Krótki tytuł (wymagany) + opcjonalne uzasadnienie
3
Kwota straty
Wpisz miesięczną LUB roczną — drugą wylicza dynamicznie. Skala (Mała/Średnia/Duża/Krytyczna) auto-derived z kwoty.

Schema bazy

CREATE TABLE waste_items (
  id             TEXT PRIMARY KEY,
  employee_id    TEXT NOT NULL,         -- autor zgłoszenia (FK do employees)
  type           TEXT NOT NULL,         -- license | purchase | process | time
  title          TEXT NOT NULL,
  description    TEXT,
  scale          TEXT,                  -- low | mid | high | huge
  estimated_cost REAL DEFAULT 0,        -- roczna strata w PLN
  status         TEXT DEFAULT 'active', -- active | in_progress | resolved
  assigned_to    TEXT,                  -- kto się tym zajmuje (FK do employees)
  created_at     TEXT,
  updated_at     TEXT,
  resolved_at    TEXT,
  tenant_id      TEXT DEFAULT 'default'
);

Urlopy

Pracownicy składają wnioski urlopowe przez panel pracownika. Manager lub HR manager zatwierdza je w panelu admina → Urlopy.

Typy wniosków

  • Urlop dzienny — zakres dat od/do, liczba dni obliczana automatycznie
  • Urlop godzinowy — konkretna data + godzina od + liczba godzin

Statusy

pending — oczekujący approved — zatwierdzony rejected — odrzucony

Panel zarządzania urlopami (admin)

Sekcja Urlopy w panelu admina pokazuje:

  • Statystyki: liczba oczekujących / zatwierdzonych / odrzuconych / łącznie
  • Tabelę wszystkich wniosków z filtrem po statusie i pracowniku
  • Przyciski Zatwierdź / Odrzuć inline przy wnioskach oczekujących

Dostępna dla: superadmin (wszystkie), org_admin (cała org), manager (tylko swój team).

Reguły zatwierdzania

  • Manager widzi i zatwierdza wnioski pracowników ze swojego działu
  • org_admin zatwierdza wszystkie wnioski w organizacji
  • Wniosek ze statusem pending może być anulowany przez pracownika

Moje urlopy (admin/manager)

Każdy zalogowany użytkownik z panelem admina (superadmin/org_admin/manager) ma w grupie MÓJ PANEL → Moje urlopy osobną sekcję do składania własnych wniosków:

  • Trzy karty statystyk: wykorzystano / pozostało / wymiar (z paskiem postępu)
  • Formularz: typ (dzień wolny / godzinowy / chorobowy / inny), daty, notatka
  • Historia własnych wniosków z badge'em statusu i przyciskiem anuluj dla pending
  • Korzysta z tych samych endpointów /api/v1/vacations/me co portal pracownika

Sprzęt

Ewidencja sprzętu firmowego z przypisaniem do konkretnego pracownika. Każdy element ma kategorię, status, opcjonalną markę/model/numer seryjny i datę przydzielenia.

Kategorie

laptop monitor phone headphones keyboard mouse other

Każda kategoria ma własną ikonę i kolor — używane spójnie w panelu admina i widoku pracownika.

Statusy sprzętu

  • active — aktywny, w użyciu
  • replace — do wymiany (np. uszkodzony, stary)
  • returned — zwrócony przez pracownika do magazynu
  • inactive — nieaktywny, wycofany z użytku

Widok pracownika

Pracownik w zakładce Sprzęt widzi wyłącznie sprzęt do niego przypisany (employee_id = jego ID). Lista pochodzi z GET /api/v1/equipment/me.

Każdy element pokazany jest jako karta: ikona kategorii, nazwa, marka·model, numer seryjny, data przydziału, badge statusu.

Panel admina

  • Status tabs z licznikami: Wszystkie / Aktywne / Do wymiany / Zwrócone / Nieprzypisane
  • Filtry: pracownik, kategoria, wyszukiwarka tekstowa (nazwa, marka, model, S/N, imię)
  • + Dodaj sprzęt — modal z formularzem (kategoria, status, nazwa, marka, model, S/N, przypisany do, notatki)
  • Edycja inline — klik na ikonę ✏ otwiera ten sam modal w trybie edycji + przycisk Usuń

Schema bazy

CREATE TABLE equipment (
  id            TEXT PRIMARY KEY,
  employee_id   TEXT,             -- FK do employees, NULL = magazyn
  name          TEXT NOT NULL,
  category      TEXT DEFAULT 'other',
  brand         TEXT,
  model         TEXT,
  serial_number TEXT,
  assigned_at   TEXT,             -- ISO timestamp, NULL gdy nieprzypisany
  status        TEXT DEFAULT 'active',
  notes         TEXT
);

Zgłoszenia sprzętowe

Każdy zalogowany pracownik (w tym admin/manager) może wysłać zgłoszenie sprzętowe trzech rodzajów:

  • newzapotrzebowanie na nowy sprzęt (wymaga wskazania kategorii)
  • faultusterka istniejącego sprzętu (wymaga wskazania własnego sprzętu)
  • replacementpotrzeba wymiany na nowszy / lepszy (wymaga wskazania własnego sprzętu)

Pracownik może zgłaszać usterki i wymianę tylko własnego sprzętu (walidacja po stronie serwera). Każde zgłoszenie ma priorytet (low / normal / high / urgent) i status, który zmienia administrator.

Statusy zgłoszeń

  • pending — oczekujące na decyzję administratora (nowe zgłoszenie)
  • approved — zaakceptowane, oczekuje realizacji (np. zamówienie wysłane)
  • in_progress — w trakcie realizacji (np. sprzęt w serwisie)
  • resolved — zrealizowane (sprzęt dostarczony / naprawiony)
  • rejected — odrzucone

Widok pracownika

Pod listą przypisanego sprzętu pracownik widzi 3 przyciski akcji: Zgłoś zapotrzebowanie, Zgłoś usterkę, Potrzebuję wymiany. Każda kafelka sprzętu ma też skrót do szybkiego zgłoszenia usterki tego konkretnego urządzenia. Poniżej widoczna jest lista moich zgłoszeń ze statusem, komentarzem administratora i opcją anulowania (gdy pending).

Panel admina (zakładka „Zgłoszenia")

  • Status tabs z licznikami: Wszystkie / Oczekujące / W trakcie / Zaakceptowane / Zrealizowane / Odrzucone
  • Badge na zakładce pokazuje liczbę pending wymagających reakcji
  • Kliknięcie Edytuj na zgłoszeniu otwiera modal: zmiana statusu + komentarz dla pracownika (widoczny u niego)
  • Manager widzi i edytuje tylko zgłoszenia pracowników ze swojego działu (scope serwerowy)
  • Usuwanie zgłoszeń: tylko superadmin / org_admin (manager nie usuwa)

Schema bazy — zgłoszenia

CREATE TABLE equipment_requests (
  id           TEXT PRIMARY KEY,
  employee_id  TEXT NOT NULL,         -- FK do employees (zgłaszający)
  type         TEXT NOT NULL,         -- 'new' | 'fault' | 'replacement'
  category     TEXT,                  -- tylko dla type='new'
  equipment_id TEXT,                  -- FK do equipment, dla fault/replacement
  title        TEXT NOT NULL,
  description  TEXT,
  priority     TEXT DEFAULT 'normal', -- low | normal | high | urgent
  status       TEXT DEFAULT 'pending',-- pending | approved | rejected | in_progress | resolved
  admin_note   TEXT,                  -- komentarz administratora (widoczny u pracownika)
  created_at   TEXT NOT NULL,
  updated_at   TEXT,
  resolved_at  TEXT,                  -- ustawiane automatycznie przy resolved/rejected
  resolved_by  TEXT,                  -- ID admina który zamknął zgłoszenie
  tenant_id    TEXT DEFAULT 'default'
);

Obiady

Codzienne menu obiadowe z wyborem przez pracowników. Obiady są darmowe dla pracowników — koszty pokrywa firma, monitoring kosztów per restauracja. Admin zarządza menu w sekcji „Operacje → Obiady", pracownik (każda rola) wybiera w „Moje obiady".

Model danych — template'owy

  • Menu = template z polami name, validFrom, validTo, isActive. Items są pogrupowane per dzień tygodnia (1=pon..5=pt).
  • Tylko jedno menu może być aktywne naraz (isActive=1). Pozostałe template'y mogą być archiwum / draftami / przygotowaniem na przyszłość.
  • Pozycje — nazwa, opis, cena (widoczna tylko adminowi/managerowi), tagi: wege/wegan/bezglut, day_of_week i position.
  • Wybór pracownika — jedna pozycja/dzień. UI to widok kalendarzowy miesiąca z kolumną numeru tygodnia ISO 8601. Klik dnia → modal z items aktywnego menu dla tego day_of_week.
  • Resolve: endpoint GET /lunch/effective?from=&to= dla każdej daty w zakresie zwraca items z aktywnego menu (jeśli data jest dniem roboczym i mieści się w validFromvalidTo).

Cykl życia zamówienia

1. Admin tworzy/aktywuje menu (np. „Menu majowe", validFrom 01.05, validTo 31.05)
2. Pracownicy nawigują kalendarzem i wybierają pozycje (status: pending)
3. Cutoff (default: tego samego dnia 09:00) — blokada edycji
4. Cutoff + delay (default: +5 min) — cron auto-generuje order do restauracji
   - Pracownicy z urlopem dziennym → status cancelled_vacation
   - Pozostali → status sent
5. Admin kopiuje treść maila do schowka i wysyła do restauracji
6. Po miesiącu admin sprawdza monthly-summary i porównuje z fakturą

Reguły blokady wyboru

  • 🚫 Weekend — UI omija (kalendarz pokazuje tylko Pn-Pt)
  • 🚫 Po cutoffie — wybór i anulowanie zablokowane (zamówienie poszło)
  • 🚫 Urlop dzienny (approved + days/sick/other) — UI nie pozwala wybierać; backend waliduje
  • 🚫 Przed datą zatrudnienia (startDate) — UI blokuje
  • 🚫 Poza maxDaysAhead — backend zwraca 400
  • 🚫 Brak aktywnego menu / poza validFrom-validTo — komórki kalendarza puste („brak menu")

Urlop zatwierdzony PO cutoffie

Restauracja nie akceptuje odwołań po ustalonym terminie — firma płaci. System pokazuje admin'owi te przypadki w „Zamówieniach → daily-summary", można je rozliczyć poza systemem.

Aktywacja menu

Endpoint PATCH /lunch/menus/:id/activate w transakcji ustawia is_active=0 dla wszystkich menu, potem is_active=1 dla wybranego. Nie da się usunąć aktywnego menu (409) — najpierw /deactivate.

Edycja menu po wyborach

Jeśli edycja PUT usuwa pozycje na które są aktywne picks (pending) — backend zwraca 409 z licznikiem. Z force: true picks są oznaczone jako cancelled_admin z reason menu_item_removed.

Konfiguracja (Operacje → Obiady → Ustawienia)

  • lunchModuleEnabled — feature flag (cron przestaje generować orderzy)
  • lunchCutoffTime — HH:MM (default 09:00)
  • lunchCutoffDayOffset0 tego samego dnia (default), -1 dzień wcześniej, 1 następnego dnia
  • lunchOrderDelayMinutes — odstęp między cutoff a wysyłką orderu (default 5)
  • lunchRestaurantEmail — adres do wysyłki (na razie informacyjny — system tylko przygotowuje treść do skopiowania)
  • lunchMaxDaysAhead — ile dni do przodu pracownik może wybierać (default 30)

Permissions

  • superadmin / org_admin: pełen zarząd menu, ustawień, raportów (sekcja Operacje → Obiady widoczna)
  • manager: tylko read aktywnego menu (do wybierania własnych obiadów)
  • employee: read aktywnego menu, create/update własnych picks (sekcja Moje obiady)

Schema bazy (po migracji 003)

CREATE TABLE lunch_menus (
  id, name, valid_from, valid_to, is_active,
  created_at, created_by, updated_at, tenant_id
);
CREATE INDEX idx_lunch_menus_active ON lunch_menus(is_active, valid_from, valid_to);

CREATE TABLE lunch_menu_items (
  id, menu_id, day_of_week,         -- 1=pon, 2=wt, ..., 5=pt
  name, description, price, position,
  is_vegetarian, is_vegan, is_gluten_free
);
CREATE INDEX idx_lunch_menu_items_menu_dow
  ON lunch_menu_items(menu_id, day_of_week, position);

CREATE TABLE lunch_orders (
  id, employee_id, date, menu_item_id,
  status,            -- pending | sent | cancelled_user | cancelled_vacation | cancelled_admin
  created_at, updated_at, cancelled_at, cancellation_reason, tenant_id
);
CREATE UNIQUE INDEX idx_lunch_orders_emp_date
  ON lunch_orders(employee_id, date);     -- max 1 pick / dzień / pracownik

CREATE TABLE lunch_restaurant_orders (
  id, date, generated_at, total_amount,
  items_json,         -- snapshot pozycji co poszło + ilości + emaily
  recipients, email_subject, email_body,
  status,             -- generated | sent | failed
  sent_at, tenant_id
);

Podwyżki i premie

Automatyczny system obliczania podwyżek i premii rocznych na podstawie wyników finansowych.

Pula podwyżek

-- Wzrost przychodu Navifleet + Buildibox
netGrowth = (przychód_obecny - przychód_poprzedni) × 2 spółki

-- Pula podwyżkowa = 20% wzrostu
raisePool = max(0, netGrowth) × 0.20

-- Podział puli
równa część   = raisePool × 50%  (wszyscy uprawnieni, po równo)
teamowa część = raisePool × 25%  (według głosowania)
zarząd        = raisePool × 25%  (ręcznie przez admina)

Pula bonusów

-- Zysk netto obu spółek × 20%
bonusPool = (naviProfit + buildiProfit) × 0.20

-- Premia proporcjonalna do stażu w roku
koefficient = dni_przepracowanych / 365
premiaOsobista = koefficient × (bonusPool / sumKoefficients)

Głosowanie

Każdy uprawniony pracownik wskazuje do 5 osób, które wg niego zasługują na teamową część puli. Wyniki są anonimowe — system zapisuje tylko sumy głosów per osoba.

ℹ️
Głosowanie jest jednorazowe na token. Po otwarciu głosowania stare wyniki są kasowane. Tokeny można zregenerować dla wszystkich pracowników naraz.

Rok finansowy

  • Dane wpisywane kwartalnie (Q1–Q4) lub rocznie
  • Aktywny rekord = podstawa obliczeń
  • Zamknięcie roku tworzy nowy rok i opcjonalnie resetuje głosowanie

Zgłoszenia (helpdesk)

Pływający przycisk pomocy na każdej stronie. Krótki opis + screen (Cmd+V), wątek czat-style, statusy lifecycle.

Entry point

Po zalogowaniu (admin lub pracownik) w prawym dolnym rogu pojawia się pomarańczowy FAB z ikoną „?". Klik otwiera modal z polami: Tytuł, Opis, Screen (paste z clipboardu lub upload). Auto-dołączamy meta (route, viewport, browser) — admin dostaje kontekst bez pytania.

4 statusy

  • new — wpłynął, admin nie tknął
  • in_progress — admin podjął
  • waiting_user — admin czeka na pracownika
  • resolved — zamknięty

Auto-przejścia: gdy admin pierwszy raz odpisze → in_progress. Druga wiadomość admina → waiting_user. Pracownik odpowiada → wraca in_progress. Status resolved ustawia tylko admin ręcznie.

Wątek

Każdy ticket ma listę wiadomości. Pracownik i admin piszą w tym samym wątku. Każda wiadomość może mieć attachment (max 1 screen, JPEG ≤2 MB po kompresji client-side). Auth-gated download — tylko właściciel ticketu i admin mogą pobrać plik.

Widoczność

  • Pracownik: widzi własne zgłoszenia w sekcji „Pomoc"
  • Admin (superadmin / org_admin): widzi wszystkie w tenancie w sekcji „Zgłoszenia"
  • Manager: widzi tylko swoje (jak zwykły pracownik)

Schema bazy

support_tickets        (id, employee_id, title, status, route, user_agent, viewport, ...)
support_messages       (id, ticket_id, author_employee_id, author_role, body, created_at)
support_attachments    (id, message_id, filename, mime, size)

Storage

Screeny zapisywane na dysku w uploads/support/<ticketId>/<uuid>.jpg — POZA bazą. Backupy DB (codzienne snapshoty) nie obejmują uploads/. To celowe — DB lekka, backupy szybkie.


Wnioski udziałowe (multi-sig governance)

Każda zmiana ownership_pct wymaga zatwierdzenia przez wszystkich pozostałych obecnych udziałowców (unanimous-minus-proposer). Bezpośrednia edycja jest zablokowana.

Po co to?

Bez tego mechanizmu superadmin (nawet bez udziałów) mógłby przyznać sobie ownership_pct, co zniekształciłoby pulę bonusów udziałowców. Każda zmiana = wniosek + głosowanie.

Cykl życia wniosku

  • pending — utworzony, czeka na głosy
  • applied — wszyscy pozostali udziałowcy zaaprobowali → zmiana wchodzi w życie
  • rejected — pojedynczy reject zamyka wniosek (z opcjonalnym komentarzem)
  • cancelled — wnioskodawca odwołał własny wniosek
  • expired — 30 dni bez decyzji → auto-zamknięty

Bootstrap

Gdy SUM(ownership_pct) = 0 (nikt jeszcze nie ma udziałów), pierwsze przyznanie udziałów stosuje się natychmiast — nie ma kogo pytać. Po pojawieniu się pierwszego udziałowca każda kolejna zmiana idzie przez wniosek.

Lone shareholder

Gdy proposer jest jedynym udziałowcem (sam sobie zmienia %), brak innych głosujących → wniosek aplikuje się natychmiast.

Walidacja

  • Suma ownership_pct po zastosowaniu wszystkich zmian: ≤100%
  • Każda wartość: 0–100
  • No-op (wszystkie zmiany == aktualnym wartościom): odrzucone

UI

  • Admin → sekcja „Wnioski udziałowe" → przycisk „+ Nowy wniosek" otwiera modal z tabelą wszystkich pracowników i edytowalnymi % (live walidator sumy)
  • Udziałowiec (rola: dowolna, byleby ownership_pct > 0) → sekcja „Wnioski udziałowe" w portalu pracownika z badge'em pending głosów
  • Każdy wniosek pokazuje: zmiany (old → new + delta), progress (głosy zatwierdzone / wymagane), listę głosów z komentarzami, opcjonalny powód odrzucenia

Zasada „pełny admin nie omija governance"

Recovery account (X-Admin-Token) także musi przejść przez wniosek + głosowanie. Inaczej kradzież tokena admina = kradzież wszystkich udziałów.

Schema bazy

ownership_proposals          (id, proposer_id, status, changes_json, expires_at, ...)
ownership_proposal_votes     (proposal_id, shareholder_id, ownership_pct_at_vote, vote, comment)

Panel admina — Pracownicy

Dodawanie pracowników

W zakładce Pracownicy wpisz imię i nazwisko, datę zatrudnienia, czy uczestniczy w podwyżkach i ewentualny procent udziałów. System automatycznie generuje token.

Token pracownika

6-znakowy kod (np. A3F8C1). Używany do:

  • Logowania w widoku pracownika (strona główna)
  • Autoryzacji API (Authorization: Bearer A3F8C1)
  • Identyfikacji głosu w głosowaniu
⚠️
Regeneracja tokenów usuwa historię głosowania. Jeśli głosowanie jest w toku — wstrzymaj się z regeneracją.

Udziałowcy

Pracownicy z ownership_pct > 0 nie uczestniczą w puli zarządowej, ale otrzymują dywidendę z puli udziałowców (netProfit × 20% × procent_udziałów).

Reset hasła pracownika

Obok każdego pracownika w tabeli jest przycisk 🔑. Klik otwiera modal generujący tymczasowe hasło:

  1. Generator tworzy 12-znakowe hasło (wielkie/małe litery, cyfry, znaki specjalne) używając crypto.getRandomValues(). Pomija znaki łatwe do pomylenia (0/O, 1/l/I). Można wygenerować nowe przyciskiem .
  2. Po zapisie API ustawia hasło i flagę must_change_password = 1.
  3. Modal pokazuje hasło z przyciskiem 📋 Kopiuj — przekaż pracownikowi bezpiecznym kanałem (Signal, 1Password, w cztery oczy).
  4. Hasło jest pokazane raz — po zamknięciu okna nie odzyskasz go (admin musiałby zresetować ponownie).
⚠️
Nie wysyłaj hasła e-mailem ani Slackiem w plain text. Po pierwszym logowaniu pracownik zobaczy blokujący modal wymuszający zmianę hasła zanim będzie mógł korzystać z aplikacji.

Powiązane endpointy: POST /api/v1/employees/:id/reset-password · POST /api/v1/profile/password


Panel admina — Dane finansowe

Wprowadź dane finansowe dla każdego kwartału lub okresu. Możesz wpisać dane dla Navifleet i Buildibox osobno.

Pola dla każdego okresu

  • Przychód obecny rok — YTD lub kwartalny
  • Przychód poprzedni rok — ten sam okres rok wcześniej (porównanie)
  • Zysk netto — podstawa puli bonusów

Aktywny rekord

Kliknij „Ustaw jako aktywny" przy wybranym kwartale — na jego podstawie zostaną wyliczone wszystkie pule. Możesz mieć wiele rekordów i przełączać między nimi.


Panel admina — Głosowanie

1
Otwórz głosowanie
Kliknij „Otwórz głosowanie". Stare wyniki zostaną skasowane. Pracownicy mogą teraz głosować.
2
Pracownicy oddają głosy
Każda osoba wskazuje do 5 kolegów. Token zostaje oznaczony jako „użyty".
3
Zamknij głosowanie
Głosowanie zamknięte — wyniki finalne. Nikt więcej nie może głosować.
4
Zatwierdź wyniki
W sekcji Zatwierdzenie kliknij „Zatwierdź podwyżki" i/lub „Zatwierdź premie". Wyniki stają się widoczne dla pracowników.

Kopie zapasowe

Auto-backup

Przy każdym starcie serwera (restart PM2, deploy) automatycznie tworzona jest kopia z etykietą autostart.

Ręczna kopia

Przycisk „Utwórz kopię teraz" w panelu admina → Kopie zapasowe. Etykieta manual.

Przywracanie

Kliknij „Przywróć" przy wybranej kopii. Serwer:

  1. Tworzy kopię zapasową aktualnego stanu (pre-restore)
  2. Flushuje WAL (wal_checkpoint(TRUNCATE))
  3. Zastępuje plik bazy danych
  4. Wykonuje process.exit(0) — PM2 restartuje proces automatycznie
⚠️
Maksymalnie 20 kopii jest przechowywanych — najstarsze są automatycznie usuwane. Pliki zapisywane w db/backups/.

Nazewnictwo plików

kokpit-{etykieta}-{YYYY-MM-DDTHH-MM-SS}.db

Przykłady:
kokpit-autostart-2026-05-01T19-15-41.db
kokpit-manual-2026-05-01T20-00-00.db
kokpit-pre-restore-2026-05-01T21-30-00.db

REST API v1

Pełna dokumentacja endpointów dostępna na osobnej stronie:

API Reference — wszystkie endpointy
40+ endpointów z przykładami odpowiedzi

Grupy endpointów

System
GET
/api/v1/health
Status serwera i liczba pracowników
Autoryzacja
POST
/api/v1/auth/admin/login
Logowanie admin — zwraca session token
POST
/api/v1/auth/employee/verify
Weryfikacja tokenu pracownika
Pracownicy
GET
/api/v1/employees
Lista pracowników — scope według roli
GET
/api/v1/employees/me
Własny profil z wynikami finansowymi
POST
/api/v1/employees
Dodaj pracownika
PATCH
/api/v1/employees/:id/role
Zmień rolę (org_admin+)
PATCH
/api/v1/employees/:id/department
Przypisz do działu
POST
/api/v1/employees/:id/reset-password
Reset hasła z wymuszoną zmianą
POST
/api/v1/profile/password
Zmień własne hasło
Filtry / Widoki / Ustawienia
GET
/api/v1/filters?context=:context
Zapisane widoki użytkownika
POST
/api/v1/filters
Zapisz widok
DELETE
/api/v1/filters/:id
Usuń widok
GET
/api/v1/settings
Ustawienia globalne
PUT
/api/v1/settings
Zaktualizuj ustawienia
Działy
GET
/api/v1/departments
Lista działów — scope według roli
POST
/api/v1/departments
Utwórz dział
GET
/api/v1/departments/:id/members
Członkowie działu
Urlopy / Waste / Sprzęt
GET
/api/v1/vacations
Lista wniosków — scope według roli
PATCH
/api/v1/vacations/:id/approve
Zatwierdź (manager+ teamu)
GET
/api/v1/waste
Lista waste items — scope według roli
PATCH
/api/v1/waste/:id/resolve
Oznacz jako rozwiązany
GET
/api/v1/equipment
Sprzęt — scope według roli
GET
/api/v1/equipment-requests
Zgłoszenia sprzętowe — scope według roli
POST
/api/v1/equipment-requests
Nowe zgłoszenie (każdy zalogowany)
PATCH
/api/v1/equipment-requests/:id/status
Zmień status (admin/manager)
Obiady
GET
/api/v1/lunch/menus
Lista template'ów menu
GET
/api/v1/lunch/effective?from=&to=
Resolve aktywnego menu na konkretne daty
POST
/api/v1/lunch/menus
Nowy template (admin)
PATCH
/api/v1/lunch/menus/:id/activate
Ustaw jako aktywne (deaktywuje pozostałe)
POST
/api/v1/lunch/orders
Wybór obiadu na dzień (UPSERT)
GET
/api/v1/lunch/admin/daily-summary?date=
Dzienne podsumowanie + treść maila
GET
/api/v1/lunch/admin/monthly-summary?year=&month=
Faktura miesięczna do weryfikacji

Autoryzacja

Dwa systemy tokenów

Admin — X-Admin-Token
X-Admin-Token: 65fef60e5ccb58cd...

64-znakowy hex. Generowany przy każdym logowaniu admina. Przechowywany w settings.adminSessionToken. Daje dostęp superadmin do wszystkich tras.

Pracownik — Bearer Token
Authorization: Bearer A3F8C1

6-znakowy kod przypisany do pracownika. Zakres danych zależy od roli i przypisanego działu. Token jest case-insensitive (automatycznie konwertowany do uppercase).

Dual-auth w v1

Middleware empAuth akceptuje oba typy tokenów. Jeśli wykryje poprawny X-Admin-Token — tworzy wirtualny kontekst superadmin z tenantId = 'default'. Dzięki temu panel admina może używać v1 API bez osobnego konta pracowniczego.

// empAuth — uproszczona logika
if (adminToken && adminToken === getSetting('adminSessionToken')) {
  req.employee = { role: 'superadmin', tenantId: 'default', ... };
  return next();
}
// lub Bearer token pracownika
const row = db.prepare('SELECT * FROM employees WHERE token = ?').get(token);
req.employee = dbEmpToJs(row); // zawiera: role, tenantId, departmentId

Odpowiedź przy braku uprawnień

// 403 — niewystarczająca rola
{
  "error": "Brak uprawnień",
  "detail": "Rola \"employee\" nie może wykonać \"approve\" w module \"vacations\""
}

// 403 — moduł wyłączony
{
  "error": "Moduł \"waste\" nie jest aktywny w Twojej organizacji"
}

Anti-escalation (security pattern)

org_admin nie może modyfikować superadmina w żadnym aspekcie — nawet jeśli adminAuth go przepuści. Zaimplementowane jako deklaratywne middleware requireNotEscalating():

// Wpinasz po adminAuth/empAuth — middleware ładuje target employee,
// sprawdza eskalację, ustawia req.targetEmployee dla handlera
v1.post('/employees/:id/reset-password', adminAuth, requireNotEscalating(), handler);
v1.put('/employees/:id',                    adminAuth, requireNotEscalating(), handler);
v1.delete('/employees/:id',                 adminAuth, requireNotEscalating(), handler);
v1.patch('/employees/:id/role',         empAuth, requireRole(...), requireNotEscalating(), handler);
v1.patch('/employees/:id/department',   empAuth, requireRole(...), requireNotEscalating(), handler);

Zwraca 403 z komunikatem "org_admin nie może modyfikować superadmina". Pattern jest opt-in — kolejny developer dorzucając endpoint /employees/:id/... po prostu wpina middleware. Brak ryzyka zapomnienia o ręcznym sprawdzeniu.

Ownership_pct — analogicznie zablokowane: zmiana udziałów wymaga multi-sig governance przez /ownership-proposals. Bezpośredni PUT/bulk zwracają 409 lub silent-skip.


API keys (integracje zewnętrzne)

Trzeci kanał auth (obok X-Admin-Token i Bearer pracownika). Dla mikrousług partnerskich które nie powinny mieć tożsamości żywego pracownika.

Po co osobno od user-tokenów?

  • Pracownik odchodzi z firmy → user token przepada → ich serwis pada. API key żyje niezależnie.
  • Audit log pokazuje „Klucz partner-HR zmienił X" zamiast „Anna zmieniła X" (bo to ich serwis udaje Annę).
  • Wąskie scope-y zamiast pełnej roli (vacations:read bez dostępu do equipment).
  • Per-klucz rate limit i revoke jednym klikiem.

Format klucza

kk_live_aB3xY2<28 znaków>

Klucz zwracany TYLKO przy generowaniu (response z POST /api/v1/api-keys). W DB trzymamy sha256(key). Frontend pokazuje raz z przyciskiem „Kopiuj" — potem jest niedostępny. Przy podejrzeniu wycieku → revoke + nowy.

Header w requestach

X-API-Key: kk_live_aB3xY2…

Scope-y

Płaska lista moduł:akcja. Akcja: read lub write.

employees:read | employees:write
vacations:read | vacations:write
equipment:read | equipment:write
lunch:read     | lunch:write
support:read   | support:write
waste:read     | waste:write
surveys:read   | surveys:write
departments:read | departments:write
financials:read

Forbidden ops (zawsze 403, niezależnie od scope-a): financials:write, backups:manage, voting:approve, employees:delete, settings:manage. To operacje zarezerwowane dla zalogowanego użytkownika.

Rate limit

Token bucket per klucz. Default 60 req/min, configurable 1–6000 przy generowaniu. Przekroczenie → 429 z headerem Retry-After: 60.

Audit log

Każde wywołanie z api-key → wpis w api_audit_log (key_id, method, path, status, duration_ms, ip, ts). Retencja 30 dni (auto-cleanup codziennie). Admin widzi feed w panelu „Integracje" → zakładka „Audyt".

Gdzie zarządzać

Panel admina (superadmin / org_admin) → sekcja Integracje. Lista kluczy z statusem (Aktywny / Cofnięty / Wygasły), generowanie nowych z wyborem scope-ów + rate limit + opcjonalnej daty wygaśnięcia.

Polling vs webhooks

Aktualnie obsługujemy tylko polling przez GET /:moduł. Webhook delivery (real-time push do partnera) — planowane w v2 jeśli pojawi się use case. Poll jest prostszy, partnerzy automatycznie nadrabiają zaległości po awariach.

Schema bazy

api_keys         (id, name, key_hash, scopes JSON, rate_limit, created_by, last_used_at, revoked_at, ...)
api_audit_log    (key_id, method, path, status, duration_ms, ip, ts)

Anti-eskalacja w Auth (related)

Niezależnie od kanału (user / admin / api-key), org_admin nie może modyfikować superadmin: reset hasła, edit, delete, role change, dept change → 403. Sprawdzane w 6 endpointach. UI ukrywa przyciski (🔑, „Usuń") na wierszu superadmina dla org_admina, mimo że backend i tak by zablokował.