Files
hrm-medpark/docs/superpowers/specs/2026-05-12-inventory-design.md
Danil Suhomlinov 33800292aa 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>
2026-06-08 17:42:45 +03:00

153 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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