Платформа для ефективного розподілу ресурсів між складами та точками доставки в умовах динамічного попиту. Автоматично будує маршрути доставки з пріоритизацією за рівнем критичності потреби.
Hackathon: Lviv Best Hackathon 2026 | Team: 503
- Демо
- Основні можливості
- Технічний стек
- Архітектура
- Структура проекту
- Запуск проекту
- API документація
- Схема бази даних
- Алгоритм маршрутизації
- Симуляція
- Авторизація та доступ
- CI/CD
- Frontend: https://team-503-2026.web.app/
- Backend API: https://api-ikacsycdva-ew.a.run.app
- Swagger API docs: https://api-ikacsycdva-ew.a.run.app/api
- Інтерактивна карта з відображенням складів, точок доставки та маршрутів (Leaflet)
- Перерахунок обсягів постачання при зміні запиту — автоматична перебудова плану доставки при CRUD операціях над запитами
- 5 рівнів критичності запитів:
urgent→critical→high→medium→normal - Двоетапна симуляція: терміновий план (urgent + critical), стандартний план (high + medium + normal)
- Алгоритм маршрутизації — Multi-Depot VRP з жадібною евристикою та пріоритетним сортуванням
- Захист даних — JWT автентифікація (Supabase Auth), CSRF захист (Double Submit Cookie), Helmet, rate limiting
- Офлайн-режим —
redux-persistкешує дані локально; мутації зберігаються в offline queue і автоматично синхронізуються при відновленні зв'язку; UI-індикатор статусу з'єднання - Координація між складами та точками — алгоритм автоматично обирає оптимальне джерело товару (склад або точка з надлишком)
- Пошук найближчих локацій з товаром —
GET /geo/nearestтаGET /geo/nearest-for-point/:pointIdз використанням PostGIS
| Технологія | Версія | Призначення |
|---|---|---|
| React | 19 | UI фреймворк |
| Vite | 8 | Збірка та dev server |
| TypeScript | 5 | Типізація |
| Tailwind CSS | 4 | Стилі |
| shadcn/ui | 4 | UI компоненти |
| Redux Toolkit | 2 | Стейт-менеджмент |
| React Router | 7 | Маршрутизація |
| React Leaflet | 5 | Інтерактивна карта |
| Supabase JS SDK | 2 | Клієнт авторизації |
| redux-persist | 6 | Офлайн кешування |
| Технологія | Версія | Призначення |
|---|---|---|
| NestJS | 11 | API фреймворк |
| Prisma | 7 | ORM (query client) |
| PostgreSQL | 17 | База даних (Supabase) |
| PostGIS | — | Геолокація, відстані, просторові індекси |
| Swagger | 11 | Автоматична API документація |
| Helmet | 8 | HTTP-заголовки безпеки |
| csrf-csrf | 4 | CSRF захист |
| Throttler | 6 | Rate limiting (3 рівні) |
| Firebase Functions | 7 | Хостинг бекенду |
- Supabase — PostgreSQL БД, Auth, RLS (регіон: eu-central-1)
- Firebase — Hosting (фронтенд), Cloud Functions (бекенд, europe-west1)
- GitHub Actions — CI/CD (lint, typecheck, автодеплой)
- Husky + lint-staged — pre-commit хуки
- Node.js 24 (
.nvmrc)
graph LR
subgraph Client
A[Frontend<br/>React SPA<br/>Firebase Hosting]
end
subgraph Server
B[NestJS API<br/>Firebase Functions<br/>europe-west1]
end
subgraph Database
C[(PostgreSQL + PostGIS<br/>Supabase<br/>eu-central-1)]
end
subgraph Auth
D[Supabase Auth<br/>JWT ES256 + RLS]
end
A -- "HTTPS / JSON + CSRF" --> B
B -- "Prisma ORM" --> C
A -. "Supabase JS SDK" .-> D
B -. "Supabase Admin SDK" .-> D
D -. "JWT verification" .-> B
/
├── api/ # Backend (NestJS)
│ ├── prisma/
│ │ └── schema.prisma # Prisma схема (introspected з Supabase)
│ ├── src/
│ │ ├── main.ts # Entry point (локальна розробка)
│ │ ├── main-firebase.ts # Entry point (Firebase Functions)
│ │ ├── setup-app.ts # CORS, Helmet, CSRF, Swagger
│ │ ├── app.module.ts # Кореневий NestJS модуль
│ │ ├── auth/ # Автентифікація (Guard, декоратори)
│ │ ├── warehouses/ # Склади (CRUD + stock)
│ │ ├── points/ # Точки доставки (CRUD + stock)
│ │ ├── products/ # Довідник товарів
│ │ ├── delivery-requests/ # Запити на доставку
│ │ ├── delivery-plans/ # Плани доставки (маршрути)
│ │ ├── simulation/ # Двоетапна симуляція
│ │ ├── geo/ # Геопошук найближчих локацій
│ │ ├── permissions/ # Управління дозволами
│ │ ├── profiles/ # Профілі користувачів
│ │ ├── csrf/ # CSRF токен
│ │ ├── healthcheck/ # Health check
│ │ ├── prisma/ # Prisma сервіс (глобальний)
│ │ ├── supabase/ # Supabase сервіс
│ │ └── common/ # Shared DTO, helpers, middleware
│ ├── config/default.yaml # Конфігурація (throttle, CORS)
│ └── .env.example
│
├── www/ # Frontend (React)
│ ├── src/
│ │ ├── App.tsx # Роутинг
│ │ ├── main.tsx # Entry point
│ │ ├── pages/ # Сторінки
│ │ │ ├── MapPage.tsx # Головна — карта з точками/складами
│ │ │ ├── PointPage.tsx # Деталі точки доставки
│ │ │ ├── WarehousePage.tsx # Деталі складу
│ │ │ ├── LoginPage.tsx # Логін
│ │ │ ├── RegisterPage.tsx # Реєстрація
│ │ │ ├── ProductsPage.tsx # Управління товарами (admin)
│ │ │ └── PermissionsPage.tsx # Управління дозволами (admin)
│ │ ├── components/ # React компоненти
│ │ ├── store/ # Redux Toolkit + slices
│ │ ├── hooks/ # useSimulation, useOnlineStatus, useTheme
│ │ ├── lib/ # Supabase клієнт, утиліти
│ │ └── types/ # TypeScript типи
│ └── .env.example
│
├── .github/workflows/ # CI/CD (GitHub Actions)
├── firebase.json # Firebase Hosting + Functions
└── .nvmrc # Node.js 24
- Node.js 24 (див.
.nvmrc) - Supabase проект з PostGIS розширенням
- Firebase проект (для деплою)
git clone https://github.com/team-503/lviv-best-hackathon-2026.git
cd lviv-best-hackathon-2026cd api
cp .env.example .envЗаповнити .env:
NODE_ENV="development"
CORS_ORIGIN="http://localhost:3000"
CSRF_SECRET="your-random-secret"
DATABASE_DIRECT_URL="postgresql://..." # Supabase Direct URL
DATABASE_URL="postgresql://..." # Supabase Connection Pooler URL
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_JWT_SECRET="your-jwt-secret"
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"npm install # також запускає prisma generate через postinstall
npm run dev # http://localhost:4000Swagger документація буде доступна на http://localhost:4000/api.
cd www
cp .env.example .envЗаповнити .env:
VITE_API_URL="http://localhost:4000"npm install
npm run dev # http://localhost:3000cd api
npx prisma db pull # introspect нову схему
npx prisma generate # оновити типи клієнта
npx prisma studio # GUI для перегляду данихПовна інтерактивна документація доступна через Swagger UI за адресою /api.
Позначки доступу: без позначки — публічний, 🔐 — залогінений, 🔒 read/write — потрібен дозвіл, 🔒 admin — тільки адмін.
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /profile |
🔐 | Поточний профіль |
| GET | /admin/users |
🔒 admin | Список всіх користувачів |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /products |
Публічний | Список всіх товарів |
| POST | /products |
🔒 admin | Створити товар |
| DELETE | /products/:id |
🔒 admin | Видалити товар |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /warehouses |
Публічний | Всі склади для карти |
| GET | /warehouses/:id |
🔒 read | Деталі складу + запаси |
| POST | /warehouses |
🔒 admin | Створити склад |
| PUT | /warehouses/:id |
🔒 admin | Оновити назву/локацію |
| PATCH | /warehouses/:id/stock |
🔒 write | Оновити запаси |
| DELETE | /warehouses/:id |
🔒 admin | Видалити склад |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /points |
Публічний | Всі точки для карти |
| GET | /points/:id |
🔒 read | Деталі точки + запаси + запити |
| POST | /points |
🔒 admin | Створити точку |
| PUT | /points/:id |
🔒 admin | Оновити назву/локацію |
| PATCH | /points/:id/stock |
🔒 write | Оновити мінімальні пороги |
| DELETE | /points/:id |
🔒 admin | Видалити точку |
| DELETE | /points/:id/stock/:productId |
🔒 write | Видалити товар зі стоку точки |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| POST | /points/:pointId/delivery-requests |
🔒 write | Створити запит (тригер перерахунку плану) |
| PUT | /points/:pointId/delivery-requests/:id |
🔒 write | Оновити запит |
| DELETE | /points/:pointId/delivery-requests/:id |
🔒 write | Видалити запит |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /delivery-plans/current |
Публічний | Поточний план (urgent + standard) |
| GET | /delivery-plans/history |
Публічний | Історія завершених планів |
| GET | /delivery-plans/:id |
Публічний | Деталі конкретного плану |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /simulation/status |
Публічний | Поточний стан симуляції |
| POST | /simulation/advance |
🔒 admin | Просунути на наступний етап |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /geo/nearest |
Публічний | Найближчі локації з конкретним товаром |
| GET | /geo/nearest-for-point/:pointId |
Публічний | Найближчі локації для всіх товарів точки |
| Метод | Ендпоінт | Доступ | Опис |
|---|---|---|---|
| GET | /admin/permissions |
🔒 admin | Всі дозволи |
| POST | /admin/permissions |
🔒 admin | Додати дозвіл |
| PUT | /admin/permissions/:id |
🔒 admin | Оновити дозвіл |
| DELETE | /admin/permissions/:id |
🔒 admin | Видалити дозвіл |
PostgreSQL 17 + PostGIS. Міграції керуються через Supabase, Prisma використовується як query client.
erDiagram
profiles {
uuid id PK "FK auth.users"
text email
text display_name
text role "user | admin"
timestamptz created_at
}
products {
serial id PK
text name UK
}
warehouses {
serial id PK
text name
geography location "PostGIS Point"
}
warehouse_stock {
int warehouse_id PK,FK
int product_id PK,FK
int quantity
}
points {
serial id PK
text name
geography location "PostGIS Point"
}
point_stock {
int point_id PK,FK
int product_id PK,FK
int quantity
int min_threshold
}
delivery_requests {
serial id PK
int point_id FK
int product_id FK
int quantity
criticality_level criticality
request_status status
timestamptz created_at
}
delivery_plans {
serial id PK
plan_type type "urgent | standard"
plan_status status "draft | executing | completed"
timestamptz created_at
}
plan_routes {
serial id PK
int plan_id FK
int vehicle_number
}
route_stops {
serial id PK
int route_id FK
int stop_order
location_type location_type "warehouse | point"
int warehouse_id FK "nullable"
int point_id FK "nullable"
int product_id FK
int quantity
stop_action action "pickup | deliver"
}
user_permissions {
serial id PK
uuid user_id FK
resource_type resource_type "point | warehouse"
int resource_id
text_array permissions "read, write"
}
profiles ||--o{ user_permissions : "has"
warehouses ||--o{ warehouse_stock : "stores"
products ||--o{ warehouse_stock : "stocked in"
points ||--o{ point_stock : "stores"
products ||--o{ point_stock : "stocked in"
points ||--o{ delivery_requests : "requests"
products ||--o{ delivery_requests : "requested"
delivery_plans ||--o{ plan_routes : "contains"
plan_routes ||--o{ route_stops : "has"
warehouses ||--o{ route_stops : "pickup from"
points ||--o{ route_stops : "deliver to"
products ||--o{ route_stops : "carries"
CREATE TYPE criticality_level AS ENUM ('normal', 'medium', 'high', 'critical', 'urgent');
CREATE TYPE request_status AS ENUM ('active', 'completed', 'cancelled');
CREATE TYPE plan_type AS ENUM ('urgent', 'standard');
CREATE TYPE plan_status AS ENUM ('draft', 'executing', 'completed');
CREATE TYPE location_type AS ENUM ('warehouse', 'point');
CREATE TYPE stop_action AS ENUM ('pickup', 'deliver');
CREATE TYPE resource_type AS ENUM ('point', 'warehouse');Тип задачі: Multi-Depot VRP (Vehicle Routing Problem) з пріоритетами. Використовується жадібна евристика з пріоритетним сортуванням.
Двоключове сортування запитів:
- Рівень критичності (спадання): urgent (5) → critical (4) → high (3) → medium (2) → normal (1)
- Кількість товару (спадання): більші обсяги першими
Критичність завжди перемагає кількість. 1 одиниця critical обробляється раніше за 1000 одиниць normal.
- Збір та сортування — активні запити відповідного плану сортуються за пріоритетом
- Пошук джерела — для кожного запиту знаходиться найближчий склад з потрібним товаром (Haversine formula). Якщо жоден склад не має товару — шукається точка з надлишком (щоб не створити дефіцит)
- Жадібне призначення — запити послідовно призначаються на машини з урахуванням вантажопідйомності
- Оптимізація маршруту — зупинки впорядковуються за nearest-neighbor TSP
- Перевірка балансу — гарантія, що жодна точка не залишається в дефіциті
Вхід: 2 склади, 3 точки, 5 запитів різної критичності
2 машини, вантажопідйомність = 15 од.
Машина 1: Склад A → Точка 1
вантаж: Паливо×10 (critical) + Їжа×5 (normal) = 15
Машина 2: Склад A → Склад B → Точка 3 → Точка 2
вантаж: Паливо×8 (high) + Ліки×3 (high) + Їжа×2 (normal) = 13
Запускається адміністратором. Кожен виклик POST /simulation/advance — один крок:
idle → stage1 → stage2 → idle (day+1) → ...
| Етап | Що виконується | Рівні критичності |
|---|---|---|
| Stage 1 (Терміновий) | Терміновий план | urgent, critical |
| Stage 2 (Стандартний) | Стандартний план | high, medium, normal |
При виконанні кожного етапу:
- Запаси на складах зменшуються (pickup)
- Запаси на точках збільшуються (deliver)
- Якщо товар вивозиться з іншої точки — запаси там зменшуються
- Виконані запити переходять у статус
completed
Supabase Auth + NestJS Guard. Користувач логіниться через Supabase Auth, отримує JWT. Бекенд верифікує токен через AuthGuard.
| Рівень | Хто | Можливості |
|---|---|---|
| Публічний | Всі | Healthcheck, список товарів |
| Залогінений | Авторизований користувач | Перегляд карти (маркери) |
read |
Користувач з дозволом на ресурс | Перегляд деталей, запасів, запитів |
write |
Користувач з дозволом на ресурс | Редагування запасів, створення запитів |
admin |
Адміністратор | Повний доступ: CRUD ресурсів, симуляція, дозволи |
- JWT — верифікація через Supabase (ES256)
- CSRF — Double Submit Cookie pattern (
csrf-csrf) в production - Helmet — стандартні HTTP-заголовки безпеки
- Rate Limiting — 3 рівні: short (3 req/1s), medium (20 req/10s), long (100 req/60s)
- RLS — Row Level Security на рівні Supabase для кожної таблиці
GitHub Actions автоматизує перевірки та деплой:
| Workflow | Тригер | Дія |
|---|---|---|
check-api.yml |
PR | Lint + typecheck бекенду |
check-www.yml |
PR | Lint + typecheck фронтенду |
firebase-functions-merge.yml |
Merge в main | Деплой Firebase Functions |
firebase-hosting-merge.yml |
Merge в main | Деплой Firebase Hosting |
firebase-hosting-pull-request.yml |
PR | Preview деплой фронтенду |
- Husky — pre-commit хуки
- lint-staged — Prettier, ESLint, typecheck на staged файлах