- 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>
7.1 KiB
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
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: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):
- Загрузить текущий
Benefit(если есть) → diffoldUniformaId vs newUniformaId - В транзакции
prisma.$transaction:- Если
oldUniformaIdсменился:stockQty++для старого - Если
newUniformaIdуказан: проверитьstockQty > 0, иначеBadRequest('Stoc epuizat'). ЗатемstockQty--
- Если
- Upsert Benefit
- 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
- Prisma migration — модель
InventoryItem, FK вBenefit, seed - API module — controller + service + DTOs
- Frontend types +
/inventorypage + drawer - BenefitDrawer rewrite — селекты вместо TextInput
- BeneficiiTab — показ резолвленных имён
- Atomic stock logic в BenefitService
- Stock adjust modal + endpoint