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

7.1 KiB
Raw Permalink Blame History

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.tsxNAV_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