Wprowadzenie
Witaj w dokumentacji REST API aplikacji maindrive. Poniżej znajdziesz wszystkie dostępne endpointy, formaty żądań i odpowiedzi.
Wszystkie żądania i odpowiedzi są w formacie application/json, kodowanie UTF-8. Wersja API: 1.0.0.
Request body: camelCase (np. employeeId, serialNumber, startDate).
Response: camelCase (od maja 2025; wcześniejsze wersje zwracały snake_case z DB). Pola w bazie SQLite pozostają w snake_case, konwersja zachodzi w warstwie API przez helper toCamel().
Pola pozostające w snake_case (do migracji): endpointy /waste/*, /financial/*, /departments/*.
Wszystkie endpointy v1 zwracają payload w polu data:
{ "data": {...} } // pojedynczy obiekt
{ "data": [...] } // lista
{ "data": { "deleted": true } } // po DELETE
Błędy zwracane są w ujednoliconym formacie JSON z opcjonalnym kodem błędu:
"error": "Opis błędu po polsku", "code": "OPTIONAL_ERROR_CODE"
Używane kody HTTP: 200 OK • 201 Created • 400 Bad Request • 401 Unauthorized • 403 Forbidden • 404 Not Found • 409 Conflict • 500 Server Error
Autentykacja
API obsługuje dwa typy uwierzytelnienia: dla administratora i dla pracownika.
Admin
Token uzyskany przez /auth/admin/login. Przekazywany w nagłówku:
X-Admin-Token: eyJhbGciOiJIUzI1NiJ9...
Daje dostęp do wszystkich endpointów jako wirtualny superadmin.
Pracownik / Manager / org_admin
Osobisty token pracownika przekazywany jako Bearer token:
Authorization: Bearer AB12CD
Zakres danych zależny od roli (own / team / org / platform). Pracownicy z rolą org_admin lub manager mogą używać tego tokenu do logowania w panelu admina.
Integracja zewnętrzna (API key)
Dedykowany klucz dla mikrousług partnerskich. Format kk_live_…. Header:
X-API-Key: kk_live_aB3xY2…
Generowany w panelu admina (Integracje · API keys). Każdy klucz ma własne scope-y (np. vacations:read), rate limit (req/min) i audit log. Pełny klucz pokazany TYLKO przy generowaniu — w DB trzymamy sha256.
Płaska lista moduł:akcja. Akcja to 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
Operacje user-only (zawsze 403 dla X-API-Key bez względu na scope): financials:write, backups:manage, voting:approve, employees:delete, settings:manage.
Limit / błędy: przekroczenie rate limitu → 429 z headerem Retry-After: 60. Klucz cofnięty/wygasły → 401. Brak scope-a → 403 z polem detail mówiącym jaki scope wymagany.
Pełne listy przez GET /:moduł. Inkrementalny sync (?updatedAfter=…) i webhook delivery — planowane w v2 zależnie od potrzeb partnerów.
Endpointy oznaczone Admin wymagają nagłówka X-Admin-Token. Endpointy oznaczone Pracownik akceptują Authorization: Bearer — zakres odpowiedzi zależy od roli. Endpointy oznaczone Publiczny nie wymagają autentykacji.
Dual-auth: middleware empAuth akceptuje oba typy — X-Admin-Token tworzy wirtualny kontekst superadmin, Bearer token tworzy kontekst pracownika z jego rolą.
System
Sprawdza stan serwera i zwraca podstawowe informacje o aplikacji.
{
"status": "ok",
"version": "1.0.0",
"employees": 8
}
Autentykacja — Endpointy
Loguje administratora i zwraca token sesji.
{
"password": "moje-haslo-admina"
}
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4ifQ.abc123",
"firstRun": false // true gdy brak hasła — pierwsze uruchomienie
}
}
Weryfikuje 6-znakowy token pracownika i zwraca dane identyfikacyjne.
{
"token": "AB12CD"
}
{
"data": {
"token": "AB12CD",
"name": "Marek Kowalski",
"employeeId": "emp_01"
}
}
Pracownicy
Zwraca listę wszystkich pracowników wraz z flagą czy oddali głos.
{
"data": [
{
"id": "emp_01",
"name": "Marek Kowalski",
"startDate": "2021-03-15",
"eligibleForRaise": true,
"ownershipPct": 12.5,
"token": "AB12CD",
"hasVoted": true
},
{
"id": "emp_02",
"name": "Anna Wiśniewska",
"startDate": "2020-07-01",
"eligibleForRaise": true,
"ownershipPct": 20.0,
"token": "XY98ZW",
"hasVoted": false
}
]
}
Zwraca szczegółowe dane pojedynczego pracownika.
{
"data": {
"id": "emp_01",
"name": "Marek Kowalski",
"startDate": "2021-03-15",
"eligibleForRaise": true,
"ownershipPct": 12.5,
"token": "AB12CD",
"hasVoted": true
}
}
Tworzy nowego pracownika i generuje dla niego unikalny 6-znakowy token.
{
"name": "Piotr Nowak",
"startDate": "2024-01-15",
"eligibleForRaise": true,
"ownershipPct": 5.0
}
{
"data": {
"id": "emp_09",
"name": "Piotr Nowak",
"startDate": "2024-01-15",
"eligibleForRaise": true,
"ownershipPct": 5.0,
"token": "QR34ST",
"hasVoted": false
}
}
Aktualizuje dane pracownika (pełna zamiana zasobu).
{
"name": "Piotr Nowak",
"startDate": "2024-01-15",
"eligibleForRaise": false,
"ownershipPct": 7.5
}
{
"data": {
"id": "emp_09",
"name": "Piotr Nowak",
"startDate": "2024-01-15",
"eligibleForRaise": false,
"ownershipPct": 7.5,
"token": "QR34ST"
}
}
Usuwa pracownika z systemu wraz z powiązanymi danymi.
{
"message": "Pracownik został usunięty"
}
Zwraca dane zalogowanego pracownika wraz z wynikami głosowania i informacją o podwyżce.
{
"data": {
"id": "emp_02",
"name": "Anna Wiśniewska",
"startDate": "2020-07-01",
"eligibleForRaise": true,
"ownershipPct": 20.0,
"hasVoted": true,
"votingResults": {
"available": true,
"votes": 5,
"rank": 2
},
"raiseAmount": 1200.00
}
}
Głosowanie
Zwraca aktualny stan głosowania — czy jest otwarte, ile osób jest uprawnionych i ile już zagłosowało.
{
"open": true,
"eligible": 8,
"voted": 3
}
Otwiera rundę głosowania — pracownicy mogą od tej chwili oddawać głosy.
{
"message": "Głosowanie zostało otwarte",
"open": true
}
Zamyka głosowanie i finalizuje wyniki.
{
"message": "Głosowanie zostało zamknięte",
"open": false
}
Oddaje głos pracownika — można wskazać maksymalnie 5 ID pracowników.
{
"votes": ["emp_01", "emp_03", "emp_05"]
}
{
"message": "Głos został zapisany",
"votesCount": 3
}
Zwraca pełne wyniki głosowania posortowane malejąco według liczby głosów.
{
"data": [
{ "employeeId": "emp_01", "name": "Marek Kowalski", "votes": 7, "rank": 1 },
{ "employeeId": "emp_02", "name": "Anna Wiśniewska", "votes": 5, "rank": 2 },
{ "employeeId": "emp_03", "name": "Tomasz Jabłoński", "votes": 3, "rank": 3 }
],
"totalVoters": 8,
"voted": 6
}
Regeneruje tokeny dostępowe dla wszystkich pracowników (np. przed nową rundą głosowania).
{
"message": "Tokeny zostały zregenerowane",
"regenerated": 8
}
Dane Finansowe
Zwraca listę wszystkich lat obrachunkowych z ich statusem i aktywnymi rekordami.
{
"data": [
{
"year": 2024,
"closed": true,
"records": 4
},
{
"year": 2025,
"closed": false,
"records": 2
}
]
}
Zwraca pule bonusowe i podwyżkowe wyliczone na podstawie aktywnych rekordów finansowych.
{
"data": {
"bonusPool": 48750.00,
"raisePool": 24000.00,
"currency": "PLN",
"basedOnPeriod": "Q1 2025"
}
}
Dodaje rekord finansowy dla danego roku i okresu rozliczeniowego.
{
"period": "Q1",
"navifleet": {
"revenueThisYear": 1250000.00,
"revenuePrevYear": 980000.00,
"netProfit": 187500.00
},
"buildibox": {
"revenueThisYear": 340000.00,
"revenuePrevYear": 210000.00,
"netProfit": 62000.00
}
}
{
"data": {
"year": 2025,
"period": "Q1",
"active": false,
"createdAt": "2025-04-05T10:30:00Z"
}
}
Usuwa rekord finansowy dla wskazanego roku i okresu.
{
"message": "Rekord został usunięty"
}
Ustawia wskazany rekord jako aktywny (deaktywuje inne rekordy w tym roku).
{
"message": "Rekord Q1 2025 jest teraz aktywny",
"active": true
}
Zamyka rok obrachunkowy i blokuje możliwość edycji jego rekordów.
{
"message": "Rok 2024 został zamknięty",
"year": 2024,
"closed": true
}
Urlopy
Zwraca wszystkie wnioski urlopowe wszystkich pracowników.
{
"data": [
{
"id": "vac_01",
"employeeId": "emp_02",
"employeeName": "Anna Wiśniewska",
"type": "days",
"startDate": "2025-06-09",
"endDate": "2025-06-20",
"note": "Wyjazd rodzinny",
"status": "pending",
"createdAt": "2025-05-10T08:15:00Z"
}
]
}
Zwraca wnioski urlopowe zalogowanego pracownika.
{
"data": [
{
"id": "vac_01",
"type": "days",
"startDate": "2025-06-09",
"endDate": "2025-06-20",
"note": "Wyjazd rodzinny",
"status": "pending"
}
]
}
Składa wniosek urlopowy — na dni lub godziny.
{
"type": "days", // "days" | "hours"
"startDate": "2025-06-09",
"endDate": "2025-06-20",
"note": "Wyjazd rodzinny"
}
{
"data": {
"id": "vac_02",
"type": "days",
"startDate": "2025-06-09",
"endDate": "2025-06-20",
"status": "pending"
}
}
Zatwierdza wniosek urlopowy pracownika.
{
"message": "Wniosek urlopowy został zatwierdzony",
"status": "approved"
}
Odrzuca wniosek urlopowy pracownika.
{
"message": "Wniosek urlopowy został odrzucony",
"status": "rejected"
}
Anuluje własny wniosek urlopowy — tylko jeśli jest w statusie pending.
{
"message": "Wniosek urlopowy został anulowany"
}
Profil i hasła
Pracownik (lub admin) zmienia własne hasło. Wymaga potwierdzenia obecnym hasłem albo tokenem (jeśli hasło nie jest jeszcze ustawione, currentCredential jest opcjonalne). Po pomyślnej zmianie zerowana jest flaga must_change_password.
{
"newPassword": "NoweHaslo2025!", // min. 6 znaków
"currentCredential": "StareHaslo" // hasło lub token
}
{ "data": { "ok": true } }
Admin ustawia tymczasowe hasło dla pracownika i ustawia flagę must_change_password = 1. Przy najbliższym logowaniu pracownik zobaczy blokujący modal wymuszający zmianę hasła zanim będzie mógł korzystać z aplikacji.
{
"newPassword": "Tymczasowe2025!" // min. 6 znaków
}
{ "data": { "ok": true, "mustChangeAtNextLogin": true } }
GET /employees/me zwraca dodatkowe flagi:
passwordSet (czy hasło już istnieje) oraz mustChangePassword (czy klient powinien wymusić zmianę po zalogowaniu).
Filtry / Zapisane widoki
Mechanizm zapisywania filtrów / widoków per użytkownik per kontekst (np. kontekst "vacations" dla listy urlopów). Pozwala każdemu użytkownikowi mieć swoje zapisane konfiguracje filtrów + sortowania.
Zwraca zapisane widoki dla zalogowanego użytkownika w danym kontekście. Sortowane od najnowszego.
context="vacations" // nazwa kontekstu (string)
{
"data": [
{
"id": "abc123-uuid",
"name": "Mój zespół",
"context": "vacations",
"filter": { // dowolny obiekt JSON },
"createdAt": "2025-05-02T..."
}
]
}
Zapisuje nowy widok dla zalogowanego użytkownika.
{
"context": "vacations",
"name": "Mój zespół",
"filter": { // dowolna struktura — będzie zwrócona 1:1 }
}
{ "data": { "id", "name", "context", "filter" } }
context, name lub filter
Usuwa zapisany widok. Tylko właściciel może go usunąć — endpoint sprawdza user_id.
{ "data": { "deleted": true } }
Ustawienia globalne
Zwraca ustawienia globalne aplikacji (konfiguracja modułów). Dostępne dla wszystkich zalogowanych — używane przez frontend do dostosowania UI.
{
"data": {
"vacationApprovalMode": "manager", // "manager" lub "auto"
"vacationDaysLimit": 26,
"votingOpen": false,
"raisesApproved": false,
"bonusesApproved": false
}
}
Aktualizuje wybrane pola w ustawieniach. Body to dowolny obiekt z polami do nadpisania — pola pominięte pozostają niezmienione.
{
"vacationApprovalMode": "auto",
"vacationDaysLimit": 28
}
{ "data": { // pełny obiekt ustawień po update } }
Głosowanie — operacje admina
Zwraca archiwum zatwierdzonych głosowań pogrupowane po roku. Każdy rok zawiera snapshot wyników wszystkich pracowników.
{
"data": {
"2024": [
{
"year": 2024,
"employeeId": "5dc3...",
"employeeName": "Marcin Smoleń",
"bonus": 2690.12,
"shareholderBonus": 0,
"raiseMonthly": 189.45,
"raiseAnnual": 2273.40,
"votesReceived": 12,
"voteShare": 0.0461,
"approvedAt": "2025-01-15T..."
}
]
}
}
Zatwierdza wyniki głosowania, robi snapshot wszystkich pracowników do tabeli result_snapshots (rok bieżący), zamyka głosowanie i ustawia flagi bonusesApproved.
{ "data": { "approved": true, "year": 2025 } }
Resetuje aktualną rundę głosowania — usuwa wszystkie głosy z tabel votes, voted_employees, used_tokens, zamyka głosowanie. Niezatwierdzone wyniki przepadają.
{ "data": { "reset": true } }
Sprzęt
Inwentarz sprzętu firmowego z przypisaniami do pracowników. Wszystkie endpointy admina wymagają autoryzacji x-admin-token lub Bearer (admin/manager/org_admin), endpoint /equipment/me wymaga sesji pracownika.
laptop, monitor, phone, headphones, keyboard, mouse, otherstatus:
active, replace, returned, inactive
Zwraca kompletny inwentarz wraz z imieniem przypisanego pracownika (LEFT JOIN). Sortowane po nazwie.
{
"data": [
{
"id": "a1f3...-uuid",
"employeeId": "5dc3e512-...",
"employeeName": "Marcin Smoleń",
"name": "MacBook Pro 14",
"category": "laptop",
"brand": "Apple",
"model": "M3 Pro 16GB/512GB",
"serialNumber": "FVFT2X3NXY12",
"assignedAt": "2024-03-12T08:00:00.000Z",
"status": "active",
"notes": null
}
]
}
Zwraca sprzęt przypisany do zalogowanego pracownika (employeeId = ID z sesji). Brak pola employeeName bo użytkownik zna swoje imię.
{
"data": [
{
"id": "a1f3...-uuid",
"employeeId": "5dc3e512-...",
"name": "MacBook Pro 14",
"category": "laptop",
"brand": "Apple",
"model": "M3 Pro 16GB/512GB",
"serialNumber": "FVFT2X3NXY12",
"assignedAt": "2024-03-12T08:00:00.000Z",
"status": "active",
"notes": null
}
]
}
Dodaje nowy sprzęt. Request używa camelCase, response snake_case (z DB).
Jeśli przekazany employeeId, automatycznie ustawiana jest data assignedAt.
{
"name": "MacBook Pro 14", // wymagane
"category": "laptop", // default: "other"
"brand": "Apple", // opcjonalne
"model": "M3 Pro 16GB", // opcjonalne
"serialNumber": "FVFT2X3NXY12", // opcjonalne
"employeeId": "5dc3e512-...", // null = magazyn
"status": "active", // default: "active"
"notes": "Notatka" // opcjonalne
}
{
"data": {
"id": "a1f3...-uuid",
"employeeId": "5dc3e512-...",
"name": "MacBook Pro 14",
"category": "laptop",
"brand": "Apple",
"model": "M3 Pro 16GB",
"serialNumber": "FVFT2X3NXY12",
"assignedAt": "2025-05-02T12:34:56.000Z",
"status": "active",
"notes": null
}
}
name
Aktualizuje pola sprzętu (oprócz przypisania — to robi PATCH /equipment/:id/assign). Pominięte pola pozostają niezmienione.
{
"name": "MacBook Pro 14 (zaktualizowany)",
"category": "laptop",
"brand": "Apple",
"model": "M3 Max 32GB/1TB",
"serialNumber": "FVFT2X3NXY12",
"status": "replace",
"notes": "Bateria do wymiany"
}
{ "data": { // pełny obiekt sprzętu (snake_case) } }
Przypisuje sprzęt do pracownika lub odpisuje (employeeId: null). Jeśli przypisanie nowemu — automatycznie ustawia assignedAt na obecny czas.
{
"employeeId": "5dc3e512-..." // null = oddziel od pracownika
}
{ "data": { // pełny obiekt sprzętu po zmianie } }
Usuwa sprzęt z inwentarza (twardo). Brak walidacji „sprzęt jest przypisany" — można usuwać sprzęt aktualnie u pracownika.
{ "data": { "deleted": true } }
Zgłoszenia sprzętowe
Zgłoszenia od pracowników: nowy sprzęt (type: "new"), usterka (type: "fault"), wymiana (type: "replacement").
Każdy zalogowany użytkownik może utworzyć zgłoszenie. Status modyfikuje superadmin / org_admin / manager (manager — tylko swojego działu).
Wszystkie odpowiedzi w camelCase.
Zwraca listę zgłoszeń. Scope zależy od roli:
superadmin / org_admin — wszystkie;
manager — zgłoszenia pracowników z jego działu;
employee — własne.
Sortowane po createdAt DESC. Lista zawiera employeeName i equipmentName (JOIN).
{
"data": [
{
"id": "a1b2c3...",
"employeeId": "emp-uuid",
"employeeName": "Anna Sokołowska",
"type": "fault", // new | fault | replacement
"category": null, // tylko dla type=new
"equipmentId": "eq-uuid", // tylko dla fault/replacement
"equipmentName": "MacBook Pro 14\"",
"title": "Laptop nie ładuje się",
"description": "Po podłączeniu zasilacza...",
"priority": "high", // low | normal | high | urgent
"status": "pending", // pending | approved | rejected | in_progress | resolved
"adminNote": null,
"createdAt": "2026-05-03T10:15:00.000Z",
"updatedAt": null,
"resolvedAt": null,
"resolvedBy": null
}
]
}
Alias filtrujący do własnych zgłoszeń (employeeId = ID z sesji). Pole employeeName nie jest zwracane (klient zna własne imię). Używane na zakładce „Twój sprzęt" / „Mój sprzęt".
{ "data": [ // jak wyżej, bez pola employeeName ] }
Tworzy nowe zgłoszenie. Pole employeeId brane z sesji — pracownik nie może wystawiać zgłoszeń w cudzym imieniu. Domyślny status = pending, domyślny priorytet = normal.
{
"type": "fault", // wymagane: new | fault | replacement
"title": "Laptop nie ładuje się", // wymagane (po trimie)
"description": "…", // opcjonalne
"priority": "high", // opcjonalne, default normal
"category": "laptop", // wymagane gdy type=new
"equipmentId": "eq-uuid" // wymagane gdy type=fault | replacement; sprzęt MUSI należeć do zgłaszającego
}
{ "data": { "id": "…", "status": "pending" } }
Aktualizuje status i/lub komentarz administratora. Dostępne dla superadmin, org_admin, manager. Manager może zmieniać tylko zgłoszenia pracowników ze swojego działu (po stronie serwera weryfikowane na podstawie departmentId). Przy przejściu w resolved lub rejected automatycznie ustawiane resolvedAt i resolvedBy (jeśli były null).
{
"status": "in_progress", // opcjonalne, jedna z: pending | approved | rejected | in_progress | resolved
"adminNote": "Zamówione, dostawa za 3 dni" // opcjonalne; null = brak zmiany
}
{ "data": { // pełne zgłoszenie po aktualizacji } }
Usuwa zgłoszenie. Autor może usunąć tylko własne zgłoszenie i tylko jeśli ma status pending (anulowanie). Superadmin / org_admin mogą usunąć każde zgłoszenie w dowolnym stanie. Manager nie może usuwać.
{ "data": { "deleted": true } }
Waste Radar
Zgłoszenia marnotrawstwa w 4 kategoriach: license (subskrypcje i licencje), purchase (zakupy i koszty), process (procesy), time (czas). Kwota roczna (estimatedCost) jest pierwszorzędna — UI ma sprzężone inputy miesięczne/roczne, wysyła zawsze konkretną liczbę. Skala (low/mid/high/huge) jest etykietą agregatu, auto-derived z kwoty wg progów <2k / <10k / <50k / ≥50k PLN. Legacy fallback: gdy nie poda się estimatedCost, serwer bierze z mapy WASTE_SCALE_COST (1k/5k/25k/75k). Wszystkie odpowiedzi w camelCase.
Zwraca listę zgłoszeń. Scope wg roli (employee → swoje, manager → dział, org_admin → org, superadmin → wszystko). Lista zawiera reporterName i assigneeName (JOIN z employees).
status: "active" | "in_progress" | "resolved" (opcjonalny) type: "license" | "purchase" | "process" | "time" (opcjonalny)
{
"data": [
{
"id": "a1b2…",
"employeeId": "emp-uuid",
"reporterName": "Anna Sokołowska",
"type": "license",
"title": "5 nieużywanych licencji Figma",
"description": "Płacimy 75 USD/mies za miejsca…",
"scale": "mid", // low | mid | high | huge
"estimatedCost": 5000, // roczna strata w PLN
"status": "active",
"assignedTo": null,
"assigneeName": null,
"createdAt": "2026-05-03T09:00:00Z",
"resolvedAt": null
}
],
"scope": "team"
}
Zwraca zagregowane statystyki marnotrawstwa. Honoruje scope wg roli — manager dostaje agregat tylko swojego działu. byType zawiera kategoryzację dla widoku admina (paski strat per kategoria).
{
"data": {
"totalAnnual": 47200, // suma estimatedCost dla otwartych zgłoszeń
"openCount": 12,
"resolvedCount": 5,
"inProgressCount": 3,
"bonusPerPerson": 589.5, // 20% z totalAnnual / liczba pracowników
"byType": {
"license": { "count": 3, "estimatedAnnual": 12000 },
"purchase": { "count": 2, "estimatedAnnual": 15000 },
"process": { "count": 5, "estimatedAnnual": 15200 },
"time": { "count": 2, "estimatedAnnual": 5000 }
}
}
}
Zgłasza nowe marnotrawstwo. employeeId brane z sesji. estimatedCost (kwota roczna w PLN) jest pierwszorzędnym źródłem wartości — frontend od maja 2026 wysyła ją zawsze (UI ma sprzężone inputy mies./rok jak Revolut). scale jest opcjonalna, służy tylko jako etykieta agregatu (Mała/Średnia/Duża/Krytyczna) i może być auto-derivowana z kwoty po stronie klienta.
{
"type": "license", // wymagane: license | purchase | process | time
"title": "5 nieużywanych licencji Figma", // wymagane (po trimie)
"description": "…", // opcjonalne
"estimatedCost": 6000, // PLN/rok — preferowane, ma priorytet
"scale": "mid" // opcjonalne, auto-derived z kwoty: <2k=low, <10k=mid, <50k=high, ≥50k=huge
}
Jeśli pominiesz estimatedCost a podasz scale, serwer wylicza koszt wg mapy: low=1000, mid=5000, high=25000, huge=75000 PLN/rok. Pominięcie obu → estimatedCost = 0 (zgłoszenie nie liczy się do agregatów strat).
{ "data": { // pełny obiekt waste w camelCase } }
Przypisuje pracownika do rozwiązania zgłoszenia (zmienia status na in_progress).
{
"message": "Zgłoszenie zostało przypisane",
"status": "in_progress",
"assignedTo": "emp_05"
}
Zatwierdza rozwiązanie zgłoszenia i zamyka je jako resolved.
{
"message": "Zgłoszenie zostało zamknięte jako rozwiązane",
"status": "resolved"
}
Usuwa zgłoszenie. Autor może usunąć własne tylko jeśli nie jest resolved. Superadmin / org_admin mogą usunąć każde.
{ "data": { "deleted": true } }
Obiady
Moduł obiadów. Model template'owy: admin tworzy menu z polami name, validFrom, validTo i listą pozycji pogrupowanych per dzień tygodnia (1=pon..5=pt). Tylko jedno menu może być aktywne (isActive=1) — jego items resolveują się na konkretne daty przez endpoint /lunch/effective. Pracownik wybiera 1 pozycję/dzień. O cutoff cron generuje zamówienie do restauracji. Urlop dzienny (approved + days/sick/other) wyklucza obiad. Wszystkie odpowiedzi w camelCase.
Ustawienia modułu — cutoff, email restauracji, max dni do przodu.
{
"data": {
"lunchModuleEnabled": true,
"lunchCutoffTime": "09:00",
"lunchCutoffDayOffset": 0, // -1 / 0 / 1 dni względem dnia obiadu
"lunchOrderDelayMinutes": 5,
"lunchRestaurantName": "Bistro Mama",
"lunchRestaurantEmail": "zamowienia@bistro.pl",
"lunchMaxDaysAhead": 30
}
}
Aktualizuje ustawienia. Można wysłać tylko zmieniane pola — pozostałe zostaną zachowane.
{
"lunchModuleEnabled": true,
"lunchCutoffTime": "09:00", // HH:MM, 0-23:0-59
"lunchCutoffDayOffset": 0, // -1, 0, 1
"lunchOrderDelayMinutes": 5, // 0-240
"lunchRestaurantName": "Bistro Mama",
"lunchRestaurantEmail": "…",
"lunchMaxDaysAhead": 30 // 1-90
}
Resolveuje aktywne menu na konkretne daty w zadanym zakresie. Dla każdej daty (która: jest dniem roboczym i mieści się w validFrom–validTo aktywnego menu) zwraca items dla odpowiedniego day_of_week. Używane przez frontend pracownika do widoku kalendarzowego.
from: "2026-05-01" // wymagany, YYYY-MM-DD to: "2026-05-31" // wymagany
{
"data": {
"activeMenuId": "…",
"activeMenuName": "Menu majowe 2026",
"validFrom": "2026-05-01",
"validTo": "2026-05-31",
"byDate": {
"2026-05-04": { // poniedziałek
"menuId": "…",
"menuName": "Menu majowe 2026",
"items": [...], // lista items dla day_of_week=1
"locked": false // czy są pendingowe picks na tę datę
},
"2026-05-05": {...} // wtorek
// soboty/niedziele i daty poza zakresem aktywnego menu pominięte
}
}
}
Lista własnych picków zalogowanego pracownika w zakresie dat. Każdy obiekt zawiera dodatkowo itemName, itemPrice, employeeName (JOIN).
{
"data": [
{
"id": "…",
"employeeId": "…",
"date": "2026-05-12",
"menuItemId": "…",
"itemName": "Schabowy z ziemniakami",
"itemPrice": 28,
"status": "pending", // pending | sent | cancelled_user | cancelled_vacation | cancelled_admin
"createdAt": "…",
"cancellationReason": null
}
]
}
Wybór obiadu — UPSERT (jeden pick na dzień per pracownik). Walidacje: nie weekend, nie po cutoffie, nie przed startDate, nie urlop dzienny, item musi należeć do aktywnego menu i pasować do day_of_week daty oraz mieścić się w validFrom–validTo.
{
"date": "2026-05-12",
"menuItemId": "abc-123"
}
Anuluje własny pick na ten dzień. Tylko przed cutoffem (po cutoffie zamówienie poszło do restauracji — nie da się odwołać).
{ "data": { "deleted": true } }
Dzienne podsumowanie zamówień — agregat per pozycja + lista pracowników.
{
"data": {
"date": "2026-05-12",
"totalAmount": 152,
"activeCount": 6,
"cancelledCount": 1,
"byItem": [
{ "itemId": "…", "name": "Schabowy", "price": 28, "qty": 3,
"employees": ["Anna", "Wojtek", "Krzysztof"], "cancelledQty": 0 }
]
}
}
Miesięczne podsumowanie — agregat per dzień + per pracownik. Używane do weryfikacji rachunku z restauracji.
{
"data": {
"year": 2026, "month": 5,
"fromDate": "2026-05-01", "toDate": "2026-05-31",
"totalAmount": 3250, // suma w PLN
"totalQty": 125, // liczba obiadów
"days": [
{ "date": "2026-05-04", "qty": 7, "amount": 182, "cancelledQty": 1 }, …
],
"byEmployee": [
{ "employeeId": "…", "employeeName": "…", "qty": 18, "amount": 468 }, …
]
}
}
Historia wygenerowanych zamówień do restauracji (tabela lunch_restaurant_orders). Limit 200 najnowszych. Każdy wpis ma snapshot items, total amount, treść maila do skopiowania.
{
"data": [
{
"id": "…", "date": "2026-05-12",
"generatedAt": "2026-05-12T09:05:00Z",
"totalAmount": 182,
"itemsJson": "[{...}]", // JSON snapshot pozycji
"recipients": "zamowienia@bistro.pl",
"emailSubject": "…", "emailBody": "…",
"status": "generated", // generated | sent | failed
"sentAt": null
}
]
}
Ręcznie generuje order do restauracji (zwykle robi to cron co minutę po cutoff+delay). Wykluczeni: pracownicy z urlopem dziennym. Picks pending → status sent. Zwraca pełen obiekt z emailBody do skopiowania / wysyłki.
{
"data": {
"id": "…", "date": "…", "totalAmount": 152,
"items": [...],
"excluded": [{ "employeeName": "…", "itemName": "…", "reason": "urlop dzienny" }],
"emailSubject": "…",
"emailBody": "…",
"recipients": "zamowienia@bistro.pl"
}
}
Podwyżki
Zwraca aktualne propozycje podwyżek dla wszystkich uprawnionych pracowników.
{
"data": {
"emp_01": { "name": "Marek Kowalski", "amount": 1500.00 },
"emp_02": { "name": "Anna Wiśniewska", "amount": 1200.00 },
"emp_03": { "name": "Tomasz Jabłoński", "amount": 900.00 }
},
"raisePool": 24000.00,
"allocated": 3600.00,
"currency": "PLN"
}
Ustawia propozycje podwyżek dla pracowników (klucz = employeeId, wartość = kwota PLN).
{
"emp_01": 1800.00,
"emp_02": 1400.00,
"emp_03": 1000.00
}
{
"message": "Propozycje podwyżek zostały zapisane",
"allocated": 4200.00
}
Zwraca zatwierdzone wyniki podwyżek po zamknięciu procesu przez zarząd.
{
"data": [
{ "employeeId": "emp_01", "name": "Marek Kowalski", "amount": 1800.00, "approved": true },
{ "employeeId": "emp_02", "name": "Anna Wiśniewska", "amount": 1400.00, "approved": true }
],
"currency": "PLN"
}
Zatwierdzenia
Zwraca aktualny status zatwierdzenia podwyżek i bonusów przez zarząd.
{
"raises": false,
"bonuses": true
}
Aktualizuje status zatwierdzenia podwyżek i/lub bonusów przez zarząd.
{
"raises": true,
"bonuses": true
}
{
"message": "Status zatwierdzeń zaktualizowany",
"raises": true,
"bonuses": true
}
Raporty
Pobiera raport podwyżek jako plik CSV gotowy do importu do kadr.
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="raporty-podwyzki-2025.csv"
Imię i Nazwisko,Kwota Podwyżki (PLN),Status
Marek Kowalski,1800.00,Zatwierdzona
Anna Wiśniewska,1400.00,Zatwierdzona
Tomasz Jabłoński,1000.00,Zatwierdzona
Zwraca JSON z pełnym podsumowaniem roku — finansami, podwyżkami, bonusami i głosowaniem.
{
"year": 2025,
"financial": {
"activePeriod": "Q1",
"totalRevenue": 1590000.00,
"totalNetProfit": 249500.00,
"bonusPool": 48750.00,
"raisePool": 24000.00
},
"raises": {
"approved": true,
"totalAmount": 4200.00,
"employees": 3
},
"bonuses": {
"approved": true,
"perPerson": 6093.75,
"wasteReduction": 5900.00
},
"voting": {
"participation": 75,
"winner": "Marek Kowalski"
},
"currency": "PLN",
"generatedAt": "2025-05-01T12:00:00Z"
}
Kopie Zapasowe
Zwraca listę dostępnych kopii zapasowych bazy danych.
{
"data": [
{
"name": "backup_2025-04-30T22-00-00.db",
"size": "2.4 MB",
"createdAt": "2025-04-30T22:00:00Z"
},
{
"name": "backup_2025-04-29T22-00-00.db",
"size": "2.3 MB",
"createdAt": "2025-04-29T22:00:00Z"
}
]
}
Tworzy natychmiastową kopię zapasową bazy danych.
{
"message": "Kopia zapasowa została utworzona",
"name": "backup_2025-05-01T10-15-30.db",
"size": "2.5 MB",
"createdAt": "2025-05-01T10:15:30Z"
}
Przywraca bazę danych z wybranej kopii zapasowej — uwaga: operacja nieodwracalna.
{
"message": "Baza danych przywrócona z backup_2025-04-30T22-00-00.db",
"restoredAt": "2025-05-01T11:05:00Z"
}
Zgłoszenia (helpdesk)
Każdy zalogowany użytkownik tworzy ticket (tytuł, opis, opcjonalny screen). Wątek czat-style. Statusy: new → in_progress → waiting_user → resolved. Body limit 2 MB (screeny w base64).
Lista ticketów. Pracownik widzi swoje, admin (superadmin/org_admin) — wszystkie w tenancie. Query: ?status=pending|in_progress|waiting_user|resolved.
{ "data": [ { "id", "title", "status", "messageCount", "lastMessage", "unread", ... } ] }
Tworzy nowy ticket z pierwszą wiadomością i opcjonalnym screenshotem (data URL).
{
"title": "Nie ładuje się lista urlopów",
"body": "Po kliknięciu Zatwierdź dostaję 500…",
"screenshot": "data:image/jpeg;base64,…", // opcjonalne, max ~2 MB
"route": "#admin/vacations", // kontekst dla admina
"userAgent": "Mozilla/5.0…",
"viewport": "1920x1080"
}
Pełen ticket z wątkiem (messages + attachments). Auto-mark-read przy odczycie. Pracownik nie-właściciel → 403.
Dodaje wiadomość do wątku. Auto-przejścia statusu: admin pierwsza odpowiedź → in_progress, druga → waiting_user, pracownik odpowiada → in_progress.
{ "body": "Sprawdziłem, nie działa też po refresh.", "screenshot": "data:image/jpeg;base64,…" }
Zmienia status (admin only). Wartości: new | in_progress | waiting_user | resolved.
Dla badge'a w sidebarze. Admin: liczba pending/in_progress ze stale last_admin_read_at. Pracownik: jego tickety z nową aktywnością.
{ "data": { "count": 3 } }
Auth-gated download screenu (image/jpeg|png|webp). Tylko właściciel ticketu lub admin. Path-traversal validation w filename.
Wnioski udziałowe (multi-sig governance)
Bezpośrednia edycja ownership_pct jest zablokowana po pierwszym przyznaniu udziałów (bootstrap). Każda zmiana wymaga proposal-a + zatwierdzenia przez wszystkich pozostałych obecnych udziałowców (unanimous-minus-proposer). Reject zamyka. 30 dni → expire.
Lista wniosków. Admin lub udziałowiec widzi pełną listę w tenancie. Query ?status=pending|applied|rejected|expired|cancelled.
{ "data": [ { "id", "status", "changes": [{"employeeId", "oldPct", "newPct"}], "votes": [...], "progress": { "approveCount", "requiredVoters" }, ... } ] }
Tworzy wniosek (admin only — superadmin/org_admin). W trybie bootstrap (suma udziałów = 0) zmiana stosuje się natychmiast.
{
"changes": [
{ "employeeId": "emp_abc", "newPct": 5 },
{ "employeeId": "emp_xyz", "newPct": 45 }
],
"description": "Anna dołącza jako udziałowiec — 5%"
}
Pending wnioski na które ja jako udziałowiec mogę głosować. Pusta lista jeśli ownership_pct = 0.
Szczegóły z listą głosów + progress. Dostęp: admin lub udziałowiec.
Głos approve/reject (tylko obecni udziałowcy, nie wnioskodawca). Reject zamyka wniosek. Approve od ostatniego brakującego udziałowca → automatyczne applied.
{ "vote": "approve", "comment": "OK z mojej strony" }
Wnioskodawca odwołuje swój pending wniosek (status → cancelled).
API keys (zarządzanie integracjami)
Endpointy do zarządzania kluczami API dla mikrousług partnerskich. Tylko admin (superadmin/org_admin). Klucze auth-ują przez header X-API-Key: kk_live_…. Pełen klucz zwracany TYLKO przy create.
Lista wszystkich kluczy w tenancie z metadata (bez pełnego klucza — tylko prefix dla identyfikacji).
{
"data": [ { "id", "name", "keyPrefix": "kk_live_aB3x", "scopes": [...], "rateLimit": 60, "lastUsedAt", "revokedAt" } ],
"validScopes": [ "employees:read", "employees:write", ... ]
}
Generuje nowy klucz. Pełny klucz zwracany RAZ — w DB tylko sha256.
{
"name": "Partner-HR — bridge do BambooHR",
"scopes": ["employees:read", "vacations:read"],
"rateLimit": 60, // req/min, 1-6000, default 60
"expiresAt": "2026-12-31T23:59:59Z", // opcjonalne
"notes": "Kontakt: john@partner.com"
}
{
"data": { "id", "name", "key": "kk_live_aB3xY2z…", "keyPrefix", "scopes", ... },
"warning": "Skopiuj klucz teraz — nie będzie pokazany ponownie"
}
Update name / scopes / rateLimit / notes. Klucza samego (kk_live_…) nie da się zmienić — trzeba revoke + nowy.
Soft delete — ustawia revokedAt. Klucz natychmiast 401 przy następnym wywołaniu. Audit log zostaje.
Ostatnie wywołania klucza. Query ?limit=100 (max 500).
{ "data": [ { "method", "path", "status", "durationMs", "ip", "ts" } ] }
Audit feed ze wszystkich kluczy w tenancie (overview dashboard).
Każdy endpoint v1 z middleware requirePerm akceptuje X-API-Key. Mapowanie: module:read → akcje read; module:write → wszystkie inne (create/update/delete/approve/manage/export).
Forbidden ops (zawsze 403): financials:write, backups:manage, voting:approve, employees:delete, settings:manage.
Retry-After: 60)
Ankiety (Surveys)
CRUD ankiet z lifecycle draft → active → closed. Audience: all/department/custom. Anonimowe (privacy by design — bez employee_id w response) lub imienne. Anti-duplicate: jeden user = jedna odpowiedź.
Lista wszystkich ankiet (admin: pełna; pracownik: tylko aktywne dla swojej audience). Query: status filter.
Aktywne ankiety w których pracownik jest w audience i jeszcze nie odpowiedział.
Pełna ankieta z pytaniami. Pracownik widzi tylko jeśli active + audience-match. Admin widzi zawsze.
Tworzy ankietę w stanie draft. Wymaga ≥1 pytania. Typy: text, longtext, single, multi, scale, nps.
{
"title": "Satysfakcja Q1",
"description": "Krótka ankieta…",
"isAnonymous": false,
"audience": "all", // "all" | "department" | "custom"
"audienceIds": ["emp_x", "emp_y"], // dla "custom"
"deadline": "2026-12-31",
"questions": [
{"type":"text", "question":"Co dobrego?", "required":true},
{"type":"scale", "question":"Ocena 1-10", "scaleMin":1, "scaleMax":10},
{"type":"single", "question":"Wybierz", "options":["A","B","C"]}
]
}
Edytuj ankietę. Tylko w stanie draft — active/closed → 409.
draft → active. Po tym pracownicy widzą + mogą odpowiadać.
active → closed. Nowe odpowiedzi blokowane (409).
Usuwa ankietę + cascade odpowiedzi.
Wypełnia ankietę. Anti-duplicate (409). Walidacja required. Anonimowa: employee_id = NULL w response, anti-duplicate przez osobną tabelę survey_responded (privacy by design).
{
"answers": [
{ "questionId": "q1", "text": "Dobra atmosfera" },
{ "questionId": "q2", "value": 8 },
{ "questionId": "q3", "value": ["A", "C"] } // multi
]
}
Wszystkie odpowiedzi + statystyki agregowane (counts, średnie dla scale/nps). Anonimowe — bez employee_id.
Eksport CSV odpowiedzi.
Działy (Departments)
Zarządzanie strukturą działów. Manager przypisany do działu auto-promote z employee. Scope: org_admin/superadmin = wszystkie, manager = tylko swój.
Lista działów ze scopem (response zawiera scope: 'org' | 'team').
Tworzy dział. Jeśli managerId wskazuje na employee → auto-promote do manager + przypisanie do nowego działu.
{ "name": "Engineering", "managerId": "emp_xyz" }
Update name / managerId.
Usuwa dział + odpina pracowników (department_id = NULL).
Członkowie działu. Manager może czytać tylko swój dział (403 dla cudzych).
Tenants (multi-tenant)
Multi-tenant infrastruktura. Każda organizacja ma własne moduły (feature flags) — np. {vacations, equipment, lunch}. Pracownicy z innego tenanta są niewidoczni.
Lista wszystkich organizacji (superadmin via X-Admin-Token).
Tworzy nową organizację. Slug auto-generowany z name jeśli nie podany.
{ "name": "Acme Corp", "slug": "acme" }
Pojedyncza organizacja. :id = "me" → tenant zalogowanego usera. Cudzą widzi tylko superadmin.
Zmiana name. org_admin tylko własną organizację.
Włącza/wyłącza moduły. Nieznane moduły są filtrowane. Wpływa na requireModule middleware (endpointy z wyłączonych modułów → 403).
{ "modules": ["dashboard", "employees", "vacations", "lunch"] }
Employees — role & department
Zmiana roli i przypisania do działu. Wszystkie chronione przez requireNotEscalating() — org_admin nie może modyfikować superadmina.
Zmiana roli. Tylko superadmin może nadać rolę superadmin. org_admin nie może zmieniać roli superadmina (anti-escalation).
{ "role": "manager" } // employee | manager | org_admin | superadmin
Przypisuje pracownika do działu. Manager może przypisywać tylko do własnego działu. Anti-escalation chroni superadmina.
{ "departmentId": "dept_xyz" } // null = odepnij
Tryb testowy — admin "loguje się" jako pracownik (zwraca jego token + dane). org_admin nie może wcielić się w superadmina (anty-eskalacja).
{
"data": {
"token": "AB12CD", "id", "name", "email", "role", "eligibleForRaise",
"impersonatedBy": { "id", "name", "role" }
}
}