chore: add Coolify deployment scaffolding (Dockerfiles, prod compose, git hygiene)

- apps/api/Dockerfile: build NestJS, run prisma migrate deploy on start
- apps/web/Dockerfile + nginx.conf: build Vite, serve static, proxy /api -> api
- docker-compose.coolify.yml: full prod stack (postgres, redis, minio, keycloak, api, web)
- .dockerignore / .gitignore / .gitattributes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Danil Suhomlinov
2026-06-08 17:42:45 +03:00
commit 33800292aa
186 changed files with 30437 additions and 0 deletions
@@ -0,0 +1,152 @@
# Design: Inventory (Vestimentație & Echipament)
**Date:** 2026-05-12
**Scope:** Полноценный модуль склада инвентаря для HR Medpark — заменяет свободные текстовые ID в `Benefit` на FK к таблице `InventoryItem`.
---
## 1. Goal
В разделе «Beneficii» сотрудника поля «Uniformă / Halat / Ciupici / Vestă / Aparat telefon» сейчас хранятся как свободный текст (`uniformaId String?`). По ТЗ (Rubrici necesare, B121-B131 — «info de la depozit») это должна быть выдача со склада с учётом остатков.
Модуль `Inventory`:
- Реестр товаров склада (SKU, наименование, тип, размер, цвет, цена, остаток).
- CRUD для `hr_admin`, read для `hr_specialist`.
- В `BenefitDrawer``<Select>` с поиском по складу вместо `<TextInput>`.
- При выдаче `Benefit` уменьшается `stockQty` (атомарно в транзакции).
- При снятии выдачи — возвращается на склад.
## 2. Out of scope (для первой итерации)
- Учёт серий/партий с истекающим сроком годности
- Списание (write-off) с причиной — пока только повышение/понижение `stockQty` через CRUD
- Резервирование за сотрудником без выдачи
- Импорт остатков из Excel
- История движений (audit идёт через общий `AuditLog`, специальной таблицы движений нет)
## 3. Data model
```prisma
enum InventoryItemType {
uniforma
halat
ciupici
vesta
aparat_telefon
alte
}
model InventoryItem {
id String @id @default(uuid())
sku String @unique // артикул
name String // "Uniformă chirurgie M albastru"
type InventoryItemType
size String? // "M", "42"
color String?
pricePerUnit Decimal? @db.Decimal(10, 2)
stockQty Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Обратные связи на Benefit (по типам)
uniformaBenefits Benefit[] @relation("BenefitUniforma")
halatBenefits Benefit[] @relation("BenefitHalat")
ciupiciBenefits Benefit[] @relation("BenefitCiupici")
vestaBenefits Benefit[] @relation("BenefitVesta")
aparatTelBenefits Benefit[] @relation("BenefitAparatTel")
@@index([type, active])
@@map("inventory_items")
}
// Изменения в Benefit:
// - uniformaId String? → FK к InventoryItem (relation "BenefitUniforma")
// - halatId, ciupiciId, vestaId, aparatTelefonId — аналогично
```
### Миграция данных
Существующие свободные String значения в `Benefit.uniformaId/...` теряют связь — поле обнуляется (NULL). Сообщить HR-у в release notes, что прошлые «текстовые» значения нужно перевыбрать из Inventory.
## 4. API endpoints
Модуль `apps/api/src/modules/inventory/`:
| Method | Path | Role | Purpose |
|--------|------|------|---------|
| GET | `/api/v1/inventory` | hr_admin, hr_specialist | List + filter (type, search, активные) + pagination |
| GET | `/api/v1/inventory/:id` | hr_admin, hr_specialist | Один товар |
| POST | `/api/v1/inventory` | hr_admin | Создать |
| PATCH | `/api/v1/inventory/:id` | hr_admin | Обновить |
| DELETE | `/api/v1/inventory/:id` | hr_admin | Soft delete (`active = false` если есть выдачи, hard delete если их нет) |
| POST | `/api/v1/inventory/:id/adjust-stock` | hr_admin | `{ delta: number, reason: string }` — приход / списание |
Read-методы — `audit.logRead({ entity: 'InventoryItem' })`. Write — `logChange`.
## 5. Frontend
### `/inventory` — список
- Таблица: SKU / Denumire / Tip / Mărime / Culoare / Stoc / Preț / Acțiuni
- Filter bar: `<Select tip>`, `<Switch active only>`, search (SKU+name)
- Кнопка «Adaugă articol» → drawer
- Highlight: `stockQty === 0` → red row, `stockQty < 5` → amber
- Click row → drawer для редактирования
### Drawer (CRUD)
- Поля: SKU, Denumire, Tip (select 6), Mărime, Culoare, Preț unitar (MDL), Stoc inițial, Active toggle
- Валидация: SKU unique (handled на бэке), name required
### `/inventory/:id` — Stock adjust modal
- Открывается из строки таблицы или drawer
- Inputs: delta (число с +/-), reason (textarea)
- Audit log пишется обязательно
### Изменения в `BenefitDrawer.tsx`
- Заменить 5 `<TextInput>` (uniformaId, halatId, ciupiciId, vestaId, aparatTelefonId) на `<Select searchable>` с фильтром по `type`:
```ts
const { data: uniforme } = useQuery(['inventory', 'uniforma'],
() => apiClient.get('/inventory', { params: { type: 'uniforma', active: true } }));
```
- Подпись опции: `${item.sku} — ${item.name} (${item.size}, stoc: ${item.stockQty})`
- Disable опции с `stockQty === 0` (за исключением уже выбранной у текущего сотрудника)
### Изменения в `BeneficiiTab.tsx`
- Резолвить `benefit.uniforma?.name` (через `include` на бэке) вместо ID
- Показ: «Uniformă: Uniformă chirurgie M albastru (SKU-001)»
### Nav
- В `App.tsx` → `NAV_ITEMS`: `{ labelKey: 'nav.inventory', path: '/inventory', icon: <IconBox />, roles: ['hr_admin', 'hr_specialist'] }`
## 6. Бизнес-логика выдачи (atomic stock adjustment)
В `BenefitService.upsert(employeeId, dto)`:
1. Загрузить текущий `Benefit` (если есть) → diff `oldUniformaId vs newUniformaId`
2. В транзакции `prisma.$transaction`:
- Если `oldUniformaId` сменился: `stockQty++` для старого
- Если `newUniformaId` указан: проверить `stockQty > 0`, иначе `BadRequest('Stoc epuizat')`. Затем `stockQty--`
3. Upsert Benefit
4. Audit log
Аналогично для всех 5 полей.
## 7. Seed data
В `apps/api/prisma/seed.ts`:
- 4 модели Uniforme (M/L/XL chirurgie, M/L/XL ATI)
- 4 модели Halate
- 3 размера Ciupici (38-42, 43-46)
- 2 модели Veste (S, M)
- 2 модели Aparate telefon (Samsung A15, iPhone SE)
Каждая со `stockQty: 50`.
## 8. Implementation order
1. **Prisma migration** — модель `InventoryItem`, FK в `Benefit`, seed
2. **API module** — controller + service + DTOs
3. **Frontend types** + `/inventory` page + drawer
4. **BenefitDrawer rewrite** — селекты вместо TextInput
5. **BeneficiiTab** — показ резолвленных имён
6. **Atomic stock logic** в BenefitService
7. **Stock adjust** modal + endpoint