Revízny systém - kompletná implementácia
- Backend: CRUD revízií, schedule endpoint (agregovaný plán), skip revízia, stats - Shared utility revisionSchedule.ts - centralizovaná logika výpočtu cyklov - Equipment detail s revíznym plánom, históriou a prílohami - Frontend: RevisionsList s tabmi (nadchádzajúce/po termíne/vykonané/preskočené) - Pozičné labeling cyklov (eliminuje drift 4×90≠365) - EquipmentRevisionSchedule model (many-to-many typy revízií) - Aktualizovaná dokumentácia HELPDESK_INIT_V2.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
**Verzia:** 2.0.0
|
**Verzia:** 2.0.0
|
||||||
**Účel:** Komplexný systém pre správu zákaziek, úloh, projektov, revíznych kontrol a reklamácií s dôrazom na flexibilitu a konfigurovateľnosť
|
**Účel:** Komplexný systém pre správu zákaziek, úloh, projektov, revíznych kontrol a reklamácií s dôrazom na flexibilitu a konfigurovateľnosť
|
||||||
**Jazyk UI:** Slovenčina
|
**Jazyk UI:** Slovenčina
|
||||||
**Dátum:** 02.02.2026
|
**Dátum:** 23.02.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -232,12 +232,13 @@ model RevisionType {
|
|||||||
description String?
|
description String?
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
revisions Revision[]
|
revisions Revision[]
|
||||||
|
equipmentSchedules EquipmentRevisionSchedule[]
|
||||||
|
|
||||||
@@index([active])
|
@@index([active])
|
||||||
@@index([order])
|
@@index([order])
|
||||||
}
|
}
|
||||||
@@ -646,24 +647,26 @@ model Equipment {
|
|||||||
partNumber String? // PN
|
partNumber String? // PN
|
||||||
serialNumber String? // SN
|
serialNumber String? // SN
|
||||||
|
|
||||||
installDate DateTime?
|
installDate DateTime?
|
||||||
warrantyEnd DateTime?
|
revisionCycleStart DateTime? // Anchor pre výpočet revíznych cyklov (default = installDate)
|
||||||
warrantyStatus String? // "ACTIVE", "EXPIRED", "EXTENDED"
|
warrantyEnd DateTime?
|
||||||
|
warrantyStatus String? // "ACTIVE", "EXPIRED", "EXTENDED"
|
||||||
|
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
|
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
|
|
||||||
createdById String
|
createdById String
|
||||||
createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id])
|
createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id])
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
revisions Revision[]
|
revisions Revision[]
|
||||||
attachments EquipmentAttachment[]
|
revisionSchedules EquipmentRevisionSchedule[] // Priradené typy revízií
|
||||||
tags EquipmentTag[]
|
attachments EquipmentAttachment[]
|
||||||
|
tags EquipmentTag[]
|
||||||
|
|
||||||
@@index([typeId])
|
@@index([typeId])
|
||||||
@@index([customerId])
|
@@index([customerId])
|
||||||
@@ -672,37 +675,57 @@ model Equipment {
|
|||||||
@@index([createdById])
|
@@index([createdById])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priradenie revíznych typov k zariadeniu (many-to-many)
|
||||||
|
model EquipmentRevisionSchedule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
equipmentId String
|
||||||
|
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
revisionTypeId String
|
||||||
|
revisionType RevisionType @relation(fields: [revisionTypeId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([equipmentId, revisionTypeId])
|
||||||
|
@@index([equipmentId])
|
||||||
|
@@index([revisionTypeId])
|
||||||
|
}
|
||||||
|
|
||||||
model Revision {
|
model Revision {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|
||||||
equipmentId String
|
equipmentId String
|
||||||
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
|
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
// Type relation (namiesto enum)
|
|
||||||
typeId String
|
typeId String
|
||||||
type RevisionType @relation(fields: [typeId], references: [id])
|
type RevisionType @relation(fields: [typeId], references: [id])
|
||||||
|
|
||||||
|
status String @default("performed") // "performed" | "skipped"
|
||||||
|
|
||||||
performedDate DateTime
|
performedDate DateTime
|
||||||
nextDueDate DateTime? // Auto-calculated: performedDate + type.intervalDays
|
nextDueDate DateTime? // Auto-calculated z cyklového anchoru
|
||||||
|
|
||||||
performedById String
|
performedById String
|
||||||
performedBy User @relation(fields: [performedById], references: [id])
|
performedBy User @relation(fields: [performedById], references: [id])
|
||||||
|
|
||||||
findings String? @db.Text
|
findings String? @db.Text
|
||||||
result String? // "OK", "MINOR_ISSUES", "CRITICAL"
|
result String? // "OK", "MINOR_ISSUES", "CRITICAL"
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
|
skipReason String? // Dôvod preskočenia (ak status = "skipped")
|
||||||
|
|
||||||
reminderSent Boolean @default(false)
|
reminderSent Boolean @default(false)
|
||||||
reminderDate DateTime? // Auto-calculated: nextDueDate - type.reminderDays
|
reminderDate DateTime? // Auto-calculated: nextDueDate - type.reminderDays
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([equipmentId])
|
@@index([equipmentId])
|
||||||
@@index([typeId])
|
@@index([typeId])
|
||||||
@@index([performedById])
|
@@index([performedById])
|
||||||
@@index([nextDueDate])
|
@@index([nextDueDate])
|
||||||
@@index([reminderDate])
|
@@index([reminderDate])
|
||||||
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
model EquipmentAttachment {
|
model EquipmentAttachment {
|
||||||
@@ -1008,25 +1031,43 @@ POST /api/customers/import // Import z externej DB
|
|||||||
### **🆕 Equipment**
|
### **🆕 Equipment**
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/equipment
|
GET /api/equipment // Zoznam zariadení (stránkovaný, search, filtre)
|
||||||
POST /api/equipment
|
POST /api/equipment // Vytvorenie zariadenia + revisionSchedules
|
||||||
GET /api/equipment/:id
|
GET /api/equipment/reminders // Upcoming revisions (PRED /:id!)
|
||||||
PUT /api/equipment/:id
|
GET /api/equipment/:id // Detail zariadenia
|
||||||
DELETE /api/equipment/:id
|
PUT /api/equipment/:id // Úprava zariadenia + revisionSchedules
|
||||||
GET /api/equipment/:id/revisions
|
DELETE /api/equipment/:id // Soft delete (deaktivácia)
|
||||||
POST /api/equipment/:id/revisions
|
GET /api/equipment/:id/schedule // Revízny plán zariadenia (nadchádzajúce dátumy)
|
||||||
POST /api/equipment/:id/attachments
|
GET /api/equipment/:id/revisions // História revízií zariadenia
|
||||||
GET /api/equipment/reminders // Upcoming revisions
|
POST /api/equipment/:id/revisions // Pridanie revízie cez equipment detail
|
||||||
|
GET /api/equipment/:id/files // Zoznam príloh
|
||||||
|
POST /api/equipment/:id/files // Upload príloh (max 10)
|
||||||
|
DELETE /api/equipment/:id/files/:fileId // Zmazanie prílohy
|
||||||
```
|
```
|
||||||
|
|
||||||
### **🆕 Revisions**
|
### **🆕 Revisions**
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/revisions
|
GET /api/revisions // Zoznam revízií (stránkovaný, status filter, search)
|
||||||
GET /api/revisions/upcoming
|
GET /api/revisions/stats // Štatistiky: { upcoming, overdue, performed, skipped }
|
||||||
GET /api/revisions/overdue
|
GET /api/revisions/schedule // Agregovaný plán zo VŠETKÝCH zariadení (view=upcoming|overdue)
|
||||||
PUT /api/revisions/:id
|
POST /api/revisions/skip // Preskočenie plánovanej revízie (s dôvodom)
|
||||||
DELETE /api/revisions/:id
|
GET /api/revisions/:id // Detail revízie
|
||||||
|
POST /api/revisions // Vytvorenie revízie
|
||||||
|
PUT /api/revisions/:id // Úprava revízie
|
||||||
|
DELETE /api/revisions/:id // Zmazanie revízie
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query parametre pre `/schedule`:** `view` (upcoming|overdue), `typeId`, `customerId`, `search`, `page`, `limit`
|
||||||
|
|
||||||
|
**Body pre `/skip`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"equipmentId": "string",
|
||||||
|
"typeId": "string",
|
||||||
|
"scheduledDate": "ISO date string",
|
||||||
|
"skipReason": "string (optional)"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### **🆕 RMA**
|
### **🆕 RMA**
|
||||||
@@ -1135,6 +1176,70 @@ GET /api/logs/:entityType/:entityId
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔧 Revízny Systém - Architektúra
|
||||||
|
|
||||||
|
### Princípy
|
||||||
|
|
||||||
|
1. **Cyklový anchor** - Revízne termíny sú ukotvené na `revisionCycleStart` (alebo `installDate`). Cyklus sa NEposúva keď sa revízia vykoná po termíne.
|
||||||
|
2. **Pozičné labeling** - Cyklové body sa generujú LEN z najkratšieho intervalu (napr. štvrťročný = 90 dní). Pozícia v ročnom cykle určuje label: 1., 2., 3. Štvrťročná, alebo Ročná pri 4. pozícii. Toto eliminuje drift problém (4×90≠365).
|
||||||
|
3. **Pokrývajúce typy** - Dlhší interval pokrýva kratší (ročná pokrýva štvrťročnú). Po ročnej revízii sa nextDueDate počíta z najkratšieho intervalu.
|
||||||
|
4. **Skip mechanizmus** - Plánovaná revízia sa dá preskočiť s dôvodom. Vytvorí sa záznam so `status: "skipped"`.
|
||||||
|
|
||||||
|
### Seed dáta - Typy revízií
|
||||||
|
|
||||||
|
```
|
||||||
|
MONTHLY - Mesačná revízia (30 dní, reminder 7 dní)
|
||||||
|
QUARTERLY - Štvrťročná revízia (90 dní, reminder 14 dní)
|
||||||
|
BIANNUAL - Polročná revízia (180 dní, reminder 21 dní)
|
||||||
|
ANNUAL - Ročná revízia (365 dní, reminder 30 dní)
|
||||||
|
BIENNIAL - Dvojročná revízia (730 dní, reminder 60 dní)
|
||||||
|
TRIENNIAL - Trojročná revízia (1095 dní, reminder 90 dní)
|
||||||
|
QUADRENNIAL - Štvorročná revízia (1460 dní, reminder 90 dní)
|
||||||
|
QUINQUENNIAL - Päťročná revízia (1825 dní, reminder 90 dní)
|
||||||
|
EMERGENCY - Mimoriadna revízia (0 dní, bez opakovania)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed dáta - Typy zariadení
|
||||||
|
|
||||||
|
```
|
||||||
|
EPS - Elektrická požiarna signalizácia
|
||||||
|
HSP - Hlasová signalizácia požiaru
|
||||||
|
EZS - Elektronický zabezpečovací systém
|
||||||
|
BLESKOZVOD - Bleskozvod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared utility: `backend/src/utils/revisionSchedule.ts`
|
||||||
|
|
||||||
|
Centralizovaná logika zdieľaná medzi `equipment.controller.ts` a `revisions.controller.ts`:
|
||||||
|
|
||||||
|
| Funkcia | Popis |
|
||||||
|
|---------|-------|
|
||||||
|
| `calculateNextDueDateFromInstall()` | Výpočet nasledujúceho termínu z anchor dátumu a intervalu |
|
||||||
|
| `detectSkippedCycles()` | Detekcia preskočených cyklových bodov |
|
||||||
|
| `computeFirstUpcomingCycleDate()` | Prvý nadchádzajúci cyklový bod po dátume |
|
||||||
|
| `getCoveringTypeIds()` | Nájde typy s dlhším intervalom (pokrývajúce) |
|
||||||
|
| `calculateReminderDate()` | Dátum pripomienky = nextDueDate - reminderDays |
|
||||||
|
| `getShortestIntervalForEquipment()` | Najkratší interval zo všetkých plánov zariadenia |
|
||||||
|
| `computeNextDueAndReminder()` | Kompletný výpočet nextDueDate + reminderDate |
|
||||||
|
| `mergeOverlappingItems()` | Zlúčenie prekrývajúcich sa termínov rôznych typov |
|
||||||
|
|
||||||
|
### Equipment Schedule (`GET /api/equipment/:id/schedule`)
|
||||||
|
|
||||||
|
Vracia:
|
||||||
|
- `schedules` - Stav každého priradeného revízneho typu (posledná revízia, nasledujúci termín)
|
||||||
|
- `upcomingDates` - Nadchádzajúce dátumy s pozičným labelom (lookAhead = 365 dní)
|
||||||
|
- `revisionHistory` - Posledných N vykonaných/preskočených revízií
|
||||||
|
|
||||||
|
### Revisions Schedule (`GET /api/revisions/schedule`)
|
||||||
|
|
||||||
|
Agregovaný plán zo VŠETKÝCH zariadení. Pre každý equipment+revisionType pár:
|
||||||
|
- Ak existuje posledná revízia → `dueDate = latestRevision.nextDueDate`
|
||||||
|
- Ak neexistuje → vypočíta sa prvý cyklový bod po dnešku z anchoru
|
||||||
|
|
||||||
|
Filtrovateľné podľa: `view` (upcoming/overdue), `typeId`, `customerId`, `search`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🎨 Frontend Komponenty
|
## 🎨 Frontend Komponenty
|
||||||
|
|
||||||
### Rozšírená Štruktúra
|
### Rozšírená Štruktúra
|
||||||
@@ -1192,11 +1297,13 @@ src/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── equipment/ # NEW
|
│ ├── equipment/ # NEW
|
||||||
│ │ ├── EquipmentList.tsx
|
│ │ ├── EquipmentList.tsx
|
||||||
│ │ ├── EquipmentDetail.tsx
|
│ │ ├── EquipmentDetail.tsx # Detail s revíznym plánom, históriou, prílohami
|
||||||
│ │ ├── EquipmentForm.tsx
|
│ │ ├── EquipmentForm.tsx # Formulár s revisionSchedules (typ revízií)
|
||||||
│ │ ├── RevisionForm.tsx
|
│ │ └── RevisionForm.tsx
|
||||||
│ │ ├── RevisionCalendar.tsx
|
│ │
|
||||||
│ │ └── EquipmentCard.tsx
|
│ ├── revisions/ # NEW
|
||||||
|
│ │ ├── RevisionsList.tsx # Hlavný zoznam s tabmi (upcoming/overdue/performed/skipped)
|
||||||
|
│ │ └── RevisionForm.tsx # Formulár pre pridanie/editáciu revízie
|
||||||
│ │
|
│ │
|
||||||
│ ├── rma/ # NEW
|
│ ├── rma/ # NEW
|
||||||
│ │ ├── RMAList.tsx
|
│ │ ├── RMAList.tsx
|
||||||
@@ -1233,6 +1340,19 @@ src/
|
|||||||
│ │ └── Toast.tsx
|
│ │ └── Toast.tsx
|
||||||
│ │
|
│ │
|
||||||
│ └── ui/ (shadcn/ui)
|
│ └── ui/ (shadcn/ui)
|
||||||
|
│
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useRevisionStatus.ts # Hook pre stav revízie (schedule/performed/skipped)
|
||||||
|
│ └── useSnoozeOptions.ts
|
||||||
|
│
|
||||||
|
├── services/
|
||||||
|
│ ├── equipment.api.ts
|
||||||
|
│ ├── revisions.api.ts # API pre revízie (CRUD, schedule, skip, stats)
|
||||||
|
│ ├── settings.api.ts
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── types/
|
||||||
|
└── index.ts # Obsahuje Revision, RevisionScheduleItem, RevisionStats
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1254,7 +1374,7 @@ src/
|
|||||||
- [x] Task CRUD (s fallback pre default status/priority)
|
- [x] Task CRUD (s fallback pre default status/priority)
|
||||||
- [x] Task Comments (s kontrolou oprávnení - len autor/priradený)
|
- [x] Task Comments (s kontrolou oprávnení - len autor/priradený)
|
||||||
- [x] **Customer CRUD**
|
- [x] **Customer CRUD**
|
||||||
- [x] **Equipment CRUD** (bez revízií zatiaľ)
|
- [x] **Equipment CRUD** (s revíznymi plánmi a prílohami)
|
||||||
- [x] **RMA CRUD** (basic, bez workflow)
|
- [x] **RMA CRUD** (basic, bez workflow)
|
||||||
- [x] **Settings API** (CRUD pre všetky config tables)
|
- [x] **Settings API** (CRUD pre všetky config tables)
|
||||||
- [x] **Dashboard API** (`/dashboard`, `/dashboard/today`, `/dashboard/stats`)
|
- [x] **Dashboard API** (`/dashboard`, `/dashboard/today`, `/dashboard/stats`)
|
||||||
@@ -1295,7 +1415,7 @@ src/
|
|||||||
✅ Priradenie používateľov na úlohy (multi-select)
|
✅ Priradenie používateľov na úlohy (multi-select)
|
||||||
✅ Dashboard s mojimi úlohami a projektmi + urgentné úlohy
|
✅ Dashboard s mojimi úlohami a projektmi + urgentné úlohy
|
||||||
✅ Zákazníci
|
✅ Zákazníci
|
||||||
✅ Zariadenia (bez revízií)
|
✅ Zariadenia (s revíznymi plánmi)
|
||||||
✅ RMA (bez workflow)
|
✅ RMA (bez workflow)
|
||||||
✅ ROOT môže konfigurovať všetko cez Settings
|
✅ ROOT môže konfigurovať všetko cez Settings
|
||||||
✅ Žiadne hardcoded ENUMs
|
✅ Žiadne hardcoded ENUMs
|
||||||
@@ -1329,10 +1449,15 @@ cd backend && npx prisma db seed
|
|||||||
**Cieľ:** Swimlanes, revízie, RMA workflow, reminders, notifikácie
|
**Cieľ:** Swimlanes, revízie, RMA workflow, reminders, notifikácie
|
||||||
|
|
||||||
**Backend:**
|
**Backend:**
|
||||||
- [ ] **Revision system**
|
- [x] **Revision system** ✅
|
||||||
- [ ] Create revision endpoint
|
- [x] CRUD endpoints (create, read, update, delete)
|
||||||
- [ ] Auto-calculate nextDueDate
|
- [x] Auto-calculate nextDueDate (cyklový anchor, pozičné labeling)
|
||||||
- [ ] Reminder scheduler
|
- [x] Skip revision endpoint (preskočenie s dôvodom)
|
||||||
|
- [x] Agregovaný schedule endpoint (zo všetkých zariadení)
|
||||||
|
- [x] Stats endpoint (upcoming, overdue, performed, skipped)
|
||||||
|
- [x] Shared utility `revisionSchedule.ts`
|
||||||
|
- [x] Equipment schedule endpoint (nadchádzajúce dátumy s labelmi)
|
||||||
|
- [ ] Reminder scheduler (cron job na posielanie pripomienok)
|
||||||
- [ ] **RMA workflow**
|
- [ ] **RMA workflow**
|
||||||
- [ ] Status transitions validation
|
- [ ] Status transitions validation
|
||||||
- [ ] Approval flow (customer RMAs)
|
- [ ] Approval flow (customer RMAs)
|
||||||
@@ -1358,10 +1483,19 @@ cd backend && npx prisma db seed
|
|||||||
- [ ] Drag & Drop
|
- [ ] Drag & Drop
|
||||||
- [ ] Collapse/expand
|
- [ ] Collapse/expand
|
||||||
- [ ] Progress indicators
|
- [ ] Progress indicators
|
||||||
- [ ] **Equipment Management**
|
- [x] **Equipment Management** ✅
|
||||||
- [ ] Revision form
|
- [x] EquipmentList - zoznam zariadení s filtrami
|
||||||
- [ ] Revision calendar view
|
- [x] EquipmentForm - formulár s revisionSchedules
|
||||||
- [ ] Reminder notifications
|
- [x] EquipmentDetail - detail s revíznym plánom, históriou, prílohami
|
||||||
|
- [x] RevisionForm - formulár pre pridanie/editáciu revízie
|
||||||
|
- [x] Revízny plán s pozičným labelingom
|
||||||
|
- [x] File attachments
|
||||||
|
- [x] **Revisions Management** ✅
|
||||||
|
- [x] RevisionsList - tabmi (nadchádzajúce, po termíne, vykonané, preskočené)
|
||||||
|
- [x] Filtrovacie tlačidlá podľa typu revízie
|
||||||
|
- [x] Preskočenie revízie s dôvodom
|
||||||
|
- [x] Vykonanie revízie z plánu (predvyplnený formulár)
|
||||||
|
- [x] useRevisionStatus hook
|
||||||
- [ ] **RMA Workflow**
|
- [ ] **RMA Workflow**
|
||||||
- [ ] Status change UI
|
- [ ] Status change UI
|
||||||
- [ ] Approval buttons (admin)
|
- [ ] Approval buttons (admin)
|
||||||
@@ -1387,7 +1521,8 @@ cd backend && npx prisma db seed
|
|||||||
```
|
```
|
||||||
✅ Všetko z Fázy 1 +
|
✅ Všetko z Fázy 1 +
|
||||||
⏳ Swimlanes board
|
⏳ Swimlanes board
|
||||||
⏳ Revízny systém funguje
|
✅ Revízny systém (CRUD, plán, skip, schedule, stats)
|
||||||
|
✅ Equipment management (detail, plán, história, prílohy)
|
||||||
⏳ RMA workflow s approval
|
⏳ RMA workflow s approval
|
||||||
⏳ Email notifikácie
|
⏳ Email notifikácie
|
||||||
⏳ Live updates (WebSocket)
|
⏳ Live updates (WebSocket)
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ model RevisionType {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
revisions Revision[]
|
revisions Revision[]
|
||||||
|
equipmentSchedules EquipmentRevisionSchedule[]
|
||||||
|
|
||||||
@@index([active])
|
@@index([active])
|
||||||
@@index([order])
|
@@index([order])
|
||||||
@@ -492,9 +493,10 @@ model Equipment {
|
|||||||
partNumber String?
|
partNumber String?
|
||||||
serialNumber String?
|
serialNumber String?
|
||||||
|
|
||||||
installDate DateTime?
|
installDate DateTime?
|
||||||
warrantyEnd DateTime?
|
revisionCycleStart DateTime?
|
||||||
warrantyStatus String?
|
warrantyEnd DateTime?
|
||||||
|
warrantyStatus String?
|
||||||
|
|
||||||
description String?
|
description String?
|
||||||
notes String?
|
notes String?
|
||||||
@@ -507,9 +509,10 @@ model Equipment {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
revisions Revision[]
|
revisions Revision[]
|
||||||
attachments EquipmentAttachment[]
|
revisionSchedules EquipmentRevisionSchedule[]
|
||||||
tags EquipmentTag[]
|
attachments EquipmentAttachment[]
|
||||||
|
tags EquipmentTag[]
|
||||||
|
|
||||||
@@index([typeId])
|
@@index([typeId])
|
||||||
@@index([customerId])
|
@@index([customerId])
|
||||||
@@ -518,6 +521,22 @@ model Equipment {
|
|||||||
@@index([createdById])
|
@@index([createdById])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model EquipmentRevisionSchedule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
equipmentId String
|
||||||
|
equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
revisionTypeId String
|
||||||
|
revisionType RevisionType @relation(fields: [revisionTypeId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([equipmentId, revisionTypeId])
|
||||||
|
@@index([equipmentId])
|
||||||
|
@@index([revisionTypeId])
|
||||||
|
}
|
||||||
|
|
||||||
model Revision {
|
model Revision {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|
||||||
@@ -527,6 +546,8 @@ model Revision {
|
|||||||
typeId String
|
typeId String
|
||||||
type RevisionType @relation(fields: [typeId], references: [id])
|
type RevisionType @relation(fields: [typeId], references: [id])
|
||||||
|
|
||||||
|
status String @default("performed") // "performed" | "skipped"
|
||||||
|
|
||||||
performedDate DateTime
|
performedDate DateTime
|
||||||
nextDueDate DateTime?
|
nextDueDate DateTime?
|
||||||
|
|
||||||
@@ -536,6 +557,7 @@ model Revision {
|
|||||||
findings String?
|
findings String?
|
||||||
result String?
|
result String?
|
||||||
notes String?
|
notes String?
|
||||||
|
skipReason String?
|
||||||
|
|
||||||
reminderSent Boolean @default(false)
|
reminderSent Boolean @default(false)
|
||||||
reminderDate DateTime?
|
reminderDate DateTime?
|
||||||
@@ -548,6 +570,7 @@ model Revision {
|
|||||||
@@index([performedById])
|
@@index([performedById])
|
||||||
@@index([nextDueDate])
|
@@index([nextDueDate])
|
||||||
@@index([reminderDate])
|
@@index([reminderDate])
|
||||||
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
model EquipmentAttachment {
|
model EquipmentAttachment {
|
||||||
|
|||||||
849
backend/prisma/seed-data.ts
Normal file
849
backend/prisma/seed-data.ts
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
// Auto-generated from old-helpdesk.sql - DO NOT EDIT MANUALLY
|
||||||
|
// Generated by: node extract-seed-data.js
|
||||||
|
|
||||||
|
// ===== AUDIT MAP: auditId -> equipmentHwId + periodicity =====
|
||||||
|
export const AUDIT_MAP: Record<number, { equipmentHwId: number; periodicity: number }> = {
|
||||||
|
3: { equipmentHwId: 466, periodicity: 90 },
|
||||||
|
4: { equipmentHwId: 467, periodicity: 90 },
|
||||||
|
5: { equipmentHwId: 469, periodicity: 90 },
|
||||||
|
6: { equipmentHwId: 470, periodicity: 90 },
|
||||||
|
7: { equipmentHwId: 471, periodicity: 90 },
|
||||||
|
8: { equipmentHwId: 472, periodicity: 90 },
|
||||||
|
9: { equipmentHwId: 473, periodicity: 90 },
|
||||||
|
10: { equipmentHwId: 474, periodicity: 90 },
|
||||||
|
12: { equipmentHwId: 480, periodicity: 365 },
|
||||||
|
13: { equipmentHwId: 489, periodicity: 90 },
|
||||||
|
14: { equipmentHwId: 490, periodicity: 90 },
|
||||||
|
15: { equipmentHwId: 491, periodicity: 90 },
|
||||||
|
16: { equipmentHwId: 492, periodicity: 365 },
|
||||||
|
17: { equipmentHwId: 493, periodicity: 365 },
|
||||||
|
18: { equipmentHwId: 494, periodicity: 365 },
|
||||||
|
19: { equipmentHwId: 495, periodicity: 90 },
|
||||||
|
20: { equipmentHwId: 496, periodicity: 90 },
|
||||||
|
21: { equipmentHwId: 497, periodicity: 90 },
|
||||||
|
22: { equipmentHwId: 498, periodicity: 90 },
|
||||||
|
23: { equipmentHwId: 499, periodicity: 90 },
|
||||||
|
24: { equipmentHwId: 500, periodicity: 365 },
|
||||||
|
25: { equipmentHwId: 501, periodicity: 365 },
|
||||||
|
26: { equipmentHwId: 502, periodicity: 365 },
|
||||||
|
27: { equipmentHwId: 505, periodicity: 90 },
|
||||||
|
28: { equipmentHwId: 506, periodicity: 90 },
|
||||||
|
29: { equipmentHwId: 525, periodicity: 90 },
|
||||||
|
30: { equipmentHwId: 526, periodicity: 90 },
|
||||||
|
33: { equipmentHwId: 535, periodicity: 90 },
|
||||||
|
34: { equipmentHwId: 536, periodicity: 90 },
|
||||||
|
35: { equipmentHwId: 667, periodicity: 365 },
|
||||||
|
36: { equipmentHwId: 682, periodicity: 90 },
|
||||||
|
37: { equipmentHwId: 681, periodicity: 90 },
|
||||||
|
38: { equipmentHwId: 726, periodicity: 90 },
|
||||||
|
39: { equipmentHwId: 785, periodicity: 365 },
|
||||||
|
40: { equipmentHwId: 786, periodicity: 730 },
|
||||||
|
41: { equipmentHwId: 787, periodicity: 365 },
|
||||||
|
43: { equipmentHwId: 789, periodicity: 90 },
|
||||||
|
44: { equipmentHwId: 790, periodicity: 90 },
|
||||||
|
45: { equipmentHwId: 791, periodicity: 90 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== PERIODICITY TO REVISION TYPE CODE =====
|
||||||
|
export const PERIOD_TO_TYPE: Record<number, string> = {
|
||||||
|
30: 'MONTHLY',
|
||||||
|
90: 'QUARTERLY',
|
||||||
|
180: 'BIANNUAL',
|
||||||
|
365: 'ANNUAL',
|
||||||
|
730: 'BIENNIAL',
|
||||||
|
1095: 'TRIENNIAL',
|
||||||
|
1460: 'QUADRENNIAL',
|
||||||
|
1825: 'QUINQUENNIAL',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== OLD TYPE ID TO NEW TYPE CODE =====
|
||||||
|
export const EQUIP_TYPE_MAP: Record<number, string> = {
|
||||||
|
19: 'EPS',
|
||||||
|
20: 'HSP',
|
||||||
|
21: 'EZS',
|
||||||
|
33: 'BLESKOZVOD',
|
||||||
|
34: 'ELEKTRO',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== ADDITIONAL EQUIPMENT TYPES (not in default seed) =====
|
||||||
|
export const ADDITIONAL_EQUIP_TYPES = [
|
||||||
|
{ code: 'EZS', name: 'Elektronický zabezpečovací systém', color: '#F59E0B' },
|
||||||
|
{ code: 'BLESKOZVOD', name: 'Bleskozvod', color: '#6366F1' },
|
||||||
|
{ code: 'ELEKTRO', name: 'Elektroinštalácia', color: '#EC4899' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== CUSTOMERS FROM OLD DB =====
|
||||||
|
export interface CustomerData {
|
||||||
|
oldId: number;
|
||||||
|
name: string;
|
||||||
|
ico: string;
|
||||||
|
dic: string;
|
||||||
|
address: string;
|
||||||
|
email: string;
|
||||||
|
contactPerson: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CUSTOMERS: CustomerData[] = [
|
||||||
|
{ oldId: 1001, name: "TEXNET s.r.o.", ico: "36 604 372", dic: "", address: "Osloboditeľov 484, 053 14 Spišský Štvrtok, Slovenská republika", email: "obchod@texnet.sk", contactPerson: "Vladimir Duncko", phone: "+421 911 776 576" },
|
||||||
|
{ oldId: 1005, name: "IMET a.s.", ico: "36 185 957", dic: "", address: "Bardejovská 1/C, Košice , 040 11", email: "miro@imet.sk", contactPerson: "MIroslav Prášil", phone: "+421903755756" },
|
||||||
|
{ oldId: 1008, name: "Popradská energetická spoločnosť, s.r.o.", ico: "50339729", dic: "", address: "Široká 4285, 05801, Poprad", email: "zavacky@poprad.energy", contactPerson: "Zavacký Michal", phone: "0902971605" },
|
||||||
|
{ oldId: 1009, name: "Tatry mountain resorts, a.s.", ico: "31560636", dic: "", address: "Demänovská Dolina 72, 03101, Liptovský Mikuláš", email: "mach@tmr.sk", contactPerson: "Libor Mach", phone: "0915834609" },
|
||||||
|
{ oldId: 1010, name: "EUROCOM Investment, s.r.o.", ico: "35756985", dic: "", address: "Bešeňová 136, 034 83, Bešeňová", email: "mach@tmr.sk", contactPerson: "Mach Libor", phone: "+421905209204" },
|
||||||
|
{ oldId: 1011, name: "AmiNet s.r.o.", ico: "50271903", dic: "", address: "L. Svobodu 73 , 058 01 Poprad ", email: "michlik@aminet.sk", contactPerson: "Michlík Ján", phone: "0" },
|
||||||
|
{ oldId: 1012, name: "Szczyrkowski Ośrodek Narciarski S.A.", ico: "9372375089", dic: "", address: "ul. Narciarska 10, 43-370 Szczyrk", email: "swierczek@tmr.sk", contactPerson: "Świerczek Szymon", phone: "+48 668 847174" },
|
||||||
|
{ oldId: 1013, name: "Śląskie Wesołe Miasteczko", ico: "627-273-82-14", dic: "", address: "Plac Atrakcji 1, 41-501 Chorzów", email: "krcmarek@tmr.sk", contactPerson: "Peter krcmarek", phone: "" },
|
||||||
|
{ oldId: 1014, name: "LARX smart&security LM, s. r. o.", ico: "52 247 562", dic: "", address: "Vrbická 1894/12, 031 01, Liptovský Mikuláš", email: "info_lm@larx.sk", contactPerson: "Jakub Smitka", phone: "0915783316" },
|
||||||
|
{ oldId: 1015, name: "Obec Jánovce", ico: "00326259", dic: "", address: "Jánovce 248, 059 13", email: "janovce@obec-janovce.sk", contactPerson: "Jozef Kacvinský", phone: "+421527793124" },
|
||||||
|
{ oldId: 1016, name: "Centrum sociálnych služieb Spišský Štvrtok, n.o.", ico: "45743118", dic: "", address: "Námestie slobody 256 , 053 14 Spišský Štvrtok", email: "csssps@csssps.sk", contactPerson: "Veronika Bajtošová", phone: "0910638004" },
|
||||||
|
{ oldId: 1017, name: "Obec Spišský Štvrtok", ico: "00329631", dic: "2020717897", address: "Tatranská 4, 053 14 Spišský Štvrtok", email: "eva.bajtosova@spisskystvrtok.sk", contactPerson: "Eva Bajtošová", phone: "0910871043" },
|
||||||
|
{ oldId: 1018, name: "DDS - Anténna a Satelitná technika s.r.o.", ico: "50319493", dic: "", address: "Jamník 188", email: "debnardds@gmail.com", contactPerson: "Jaroslav Debnár", phone: "+421918390269" },
|
||||||
|
{ oldId: 1019, name: "Smartdental", ico: "46597786", dic: "2023491503", address: "Gerlacho 193, 086 04 Gerlachov", email: "mariantulenko@gmail.com", contactPerson: "MUDr. Tulenko Mariánm", phone: "0948166948" },
|
||||||
|
{ oldId: 1020, name: "INTEN - EURA, správa domov, s. r. o.", ico: "46779841", dic: "", address: "M.Pišúta 1119/23 , Liptovský Mikuláš 031 01", email: "cupra@inteneura.sk", contactPerson: "Vladimír Cupra", phone: "0908223308" },
|
||||||
|
{ oldId: 1021, name: "Slovenská autobusová doprava Poprad a.s.", ico: "36479560", dic: "2020020178", address: "Wolkerova 466, 058 49 Poprad", email: "marcel.globinovsky@sadpp.sk", contactPerson: "Marcel Globinovský", phone: "0917600194" },
|
||||||
|
{ oldId: 1022, name: "Penzión Energetik s.r.o.", ico: "35768878", dic: "2020255424", address: "Pribinova 25, 81109 Bratislava - mestská časť Staré Mesto", email: "energetik@penzion-energetik.sk", contactPerson: "Peter Šplha", phone: "0904631906" },
|
||||||
|
{ oldId: 1023, name: "SPI Global Play s.r.o.", ico: "50930851", dic: "2120529235", address: "Stavbárska 11946/8, 08001 Prešov", email: "sales@spiglobalplay.com", contactPerson: "Filip Miščík", phone: "0917914287" },
|
||||||
|
{ oldId: 1024, name: "K_Corp", ico: "36215791", dic: "2020035083", address: "Radlinského 20/2231, 05201 Spišská Nová Ves", email: "tomas.kosalko@kcorp.sk", contactPerson: "Ing. Tomáš Košalko", phone: "0911 635 633" },
|
||||||
|
{ oldId: 1025, name: "hascon, s.r.o.", ico: "46751424", dic: "2023560561", address: "Bottova 10, Chorvátsky Grob, 90025", email: "info@hascon.sk", contactPerson: "Slavomir Petras", phone: "+421 903 288 654" },
|
||||||
|
{ oldId: 1026, name: "Desať, s.r.o.", ico: "36692620", dic: "2022262803", address: "Záhradná 543/19, Veľká Lomnica, 05952", email: "desat@desat.sk", contactPerson: "Pavol Duchnický", phone: "+421 903 575 153" },
|
||||||
|
{ oldId: 1027, name: "STONE COMPANY s.r.o.", ico: "51700387", dic: "2120763469", address: "Družstevná 74, 03403 Ružomberok", email: "sklad@texnet.sk", contactPerson: "Kristián Gardošík", phone: "0911676576" },
|
||||||
|
{ oldId: 1028, name: "ECO PRODUKT s. r. o.", ico: "45502471", dic: "2023016369", address: "Banícka 360/7, 972 17 Kanianka", email: "info@ecoprodukt.sk", contactPerson: "Jakub Harmat", phone: "0918623931" },
|
||||||
|
{ oldId: 1029, name: "Nové technológie a služby, s.r.o.", ico: "36170771", dic: "2020056280", address: "Cesta pod Hradovou 13/A, 04001 Košice", email: "Lubomir_Palusek@ntes.sk", contactPerson: "Lubomir Palusek", phone: "+421 915 959 636" },
|
||||||
|
{ oldId: 1030, name: "Tyco Fire & Security Czech Republic s.r.o.", ico: "61055077", dic: "61055077", address: "Líbalova 2348/1, 149 00 Praha 4 - Chodov, Praha Česko", email: "sales.liberec@tycoint.com", contactPerson: "Sofia Heizerová", phone: "+421 233 002 677/3303" },
|
||||||
|
{ oldId: 1031, name: "CANEX, spol. s r. o.", ico: "31378854", dic: "2020353951", address: "Máchova 6, 82106 Bratislava", email: "alena.potancakova@canex.sk", contactPerson: "Alena Potančáková", phone: "+421918752691" },
|
||||||
|
{ oldId: 1032, name: "TRACON SLOVAKIA s.r.o.", ico: "34140603", dic: "2020400811", address: "Rozmarínová 10, 945 01 Komárno", email: "petrus@tracon.sk", contactPerson: "Tomáš Petruš", phone: "0915 646 333" },
|
||||||
|
{ oldId: 1033, name: "BAUSKA, s. r. o.", ico: "46447156", dic: "2023391205", address: "Magnezitárska 11/B, 04001 Košice-Sídlisko Ťahanovce", email: "jakub.palko@bauska.sk", contactPerson: "Jakub Palko", phone: "0944951832" },
|
||||||
|
{ oldId: 1034, name: "Retail Park Prúdy s.r.o.", ico: "53521323", dic: "2121405000", address: "Železničná 3086/60, 92601 Sereď", email: "jakub.palko@bauska.sk", contactPerson: "Jakub Palko", phone: "0944951832" },
|
||||||
|
{ oldId: 1035, name: "SH Therm, s.r.o.", ico: "48108481", dic: "2120045356", address: "Zimná 58, 05201 Spišská nová Ves", email: "jozef.comba@texnet.sk", contactPerson: "Jozef Comba", phone: "0911020951" },
|
||||||
|
{ oldId: 1036, name: "ELLANO s.r.o.", ico: "50747681", dic: "2120466414", address: "Štiavnička 211/49 976 81 Podbrezová ", email: "satelity@ellano.sk", contactPerson: "Obchod ", phone: "0911072878" },
|
||||||
|
{ oldId: 1037, name: "Kiradent s.r.o.", ico: "44320205", dic: "2022662741", address: "J. Curie 730 058 01 Poprad", email: "peter.tropp@texnet.sk", contactPerson: "Peter Tropp", phone: "+421911058054" },
|
||||||
|
{ oldId: 1038, name: "Ústredná vojenská nemocnica SNP Ružomberok - FN", ico: "31936415", dic: "2020590187", address: "Areál UVN1, Ústredná vojenská nemocnica SNP Ružomberok - FN, ul. gen. Miloša Vesela 21, 03426 Ružomberok", email: "vladimir.duncko@texnet.sk", contactPerson: "Vladimír Dunčko", phone: "+421905704895" },
|
||||||
|
{ oldId: 1039, name: "MARSUPIUM s.r.o.", ico: "50980556", dic: "2120591110", address: "Krásno nad Kysucou 1682, 023 02 Krásno nad Kysucou", email: "vladimir.duncko@texnet.sk", contactPerson: "Vladimír Dunčko", phone: "+421905704895" },
|
||||||
|
{ oldId: 1040, name: "Poľnohospodárske družstvo \\\"Čingov\\\" Smižany", ico: "00204251", dic: "2020502616", address: "Tatranská 126 053 11 Smižany", email: "ladislav.javorsky@texnet.sk", contactPerson: "Ladislav Javorský", phone: "0907911819" },
|
||||||
|
{ oldId: 1041, name: "Techfors SK s.r.o.", ico: "47549599", dic: "2023945660", address: "Jókaiho 1 821 06 Bratislava-Podunajské Biskupice", email: "bratislava@techfors.sk", contactPerson: "-", phone: "+421 915 575 190" },
|
||||||
|
{ oldId: 1042, name: "KU - BAU spol. s r.o.", ico: "43871534", dic: "2022508301", address: "Ulica Mieru 61, 98511 Halíč", email: "vladimir.duncko@texnet.sk", contactPerson: "Vladimír Dunčko", phone: "0905704895" },
|
||||||
|
{ oldId: 1043, name: "TATRAVAGÓNKA a.s.", ico: "31699847", dic: "2020514496", address: "Štefánikova 887/53 058 01 Poprad", email: "ladislav.javorsky@texnet.sk", contactPerson: "Laco", phone: "0907911819" },
|
||||||
|
{ oldId: 1044, name: "PLEX s.r.o.", ico: "36592846", dic: "2021997615", address: "Duklianska 38/1250 Spišská Nová Ves", email: "vladimir.duncko@texnet.sk", contactPerson: "Vladimir Dunčko", phone: "0905704895" },
|
||||||
|
{ oldId: 1045, name: "ESIN group, a. s.", ico: "36787302", dic: "2022389842", address: "Naše Farmy, a. s., Dlhé hony 4991 058 01 Poprad", email: "karpinsky@esin.sk", contactPerson: "Miroslav Karpinsky ", phone: "+421 949 548 383" },
|
||||||
|
{ oldId: 1046, name: "ESIN group, a. s.", ico: "36787302", dic: "2022389842", address: "Naše Farmy, a. s., Dlhé hony 4991 058 01 Poprad", email: "karpinsky@esin.sk", contactPerson: "Miroslav Karpinsky ", phone: "+421 949 548 383" },
|
||||||
|
{ oldId: 1047, name: "Commend Slovakia, spol. s r.o.", ico: "43834108", dic: "2022485201", address: "Slávičie údolie 106 811 02 Bratislava-mestská časť Staré Mesto", email: "info@commend.sk", contactPerson: ".", phone: "+421 2 58101040" },
|
||||||
|
{ oldId: 1048, name: "Tomáš Findura", ico: "-", dic: "-", address: "Agátová 8, 05311 Smižany", email: "tomasfindura27@gmail.com", contactPerson: "Tomáš Findura", phone: "0949787218" },
|
||||||
|
{ oldId: 1049, name: "Trika Poprad s.r.o.", ico: "45620504", dic: "2023058972", address: "Poprad", email: "kollar@ockrivan.sk", contactPerson: "Kollár Michal", phone: "+421" },
|
||||||
|
{ oldId: 1050, name: "Metrostav a.s. - organizačná zložka Bratislava", ico: "31792693", dic: "2020253301", address: "Mlynské Nivy 68, 82105, Bratislava - mestská časť Ružinov", email: "jozef.comba@texnet.sk", contactPerson: "Jozef Comba", phone: "0911 020 951" },
|
||||||
|
{ oldId: 1051, name: "E N E R G Y R spol. s r.o.", ico: "31568645", dic: "2020452720", address: "Rudlovská cesta 53, 97400, Banská BYstrica", email: "nagel@energyrbb.sk", contactPerson: "Miroslav Nágel", phone: "0911296111" },
|
||||||
|
{ oldId: 1052, name: "Resort Lúčky s.r.o.", ico: "55757618", dic: "2122092049", address: "Demänovská Dolina", email: "matejweck@airavata.sk", contactPerson: "Weck Matej", phone: "0911722837" },
|
||||||
|
{ oldId: 1053, name: "Eminox Slovakia, s.r.o.", ico: "53941195", dic: "2121541730", address: "Štúrova 101, 059 21 Svit ", email: "tomas.zolnai@eminox.com", contactPerson: "Tomáš Zolnai", phone: "+421907386785" },
|
||||||
|
{ oldId: 1056, name: "Farský úrad Spišský Štvrtok", ico: "0", dic: "0", address: "Spišský Štvrtok", email: "a@a.sk", contactPerson: "Farár", phone: "0" },
|
||||||
|
{ oldId: 1057, name: "MOLD-TRADE s.r.o.", ico: "31730337", dic: "2020496819", address: "Cestice 1023, 044 71, Cestice", email: "karpinsky@nasefarmy.eu", contactPerson: "Karpinsky Miroslav", phone: "+421 949 548 383" },
|
||||||
|
{ oldId: 1058, name: "MAR SK, s.r.o.", ico: "36428094", dic: "2021962448", address: "Ulica priemyselná 1940/14, Sučany 038 52, Slovensko", email: "pavol.sedmak@marsk.sk", contactPerson: "Ing. Pavol Sedmák", phone: "+421 915 993733" },
|
||||||
|
{ oldId: 1059, name: "Správa majetku Košického samosprávneho kraja", ico: "42093937", dic: "2022359669", address: "Tatranská 25, 04 001 Košice", email: "roland.furda@vucke.sk", contactPerson: "Roland Furda", phone: "0918 766 133" },
|
||||||
|
{ oldId: 1060, name: "Štrbské Pleso resort, s. r. o.", ico: "55737854", dic: "", address: "K vodopádom 4028/26, 059 85 Štrba", email: "it@pekyho.sk", contactPerson: "Šarišský", phone: "+421902345093" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== EQUIPMENT FROM OLD DB =====
|
||||||
|
export interface EquipmentData {
|
||||||
|
hwId: number;
|
||||||
|
companyId: number;
|
||||||
|
typeCode: string;
|
||||||
|
brand: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
partNumber: string;
|
||||||
|
serialNumber: string;
|
||||||
|
installDate: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EQUIPMENT: EquipmentData[] = [
|
||||||
|
{ hwId: 466, companyId: 1005, typeCode: 'EPS', brand: "ESSER", name: "Hotel Galícia Nueva EPS", description: "EPS", location: "Halíč", partNumber: "", serialNumber: "", installDate: '2017-10-24' },
|
||||||
|
{ hwId: 467, companyId: 1005, typeCode: 'HSP', brand: "Bosch", name: "Hotel Galícia Nueva HSP", description: "Presideo", location: "Halíč", partNumber: "", serialNumber: "", installDate: '2017-10-24' },
|
||||||
|
{ hwId: 469, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "Hlavný vstup EPS", description: "-", location: "Bešeňová", partNumber: "", serialNumber: "", installDate: '2018-01-25' },
|
||||||
|
{ hwId: 470, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "Luka", description: "-", location: "Bešeňová", partNumber: "", serialNumber: "", installDate: '2018-01-25' },
|
||||||
|
{ hwId: 471, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "Hotel Pošta EPS", description: "Východzia revízia 21.4.2017", location: "Demänovská dolina", partNumber: "", serialNumber: "", installDate: '2018-01-25' },
|
||||||
|
{ hwId: 472, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "Hotel Pošta HSP", description: "-", location: "Demänovská dolina", partNumber: "", serialNumber: "", installDate: '2016-12-15' },
|
||||||
|
{ hwId: 473, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "Hotel Galéria Bešeňová EPS", description: "-", location: "Bešeňová", partNumber: " ", serialNumber: "1", installDate: '2017-05-17' },
|
||||||
|
{ hwId: 474, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "Hotel Galéria Bešeňová HSP", description: "-", location: "Bešeňová", partNumber: " ", serialNumber: "2", installDate: '2017-05-17' },
|
||||||
|
{ hwId: 489, companyId: 1009, typeCode: 'EPS', brand: "ESSER", name: "Hotel Srdiečko EPS", description: "ESSER IQ8 Control", location: "Chopok Juh", partNumber: "-", serialNumber: "-", installDate: '2019-11-18' },
|
||||||
|
{ hwId: 490, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "EPS - Polyfunkčný objekt Tatran ", description: "", location: "V. Lomnica", partNumber: "-", serialNumber: "--", installDate: '2019-12-11' },
|
||||||
|
{ hwId: 491, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "HSP - Polyfunkčný objekt Tatran ", description: "", location: "V. Lomnica", partNumber: "-", serialNumber: "---", installDate: '2019-12-11' },
|
||||||
|
{ hwId: 492, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "Habarka HSP", description: "", location: "Jasna", partNumber: "-", serialNumber: "----", installDate: '2018-10-05' },
|
||||||
|
{ hwId: 493, companyId: 1009, typeCode: 'EZS', brand: "Jablotron", name: "Habarka EZS", description: "", location: "Jasna", partNumber: ",", serialNumber: ", ", installDate: '2018-11-26' },
|
||||||
|
{ hwId: 494, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "Skalnaté Pleso reštaurácia - HSP", description: "", location: "Vysoké Tatry", partNumber: ",-", serialNumber: ",-", installDate: '2018-12-17' },
|
||||||
|
{ hwId: 495, companyId: 1009, typeCode: 'EPS', brand: "Aritech", name: "Hotel Grand Jasná - EPS", description: "", location: "Jasna", partNumber: ",,", serialNumber: ",,", installDate: '2018-06-24' },
|
||||||
|
{ hwId: 496, companyId: 1009, typeCode: 'HSP', brand: "Bosch", name: "Hotel Grand Jasná - HSP", description: "", location: "Jasna", partNumber: "-,", serialNumber: ",--", installDate: '2018-06-24' },
|
||||||
|
{ hwId: 497, companyId: 1009, typeCode: 'EPS', brand: "Menvier", name: "Happy end - EPS", description: "", location: "Jasna", partNumber: ".", serialNumber: ".", installDate: '2019-05-22' },
|
||||||
|
{ hwId: 498, companyId: 1009, typeCode: 'HSP', brand: "Bosch", name: "Happy end - HSP", description: "", location: "Jasna", partNumber: "..", serialNumber: "..", installDate: '2019-05-22' },
|
||||||
|
{ hwId: 499, companyId: 1009, typeCode: 'EPS', brand: "Zettler", name: "Tri studničky - EPS", description: "", location: "Demänová", partNumber: ".-", serialNumber: ".-", installDate: '2019-05-31' },
|
||||||
|
{ hwId: 500, companyId: 1009, typeCode: 'HSP', brand: "Bosch", name: "Centrum východ Jasná - HSP", description: "", location: "Jasna", partNumber: ",.", serialNumber: ",.", installDate: '2019-06-25' },
|
||||||
|
{ hwId: 501, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "HSP - Rotunda, Kosodrevina", description: "", location: "Chopok Juh", partNumber: "..", serialNumber: "...", installDate: '2019-10-28' },
|
||||||
|
{ hwId: 502, companyId: 1015, typeCode: 'EZS', brand: "Jablotron", name: "EZS - OU Janovce", description: "", location: "Janovce", partNumber: ":", serialNumber: ":", installDate: '2021-01-28' },
|
||||||
|
{ hwId: 505, companyId: 1018, typeCode: 'HSP', brand: "ESSER", name: "Strachanovka HSP", description: "", location: "Jasna", partNumber: ".,", serialNumber: ".,", installDate: '2019-11-25' },
|
||||||
|
{ hwId: 506, companyId: 1018, typeCode: 'EPS', brand: "ESSER", name: "Strachanovka EPS", description: "", location: "Jasna", partNumber: "---", serialNumber: "-----", installDate: '2019-11-25' },
|
||||||
|
{ hwId: 667, companyId: 1009, typeCode: 'HSP', brand: "ESSER", name: "HSP - KLD A6", description: " ", location: "Biela púť", partNumber: "1", serialNumber: "123", installDate: '2022-12-11' },
|
||||||
|
{ hwId: 681, companyId: 1049, typeCode: 'EPS', brand: "ESSER", name: "OC Kriváň EPS", description: "", location: "OC Kriváň", partNumber: "12", serialNumber: "12345", installDate: '2024-07-02' },
|
||||||
|
{ hwId: 682, companyId: 1049, typeCode: 'HSP', brand: "ESSER", name: "OC Kriváň HSP", description: "", location: "OC Kriváň", partNumber: "1", serialNumber: "123456", installDate: '2024-07-02' },
|
||||||
|
{ hwId: 726, companyId: 1052, typeCode: 'EPS', brand: "Aritech", name: "EPS Penzión Energetik", description: "", location: "Demänovská dolina", partNumber: "0", serialNumber: "0", installDate: '2025-01-01' },
|
||||||
|
{ hwId: 785, companyId: 1056, typeCode: 'BLESKOZVOD', brand: "LPS", name: "LPS - kostolná veža", description: " ", location: "Spišský štvrtok", partNumber: "0", serialNumber: "0123", installDate: '2025-05-07' },
|
||||||
|
{ hwId: 786, companyId: 1057, typeCode: 'BLESKOZVOD', brand: "LPS", name: "LPS - garáž HD Dobogov", description: "Uzemnenie a bleskozvod garáže", location: "Dobogov-Cestice", partNumber: "TRE 007/021", serialNumber: "007/021", installDate: '2025-03-15' },
|
||||||
|
{ hwId: 787, companyId: 1057, typeCode: 'BLESKOZVOD', brand: "LPS", name: "LPS - administratívna budova HD Dobogov", description: "Silnoprúdová elektroinštalácia - administratívna budova HD Dobogov", location: "Dobogov-Cestice", partNumber: "TRE 002/021", serialNumber: "002/021", installDate: '2024-12-21' },
|
||||||
|
{ hwId: 789, companyId: 1059, typeCode: 'EPS', brand: "Aritech", name: "Telocvičňa SNV - Aritech 2X", description: " ", location: "SNV", partNumber: "123 ", serialNumber: "41234", installDate: '2023-12-29' },
|
||||||
|
{ hwId: 790, companyId: 1059, typeCode: 'HSP', brand: "Bosch", name: "Telocvičňa SNV - Bosch Paviro", description: "", location: "SNV", partNumber: "54320", serialNumber: "54320", installDate: '2023-12-29' },
|
||||||
|
{ hwId: 791, companyId: 1060, typeCode: 'EPS', brand: "Aritech", name: "Hotel Solisko EPS", description: "", location: "Štrbské Pleso", partNumber: "6589", serialNumber: "6589", installDate: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== SERVICE RECORDS (REVISIONS) =====
|
||||||
|
// Format: [auditId, date, nextDate, userEmail, note]
|
||||||
|
export type ServiceRecord = [number, string, string, string, string];
|
||||||
|
|
||||||
|
export const SERVICE_RECORDS: ServiceRecord[] = [
|
||||||
|
[4,'2017-08-01','2017-11-01','peter.slebodnik@texnet.sk','Ročná revizia'],
|
||||||
|
[3,'2017-08-01','2017-11-01','peter.slebodnik@texnet.sk','Ročná revízia'],
|
||||||
|
[4,'2017-10-30','2018-01-30','peter.slebodnik@texnet.sk','Štvrťročná revízia'],
|
||||||
|
[3,'2017-10-30','2018-01-30','peter.slebodnik@texnet.sk','Štvrťročná revízia'],
|
||||||
|
[5,'2017-10-23','2018-01-23','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2017-07-21','2017-10-21','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[6,'2017-07-27','2017-10-27','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2017-09-07','2017-12-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2017-12-12','2018-03-12','vladimir.duncko@texnet.sk','Ročná revízia'],
|
||||||
|
[7,'2017-04-21','2017-07-21','vladimir.duncko@texnet.sk','Východzia revízia.'],
|
||||||
|
[7,'2017-08-03','2017-11-03','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2017-11-02','2018-02-02','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2017-04-21','2017-07-21','vladimir.duncko@texnet.sk','Východzia revízia.'],
|
||||||
|
[8,'2017-08-03','2017-11-03','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2017-11-02','2018-02-02','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2017-05-17','2017-08-17','vladimir.duncko@texnet.sk','Východzia revízia.'],
|
||||||
|
[10,'2017-08-04','2017-11-04','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2017-12-06','2018-03-06','vladimir.duncko@texnet.sk','štvrťročná. 1001, 1006, 1101, 1104, 1117, 1202, 1305'],
|
||||||
|
[9,'2017-05-17','2017-08-17','vladimir.duncko@texnet.sk','Východzia revízia.'],
|
||||||
|
[9,'2017-08-04','2017-11-04','vladimir.duncko@texnet.sk','Štvrťročná.'],
|
||||||
|
[9,'2017-11-06','2018-02-06','vladimir.duncko@texnet.sk','Štvrťročná.'],
|
||||||
|
[5,'2018-01-22','2018-04-22','peter.slebodnik@texnet.sk','-'],
|
||||||
|
[8,'2018-01-30','2018-04-30','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2018-01-30','2018-04-30','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2018-02-05','2018-05-05','peter.slebodnik@texnet.sk','Štvrťročná revízia'],
|
||||||
|
[4,'2018-02-05','2018-05-05','peter.slebodnik@texnet.sk','Štvrťročná revízia'],
|
||||||
|
[6,'2018-04-09','2018-07-09','peter.slebodnik@texnet.sk','Štvrťročná revizia'],
|
||||||
|
[10,'2018-04-10','2018-07-10','peter.slebodnik@texnet.sk','Ročná revízia'],
|
||||||
|
[9,'2018-04-10','2018-07-10','peter.slebodnik@texnet.sk','Ročná revízia'],
|
||||||
|
[8,'2018-04-19','2018-07-19','peter.slebodnik@texnet.sk','Ročná revízia'],
|
||||||
|
[7,'2018-04-19','2018-07-19','peter.slebodnik@texnet.sk','Ročná revízia'],
|
||||||
|
[5,'2018-04-25','2018-07-25','peter.slebodnik@texnet.sk','Ročná revizia'],
|
||||||
|
[4,'2018-05-07','2018-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2018-05-07','2018-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2018-07-09','2018-10-09','peter.slebodnik@texnet.sk','Štvrťročná revízia'],
|
||||||
|
[10,'2018-07-12','2018-10-12','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[9,'2018-07-12','2018-10-12','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[4,'2018-08-09','2018-11-09','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2018-08-09','2018-11-09','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2018-07-30','2018-10-30','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[8,'2018-07-18','2018-10-18','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[7,'2018-07-18','2018-10-18','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[6,'2018-10-12','2019-01-12','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[9,'2018-10-12','2019-01-12','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[10,'2018-10-12','2019-01-12','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[8,'2018-10-16','2019-01-16','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[7,'2018-10-16','2019-01-16','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[5,'2018-10-30','2019-01-30','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[4,'2018-11-07','2019-02-07','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[3,'2018-11-07','2019-02-07','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[6,'2019-01-09','2019-04-09','peter.slebodnik@texnet.sk','Ročna'],
|
||||||
|
[9,'2019-01-09','2019-04-09','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[10,'2019-01-09','2019-04-09','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[8,'2019-01-16','2019-04-16','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[7,'2019-01-16','2019-04-16','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[5,'2019-01-30','2019-04-30','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[4,'2019-02-07','2019-05-07','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[3,'2019-02-07','2019-05-07','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[9,'2019-04-08','2019-07-08','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[10,'2019-04-09','2019-07-09','peter.slebodnik@texnet.sk','Ročna'],
|
||||||
|
[6,'2019-04-09','2019-07-09','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[8,'2019-04-17','2019-07-17','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[7,'2019-04-17','2019-07-17','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2019-04-30','2019-07-30','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2019-05-07','2019-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2019-05-07','2019-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2019-07-08','2019-10-08','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[10,'2019-07-08','2019-10-08','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[9,'2019-07-08','2019-10-08','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[7,'2019-07-17','2019-10-17','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[8,'2019-07-17','2019-10-17','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[5,'2019-07-29','2019-10-29','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2019-08-07','2019-11-07','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[4,'2019-08-07','2019-11-07','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[10,'2019-10-07','2020-01-07','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[9,'2019-10-07','2020-01-07','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[6,'2019-10-07','2020-01-07','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[7,'2019-10-17','2020-01-17','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[8,'2019-10-17','2020-01-17','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[5,'2019-10-29','2020-01-29','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[3,'2019-11-05','2020-02-05','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2019-11-05','2020-02-05','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2020-01-09','2020-04-09','peter.slebodnik@texnet.sk','Štrvťročná'],
|
||||||
|
[9,'2020-01-09','2020-04-09','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[6,'2020-01-09','2020-04-09','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[7,'2020-01-22','2020-04-22','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[8,'2020-01-22','2020-04-22','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[5,'2020-01-30','2020-04-30','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[3,'2020-02-10','2020-05-10','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[4,'2020-02-10','2020-05-10','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[13,'2020-02-25','2020-05-25','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[14,'2019-12-11','2020-03-11','peter.slebodnik@texnet.sk','Štvrťročná'],
|
||||||
|
[14,'2020-03-11','2020-06-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2020-03-11','2020-06-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2020-04-02','2020-07-02','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[9,'2020-04-02','2020-07-02','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[6,'2020-04-08','2020-07-08','peter.slebodnik@texnet.sk','Štvrťročna'],
|
||||||
|
[7,'2020-04-22','2020-07-22','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[8,'2020-04-22','2020-07-22','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2020-04-30','2020-07-30','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2020-05-07','2020-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2020-05-07','2020-08-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[16,'2019-05-13','2020-05-13','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[19,'2020-04-07','2020-07-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2020-04-07','2020-07-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[17,'2020-05-13','2021-05-13','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[16,'2020-05-13','2021-05-13','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[23,'2020-03-27','2020-06-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2020-03-31','2020-07-01','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2020-03-31','2020-07-01','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[24,'2019-09-25','2020-09-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[13,'2020-05-25','2020-08-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[18,'2020-05-26','2021-05-26','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[14,'2020-06-11','2020-09-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2020-06-11','2020-09-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2020-06-26','2020-09-26','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2020-06-26','2020-09-26','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2020-06-24','2020-09-24','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[9,'2020-06-25','2020-09-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2020-06-25','2020-09-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2020-07-07','2020-10-07','peter.slebodnik@texnet.sk','štvrťročna'],
|
||||||
|
[20,'2020-07-07','2020-10-07','peter.slebodnik@texnet.sk','štvrťročna'],
|
||||||
|
[6,'2020-07-08','2020-10-08','peter.slebodnik@texnet.sk','štvrťročna'],
|
||||||
|
[5,'2020-07-30','2020-10-30','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2020-07-22','2020-10-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2020-07-22','2020-10-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2020-08-10','2020-11-10','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[4,'2020-08-10','2020-11-10','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[13,'2020-08-20','2020-11-20','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2020-09-11','2020-12-11','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[15,'2020-09-11','2020-12-11','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[23,'2020-09-30','2020-12-30','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[9,'2020-09-24','2020-12-24','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2020-09-24','2020-12-24','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2020-09-28','2020-12-28','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[22,'2020-09-28','2020-12-28','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[24,'2020-09-25','2021-09-25','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[6,'2020-10-08','2021-01-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2020-10-14','2021-01-14','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[20,'2020-10-14','2021-01-14','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[7,'2020-10-22','2021-01-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2020-10-22','2021-01-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[25,'2020-10-28','2021-10-28','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2020-10-30','2021-01-30','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2020-11-10','2021-02-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2020-11-10','2021-02-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[13,'2020-11-23','2021-02-23','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[9,'2020-12-03','2021-03-03','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2020-12-03','2021-03-03','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2020-12-15','2021-03-15','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2020-12-15','2021-03-15','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2020-12-14','2021-03-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2020-12-14','2021-03-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2020-12-16','2021-03-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2021-01-11','2021-04-11','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[19,'2021-01-14','2021-04-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2021-01-14','2021-04-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2021-01-22','2021-04-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2021-01-22','2021-04-22','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2021-01-29','2021-04-29','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[26,'2021-02-01','2022-02-01','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[3,'2021-02-17','2021-05-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2021-02-17','2021-05-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[13,'2021-03-02','2021-06-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2021-03-08','2021-06-08','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[9,'2021-03-10','2021-06-10','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[22,'2021-03-15','2021-06-15','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2021-03-15','2021-06-15','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2021-03-12','2021-06-12','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2021-03-12','2021-06-12','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2021-03-18','2021-06-18','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2021-04-09','2021-07-09','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2021-04-16','2021-07-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2021-04-16','2021-07-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2021-04-21','2021-07-21','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[8,'2021-04-21','2021-07-21','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2021-04-29','2021-07-29','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2021-05-17','2021-08-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2021-05-17','2021-08-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[18,'2021-05-27','2022-05-27','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[13,'2021-06-04','2021-09-04','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[9,'2021-06-09','2021-09-09','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2021-06-09','2021-09-09','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2021-06-10','2021-09-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2021-06-10','2021-09-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2021-06-18','2021-09-18','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2021-06-21','2021-09-21','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2021-06-21','2021-09-21','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[16,'2021-06-22','2022-06-22','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[17,'2021-06-22','2022-06-22','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[27,'2020-12-02','2021-03-02','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[28,'2020-12-02','2021-03-02','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[27,'2021-06-25','2021-09-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2021-06-25','2021-09-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2021-07-16','2021-10-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2021-07-16','2021-10-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2021-07-09','2021-10-09','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2021-07-29','2021-10-29','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2021-07-28','2021-10-28','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2021-07-28','2021-10-28','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2021-08-25','2021-11-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2021-08-25','2021-11-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[29,'2021-06-23','2021-09-23','peter.slebodnik@texnet.sk','Vychodisková'],
|
||||||
|
[30,'2021-06-23','2021-09-23','peter.slebodnik@texnet.sk','Vychodisková'],
|
||||||
|
[13,'2021-08-23','2021-11-23','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2021-09-09','2021-12-09','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[15,'2021-09-09','2021-12-09','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[9,'2021-09-10','2021-12-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2021-09-10','2021-12-10','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2021-09-17','2021-12-17','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[27,'2021-09-27','2021-12-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2021-09-27','2021-12-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[29,'2021-09-21','2021-12-21','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[30,'2021-09-21','2021-12-21','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2021-09-23','2021-12-23','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[22,'2021-09-23','2021-12-23','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[6,'2021-10-07','2022-01-07','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[24,'2021-10-01','2022-10-01','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[20,'2021-10-25','2022-01-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[19,'2021-10-25','2022-01-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[7,'2021-10-27','2022-01-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2021-10-27','2022-01-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2021-11-02','2022-02-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[33,'2021-10-15','2022-01-15','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[33,'2021-10-15','2022-01-15','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[13,'2021-11-24','2022-02-24','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[34,'2021-10-15','2022-01-15','vladimir.duncko@texnet.sk','Ročná'],
|
||||||
|
[3,'2021-11-25','2022-02-25','vladimir.duncko@texnet.sk','stvrťročná'],
|
||||||
|
[4,'2021-11-25','2022-02-25','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[27,'2021-12-03','2022-03-03','vladimir.duncko@texnet.sk','Ročná'],
|
||||||
|
[28,'2021-12-03','2022-03-03','vladimir.duncko@texnet.sk','Ročná'],
|
||||||
|
[14,'2021-12-07','2022-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2021-12-09','2022-03-09','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[9,'2021-12-09','2022-03-09','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2021-12-09','2022-03-09','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[25,'2021-10-29','2022-10-29','peter.slebodnik@texnet.sk','Ročna'],
|
||||||
|
[23,'2022-01-10','2022-04-10','vladimir.duncko@texnet.sk','štvrťročná.'],
|
||||||
|
[6,'2022-01-11','2022-04-11','vladimir.duncko@texnet.sk','Ročná'],
|
||||||
|
[29,'2022-01-13','2022-04-13','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[30,'2022-01-13','2022-04-13','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2022-01-14','2022-04-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[34,'2022-01-17','2022-04-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[33,'2022-01-17','2022-04-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2022-01-25','2022-04-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2022-01-25','2022-04-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2022-01-27','2022-04-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2022-01-27','2022-04-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2022-02-03','2022-05-03','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[26,'2022-02-04','2023-02-04','peter.slebodnik@texnet.sk','ročná'],
|
||||||
|
[13,'2022-02-25','2022-05-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[3,'2022-02-28','2022-05-28','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2022-02-28','2022-05-28','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[27,'2022-03-03','2022-06-03','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2022-03-03','2022-06-03','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2022-03-04','2022-06-04','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2022-03-04','2022-06-04','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[9,'2022-03-16','2022-06-16','peter.slebodnik@texnet.sk','ročna'],
|
||||||
|
[10,'2022-03-16','2022-06-16','peter.slebodnik@texnet.sk','ročna'],
|
||||||
|
[23,'2022-04-08','2022-07-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2022-01-14','2022-04-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2022-04-08','2022-07-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2022-04-08','2022-07-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2022-04-11','2022-07-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[29,'2022-04-13','2022-07-13','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[30,'2022-04-13','2022-07-13','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[33,'2022-04-14','2022-07-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[34,'2022-04-14','2022-07-14','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[19,'2022-04-25','2022-07-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2022-04-25','2022-07-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2022-04-27','2022-07-27','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[8,'2022-04-27','2022-07-27','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[5,'2022-05-02','2022-08-02','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[18,'2022-05-27','2023-05-27','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2022-05-26','2022-08-26','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2022-05-26','2022-08-26','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[13,'2022-05-30','2022-08-30','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[27,'2022-06-02','2022-09-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2022-06-02','2022-09-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2022-06-06','2022-09-06','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[14,'2022-06-06','2022-09-06','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[9,'2022-06-17','2022-09-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2022-06-17','2022-09-17','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[16,'2022-06-30','2023-06-30','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[17,'2022-06-30','2023-06-30','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[21,'2022-07-08','2022-10-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[22,'2022-07-08','2022-10-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2022-07-08','2022-10-08','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2022-07-11','2022-10-11','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[34,'2022-07-12','2022-10-12','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[33,'2022-07-12','2022-10-12','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[29,'2022-07-13','2022-10-13','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[30,'2022-07-13','2022-10-13','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[19,'2022-07-25','2022-10-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[20,'2022-07-25','2022-10-25','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2022-07-27','2022-10-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2022-07-27','2022-10-27','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[5,'2022-08-02','2022-11-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[4,'2022-08-25','2022-11-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[3,'2022-08-25','2022-11-25','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[13,'2022-09-02','2022-12-02','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[27,'2022-09-05','2022-12-05','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2022-09-05','2022-12-05','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2022-09-06','2022-12-06','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[14,'2022-09-06','2022-12-06','peter.slebodnik@texnet.sk','Ročná'],
|
||||||
|
[9,'2022-09-16','2022-12-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[10,'2022-09-16','2022-12-16','peter.slebodnik@texnet.sk','štvrťročná'],
|
||||||
|
[23,'2022-10-11','2023-01-11','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[19,'2022-10-28','2023-01-28','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[20,'2022-10-24','2023-01-24','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[33,'2022-12-06','2023-03-06','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[34,'2022-12-06','2023-03-06','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[6,'2022-12-05','2023-03-05','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[30,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[29,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[21,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[22,'2022-12-02','2023-03-02','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[24,'2022-12-01','2023-12-01','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[14,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[15,'2022-12-07','2023-03-07','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[7,'2022-12-08','2023-03-08','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[8,'2022-12-08','2023-03-08','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[25,'2022-12-13','2023-12-13','vladimir.duncko@texnet.sk','ročná kontrola'],
|
||||||
|
[13,'2022-12-19','2023-03-19','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[27,'2022-12-20','2023-03-20','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[28,'2022-12-20','2023-03-20','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[4,'2022-12-21','2023-03-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[10,'2022-12-21','2023-03-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[9,'2022-12-21','2023-03-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[5,'2022-12-21','2023-03-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[3,'2022-12-22','2023-03-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[23,'2023-01-11','2023-04-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[20,'2023-01-27','2023-04-27','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[19,'2023-01-27','2023-04-27','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[26,'2023-02-14','2024-02-14','vladimir.duncko@texnet.sk','Ročná kontrola EZS'],
|
||||||
|
[6,'2023-03-07','2023-06-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[33,'2023-03-06','2023-06-06','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[34,'2023-03-06','2023-06-06','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[14,'2023-03-13','2023-06-13','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[15,'2023-03-13','2023-06-13','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[29,'2023-03-13','2023-06-13','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[30,'2023-03-13','2023-06-13','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[22,'2023-03-09','2023-06-09','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[21,'2023-03-09','2023-06-09','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[28,'2023-03-15','2023-06-15','vladimir.duncko@texnet.sk','štrťročná kontrola'],
|
||||||
|
[27,'2023-03-15','2023-06-15','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[9,'2023-03-20','2023-06-20','vladimir.duncko@texnet.sk','Ročna kontrola.'],
|
||||||
|
[10,'2023-03-20','2023-06-20','vladimir.duncko@texnet.sk','Ročna kontrola.'],
|
||||||
|
[13,'2023-03-27','2023-06-27','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2023-03-16','2023-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[8,'2023-03-16','2023-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[8,'2023-03-16','2023-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[4,'2023-04-19','2023-07-19','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[5,'2023-04-19','2023-07-19','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[3,'2023-04-25','2023-07-25','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[23,'2023-04-25','2023-07-25','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[23,'2023-08-15','2023-11-15','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[19,'2023-05-15','2023-08-16','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[20,'2023-05-15','2023-08-15','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[33,'2023-06-19','2023-09-19','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[34,'2023-06-19','2023-09-19','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[34,'2023-06-19','2023-09-19','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[34,'2023-06-19','2023-09-19','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[18,'2023-07-26','2024-07-25','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[21,'2023-06-20','2023-09-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[22,'2023-06-20','2023-09-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[14,'2023-07-03','2023-10-03','vladimir.duncko@texnet.sk','švrťročná kontrola'],
|
||||||
|
[15,'2023-07-03','2023-10-03','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[29,'2023-07-06','2023-10-06','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[30,'2023-07-06','2023-10-06','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[27,'2023-07-13','2023-10-13','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[28,'2023-07-13','2023-10-13','vladimir.duncko@texnet.sk','štvrťročná'],
|
||||||
|
[6,'2023-06-26','2023-09-26','vladimir.duncko@texnet.sk','ročná'],
|
||||||
|
[13,'2023-07-20','2023-10-20','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[7,'2023-04-26','2023-07-25','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[8,'2023-04-26','2023-08-07','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[7,'2023-07-21','2023-10-20','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[8,'2023-07-21','2023-10-20','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[8,'2023-07-21','2023-10-20','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[4,'2023-08-02','2023-11-02','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[3,'2023-08-02','2023-11-02','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[9,'2023-07-25','2023-10-23','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[10,'2023-07-25','2023-10-25','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[5,'2023-08-08','2023-11-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[20,'2023-08-15','2023-10-24','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[19,'2023-08-23','2023-10-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[33,'2023-09-20','2023-12-06','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2023-09-20','2023-12-06','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[21,'2023-09-20','2023-12-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[22,'2023-09-20','2023-12-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[30,'2023-10-02','2023-12-18','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[29,'2023-10-02','2023-12-18','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[13,'2023-10-04','2023-12-20','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[14,'2023-10-10','2024-01-08','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[15,'2023-10-10','2024-01-08','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[16,'2023-10-18','2024-10-17','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[17,'2023-10-18','2024-10-17','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[27,'2023-10-25','2024-01-23','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[28,'2023-10-25','2024-01-23','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[9,'2023-10-16','2024-01-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2023-10-16','2024-01-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2023-10-16','2024-01-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2023-11-08','2024-02-06','vladimir.duncko@texnet.sk','ročná kontrola'],
|
||||||
|
[19,'2023-11-08','2024-02-06','vladimir.duncko@texnet.sk','ročná kontrola'],
|
||||||
|
[7,'2023-11-09','2024-02-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2023-11-09','2024-02-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[3,'2023-11-07','2024-02-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2023-11-07','2024-02-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[5,'2023-12-11','2024-03-10','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[21,'2023-12-12','2024-03-11','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[22,'2023-12-12','2024-03-11','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[23,'2023-11-22','2024-02-20','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[33,'2023-12-19','2024-03-18','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[34,'2023-12-19','2024-03-18','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[24,'2023-12-18','2024-12-17','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[13,'2024-01-08','2024-04-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2024-01-16','2024-04-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-01-16','2024-04-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2024-01-16','2024-04-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[25,'2024-01-11','2025-01-10','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[35,'2024-01-11','2025-01-10','vladimir.duncko@texnet.sk','Ročná kontrola HSP.'],
|
||||||
|
[30,'2024-01-23','2024-04-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[29,'2024-01-23','2024-04-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[14,'2024-01-23','2024-04-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[15,'2024-01-23','2024-04-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[3,'2024-02-07','2024-05-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2024-02-07','2024-05-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[19,'2024-02-13','2024-05-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2024-02-13','2024-05-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2024-02-13','2024-05-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2024-02-13','2024-05-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[23,'2024-02-26','2024-05-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[5,'2024-02-26','2024-05-26','vladimir.duncko@texnet.sk','Štvrťročna kontrola.'],
|
||||||
|
[33,'2024-03-18','2024-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2024-03-18','2024-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[21,'2024-03-18','2024-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2024-03-18','2024-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[27,'2024-02-16','2024-05-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[28,'2024-02-16','2024-05-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[13,'2024-04-09','2024-07-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2024-04-19','2024-05-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[9,'2024-04-16','2024-07-15','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[10,'2024-04-16','2024-07-15','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[26,'2024-04-12','2025-04-12','vladimir.duncko@texnet.sk','Ročná kontrola EZS.'],
|
||||||
|
[14,'2024-04-24','2024-07-23','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[15,'2024-04-24','2024-07-23','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[29,'2024-04-24','2024-07-23','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[30,'2024-04-24','2024-07-23','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[3,'2024-04-30','2024-08-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2024-04-30','2024-08-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2024-05-15','2024-08-13','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[8,'2024-05-15','2024-08-13','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[19,'2024-05-13','2024-08-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2024-05-13','2024-08-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola'],
|
||||||
|
[5,'2024-05-09','2024-08-07','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[23,'2024-06-06','2024-09-04','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[27,'2024-06-06','2024-09-04','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[28,'2024-06-06','2024-09-04','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[33,'2024-06-18','2024-09-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2024-06-18','2024-09-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[21,'2024-06-18','2024-09-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2024-06-18','2024-09-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[36,'2024-04-05','2024-07-04','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[37,'2024-04-05','2024-07-04','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[13,'2024-07-17','2024-10-04','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2024-07-22','2024-10-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-07-22','2024-10-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-07-22','2024-10-20','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2024-07-23','2024-10-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[30,'2024-07-23','2024-10-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[36,'2024-07-03','2024-10-01','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2024-07-03','2024-10-01','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[14,'2024-07-24','2024-10-10','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[15,'2024-07-24','2024-10-10','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[18,'2024-07-24','2025-07-24','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[6,'2024-06-05','2024-09-03','vladimir.duncko@texnet.sk','Ročná kontrola EPS'],
|
||||||
|
[3,'2024-08-13','2024-11-11','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[4,'2024-08-13','2024-11-11','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[5,'2024-08-28','2024-11-26','vladimir.duncko@texnet.sk','Švrťročná kontrola.'],
|
||||||
|
[5,'2024-08-28','2024-11-26','vladimir.duncko@texnet.sk','Švrťročná kontrola.'],
|
||||||
|
[19,'2024-08-28','2024-11-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2024-08-28','2024-11-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2024-08-23','2024-11-21','vladimir.duncko@texnet.sk','Štvrťročná kontrrola.'],
|
||||||
|
[8,'2024-08-23','2024-11-21','vladimir.duncko@texnet.sk','Švrťročná kontrola.'],
|
||||||
|
[23,'2024-09-12','2024-12-11','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[27,'2024-09-24','2024-12-23','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[28,'2024-09-24','2024-12-23','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[21,'2024-09-17','2024-12-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2024-09-17','2024-12-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2024-09-17','2024-12-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[33,'2024-10-07','2025-01-05','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[34,'2024-10-07','2025-01-05','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[36,'2024-10-03','2025-01-01','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2024-10-03','2025-01-01','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[16,'2024-09-19','2025-09-19','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[17,'2024-09-19','2025-09-19','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[9,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2024-10-30','2025-01-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[13,'2024-11-20','2025-02-18','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[19,'2024-11-05','2025-02-03','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[20,'2024-11-05','2025-02-03','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[20,'2024-11-05','2025-02-03','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[3,'2024-11-28','2025-02-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2024-11-28','2025-02-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2024-11-28','2025-02-26','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[5,'2024-12-09','2025-03-09','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2024-12-09','2025-03-09','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2024-12-09','2025-03-09','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2024-11-29','2025-02-27','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[30,'2024-11-29','2025-02-27','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[14,'2024-11-21','2025-02-19','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[15,'2024-11-21','2025-02-19','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[21,'2024-12-16','2025-03-16','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[22,'2024-12-16','2025-03-16','vladimir.duncko@texnet.sk','Ročná kontrola'],
|
||||||
|
[36,'2025-01-07','2025-04-03','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2025-01-07','2025-04-03','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[24,'2025-02-18','2025-11-02','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[6,'2024-12-19','2025-03-19','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[23,'2024-12-17','2025-03-17','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[33,'2025-01-08','2025-04-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2025-01-08','2025-04-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[27,'2025-01-08','2025-04-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[28,'2025-01-08','2025-04-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2025-01-09','2025-04-09','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[10,'2025-01-09','2025-04-09','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[38,'2025-01-27','2025-04-27','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[19,'2025-02-10','2025-05-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2025-02-10','2025-05-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[25,'2025-02-18','2025-11-02','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[35,'2025-02-18','2025-11-02','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[13,'2025-03-04','2025-02-24','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[3,'2025-03-10','2025-06-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2025-03-10','2025-06-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[14,'2025-03-13','2025-06-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[15,'2025-03-13','2025-06-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2025-03-13','2025-06-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[30,'2025-03-13','2025-06-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2025-03-14','2025-06-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-03-14','2025-06-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[21,'2025-03-18','2025-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2025-03-18','2025-06-16','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[5,'2025-03-17','2025-05-09','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[23,'2025-03-18','2025-06-16','vladimir.duncko@texnet.sk','štvrťročná kontrola'],
|
||||||
|
[6,'2025-03-19','2025-06-17','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[36,'2025-04-07','2025-07-06','vladimir.duncko@texnet.sk','Ričná kontrola.'],
|
||||||
|
[37,'2025-04-07','2025-07-06','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[9,'2025-04-14','2025-07-13','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[10,'2025-04-14','2025-07-13','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[27,'2025-04-16','2025-07-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[28,'2025-04-16','2025-07-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[33,'2025-04-16','2025-07-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2025-04-16','2025-07-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[38,'2025-04-28','2025-07-27','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[19,'2025-05-19','2025-08-17','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2025-05-19','2025-08-17','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[40,'2025-05-21','2029-05-20','vladimir.duncko@texnet.sk','NN revízia.'],
|
||||||
|
[41,'2025-05-21','2029-05-20','vladimir.duncko@texnet.sk','Periodická revízia NN.'],
|
||||||
|
[41,'2025-05-21','2029-05-20','vladimir.duncko@texnet.sk','Periodická revízia NN.'],
|
||||||
|
[39,'2025-05-06','2026-05-06','jakub.smitka@gmail.com','fix'],
|
||||||
|
[40,'2025-05-21','2025-06-20','jakub.smitka@gmail.com','fix'],
|
||||||
|
[41,'2025-05-21','2026-05-21','jakub.smitka@gmail.com','fix'],
|
||||||
|
[13,'2025-06-02','2025-08-31','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[26,'2025-05-26','2026-05-26','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[3,'2025-06-04','2025-09-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2025-06-04','2025-09-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2025-06-04','2025-09-02','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[7,'2025-06-12','2025-09-10','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[8,'2025-06-12','2025-09-10','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[14,'2025-06-13','2025-09-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[30,'2025-06-12','2025-09-10','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2025-06-12','2025-09-10','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[15,'2025-06-13','2025-09-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[40,'2025-06-25','2027-06-25','vladimir.duncko@texnet.sk','Periodická revízia.'],
|
||||||
|
[21,'2025-06-26','2025-09-24','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2025-06-26','2025-09-24','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[23,'2025-06-26','2025-09-24','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[5,'2025-06-25','2025-09-23','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[6,'2025-07-07','2025-10-05','vladimir.duncko@texnet.sk','Ročná kontrola EPS'],
|
||||||
|
[36,'2025-07-10','2025-10-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2025-07-10','2025-10-08','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2025-07-14','2025-10-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2025-07-14','2025-10-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[27,'2025-07-15','2025-10-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[28,'2025-07-15','2025-10-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[33,'2025-07-15','2025-10-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[34,'2025-07-15','2025-10-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[44,'2025-07-22','2025-10-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[43,'2025-07-22','2025-10-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[38,'2025-07-30','2025-10-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[18,'2025-08-11','2026-08-11','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[19,'2025-08-19','2025-11-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[20,'2025-08-19','2025-11-05','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[3,'2025-08-06','2025-11-04','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[4,'2025-08-06','2025-11-04','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[13,'2025-09-08','2025-12-07','jakub.smitka@gmail.com','Štvrťročná kontrola.'],
|
||||||
|
[7,'2025-09-16','2025-12-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-09-16','2025-12-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-09-16','2025-12-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-09-16','2025-12-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-09-16','2025-12-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[23,'2025-09-24','2025-12-17','vladimir.duncko@texnet.sk','štvrťročná kontrola.'],
|
||||||
|
[5,'2025-09-24','2025-12-23','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[21,'2025-09-25','2025-11-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[22,'2025-09-25','2025-11-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2025-09-28','2025-11-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[30,'2025-09-28','2025-11-28','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[14,'2025-09-18','2025-11-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[15,'2025-09-18','2025-11-21','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2025-10-14','2026-01-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2025-10-14','2026-01-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2025-10-14','2026-01-12','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[36,'2025-10-13','2026-01-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2025-10-13','2026-01-11','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[33,'2025-10-22','2026-01-20','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[34,'2025-10-22','2026-01-20','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[27,'2025-10-23','2026-01-21','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[28,'2025-10-23','2026-01-21','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[44,'2025-10-15','2026-01-13','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[21,'2025-11-10','2026-02-08','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[22,'2025-11-10','2026-02-08','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[16,'2025-11-04','2026-11-04','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[17,'2025-11-04','2026-11-04','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[19,'2025-11-17','2026-02-15','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[20,'2025-11-17','2026-02-15','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[23,'2025-11-18','2026-02-16','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[3,'2025-11-25','2026-02-23','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[4,'2025-11-25','2026-02-23','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[38,'2025-11-21','2026-02-19','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[29,'2025-12-09','2026-03-09','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[6,'2026-01-14','2026-04-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[6,'2026-01-14','2026-04-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[5,'2026-01-19','2026-04-19','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[9,'2026-01-14','2026-04-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[10,'2026-01-14','2026-04-14','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[45,'2025-12-17','2026-03-17','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[7,'2025-12-15','2026-03-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[8,'2025-12-15','2026-03-15','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[24,'2025-12-15','2026-12-15','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[25,'2025-12-11','2026-12-11','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[30,'2025-12-09','2026-03-09','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[14,'2025-12-10','2026-03-10','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[15,'2025-12-10','2026-03-10','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[13,'2025-10-31','2026-01-29','vladimir.duncko@texnet.sk','Ročná kontrola.'],
|
||||||
|
[44,'2026-01-22','2026-04-22','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[36,'2026-01-22','2026-04-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
[37,'2026-01-22','2026-04-07','vladimir.duncko@texnet.sk','Štvrťročná kontrola.'],
|
||||||
|
];
|
||||||
@@ -1,8 +1,44 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import {
|
||||||
|
AUDIT_MAP,
|
||||||
|
PERIOD_TO_TYPE,
|
||||||
|
ADDITIONAL_EQUIP_TYPES,
|
||||||
|
CUSTOMERS,
|
||||||
|
EQUIPMENT,
|
||||||
|
SERVICE_RECORDS,
|
||||||
|
} from './seed-data';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Klasifikuje poznámku revízie na kód typu revízie.
|
||||||
|
* Staré dáta používali voľný text na zaznamenanie druhu revízie.
|
||||||
|
*/
|
||||||
|
function classifyNote(note: string, basePeriodicity: number): string {
|
||||||
|
const normalized = note.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||||
|
|
||||||
|
// Štvrťročná (rôzne varianty s preklepmi) - musí byť pred ročnou, lebo "stvrtrocna" obsahuje "rocn"
|
||||||
|
// s[trv]{1,4}rocn pokrýva: stvrtrocn, svrtrocn, strtrocn, strvtrocn a ďalšie preklepy
|
||||||
|
if (/s[trv]{1,4}rocn/.test(normalized)) return 'QUARTERLY';
|
||||||
|
// Ročná (vrátane preklepu "Ričná")
|
||||||
|
if (/rocn|ricn/.test(normalized)) return 'ANNUAL';
|
||||||
|
// Mesačná
|
||||||
|
if (/mesacn/.test(normalized)) return 'MONTHLY';
|
||||||
|
// Východzia / Vychodisková → základný typ z auditu
|
||||||
|
if (/vychodz|vychodisk/.test(normalized)) return PERIOD_TO_TYPE[basePeriodicity] || 'QUARTERLY';
|
||||||
|
// Default → typ z auditu
|
||||||
|
return PERIOD_TO_TYPE[basePeriodicity] || 'QUARTERLY';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zistí, či poznámka indikuje "Východziu revíziu".
|
||||||
|
*/
|
||||||
|
function isVychodziaNote(note: string): boolean {
|
||||||
|
const normalized = note.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||||
|
return /vychodz|vychodisk/.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('Seeding database...');
|
console.log('Seeding database...');
|
||||||
|
|
||||||
@@ -88,10 +124,17 @@ async function seed() {
|
|||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
data: [
|
data: [
|
||||||
{ code: 'EPS', name: 'Elektrická požiarna signalizácia', color: '#3B82F6', order: 1 },
|
{ code: 'EPS', name: 'Elektrická požiarna signalizácia', color: '#3B82F6', order: 1 },
|
||||||
{ code: 'HSP', name: 'Hasiaci systém', color: '#EF4444', order: 2 },
|
{ code: 'HSP', name: 'Hlasová signalizácia požiaru', color: '#EF4444', order: 2 },
|
||||||
{ code: 'CAMERA', name: 'Kamerový systém', color: '#10B981', order: 3 },
|
{ code: 'CAMERA', name: 'Kamerový systém', color: '#10B981', order: 3 },
|
||||||
{ code: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 },
|
{ code: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 },
|
||||||
{ code: 'OTHER', name: 'Iné zariadenie', color: '#6B7280', order: 5 },
|
{ code: 'OTHER', name: 'Iné zariadenie', color: '#6B7280', order: 5 },
|
||||||
|
// Additional types from old DB
|
||||||
|
...ADDITIONAL_EQUIP_TYPES.map((t, i) => ({
|
||||||
|
code: t.code,
|
||||||
|
name: t.name,
|
||||||
|
color: t.color,
|
||||||
|
order: 6 + i,
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,10 +143,15 @@ async function seed() {
|
|||||||
await prisma.revisionType.createMany({
|
await prisma.revisionType.createMany({
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
data: [
|
data: [
|
||||||
{ code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 1 },
|
{ code: 'MONTHLY', name: 'Mesačná revízia', intervalDays: 30, reminderDays: 7, color: '#6366F1', order: 1 },
|
||||||
{ code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 2 },
|
{ code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 2 },
|
||||||
{ code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 3 },
|
{ code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 3 },
|
||||||
{ code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#DC2626', order: 4 },
|
{ code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 4 },
|
||||||
|
{ code: 'BIENNIAL', name: 'Revízia každé 2 roky', intervalDays: 730, reminderDays: 60, color: '#8B5CF6', order: 5 },
|
||||||
|
{ code: 'TRIENNIAL', name: 'Revízia každé 3 roky', intervalDays: 1095, reminderDays: 90, color: '#0EA5E9', order: 6 },
|
||||||
|
{ code: 'QUADRENNIAL', name: 'Revízia každé 4 roky', intervalDays: 1460, reminderDays: 90, color: '#14B8A6', order: 7 },
|
||||||
|
{ code: 'QUINQUENNIAL', name: 'Revízia každých 5 rokov', intervalDays: 1825, reminderDays: 120, color: '#F97316', order: 8 },
|
||||||
|
{ code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#EF4444', order: 9 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,56 +257,451 @@ async function seed() {
|
|||||||
{ label: '30 minút', minutes: 30 },
|
{ label: '30 minút', minutes: 30 },
|
||||||
{ label: '1 hodina', minutes: 60 },
|
{ label: '1 hodina', minutes: 60 },
|
||||||
{ label: '3 hodiny', minutes: 180 },
|
{ label: '3 hodiny', minutes: 180 },
|
||||||
{ label: 'Zajtra ráno', type: 'tomorrow', hour: 9 },
|
{ label: 'Dnes poobede o 13:00', type: 'today', hour: 13 },
|
||||||
|
{ label: 'Zajtra ráno o 8:00', type: 'tomorrow', hour: 8 },
|
||||||
|
{ label: 'Zajtra poobede o 13:00', type: 'tomorrow', hour: 13 },
|
||||||
],
|
],
|
||||||
category: 'NOTIFICATIONS',
|
category: 'NOTIFICATIONS',
|
||||||
label: 'Možnosti odloženia notifikácií',
|
label: 'Možnosti odloženia notifikácií',
|
||||||
description: 'Pole objektov. Každý má "label" a buď "minutes" (relatívny čas) alebo "type" + "hour" (konkrétny čas). Type: "today" (ak čas prešiel, skryje sa), "tomorrow".',
|
description: 'Pole objektov. Každý má "label" a buď "minutes" (relatívny čas) alebo "type" + "hour" (konkrétny čas). Type: "today" (ak čas prešiel, skryje sa), "tomorrow".',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'REVISION_STATUS_THRESHOLDS',
|
||||||
|
value: [
|
||||||
|
{ days: 30, label: 'Blíži sa', color: '#EAB308' },
|
||||||
|
{ days: 14, label: 'Blíži sa!', color: '#F97316' },
|
||||||
|
{ days: 7, label: 'Urgentné!', color: '#EF4444' },
|
||||||
|
],
|
||||||
|
category: 'REVISIONS',
|
||||||
|
label: 'Prahy upozornení revízií',
|
||||||
|
description: 'Pole prahov zoradených od najväčšieho po najmenší počet dní. Každý prah má: "days" (počet dní pred termínom), "label" (text), "color" (hex farba). Po termíne sa vždy zobrazuje červená.',
|
||||||
|
dataType: 'json',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== DEMO USERS =====
|
// ===== USERS (demo + real from old DB) =====
|
||||||
console.log('Creating demo users...');
|
console.log('Creating users...');
|
||||||
const rootRole = roles.find(r => r.code === 'ROOT');
|
const rootRole = roles.find(r => r.code === 'ROOT');
|
||||||
const adminRole = roles.find(r => r.code === 'ADMIN');
|
const adminRole = roles.find(r => r.code === 'ADMIN');
|
||||||
const userRole = roles.find(r => r.code === 'USER');
|
const userRole = roles.find(r => r.code === 'USER');
|
||||||
|
const customerRole = roles.find(r => r.code === 'CUSTOMER');
|
||||||
|
|
||||||
if (rootRole && adminRole && userRole) {
|
if (!rootRole || !adminRole || !userRole || !customerRole) {
|
||||||
await prisma.user.upsert({
|
throw new Error('Required roles not found');
|
||||||
where: { email: 'root@helpdesk.sk' },
|
}
|
||||||
update: { password: await bcrypt.hash('root123', 10) },
|
|
||||||
|
const [hashedPassword, hashedRoot, hashedAdmin, hashedUser, hashedZakaznik] = await Promise.all([
|
||||||
|
bcrypt.hash('heslo123', 10),
|
||||||
|
bcrypt.hash('root123', 10),
|
||||||
|
bcrypt.hash('admin123', 10),
|
||||||
|
bcrypt.hash('user123', 10),
|
||||||
|
bcrypt.hash('zakaznik123', 10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rootUser = await prisma.user.upsert({
|
||||||
|
where: { email: 'root@helpdesk.sk' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'root@helpdesk.sk',
|
||||||
|
password: hashedPassword,
|
||||||
|
name: 'Root Admin',
|
||||||
|
roleId: rootRole.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo users - jeden pre každú rolu
|
||||||
|
const [demoRoot, demoAdmin, user1, user2, user3, demoZakaznik] = await Promise.all([
|
||||||
|
prisma.user.upsert({
|
||||||
|
where: { email: 'root@root.sk' },
|
||||||
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
email: 'root@helpdesk.sk',
|
email: 'root@root.sk',
|
||||||
password: await bcrypt.hash('root123', 10),
|
password: hashedRoot,
|
||||||
name: 'Root Admin',
|
name: 'Root Používateľ',
|
||||||
roleId: rootRole.id,
|
roleId: rootRole.id,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
prisma.user.upsert({
|
||||||
await prisma.user.upsert({
|
where: { email: 'admin@admin.sk' },
|
||||||
where: { email: 'admin@helpdesk.sk' },
|
update: {},
|
||||||
update: { password: await bcrypt.hash('admin123', 10) },
|
|
||||||
create: {
|
create: {
|
||||||
email: 'admin@helpdesk.sk',
|
email: 'admin@admin.sk',
|
||||||
password: await bcrypt.hash('admin123', 10),
|
password: hashedAdmin,
|
||||||
name: 'Peter Admin',
|
name: 'Admin Používateľ',
|
||||||
roleId: adminRole.id,
|
roleId: adminRole.id,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
prisma.user.upsert({
|
||||||
await prisma.user.upsert({
|
where: { email: 'user1@user.sk' },
|
||||||
where: { email: 'user@helpdesk.sk' },
|
update: {},
|
||||||
update: { password: await bcrypt.hash('user123', 10) },
|
|
||||||
create: {
|
create: {
|
||||||
email: 'user@helpdesk.sk',
|
email: 'user1@user.sk',
|
||||||
password: await bcrypt.hash('user123', 10),
|
password: hashedUser,
|
||||||
name: 'Martin Používateľ',
|
name: 'Ján Technik',
|
||||||
roleId: userRole.id,
|
roleId: userRole.id,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.upsert({
|
||||||
|
where: { email: 'user2@user.sk' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'user2@user.sk',
|
||||||
|
password: hashedUser,
|
||||||
|
name: 'Peter Sieťar',
|
||||||
|
roleId: userRole.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.upsert({
|
||||||
|
where: { email: 'user3@user.sk' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'user3@user.sk',
|
||||||
|
password: hashedUser,
|
||||||
|
name: 'Marek Montážnik',
|
||||||
|
roleId: userRole.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.upsert({
|
||||||
|
where: { email: 'zakaznik@zakaznik.sk' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'zakaznik@zakaznik.sk',
|
||||||
|
password: hashedZakaznik,
|
||||||
|
name: 'Zákazník Demo',
|
||||||
|
roleId: customerRole.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
console.log(' Created demo users: root@root.sk, admin@admin.sk, user1-3@user.sk, zakaznik@zakaznik.sk');
|
||||||
|
|
||||||
|
// All service records are assigned to root user
|
||||||
|
const defaultUserId = rootUser.id;
|
||||||
|
|
||||||
|
// ===== CUSTOMERS FROM OLD DB =====
|
||||||
|
console.log('Creating customers from old DB...');
|
||||||
|
const customerMap: Record<number, string> = {};
|
||||||
|
|
||||||
|
for (const c of CUSTOMERS) {
|
||||||
|
const customer = await prisma.customer.upsert({
|
||||||
|
where: { externalId: String(c.oldId) },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
name: c.name,
|
||||||
|
ico: c.ico && c.ico !== '0' ? c.ico : undefined,
|
||||||
|
dic: c.dic && c.dic !== '0' ? c.dic : undefined,
|
||||||
|
address: c.address,
|
||||||
|
email: c.email,
|
||||||
|
contactPerson: c.contactPerson,
|
||||||
|
contactPhone: c.phone,
|
||||||
|
externalId: String(c.oldId),
|
||||||
|
externalSource: 'old-helpdesk',
|
||||||
|
createdById: rootUser.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
customerMap[c.oldId] = customer.id;
|
||||||
}
|
}
|
||||||
|
console.log(` Created ${Object.keys(customerMap).length} customers`);
|
||||||
|
|
||||||
|
// ===== ANALYZE SERVICE RECORDS =====
|
||||||
|
// Zistiť pre každé zariadenie: aké typy revízií má a kedy bola východzia revízia
|
||||||
|
console.log('Analyzing service records for revision metadata...');
|
||||||
|
|
||||||
|
interface EquipMeta {
|
||||||
|
typeCodes: Set<string>;
|
||||||
|
vychodziaDate: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipMeta: Map<number, EquipMeta> = new Map();
|
||||||
|
|
||||||
|
for (const [auditId, date, , , note] of SERVICE_RECORDS) {
|
||||||
|
const audit = AUDIT_MAP[auditId];
|
||||||
|
if (!audit) continue;
|
||||||
|
|
||||||
|
const hwId = audit.equipmentHwId;
|
||||||
|
if (!equipMeta.has(hwId)) {
|
||||||
|
equipMeta.set(hwId, { typeCodes: new Set(), vychodziaDate: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = equipMeta.get(hwId)!;
|
||||||
|
|
||||||
|
// Klasifikovať typ revízie z poznámky
|
||||||
|
const typeCode = classifyNote(note, audit.periodicity);
|
||||||
|
meta.typeCodes.add(typeCode);
|
||||||
|
|
||||||
|
// Detekovať dátum "Východzej revízie"
|
||||||
|
if (isVychodziaNote(note) && !meta.vychodziaDate) {
|
||||||
|
meta.vychodziaDate = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Found revision metadata for ${equipMeta.size} equipment items`);
|
||||||
|
for (const [hwId, meta] of equipMeta) {
|
||||||
|
if (meta.vychodziaDate) {
|
||||||
|
console.log(` hwId ${hwId}: východzia=${meta.vychodziaDate}, types=[${[...meta.typeCodes].join(', ')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== EQUIPMENT FROM OLD DB =====
|
||||||
|
console.log('Creating equipment from old DB...');
|
||||||
|
|
||||||
|
// Fetch equipment type IDs
|
||||||
|
const equipTypes = await prisma.equipmentType.findMany();
|
||||||
|
const equipTypeMap: Record<string, string> = {};
|
||||||
|
for (const t of equipTypes) {
|
||||||
|
equipTypeMap[t.code] = t.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipmentIdMap: Record<number, string> = {}; // hwId -> new ID
|
||||||
|
|
||||||
|
for (const e of EQUIPMENT) {
|
||||||
|
const typeId = equipTypeMap[e.typeCode] || equipTypeMap['OTHER'];
|
||||||
|
const customerId = customerMap[e.companyId] || undefined;
|
||||||
|
const meta = equipMeta.get(e.hwId);
|
||||||
|
|
||||||
|
const equip = await prisma.equipment.create({
|
||||||
|
data: {
|
||||||
|
name: e.name,
|
||||||
|
typeId,
|
||||||
|
brand: e.brand || undefined,
|
||||||
|
customerId,
|
||||||
|
address: e.location || '-',
|
||||||
|
location: e.location || undefined,
|
||||||
|
partNumber: e.partNumber && !['-', ' ', '', '0'].includes(e.partNumber.trim()) ? e.partNumber : undefined,
|
||||||
|
serialNumber: e.serialNumber && !['-', ' ', '', '0', '.', '..', '...', ':', ',', '.,', ',.', ',,', '.-', '---', '-----', '-,'].includes(e.serialNumber.trim()) ? e.serialNumber : undefined,
|
||||||
|
description: e.description && !['-', ' ', '', 'ok'].includes(e.description.trim()) ? e.description : undefined,
|
||||||
|
installDate: e.installDate ? new Date(e.installDate) : undefined,
|
||||||
|
revisionCycleStart: meta?.vychodziaDate ? new Date(meta.vychodziaDate) : undefined,
|
||||||
|
createdById: rootUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
equipmentIdMap[e.hwId] = equip.id;
|
||||||
|
}
|
||||||
|
console.log(` Created ${Object.keys(equipmentIdMap).length} equipment items`);
|
||||||
|
|
||||||
|
// ===== EQUIPMENT REVISION SCHEDULES =====
|
||||||
|
// Pre každé zariadenie vytvoriť záznamy o tom, aké typy revízií má priradené
|
||||||
|
console.log('Creating equipment revision schedules...');
|
||||||
|
|
||||||
|
const revisionTypes = await prisma.revisionType.findMany();
|
||||||
|
const revTypeMap: Record<string, string> = {};
|
||||||
|
for (const t of revisionTypes) {
|
||||||
|
revTypeMap[t.code] = t.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scheduleCount = 0;
|
||||||
|
for (const [hwId, meta] of equipMeta) {
|
||||||
|
const equipId = equipmentIdMap[hwId];
|
||||||
|
if (!equipId) continue;
|
||||||
|
|
||||||
|
for (const typeCode of meta.typeCodes) {
|
||||||
|
const revTypeId = revTypeMap[typeCode];
|
||||||
|
if (!revTypeId) continue;
|
||||||
|
|
||||||
|
await prisma.equipmentRevisionSchedule.create({
|
||||||
|
data: {
|
||||||
|
equipmentId: equipId,
|
||||||
|
revisionTypeId: revTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
scheduleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` Created ${scheduleCount} equipment revision schedules`);
|
||||||
|
|
||||||
|
// ===== REVISION RECORDS FROM OLD DB =====
|
||||||
|
console.log('Creating revision records from old DB...');
|
||||||
|
|
||||||
|
let createdRevisions = 0;
|
||||||
|
let skippedRevisions = 0;
|
||||||
|
|
||||||
|
// Deduplicate: same equipment + type + date = duplicate in old data
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const uniqueRevisions = SERVICE_RECORDS
|
||||||
|
.map(([auditId, date, nextDate, _userEmail, note]) => {
|
||||||
|
const audit = AUDIT_MAP[auditId];
|
||||||
|
if (!audit) return null;
|
||||||
|
|
||||||
|
const equipId = equipmentIdMap[audit.equipmentHwId];
|
||||||
|
if (!equipId) return null;
|
||||||
|
|
||||||
|
// Klasifikovať typ revízie podľa poznámky (nie len podľa audit periodicity)
|
||||||
|
const typeCode = classifyNote(note, audit.periodicity);
|
||||||
|
const typeId = revTypeMap[typeCode];
|
||||||
|
if (!typeId) return null;
|
||||||
|
|
||||||
|
// Deduplicate by equipment + type + date
|
||||||
|
const key = `${equipId}-${typeId}-${date}`;
|
||||||
|
if (seen.has(key)) return null;
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
return {
|
||||||
|
equipmentId: equipId,
|
||||||
|
typeId,
|
||||||
|
performedDate: new Date(date),
|
||||||
|
nextDueDate: nextDate ? new Date(nextDate) : null,
|
||||||
|
performedById: defaultUserId,
|
||||||
|
notes: note && note.trim() ? note.trim() : undefined,
|
||||||
|
result: 'OK',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||||
|
|
||||||
|
// Process in batches
|
||||||
|
const BATCH_SIZE = 50;
|
||||||
|
for (let i = 0; i < uniqueRevisions.length; i += BATCH_SIZE) {
|
||||||
|
const batch = uniqueRevisions.slice(i, i + BATCH_SIZE);
|
||||||
|
await prisma.revision.createMany({ data: batch });
|
||||||
|
createdRevisions += batch.length;
|
||||||
|
}
|
||||||
|
skippedRevisions = SERVICE_RECORDS.length - uniqueRevisions.length;
|
||||||
|
console.log(` Created ${createdRevisions} revisions (${skippedRevisions} duplicates skipped)`);
|
||||||
|
|
||||||
|
// ===== DEMO TASKS =====
|
||||||
|
console.log('Creating demo tasks...');
|
||||||
|
|
||||||
|
const statuses = await prisma.taskStatus.findMany();
|
||||||
|
const priorities = await prisma.priority.findMany();
|
||||||
|
|
||||||
|
const statusByCode = (code: string) => statuses.find(s => s.code === code)!.id;
|
||||||
|
const priorityByCode = (code: string) => priorities.find(p => p.code === code)!.id;
|
||||||
|
|
||||||
|
const demoTasks = [
|
||||||
|
// --- Admin zadáva úlohy pre userov ---
|
||||||
|
{
|
||||||
|
title: 'Objednať UTP káble Cat6a (100m)',
|
||||||
|
description: 'Objednať 3 balenia UTP káblov Cat6a po 100m pre nový serverový rack. Dodávateľ: Senetic alebo TS Bohemia.',
|
||||||
|
statusCode: 'NEW',
|
||||||
|
priorityCode: 'MEDIUM',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [user1.id],
|
||||||
|
deadline: new Date('2026-03-10'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Výmena UPS batérií v racku R3',
|
||||||
|
description: 'APC Smart-UPS 1500VA - batérie sú po záruke, hlási "Replace Battery". Objednať RBC7 a vymeniť.',
|
||||||
|
statusCode: 'NEW',
|
||||||
|
priorityCode: 'URGENT',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [user2.id],
|
||||||
|
deadline: new Date('2026-02-28'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Inštalácia kamerového systému - vstupná hala',
|
||||||
|
description: 'Namontovať 2x Hikvision DS-2CD2143G2 + nastaviť NVR záznam na 30 dní. Kabeláž už je pripravená.',
|
||||||
|
statusCode: 'IN_PROGRESS',
|
||||||
|
priorityCode: 'MEDIUM',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [user3.id],
|
||||||
|
deadline: new Date('2026-03-15'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Oprava Wi-Fi pokrytia v zasadačke B2',
|
||||||
|
description: 'Zákazník hlási slabý signál. Skontrolovať site survey, prípadne pridať ďalší AP alebo presunúť existujúci.',
|
||||||
|
statusCode: 'NEW',
|
||||||
|
priorityCode: 'HIGH',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [user1.id, user2.id],
|
||||||
|
deadline: new Date('2026-03-08'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Objednať toner do tlačiarne HP LaserJet Pro',
|
||||||
|
description: 'Kancelária 3. poschodie - toner CF259X je takmer prázdny. Objednať 2 kusy na sklad.',
|
||||||
|
statusCode: 'COMPLETED',
|
||||||
|
priorityCode: 'LOW',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [user1.id],
|
||||||
|
},
|
||||||
|
// --- Root zadáva úlohy pre userov a admina ---
|
||||||
|
{
|
||||||
|
title: 'Nastaviť switch Cisco SG350 v serverovni',
|
||||||
|
description: 'Konfigurácia VLAN 10, 20, 30. Trunk port na uplink. Nastaviť SNMP monitoring a port security na access portoch.',
|
||||||
|
statusCode: 'IN_PROGRESS',
|
||||||
|
priorityCode: 'HIGH',
|
||||||
|
createdById: demoRoot.id,
|
||||||
|
assignees: [demoAdmin.id, user1.id],
|
||||||
|
deadline: new Date('2026-03-05'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Nastaviť VPN tunel medzi pobočkami',
|
||||||
|
description: 'IPSec site-to-site VPN medzi Bratislava a Košice. MikroTik RB4011 na oboch stranách. Zdieľaný subnet 10.10.0.0/24.',
|
||||||
|
statusCode: 'IN_PROGRESS',
|
||||||
|
priorityCode: 'HIGH',
|
||||||
|
createdById: demoRoot.id,
|
||||||
|
assignees: [user2.id, user3.id],
|
||||||
|
deadline: new Date('2026-03-01'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Migrácia mailboxov na Microsoft 365',
|
||||||
|
description: 'Presunúť 25 mailboxov z on-prem Exchange na M365. Pripraviť migračný plán, otestovať na 3 pilotných užívateľoch.',
|
||||||
|
statusCode: 'NEW',
|
||||||
|
priorityCode: 'MEDIUM',
|
||||||
|
createdById: demoRoot.id,
|
||||||
|
assignees: [demoAdmin.id, user1.id, user2.id],
|
||||||
|
deadline: new Date('2026-04-01'),
|
||||||
|
},
|
||||||
|
// --- Úlohy kde admin robí sám ---
|
||||||
|
{
|
||||||
|
title: 'Zálohovanie konfigurácie sieťových zariadení',
|
||||||
|
description: 'Exportovať running-config zo všetkých switchov a routerov. Uložiť do Git repozitára na NAS.',
|
||||||
|
statusCode: 'COMPLETED',
|
||||||
|
priorityCode: 'MEDIUM',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [demoAdmin.id],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Aktualizovať firmware na AP Ubiquiti U6-Pro',
|
||||||
|
description: 'Nová verzia firmware opravuje bug s roamingom klientov. Aktualizovať všetkých 8 AP cez UniFi Controller.',
|
||||||
|
statusCode: 'REVIEW',
|
||||||
|
priorityCode: 'LOW',
|
||||||
|
createdById: demoAdmin.id,
|
||||||
|
assignees: [demoAdmin.id, user3.id],
|
||||||
|
},
|
||||||
|
// --- Root + admin spoločne ---
|
||||||
|
{
|
||||||
|
title: 'Audit sieťovej infraštruktúry',
|
||||||
|
description: 'Kompletný audit všetkých aktívnych prvkov, kontrola firmware verzií, kontrola prístupových práv na zariadeniach.',
|
||||||
|
statusCode: 'NEW',
|
||||||
|
priorityCode: 'MEDIUM',
|
||||||
|
createdById: demoRoot.id,
|
||||||
|
assignees: [demoAdmin.id],
|
||||||
|
deadline: new Date('2026-03-20'),
|
||||||
|
},
|
||||||
|
// --- User si vytvára vlastnú úlohu ---
|
||||||
|
{
|
||||||
|
title: 'Zdokumentovať zapojenie patch panelov v racku R1',
|
||||||
|
description: 'Vytvoriť schému zapojenia patch panelov a označiť káble podľa štandardu TIA-606.',
|
||||||
|
statusCode: 'IN_PROGRESS',
|
||||||
|
priorityCode: 'LOW',
|
||||||
|
createdById: user2.id,
|
||||||
|
assignees: [user2.id],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of demoTasks) {
|
||||||
|
const task = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
title: t.title,
|
||||||
|
description: t.description,
|
||||||
|
statusId: statusByCode(t.statusCode),
|
||||||
|
priorityId: priorityByCode(t.priorityCode),
|
||||||
|
createdById: t.createdById,
|
||||||
|
deadline: t.deadline,
|
||||||
|
completedAt: t.statusCode === 'COMPLETED' ? new Date() : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (t.assignees.length > 0) {
|
||||||
|
await prisma.taskAssignee.createMany({
|
||||||
|
data: t.assignees.map(userId => ({
|
||||||
|
taskId: task.id,
|
||||||
|
userId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` Created ${demoTasks.length} demo tasks`);
|
||||||
|
|
||||||
console.log('Seeding completed!');
|
console.log('Seeding completed!');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,13 +81,15 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis
|
|||||||
|
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
|
|
||||||
const [myTasks, myProjects, recentRMAs] = await Promise.all([
|
const myTasksWhere = {
|
||||||
// Tasks assigned to me that are not completed
|
assignees: { some: { userId } },
|
||||||
|
status: { isFinal: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const [myTasks, myTasksTotal, myProjects, recentRMAs] = await Promise.all([
|
||||||
|
// Tasks assigned to me that are not completed (top 10 by priority)
|
||||||
prisma.task.findMany({
|
prisma.task.findMany({
|
||||||
where: {
|
where: myTasksWhere,
|
||||||
assignees: { some: { userId } },
|
|
||||||
status: { isFinal: false },
|
|
||||||
},
|
|
||||||
include: {
|
include: {
|
||||||
status: true,
|
status: true,
|
||||||
priority: true,
|
priority: true,
|
||||||
@@ -99,6 +101,9 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis
|
|||||||
take: 10,
|
take: 10,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Total count of my active tasks (without limit)
|
||||||
|
prisma.task.count({ where: myTasksWhere }),
|
||||||
|
|
||||||
// My active projects
|
// My active projects
|
||||||
prisma.project.findMany({
|
prisma.project.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -133,6 +138,7 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis
|
|||||||
|
|
||||||
successResponse(res, {
|
successResponse(res, {
|
||||||
myTasks,
|
myTasks,
|
||||||
|
myTasksTotal,
|
||||||
myProjects,
|
myProjects,
|
||||||
recentRMAs,
|
recentRMAs,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import prisma from '../config/database';
|
|||||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||||
import { AuthRequest } from '../middleware/auth.middleware';
|
import { AuthRequest } from '../middleware/auth.middleware';
|
||||||
import { movePendingFilesToEntity } from './upload.controller';
|
import { movePendingFilesToEntity } from './upload.controller';
|
||||||
|
import {
|
||||||
|
calculateNextDueDateFromInstall,
|
||||||
|
detectSkippedCycles,
|
||||||
|
getCoveringTypeIds,
|
||||||
|
calculateReminderDate,
|
||||||
|
MS_PER_DAY,
|
||||||
|
} from '../utils/revisionSchedule';
|
||||||
|
|
||||||
export const getEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
export const getEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -14,19 +21,23 @@ export const getEquipment = async (req: AuthRequest, res: Response): Promise<voi
|
|||||||
const typeId = getQueryString(req, 'typeId');
|
const typeId = getQueryString(req, 'typeId');
|
||||||
const customerId = getQueryString(req, 'customerId');
|
const customerId = getQueryString(req, 'customerId');
|
||||||
|
|
||||||
const where = {
|
const where: Record<string, unknown> = {
|
||||||
...(active !== undefined && { active: active === 'true' }),
|
...(active !== undefined && { active: active === 'true' }),
|
||||||
...(typeId && { typeId }),
|
...(typeId && { typeId }),
|
||||||
...(customerId && { customerId }),
|
...(customerId && { customerId }),
|
||||||
...(search && {
|
|
||||||
OR: [
|
|
||||||
{ name: { contains: search, mode: 'insensitive' as const } },
|
|
||||||
{ serialNumber: { contains: search, mode: 'insensitive' as const } },
|
|
||||||
{ address: { contains: search, mode: 'insensitive' as const } },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Vyhľadávanie bez diakritiky pomocou unaccent
|
||||||
|
if (search) {
|
||||||
|
const matchingIds = await prisma.$queryRaw<Array<{ id: string }>>`
|
||||||
|
SELECT id FROM "Equipment"
|
||||||
|
WHERE unaccent("name") ILIKE unaccent(${`%${search}%`})
|
||||||
|
OR unaccent(COALESCE("serialNumber", '')) ILIKE unaccent(${`%${search}%`})
|
||||||
|
OR unaccent("address") ILIKE unaccent(${`%${search}%`})
|
||||||
|
`;
|
||||||
|
where.id = { in: matchingIds.map(r => r.id) };
|
||||||
|
}
|
||||||
|
|
||||||
const [equipment, total] = await Promise.all([
|
const [equipment, total] = await Promise.all([
|
||||||
prisma.equipment.findMany({
|
prisma.equipment.findMany({
|
||||||
where,
|
where,
|
||||||
@@ -37,6 +48,7 @@ export const getEquipment = async (req: AuthRequest, res: Response): Promise<voi
|
|||||||
type: true,
|
type: true,
|
||||||
customer: { select: { id: true, name: true } },
|
customer: { select: { id: true, name: true } },
|
||||||
createdBy: { select: { id: true, name: true } },
|
createdBy: { select: { id: true, name: true } },
|
||||||
|
revisionSchedules: { include: { revisionType: true } },
|
||||||
_count: { select: { revisions: true } },
|
_count: { select: { revisions: true } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -62,7 +74,6 @@ export const getEquipmentById = async (req: AuthRequest, res: Response): Promise
|
|||||||
createdBy: { select: { id: true, name: true, email: true } },
|
createdBy: { select: { id: true, name: true, email: true } },
|
||||||
revisions: {
|
revisions: {
|
||||||
orderBy: { performedDate: 'desc' },
|
orderBy: { performedDate: 'desc' },
|
||||||
take: 5,
|
|
||||||
include: {
|
include: {
|
||||||
type: true,
|
type: true,
|
||||||
performedBy: { select: { id: true, name: true } },
|
performedBy: { select: { id: true, name: true } },
|
||||||
@@ -71,6 +82,10 @@ export const getEquipmentById = async (req: AuthRequest, res: Response): Promise
|
|||||||
attachments: {
|
attachments: {
|
||||||
orderBy: { uploadedAt: 'desc' },
|
orderBy: { uploadedAt: 'desc' },
|
||||||
},
|
},
|
||||||
|
revisionSchedules: {
|
||||||
|
include: { revisionType: true },
|
||||||
|
orderBy: { revisionType: { intervalDays: 'asc' } },
|
||||||
|
},
|
||||||
tags: { include: { tag: true } },
|
tags: { include: { tag: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -89,6 +104,8 @@ export const getEquipmentById = async (req: AuthRequest, res: Response): Promise
|
|||||||
|
|
||||||
export const createEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
export const createEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
const revisionTypeIds: string[] = req.body.revisionTypeIds || [];
|
||||||
|
|
||||||
const equipment = await prisma.equipment.create({
|
const equipment = await prisma.equipment.create({
|
||||||
data: {
|
data: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
@@ -101,15 +118,22 @@ export const createEquipment = async (req: AuthRequest, res: Response): Promise<
|
|||||||
partNumber: req.body.partNumber,
|
partNumber: req.body.partNumber,
|
||||||
serialNumber: req.body.serialNumber,
|
serialNumber: req.body.serialNumber,
|
||||||
installDate: req.body.installDate ? new Date(req.body.installDate) : null,
|
installDate: req.body.installDate ? new Date(req.body.installDate) : null,
|
||||||
|
revisionCycleStart: req.body.revisionCycleStart ? new Date(req.body.revisionCycleStart) : null,
|
||||||
warrantyEnd: req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null,
|
warrantyEnd: req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null,
|
||||||
warrantyStatus: req.body.warrantyStatus,
|
warrantyStatus: req.body.warrantyStatus,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
notes: req.body.notes,
|
notes: req.body.notes,
|
||||||
createdById: req.user!.userId,
|
createdById: req.user!.userId,
|
||||||
|
...(revisionTypeIds.length > 0 && {
|
||||||
|
revisionSchedules: {
|
||||||
|
create: revisionTypeIds.map((rtId: string) => ({ revisionTypeId: rtId })),
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
type: true,
|
type: true,
|
||||||
customer: { select: { id: true, name: true } },
|
customer: { select: { id: true, name: true } },
|
||||||
|
revisionSchedules: { include: { revisionType: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,16 +180,34 @@ export const updateEquipment = async (req: AuthRequest, res: Response): Promise<
|
|||||||
if (req.body.installDate !== undefined) {
|
if (req.body.installDate !== undefined) {
|
||||||
updateData.installDate = req.body.installDate ? new Date(req.body.installDate) : null;
|
updateData.installDate = req.body.installDate ? new Date(req.body.installDate) : null;
|
||||||
}
|
}
|
||||||
|
if (req.body.revisionCycleStart !== undefined) {
|
||||||
|
updateData.revisionCycleStart = req.body.revisionCycleStart ? new Date(req.body.revisionCycleStart) : null;
|
||||||
|
}
|
||||||
if (req.body.warrantyEnd !== undefined) {
|
if (req.body.warrantyEnd !== undefined) {
|
||||||
updateData.warrantyEnd = req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null;
|
updateData.warrantyEnd = req.body.warrantyEnd ? new Date(req.body.warrantyEnd) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync revision schedules ak boli poskytnuté
|
||||||
|
if (req.body.revisionTypeIds !== undefined) {
|
||||||
|
const revisionTypeIds: string[] = req.body.revisionTypeIds || [];
|
||||||
|
await prisma.equipmentRevisionSchedule.deleteMany({ where: { equipmentId: id } });
|
||||||
|
if (revisionTypeIds.length > 0) {
|
||||||
|
await prisma.equipmentRevisionSchedule.createMany({
|
||||||
|
data: revisionTypeIds.map((rtId: string) => ({
|
||||||
|
equipmentId: id,
|
||||||
|
revisionTypeId: rtId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const equipment = await prisma.equipment.update({
|
const equipment = await prisma.equipment.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
include: {
|
include: {
|
||||||
type: true,
|
type: true,
|
||||||
customer: { select: { id: true, name: true } },
|
customer: { select: { id: true, name: true } },
|
||||||
|
revisionSchedules: { include: { revisionType: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,11 +290,37 @@ export const createEquipmentRevision = async (req: AuthRequest, res: Response):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const performedDate = new Date(req.body.performedDate);
|
const performedDate = new Date(req.body.performedDate);
|
||||||
const nextDueDate = new Date(performedDate);
|
let nextDueDate: Date;
|
||||||
nextDueDate.setDate(nextDueDate.getDate() + revisionType.intervalDays);
|
|
||||||
|
|
||||||
const reminderDate = new Date(nextDueDate);
|
const cycleAnchor = equipment.revisionCycleStart || equipment.installDate;
|
||||||
reminderDate.setDate(reminderDate.getDate() - revisionType.reminderDays);
|
|
||||||
|
if (req.body.nextDueDate) {
|
||||||
|
nextDueDate = new Date(req.body.nextDueDate);
|
||||||
|
} else if (revisionType.intervalDays > 0 && cycleAnchor) {
|
||||||
|
nextDueDate = calculateNextDueDateFromInstall(
|
||||||
|
cycleAnchor, revisionType.intervalDays, performedDate
|
||||||
|
);
|
||||||
|
} else if (revisionType.intervalDays > 0) {
|
||||||
|
nextDueDate = new Date(performedDate);
|
||||||
|
nextDueDate.setDate(nextDueDate.getDate() + revisionType.intervalDays);
|
||||||
|
} else {
|
||||||
|
nextDueDate = performedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminderDate = calculateReminderDate(nextDueDate, revisionType.reminderDays);
|
||||||
|
|
||||||
|
// Detekovať preskočené cykly - nájsť poslednú revíziu pre toto zariadenie
|
||||||
|
const lastRevision = await prisma.revision.findFirst({
|
||||||
|
where: { equipmentId: id },
|
||||||
|
orderBy: { performedDate: 'desc' },
|
||||||
|
select: { performedDate: true },
|
||||||
|
});
|
||||||
|
const skippedCycles = detectSkippedCycles(
|
||||||
|
cycleAnchor,
|
||||||
|
revisionType.intervalDays,
|
||||||
|
lastRevision?.performedDate || null,
|
||||||
|
performedDate
|
||||||
|
);
|
||||||
|
|
||||||
const revision = await prisma.revision.create({
|
const revision = await prisma.revision.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -279,7 +347,11 @@ export const createEquipmentRevision = async (req: AuthRequest, res: Response):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
successResponse(res, revision, 'Revízia bola vytvorená.', 201);
|
const message = skippedCycles.length > 0
|
||||||
|
? `Revízia bola vytvorená. Upozornenie: ${skippedCycles.length} cyklus(y) boli preskočené!`
|
||||||
|
: 'Revízia bola vytvorená.';
|
||||||
|
|
||||||
|
successResponse(res, { ...revision, skippedCycles }, message, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating revision:', error);
|
console.error('Error creating revision:', error);
|
||||||
errorResponse(res, 'Chyba pri vytváraní revízie.', 500);
|
errorResponse(res, 'Chyba pri vytváraní revízie.', 500);
|
||||||
@@ -317,3 +389,197 @@ export const getEquipmentReminders = async (req: AuthRequest, res: Response): Pr
|
|||||||
errorResponse(res, 'Chyba pri načítaní upomienok.', 500);
|
errorResponse(res, 'Chyba pri načítaní upomienok.', 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vráti nadchádzajúci revízny plán pre zariadenie.
|
||||||
|
* Pre každý priradený typ revízie vypočíta nasledujúce cyklové body s číslom cyklu.
|
||||||
|
* Ak sa cykly rôznych typov kryjú (napr. 4. štvrťročná = ročná), označí dlhší typ ako dominantný.
|
||||||
|
*/
|
||||||
|
export const getEquipmentSchedule = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const id = getParam(req, 'id');
|
||||||
|
|
||||||
|
const equipment = await prisma.equipment.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
revisionSchedules: {
|
||||||
|
include: { revisionType: true },
|
||||||
|
orderBy: { revisionType: { intervalDays: 'asc' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!equipment) {
|
||||||
|
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycleAnchor = equipment.revisionCycleStart || equipment.installDate;
|
||||||
|
if (!cycleAnchor || equipment.revisionSchedules.length === 0) {
|
||||||
|
successResponse(res, { schedules: [], upcomingDates: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const anchorTime = cycleAnchor.getTime();
|
||||||
|
const lookAheadDays = parseQueryInt(req.query.days, 365);
|
||||||
|
const lookAheadEnd = new Date(now);
|
||||||
|
lookAheadEnd.setDate(lookAheadEnd.getDate() + lookAheadDays);
|
||||||
|
|
||||||
|
// Pre každý revisionType nájsť posledný vykonaný a vypočítať nasledujúce cykly
|
||||||
|
interface CyclePoint { date: Date; cycleNumber: number }
|
||||||
|
const scheduleItems: Array<{
|
||||||
|
revisionType: typeof equipment.revisionSchedules[0]['revisionType'];
|
||||||
|
lastPerformed: Date | null;
|
||||||
|
nextDueDate: Date | null;
|
||||||
|
upcomingCycles: CyclePoint[];
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const allTypeIds = equipment.revisionSchedules.map(s => ({
|
||||||
|
id: s.revisionType.id,
|
||||||
|
intervalDays: s.revisionType.intervalDays,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const schedule of equipment.revisionSchedules) {
|
||||||
|
const rt = schedule.revisionType;
|
||||||
|
|
||||||
|
// Dlhší typ pokrýva kratší (ročná pokrýva štvrťročnú)
|
||||||
|
const coveringIds = getCoveringTypeIds(rt.intervalDays, allTypeIds);
|
||||||
|
|
||||||
|
const lastRevision = await prisma.revision.findFirst({
|
||||||
|
where: { equipmentId: id, typeId: { in: coveringIds } },
|
||||||
|
orderBy: { performedDate: 'desc' },
|
||||||
|
select: { performedDate: true, nextDueDate: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepočítať nextDueDate z cyklového anchoru (nie z DB, kde staré dáta majú chybné hodnoty)
|
||||||
|
let computedNextDueDate: Date | null = null;
|
||||||
|
if (lastRevision && rt.intervalDays > 0) {
|
||||||
|
computedNextDueDate = calculateNextDueDateFromInstall(
|
||||||
|
new Date(anchorTime), rt.intervalDays, lastRevision.performedDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upcomingCycles: CyclePoint[] = [];
|
||||||
|
if (rt.intervalDays > 0) {
|
||||||
|
const intervalMs = rt.intervalDays * MS_PER_DAY;
|
||||||
|
const nowMs = now.getTime();
|
||||||
|
const endMs = lookAheadEnd.getTime();
|
||||||
|
|
||||||
|
// Priamy výpočet: anchor + n * interval (bez kumulatívneho driftu z setDate)
|
||||||
|
const startN = Math.max(1, Math.floor((nowMs - anchorTime) / intervalMs));
|
||||||
|
for (let n = startN; ; n++) {
|
||||||
|
const cycleDateMs = anchorTime + n * intervalMs;
|
||||||
|
if (cycleDateMs > endMs) break;
|
||||||
|
if (cycleDateMs > nowMs) {
|
||||||
|
upcomingCycles.push({ date: new Date(cycleDateMs), cycleNumber: n });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odstrániť cykly, ktoré sú už pokryté poslednou revíziou
|
||||||
|
if (computedNextDueDate && upcomingCycles.length > 0) {
|
||||||
|
const nextDueMs = computedNextDueDate.getTime();
|
||||||
|
const toleranceMs = MS_PER_DAY;
|
||||||
|
const firstValidIdx = upcomingCycles.findIndex(c => c.date.getTime() >= nextDueMs - toleranceMs);
|
||||||
|
if (firstValidIdx > 0) {
|
||||||
|
upcomingCycles.splice(0, firstValidIdx);
|
||||||
|
} else if (firstValidIdx === -1) {
|
||||||
|
upcomingCycles.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleItems.push({
|
||||||
|
revisionType: rt,
|
||||||
|
lastPerformed: lastRevision?.performedDate || null,
|
||||||
|
nextDueDate: computedNextDueDate || (upcomingCycles.length > 0 ? upcomingCycles[0].date : null),
|
||||||
|
upcomingCycles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zostaviť výsledný zoznam nadchádzajúcich dátumov.
|
||||||
|
// Použiť LEN cyklové body najkratšieho intervalu.
|
||||||
|
// Pozícia v ročnom cykle určí label (napr. "3. Štvrťročná", "Ročná").
|
||||||
|
// Toto eliminuje problém s driftom nezávislých cyklov (4×90≠365).
|
||||||
|
const sortedItems = [...scheduleItems].sort(
|
||||||
|
(a, b) => a.revisionType.intervalDays - b.revisionType.intervalDays
|
||||||
|
);
|
||||||
|
const shortestItem = sortedItems[0];
|
||||||
|
const longestItem = sortedItems[sortedItems.length - 1];
|
||||||
|
const hasMultipleTypes = scheduleItems.length >= 2
|
||||||
|
&& shortestItem.revisionType.id !== longestItem.revisionType.id;
|
||||||
|
|
||||||
|
const allDates: Array<{
|
||||||
|
date: Date;
|
||||||
|
revisionTypes: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color?: string | null;
|
||||||
|
intervalDays: number;
|
||||||
|
cycleNumber: number;
|
||||||
|
}>;
|
||||||
|
label: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (hasMultipleTypes) {
|
||||||
|
const shortRT = shortestItem.revisionType;
|
||||||
|
const longRT = longestItem.revisionType;
|
||||||
|
const stepsPerLongCycle = Math.round(longRT.intervalDays / shortRT.intervalDays);
|
||||||
|
|
||||||
|
for (const cycle of shortestItem.upcomingCycles) {
|
||||||
|
const dateMs = cycle.date.getTime();
|
||||||
|
const daysSinceAnchor = Math.round((dateMs - anchorTime) / MS_PER_DAY);
|
||||||
|
|
||||||
|
const prevLongDay = Math.floor(daysSinceAnchor / longRT.intervalDays) * longRT.intervalDays;
|
||||||
|
const daysInCycle = daysSinceAnchor - prevLongDay;
|
||||||
|
const pos = Math.round(daysInCycle / shortRT.intervalDays);
|
||||||
|
|
||||||
|
const isLongTypePosition = pos <= 0 || pos >= stepsPerLongCycle;
|
||||||
|
const activeType = isLongTypePosition ? longRT : shortRT;
|
||||||
|
const label = isLongTypePosition
|
||||||
|
? longRT.name
|
||||||
|
: `${pos}. ${shortRT.name}`;
|
||||||
|
|
||||||
|
allDates.push({
|
||||||
|
date: cycle.date,
|
||||||
|
revisionTypes: [{
|
||||||
|
id: activeType.id,
|
||||||
|
name: activeType.name,
|
||||||
|
color: activeType.color,
|
||||||
|
intervalDays: activeType.intervalDays,
|
||||||
|
cycleNumber: cycle.cycleNumber,
|
||||||
|
}],
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const cycle of shortestItem.upcomingCycles) {
|
||||||
|
allDates.push({
|
||||||
|
date: cycle.date,
|
||||||
|
revisionTypes: [{
|
||||||
|
id: shortestItem.revisionType.id,
|
||||||
|
name: shortestItem.revisionType.name,
|
||||||
|
color: shortestItem.revisionType.color,
|
||||||
|
intervalDays: shortestItem.revisionType.intervalDays,
|
||||||
|
cycleNumber: cycle.cycleNumber,
|
||||||
|
}],
|
||||||
|
label: shortestItem.revisionType.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allDates.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||||
|
|
||||||
|
successResponse(res, {
|
||||||
|
cycleAnchor,
|
||||||
|
schedules: scheduleItems.map((s) => ({
|
||||||
|
...s,
|
||||||
|
upcomingCycles: s.upcomingCycles.map((c) => ({ date: c.date, cycleNumber: c.cycleNumber })),
|
||||||
|
})),
|
||||||
|
upcomingDates: allDates,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching equipment schedule:', error);
|
||||||
|
errorResponse(res, 'Chyba pri načítaní revízneho plánu.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
615
backend/src/controllers/revisions.controller.ts
Normal file
615
backend/src/controllers/revisions.controller.ts
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import prisma from '../config/database';
|
||||||
|
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||||
|
import { AuthRequest } from '../middleware/auth.middleware';
|
||||||
|
import {
|
||||||
|
calculateNextDueDateFromInstall,
|
||||||
|
detectSkippedCycles,
|
||||||
|
computeFirstUpcomingCycleDate,
|
||||||
|
getCoveringTypeIds,
|
||||||
|
calculateReminderDate,
|
||||||
|
computeNextDueAndReminder,
|
||||||
|
mergeOverlappingItems,
|
||||||
|
MS_PER_DAY,
|
||||||
|
} from '../utils/revisionSchedule';
|
||||||
|
|
||||||
|
export const getRevisions = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const page = parseQueryInt(req.query.page, 1);
|
||||||
|
const limit = parseQueryInt(req.query.limit, 20);
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const search = getQueryString(req, 'search');
|
||||||
|
const equipmentId = getQueryString(req, 'equipmentId');
|
||||||
|
const typeId = getQueryString(req, 'typeId');
|
||||||
|
const customerId = getQueryString(req, 'customerId');
|
||||||
|
const dueSoon = getQueryString(req, 'dueSoon');
|
||||||
|
const overdue = getQueryString(req, 'overdue');
|
||||||
|
|
||||||
|
const status = getQueryString(req, 'status');
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = {
|
||||||
|
...(status && { status }),
|
||||||
|
...(equipmentId && { equipmentId }),
|
||||||
|
...(typeId && { typeId }),
|
||||||
|
...(customerId && { equipment: { customerId } }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vyhľadávanie bez diakritiky pomocou unaccent
|
||||||
|
if (search) {
|
||||||
|
const matchingIds = await prisma.$queryRaw<Array<{ id: string }>>`
|
||||||
|
SELECT r.id FROM "Revision" r
|
||||||
|
LEFT JOIN "Equipment" e ON r."equipmentId" = e.id
|
||||||
|
WHERE unaccent(COALESCE(r."notes", '')) ILIKE unaccent(${`%${search}%`})
|
||||||
|
OR unaccent(COALESCE(r."findings", '')) ILIKE unaccent(${`%${search}%`})
|
||||||
|
OR unaccent(e."name") ILIKE unaccent(${`%${search}%`})
|
||||||
|
OR unaccent(e."address") ILIKE unaccent(${`%${search}%`})
|
||||||
|
`;
|
||||||
|
where.id = { in: matchingIds.map(r => r.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// For overdue/dueSoon filters, only consider latest revision per equipment
|
||||||
|
if (dueSoon || overdue === 'true') {
|
||||||
|
const latestForFilter = await prisma.$queryRaw<Array<{ id: string }>>`
|
||||||
|
SELECT DISTINCT ON ("equipmentId") id
|
||||||
|
FROM "Revision"
|
||||||
|
ORDER BY "equipmentId", "performedDate" DESC
|
||||||
|
`;
|
||||||
|
const latestFilterIds = latestForFilter.map(r => r.id);
|
||||||
|
|
||||||
|
if (overdue === 'true') {
|
||||||
|
where.id = { in: latestFilterIds };
|
||||||
|
where.nextDueDate = { lt: now };
|
||||||
|
} else if (dueSoon) {
|
||||||
|
const days = parseInt(dueSoon, 10) || 30;
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setDate(futureDate.getDate() + days);
|
||||||
|
where.id = { in: latestFilterIds };
|
||||||
|
where.nextDueDate = { gte: now, lte: futureDate };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IDs of latest revision per equipment
|
||||||
|
const latestIds = await prisma.$queryRaw<Array<{ id: string }>>`
|
||||||
|
SELECT DISTINCT ON ("equipmentId") id
|
||||||
|
FROM "Revision"
|
||||||
|
ORDER BY "equipmentId", "performedDate" DESC
|
||||||
|
`;
|
||||||
|
const latestIdSet = new Set(latestIds.map(r => r.id));
|
||||||
|
|
||||||
|
const [revisions, total] = await Promise.all([
|
||||||
|
prisma.revision.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { performedDate: 'desc' },
|
||||||
|
include: {
|
||||||
|
equipment: {
|
||||||
|
include: {
|
||||||
|
type: true,
|
||||||
|
customer: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: true,
|
||||||
|
performedBy: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.revision.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revisionsWithLatest = revisions.map(r => ({
|
||||||
|
...r,
|
||||||
|
isLatest: latestIdSet.has(r.id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
paginatedResponse(res, revisionsWithLatest, total, page, limit);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching revisions:', error);
|
||||||
|
errorResponse(res, 'Chyba pri načítaní revízií.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const id = getParam(req, 'id');
|
||||||
|
|
||||||
|
const revision = await prisma.revision.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
equipment: {
|
||||||
|
include: {
|
||||||
|
type: true,
|
||||||
|
customer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: true,
|
||||||
|
performedBy: { select: { id: true, name: true, email: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!revision) {
|
||||||
|
errorResponse(res, 'Revízia nebola nájdená.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
successResponse(res, revision);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching revision:', error);
|
||||||
|
errorResponse(res, 'Chyba pri načítaní revízie.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const equipment = await prisma.equipment.findUnique({ where: { id: req.body.equipmentId } });
|
||||||
|
if (!equipment) {
|
||||||
|
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisionType = await prisma.revisionType.findUnique({
|
||||||
|
where: { id: req.body.typeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!revisionType) {
|
||||||
|
errorResponse(res, 'Typ revízie nebol nájdený.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const performedDate = new Date(req.body.performedDate);
|
||||||
|
const cycleAnchor = equipment.revisionCycleStart || equipment.installDate;
|
||||||
|
|
||||||
|
const { nextDueDate, reminderDate } = await computeNextDueAndReminder({
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
cycleAnchor,
|
||||||
|
performedDate,
|
||||||
|
typeIntervalDays: revisionType.intervalDays,
|
||||||
|
typeReminderDays: revisionType.reminderDays,
|
||||||
|
manualNextDueDate: req.body.nextDueDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detekovať preskočené cykly
|
||||||
|
const lastRevision = await prisma.revision.findFirst({
|
||||||
|
where: { equipmentId: req.body.equipmentId },
|
||||||
|
orderBy: { performedDate: 'desc' },
|
||||||
|
select: { performedDate: true },
|
||||||
|
});
|
||||||
|
const skippedCycles = detectSkippedCycles(
|
||||||
|
cycleAnchor,
|
||||||
|
revisionType.intervalDays,
|
||||||
|
lastRevision?.performedDate || null,
|
||||||
|
performedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
const revision = await prisma.revision.create({
|
||||||
|
data: {
|
||||||
|
equipmentId: req.body.equipmentId,
|
||||||
|
typeId: req.body.typeId,
|
||||||
|
status: 'performed',
|
||||||
|
performedDate,
|
||||||
|
nextDueDate,
|
||||||
|
reminderDate,
|
||||||
|
performedById: req.user!.userId,
|
||||||
|
findings: req.body.findings,
|
||||||
|
result: req.body.result,
|
||||||
|
notes: req.body.notes,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
equipment: {
|
||||||
|
include: {
|
||||||
|
type: true,
|
||||||
|
customer: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: true,
|
||||||
|
performedBy: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.logActivity) {
|
||||||
|
await req.logActivity('CREATE', 'Revision', revision.id, {
|
||||||
|
equipmentId: req.body.equipmentId,
|
||||||
|
type: revisionType.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = skippedCycles.length > 0
|
||||||
|
? `Revízia bola vytvorená. Upozornenie: ${skippedCycles.length} cyklus(y) boli preskočené!`
|
||||||
|
: 'Revízia bola vytvorená.';
|
||||||
|
|
||||||
|
successResponse(res, { ...revision, skippedCycles }, message, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating revision:', error);
|
||||||
|
errorResponse(res, 'Chyba pri vytváraní revízie.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const id = getParam(req, 'id');
|
||||||
|
|
||||||
|
const existing = await prisma.revision.findUnique({ where: { id } });
|
||||||
|
if (!existing) {
|
||||||
|
errorResponse(res, 'Revízia nebola nájdená.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (req.body.typeId) {
|
||||||
|
updateData.typeId = req.body.typeId;
|
||||||
|
}
|
||||||
|
if (req.body.performedDate) {
|
||||||
|
updateData.performedDate = new Date(req.body.performedDate);
|
||||||
|
}
|
||||||
|
if (req.body.nextDueDate !== undefined) {
|
||||||
|
updateData.nextDueDate = req.body.nextDueDate ? new Date(req.body.nextDueDate) : null;
|
||||||
|
}
|
||||||
|
if (req.body.findings !== undefined) updateData.findings = req.body.findings;
|
||||||
|
if (req.body.result !== undefined) updateData.result = req.body.result;
|
||||||
|
if (req.body.notes !== undefined) updateData.notes = req.body.notes;
|
||||||
|
|
||||||
|
// Ak sa zmenil performedDate alebo typeId a nextDueDate nebol manuálne zadaný,
|
||||||
|
// prepočítať nextDueDate na základe cyklu od inštalácie (s najkratším intervalom)
|
||||||
|
if ((updateData.performedDate || updateData.typeId) && req.body.nextDueDate === undefined) {
|
||||||
|
const typeId = (updateData.typeId as string) || existing.typeId;
|
||||||
|
const revType = await prisma.revisionType.findUnique({ where: { id: typeId } });
|
||||||
|
if (revType && revType.intervalDays > 0) {
|
||||||
|
const equipment = await prisma.equipment.findUnique({ where: { id: existing.equipmentId } });
|
||||||
|
const perfDate = (updateData.performedDate as Date) || existing.performedDate;
|
||||||
|
const cycleAnchorUpdate = equipment?.revisionCycleStart || equipment?.installDate;
|
||||||
|
|
||||||
|
const { nextDueDate, reminderDate } = await computeNextDueAndReminder({
|
||||||
|
equipmentId: existing.equipmentId,
|
||||||
|
cycleAnchor: cycleAnchorUpdate || null,
|
||||||
|
performedDate: perfDate,
|
||||||
|
typeIntervalDays: revType.intervalDays,
|
||||||
|
typeReminderDays: revType.reminderDays,
|
||||||
|
});
|
||||||
|
updateData.nextDueDate = nextDueDate;
|
||||||
|
updateData.reminderDate = reminderDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepočítať reminderDate ak sa zmenil len typeId (bez performedDate)
|
||||||
|
if (!updateData.reminderDate && updateData.typeId) {
|
||||||
|
const typeId = updateData.typeId as string;
|
||||||
|
const revType = await prisma.revisionType.findUnique({ where: { id: typeId } });
|
||||||
|
if (revType) {
|
||||||
|
const nextDate = (updateData.nextDueDate as Date) || existing.nextDueDate;
|
||||||
|
if (nextDate) {
|
||||||
|
updateData.reminderDate = calculateReminderDate(nextDate, revType.reminderDays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const revision = await prisma.revision.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
include: {
|
||||||
|
equipment: {
|
||||||
|
include: {
|
||||||
|
type: true,
|
||||||
|
customer: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: true,
|
||||||
|
performedBy: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.logActivity) {
|
||||||
|
await req.logActivity('UPDATE', 'Revision', id, updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
successResponse(res, revision, 'Revízia bola aktualizovaná.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating revision:', error);
|
||||||
|
errorResponse(res, 'Chyba pri aktualizácii revízie.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const id = getParam(req, 'id');
|
||||||
|
|
||||||
|
const revision = await prisma.revision.findUnique({ where: { id } });
|
||||||
|
if (!revision) {
|
||||||
|
errorResponse(res, 'Revízia nebola nájdená.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.revision.delete({ where: { id } });
|
||||||
|
|
||||||
|
if (req.logActivity) {
|
||||||
|
await req.logActivity('DELETE', 'Revision', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
successResponse(res, null, 'Revízia bola vymazaná.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting revision:', error);
|
||||||
|
errorResponse(res, 'Chyba pri mazaní revízie.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Načíta všetky equipment+type páry s ich nextDueDate.
|
||||||
|
* Spoločná logika pre schedule a stats.
|
||||||
|
*/
|
||||||
|
async function buildScheduleItems(filters?: {
|
||||||
|
typeId?: string;
|
||||||
|
customerId?: string;
|
||||||
|
search?: string;
|
||||||
|
}): Promise<Array<{
|
||||||
|
equipment: { id: string; name: string; address: string; customer: { id: string; name: string } | null };
|
||||||
|
revisionType: { id: string; name: string; color: string | null; intervalDays: number };
|
||||||
|
dueDate: Date;
|
||||||
|
lastPerformedDate: Date | null;
|
||||||
|
}>> {
|
||||||
|
const equipmentWhere: Record<string, unknown> = {
|
||||||
|
active: true,
|
||||||
|
revisionSchedules: { some: {} },
|
||||||
|
};
|
||||||
|
if (filters?.customerId) equipmentWhere.customerId = filters.customerId;
|
||||||
|
|
||||||
|
// Vyhľadávanie bez diakritiky pomocou unaccent
|
||||||
|
if (filters?.search) {
|
||||||
|
const matchingEqIds = await prisma.$queryRaw<Array<{ id: string }>>`
|
||||||
|
SELECT id FROM "Equipment"
|
||||||
|
WHERE unaccent("name") ILIKE unaccent(${`%${filters.search}%`})
|
||||||
|
OR unaccent("address") ILIKE unaccent(${`%${filters.search}%`})
|
||||||
|
`;
|
||||||
|
equipmentWhere.id = { in: matchingEqIds.map(r => r.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipment = await prisma.equipment.findMany({
|
||||||
|
where: equipmentWhere,
|
||||||
|
include: {
|
||||||
|
customer: { select: { id: true, name: true } },
|
||||||
|
revisionSchedules: {
|
||||||
|
include: { revisionType: true },
|
||||||
|
...(filters?.typeId ? { where: { revisionTypeId: filters.typeId } } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (equipment.length === 0) return [];
|
||||||
|
|
||||||
|
const equipmentIds = equipment.map(e => e.id);
|
||||||
|
|
||||||
|
// Jedným dotazom načítať posledné revízie pre všetky equipment+type páry
|
||||||
|
const latestRevisions = await prisma.$queryRaw<Array<{
|
||||||
|
equipment_id: string;
|
||||||
|
type_id: string;
|
||||||
|
next_due_date: Date | null;
|
||||||
|
performed_date: Date;
|
||||||
|
}>>`
|
||||||
|
SELECT DISTINCT ON ("equipmentId", "typeId")
|
||||||
|
"equipmentId" as equipment_id,
|
||||||
|
"typeId" as type_id,
|
||||||
|
"nextDueDate" as next_due_date,
|
||||||
|
"performedDate" as performed_date
|
||||||
|
FROM "Revision"
|
||||||
|
WHERE "equipmentId" = ANY(${equipmentIds})
|
||||||
|
ORDER BY "equipmentId", "typeId", "performedDate" DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const latestMap = new Map<string, { nextDueDate: Date | null; performedDate: Date }>();
|
||||||
|
for (const r of latestRevisions) {
|
||||||
|
latestMap.set(`${r.equipment_id}:${r.type_id}`, {
|
||||||
|
nextDueDate: r.next_due_date,
|
||||||
|
performedDate: r.performed_date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const items: Array<{
|
||||||
|
equipment: { id: string; name: string; address: string; customer: { id: string; name: string } | null };
|
||||||
|
revisionType: { id: string; name: string; color: string | null; intervalDays: number };
|
||||||
|
dueDate: Date;
|
||||||
|
lastPerformedDate: Date | null;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const eq of equipment) {
|
||||||
|
const cycleAnchor = eq.revisionCycleStart || eq.installDate;
|
||||||
|
|
||||||
|
const allScheduleTypes = eq.revisionSchedules.map(s => ({
|
||||||
|
id: s.revisionType.id,
|
||||||
|
intervalDays: s.revisionType.intervalDays,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const schedule of eq.revisionSchedules) {
|
||||||
|
const rt = schedule.revisionType;
|
||||||
|
|
||||||
|
// Dlhší typ pokrýva kratší (ročná pokrýva štvrťročnú)
|
||||||
|
const coveringTypeIds = getCoveringTypeIds(rt.intervalDays, allScheduleTypes);
|
||||||
|
|
||||||
|
let latest: { nextDueDate: Date | null; performedDate: Date } | undefined;
|
||||||
|
for (const covTypeId of coveringTypeIds) {
|
||||||
|
const candidate = latestMap.get(`${eq.id}:${covTypeId}`);
|
||||||
|
if (candidate && (!latest || candidate.performedDate > latest.performedDate)) {
|
||||||
|
latest = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dueDate: Date | null = null;
|
||||||
|
if (latest) {
|
||||||
|
if (cycleAnchor && rt.intervalDays > 0) {
|
||||||
|
// Vždy prepočítať z cyklového anchoru a skutočného intervalu typu
|
||||||
|
dueDate = calculateNextDueDateFromInstall(cycleAnchor, rt.intervalDays, latest.performedDate);
|
||||||
|
} else if (latest.nextDueDate) {
|
||||||
|
// Fallback na uložený nextDueDate ak nemáme anchor
|
||||||
|
dueDate = new Date(latest.nextDueDate);
|
||||||
|
}
|
||||||
|
} else if (cycleAnchor && rt.intervalDays > 0) {
|
||||||
|
dueDate = computeFirstUpcomingCycleDate(cycleAnchor, rt.intervalDays, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dueDate) continue;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
equipment: {
|
||||||
|
id: eq.id,
|
||||||
|
name: eq.name,
|
||||||
|
address: eq.address,
|
||||||
|
customer: eq.customer,
|
||||||
|
},
|
||||||
|
revisionType: {
|
||||||
|
id: rt.id,
|
||||||
|
name: rt.name,
|
||||||
|
color: rt.color,
|
||||||
|
intervalDays: rt.intervalDays,
|
||||||
|
},
|
||||||
|
dueDate,
|
||||||
|
lastPerformedDate: latest?.performedDate || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zlúčiť prekrývajúce sa termíny: ročná pokrýva štvrťročnú
|
||||||
|
return mergeOverlappingItems(
|
||||||
|
items,
|
||||||
|
item => item.equipment.id,
|
||||||
|
item => item.revisionType.intervalDays,
|
||||||
|
item => item.dueDate.getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRevisionSchedule = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const view = getQueryString(req, 'view') || 'upcoming';
|
||||||
|
const typeId = getQueryString(req, 'typeId');
|
||||||
|
const customerId = getQueryString(req, 'customerId');
|
||||||
|
const search = getQueryString(req, 'search');
|
||||||
|
const page = parseQueryInt(req.query.page, 1);
|
||||||
|
const limit = parseQueryInt(req.query.limit, 25);
|
||||||
|
|
||||||
|
const allItems = await buildScheduleItems({ typeId: typeId || undefined, customerId: customerId || undefined, search: search || undefined });
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Filtrovať podľa view
|
||||||
|
const filtered = allItems.filter(item => {
|
||||||
|
if (view === 'overdue') return item.dueDate < now;
|
||||||
|
return item.dueDate >= now; // upcoming
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zoradiť podľa dueDate
|
||||||
|
filtered.sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());
|
||||||
|
|
||||||
|
// Stránkovanie
|
||||||
|
const total = filtered.length;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const paged = filtered.slice(skip, skip + limit);
|
||||||
|
|
||||||
|
// Formátovať výstup
|
||||||
|
const data = paged.map(item => ({
|
||||||
|
equipmentId: item.equipment.id,
|
||||||
|
equipmentName: item.equipment.name,
|
||||||
|
equipmentAddress: item.equipment.address,
|
||||||
|
customer: item.equipment.customer,
|
||||||
|
revisionType: item.revisionType,
|
||||||
|
dueDate: item.dueDate.toISOString(),
|
||||||
|
daysUntil: Math.ceil((item.dueDate.getTime() - now.getTime()) / msPerDay),
|
||||||
|
lastPerformedDate: item.lastPerformedDate?.toISOString() || null,
|
||||||
|
label: item.revisionType.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
paginatedResponse(res, data, total, page, limit);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching revision schedule:', error);
|
||||||
|
errorResponse(res, 'Chyba pri načítaní revízneho plánu.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const skipRevision = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { equipmentId, typeId, scheduledDate, skipReason } = req.body;
|
||||||
|
|
||||||
|
const equipment = await prisma.equipment.findUnique({ where: { id: equipmentId } });
|
||||||
|
if (!equipment) {
|
||||||
|
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisionType = await prisma.revisionType.findUnique({ where: { id: typeId } });
|
||||||
|
if (!revisionType) {
|
||||||
|
errorResponse(res, 'Typ revízie nebol nájdený.', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduled = new Date(scheduledDate);
|
||||||
|
const cycleAnchor = equipment.revisionCycleStart || equipment.installDate;
|
||||||
|
|
||||||
|
const { nextDueDate, reminderDate } = await computeNextDueAndReminder({
|
||||||
|
equipmentId,
|
||||||
|
cycleAnchor,
|
||||||
|
performedDate: scheduled,
|
||||||
|
typeIntervalDays: revisionType.intervalDays,
|
||||||
|
typeReminderDays: revisionType.reminderDays,
|
||||||
|
});
|
||||||
|
|
||||||
|
const revision = await prisma.revision.create({
|
||||||
|
data: {
|
||||||
|
equipmentId,
|
||||||
|
typeId,
|
||||||
|
status: 'skipped',
|
||||||
|
performedDate: scheduled,
|
||||||
|
nextDueDate,
|
||||||
|
reminderDate,
|
||||||
|
performedById: req.user!.userId,
|
||||||
|
skipReason: skipReason || null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
equipment: {
|
||||||
|
include: {
|
||||||
|
type: true,
|
||||||
|
customer: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: true,
|
||||||
|
performedBy: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.logActivity) {
|
||||||
|
await req.logActivity('CREATE', 'Revision', revision.id, {
|
||||||
|
equipmentId,
|
||||||
|
type: revisionType.name,
|
||||||
|
action: 'skip',
|
||||||
|
reason: skipReason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
successResponse(res, revision, 'Revízia bola preskočená.', 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error skipping revision:', error);
|
||||||
|
errorResponse(res, 'Chyba pri preskočení revízie.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRevisionStats = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Spočítať upcoming a overdue z agregovaného plánu
|
||||||
|
const allItems = await buildScheduleItems();
|
||||||
|
let upcoming = 0;
|
||||||
|
let overdue = 0;
|
||||||
|
for (const item of allItems) {
|
||||||
|
if (item.dueDate < now) overdue++;
|
||||||
|
else upcoming++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [performed, skipped] = await Promise.all([
|
||||||
|
prisma.revision.count({ where: { status: 'performed' } }),
|
||||||
|
prisma.revision.count({ where: { status: 'skipped' } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
successResponse(res, { upcoming, overdue, performed, skipped });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching revision stats:', error);
|
||||||
|
errorResponse(res, 'Chyba pri načítaní štatistík revízií.', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,21 +18,36 @@ export const getTasks = async (req: AuthRequest, res: Response): Promise<void> =
|
|||||||
const createdById = getQueryString(req, 'createdById');
|
const createdById = getQueryString(req, 'createdById');
|
||||||
const assigneeId = getQueryString(req, 'assigneeId');
|
const assigneeId = getQueryString(req, 'assigneeId');
|
||||||
|
|
||||||
const where = {
|
// ROOT a ADMIN vidia všetky úlohy, ostatní len svoje (priradené alebo vytvorené)
|
||||||
...(projectId && { projectId }),
|
const userId = req.user!.userId;
|
||||||
...(statusId && { statusId }),
|
const roleCode = req.user!.roleCode;
|
||||||
...(priorityId && { priorityId }),
|
const isAdmin = roleCode === 'ROOT' || roleCode === 'ADMIN';
|
||||||
...(createdById && { createdById }),
|
|
||||||
...(assigneeId && {
|
const conditions: object[] = [];
|
||||||
assignees: { some: { userId: assigneeId } },
|
|
||||||
}),
|
if (!isAdmin) {
|
||||||
...(search && {
|
conditions.push({
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { userId } } },
|
||||||
|
{ createdById: userId },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (projectId) conditions.push({ projectId });
|
||||||
|
if (statusId) conditions.push({ statusId });
|
||||||
|
if (priorityId) conditions.push({ priorityId });
|
||||||
|
if (createdById) conditions.push({ createdById });
|
||||||
|
if (assigneeId) conditions.push({ assignees: { some: { userId: assigneeId } } });
|
||||||
|
if (search) {
|
||||||
|
conditions.push({
|
||||||
OR: [
|
OR: [
|
||||||
{ title: { contains: search, mode: 'insensitive' as const } },
|
{ title: { contains: search, mode: 'insensitive' as const } },
|
||||||
{ description: { contains: search, mode: 'insensitive' as const } },
|
{ description: { contains: search, mode: 'insensitive' as const } },
|
||||||
],
|
],
|
||||||
}),
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? { AND: conditions } : {};
|
||||||
|
|
||||||
const [tasks, total] = await Promise.all([
|
const [tasks, total] = await Promise.all([
|
||||||
prisma.task.findMany({
|
prisma.task.findMany({
|
||||||
@@ -95,6 +110,19 @@ export const getTask = async (req: AuthRequest, res: Response): Promise<void> =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kontrola prístupu - non-admin používatelia vidia len svoje úlohy
|
||||||
|
const roleCode = req.user!.roleCode;
|
||||||
|
const isAdmin = roleCode === 'ROOT' || roleCode === 'ADMIN';
|
||||||
|
if (!isAdmin) {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const isAssignee = task.assignees.some(a => a.user.id === userId);
|
||||||
|
const isCreator = task.createdById === userId;
|
||||||
|
if (!isAssignee && !isCreator) {
|
||||||
|
errorResponse(res, 'Nemáte oprávnenie zobraziť túto úlohu.', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
successResponse(res, task);
|
successResponse(res, task);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching task:', error);
|
console.error('Error fetching task:', error);
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ router.get('/:id', canRead('equipment'), equipmentController.getEquipmentById);
|
|||||||
router.put('/:id', canUpdate('equipment'), equipmentController.updateEquipment);
|
router.put('/:id', canUpdate('equipment'), equipmentController.updateEquipment);
|
||||||
router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipment);
|
router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipment);
|
||||||
|
|
||||||
|
// Schedule
|
||||||
|
router.get('/:id/schedule', canRead('equipment'), equipmentController.getEquipmentSchedule);
|
||||||
|
|
||||||
// Revisions
|
// Revisions
|
||||||
router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions);
|
router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions);
|
||||||
router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision);
|
router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import dashboardRoutes from './dashboard.routes';
|
|||||||
import uploadRoutes from './upload.routes';
|
import uploadRoutes from './upload.routes';
|
||||||
import zakazkyRoutes from './zakazky.routes';
|
import zakazkyRoutes from './zakazky.routes';
|
||||||
import notificationRoutes from './notification.routes';
|
import notificationRoutes from './notification.routes';
|
||||||
|
import revisionsRoutes from './revisions.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ router.use('/customers', customersRoutes);
|
|||||||
router.use('/projects', projectsRoutes);
|
router.use('/projects', projectsRoutes);
|
||||||
router.use('/tasks', tasksRoutes);
|
router.use('/tasks', tasksRoutes);
|
||||||
router.use('/equipment', equipmentRoutes);
|
router.use('/equipment', equipmentRoutes);
|
||||||
|
router.use('/revisions', revisionsRoutes);
|
||||||
router.use('/rma', rmaRoutes);
|
router.use('/rma', rmaRoutes);
|
||||||
router.use('/settings', settingsRoutes);
|
router.use('/settings', settingsRoutes);
|
||||||
router.use('/dashboard', dashboardRoutes);
|
router.use('/dashboard', dashboardRoutes);
|
||||||
|
|||||||
23
backend/src/routes/revisions.routes.ts
Normal file
23
backend/src/routes/revisions.routes.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as revisionsController from '../controllers/revisions.controller';
|
||||||
|
import { authenticate } from '../middleware/auth.middleware';
|
||||||
|
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||||
|
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||||
|
import { validate } from '../middleware/validate.middleware';
|
||||||
|
import { revisionSchema, updateRevisionSchema, skipRevisionSchema } from '../utils/validators';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(authenticate);
|
||||||
|
router.use(activityLogger);
|
||||||
|
|
||||||
|
router.get('/', canRead('equipment'), revisionsController.getRevisions);
|
||||||
|
router.get('/stats', canRead('equipment'), revisionsController.getRevisionStats);
|
||||||
|
router.get('/schedule', canRead('equipment'), revisionsController.getRevisionSchedule);
|
||||||
|
router.post('/skip', canCreate('equipment'), validate(skipRevisionSchema), revisionsController.skipRevision);
|
||||||
|
router.get('/:id', canRead('equipment'), revisionsController.getRevision);
|
||||||
|
router.post('/', canCreate('equipment'), validate(revisionSchema), revisionsController.createRevision);
|
||||||
|
router.put('/:id', canUpdate('equipment'), validate(updateRevisionSchema), revisionsController.updateRevision);
|
||||||
|
router.delete('/:id', canDelete('equipment'), revisionsController.deleteRevision);
|
||||||
|
|
||||||
|
export default router;
|
||||||
184
backend/src/utils/revisionSchedule.ts
Normal file
184
backend/src/utils/revisionSchedule.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Zdieľané utility funkcie pre výpočet revíznych plánov.
|
||||||
|
* Konsoliduje logiku zdieľanú medzi equipment.controller a revisions.controller.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import prisma from '../config/database';
|
||||||
|
|
||||||
|
export const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vypočíta nasledujúci termín revízie na základe cyklu ukotveného na dátum inštalácie.
|
||||||
|
* Cyklus sa neposúva ani keď sa revízia vykoná po termíne.
|
||||||
|
*/
|
||||||
|
export function calculateNextDueDateFromInstall(
|
||||||
|
installDate: Date,
|
||||||
|
intervalDays: number,
|
||||||
|
performedDate: Date
|
||||||
|
): Date {
|
||||||
|
let cycleDate = new Date(installDate);
|
||||||
|
|
||||||
|
// Posúvame cyklové body dopredu, kým neprejdeme performedDate
|
||||||
|
while (cycleDate <= performedDate) {
|
||||||
|
cycleDate.setDate(cycleDate.getDate() + intervalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cycleDate je teraz prvý cyklový bod PO performedDate
|
||||||
|
const diffMs = cycleDate.getTime() - performedDate.getTime();
|
||||||
|
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
// Ak je najbližší cyklový bod menej ako polovicu intervalu vzdialený,
|
||||||
|
// revízia pokrýva TENTO cyklový bod → nextDueDate je ten nasledujúci
|
||||||
|
if (diffDays < intervalDays / 2) {
|
||||||
|
cycleDate.setDate(cycleDate.getDate() + intervalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cycleDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detekuje preskočené cykly medzi poslednou revíziou a novou.
|
||||||
|
* Vracia zoznam cyklových dátumov, ktoré boli preskočené.
|
||||||
|
*/
|
||||||
|
export function detectSkippedCycles(
|
||||||
|
installDate: Date | null,
|
||||||
|
intervalDays: number,
|
||||||
|
lastPerformedDate: Date | null,
|
||||||
|
newPerformedDate: Date
|
||||||
|
): Date[] {
|
||||||
|
if (!installDate || intervalDays <= 0) return [];
|
||||||
|
|
||||||
|
const startFrom = lastPerformedDate || installDate;
|
||||||
|
const missedCycles: Date[] = [];
|
||||||
|
let cycleDate = new Date(installDate);
|
||||||
|
|
||||||
|
// Posunieme sa na prvý cyklový bod po startFrom
|
||||||
|
while (cycleDate <= startFrom) {
|
||||||
|
cycleDate.setDate(cycleDate.getDate() + intervalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zbierame cyklové body medzi lastPerformedDate a newPerformedDate
|
||||||
|
while (cycleDate < newPerformedDate) {
|
||||||
|
missedCycles.push(new Date(cycleDate));
|
||||||
|
cycleDate.setDate(cycleDate.getDate() + intervalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posledný cyklový bod je ten, ktorý táto revízia pokrýva → odoberieme
|
||||||
|
if (missedCycles.length > 0) {
|
||||||
|
missedCycles.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return missedCycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vypočíta prvý nadchádzajúci cyklový bod po danom dátume.
|
||||||
|
*/
|
||||||
|
export function computeFirstUpcomingCycleDate(
|
||||||
|
anchor: Date,
|
||||||
|
intervalDays: number,
|
||||||
|
after: Date
|
||||||
|
): Date | null {
|
||||||
|
if (intervalDays <= 0) return null;
|
||||||
|
const intervalMs = intervalDays * MS_PER_DAY;
|
||||||
|
const anchorMs = anchor.getTime();
|
||||||
|
const afterMs = after.getTime();
|
||||||
|
const n = Math.ceil((afterMs - anchorMs) / intervalMs);
|
||||||
|
return new Date(anchorMs + Math.max(1, n) * intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre daný typ revízie nájde ID typov, ktoré ho pokrývajú (vrátane seba samého).
|
||||||
|
* Napr. ročná revízia pokrýva aj štvrťročný cyklový bod.
|
||||||
|
*/
|
||||||
|
export function getCoveringTypeIds(
|
||||||
|
intervalDays: number,
|
||||||
|
allScheduleTypes: Array<{ id: string; intervalDays: number }>
|
||||||
|
): string[] {
|
||||||
|
return allScheduleTypes
|
||||||
|
.filter(t => t.intervalDays >= intervalDays)
|
||||||
|
.map(t => t.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vypočíta dátum pripomienky z nextDueDate a počtu dní pred termínom.
|
||||||
|
*/
|
||||||
|
export function calculateReminderDate(nextDueDate: Date, reminderDays: number): Date {
|
||||||
|
const reminderDate = new Date(nextDueDate);
|
||||||
|
reminderDate.setDate(reminderDate.getDate() - reminderDays);
|
||||||
|
return reminderDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nájde najkratší revízny interval spomedzi všetkých plánov zariadenia.
|
||||||
|
* Po ročnej revízii nasleduje štvrťročná, nie ďalšia ročná.
|
||||||
|
*/
|
||||||
|
export async function getShortestIntervalForEquipment(
|
||||||
|
equipmentId: string,
|
||||||
|
fallbackIntervalDays: number
|
||||||
|
): Promise<number> {
|
||||||
|
const schedules = await prisma.equipmentRevisionSchedule.findMany({
|
||||||
|
where: { equipmentId },
|
||||||
|
include: { revisionType: { select: { intervalDays: true } } },
|
||||||
|
});
|
||||||
|
const shortest = schedules
|
||||||
|
.map(s => s.revisionType.intervalDays)
|
||||||
|
.filter(d => d > 0)
|
||||||
|
.sort((a, b) => a - b)[0] || fallbackIntervalDays;
|
||||||
|
return Math.min(shortest, fallbackIntervalDays) || fallbackIntervalDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vypočíta nextDueDate a reminderDate pre novú/aktualizovanú revíziu.
|
||||||
|
* Používa najkratší interval zariadenia (po ročnej → štvrťročná).
|
||||||
|
*/
|
||||||
|
export async function computeNextDueAndReminder(params: {
|
||||||
|
equipmentId: string;
|
||||||
|
cycleAnchor: Date | null;
|
||||||
|
performedDate: Date;
|
||||||
|
typeIntervalDays: number;
|
||||||
|
typeReminderDays: number;
|
||||||
|
manualNextDueDate?: string | null;
|
||||||
|
}): Promise<{ nextDueDate: Date; reminderDate: Date }> {
|
||||||
|
const { equipmentId, cycleAnchor, performedDate, typeIntervalDays, typeReminderDays, manualNextDueDate } = params;
|
||||||
|
|
||||||
|
let nextDueDate: Date;
|
||||||
|
|
||||||
|
if (manualNextDueDate) {
|
||||||
|
nextDueDate = new Date(manualNextDueDate);
|
||||||
|
} else {
|
||||||
|
const intervalForNextDue = await getShortestIntervalForEquipment(equipmentId, typeIntervalDays);
|
||||||
|
|
||||||
|
if (intervalForNextDue > 0 && cycleAnchor) {
|
||||||
|
nextDueDate = calculateNextDueDateFromInstall(cycleAnchor, intervalForNextDue, performedDate);
|
||||||
|
} else if (intervalForNextDue > 0) {
|
||||||
|
nextDueDate = new Date(performedDate);
|
||||||
|
nextDueDate.setDate(nextDueDate.getDate() + intervalForNextDue);
|
||||||
|
} else {
|
||||||
|
nextDueDate = performedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminderDate = calculateReminderDate(nextDueDate, typeReminderDays);
|
||||||
|
return { nextDueDate, reminderDate };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtruje items tak, že odstráni kratšie typy ak sa ich termín kryje s dlhším typom.
|
||||||
|
* Ročná pokrýva štvrťročnú → štvrťročná sa nezobrazí.
|
||||||
|
*/
|
||||||
|
export function mergeOverlappingItems<T>(
|
||||||
|
items: T[],
|
||||||
|
getEquipmentId: (item: T) => string,
|
||||||
|
getIntervalDays: (item: T) => number,
|
||||||
|
getDueDateMs: (item: T) => number,
|
||||||
|
): T[] {
|
||||||
|
return items.filter(item => {
|
||||||
|
const longerMatch = items.find(other =>
|
||||||
|
getEquipmentId(other) === getEquipmentId(item) &&
|
||||||
|
getIntervalDays(other) > getIntervalDays(item) &&
|
||||||
|
Math.abs(getDueDateMs(other) - getDueDateMs(item)) <= (getIntervalDays(item) / 2) * MS_PER_DAY
|
||||||
|
);
|
||||||
|
return !longerMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -84,10 +84,12 @@ export const equipmentSchema = z.object({
|
|||||||
partNumber: z.string().optional(),
|
partNumber: z.string().optional(),
|
||||||
serialNumber: z.string().optional(),
|
serialNumber: z.string().optional(),
|
||||||
installDate: z.string().datetime().optional().or(z.literal('')),
|
installDate: z.string().datetime().optional().or(z.literal('')),
|
||||||
|
revisionCycleStart: z.string().datetime().optional().or(z.literal('')),
|
||||||
warrantyEnd: z.string().datetime().optional().or(z.literal('')),
|
warrantyEnd: z.string().datetime().optional().or(z.literal('')),
|
||||||
warrantyStatus: z.string().optional(),
|
warrantyStatus: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
|
revisionTypeIds: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// RMA validators
|
// RMA validators
|
||||||
@@ -187,6 +189,33 @@ export const tagSchema = z.object({
|
|||||||
active: z.boolean().optional(),
|
active: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Revision validators
|
||||||
|
export const revisionSchema = z.object({
|
||||||
|
equipmentId: z.string().min(1, 'Zariadenie je povinné'),
|
||||||
|
typeId: z.string().min(1, 'Typ revízie je povinný'),
|
||||||
|
performedDate: z.string().min(1, 'Dátum vykonania je povinný'),
|
||||||
|
nextDueDate: z.string().optional().or(z.literal('')),
|
||||||
|
findings: z.string().optional(),
|
||||||
|
result: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateRevisionSchema = z.object({
|
||||||
|
typeId: z.string().optional(),
|
||||||
|
performedDate: z.string().optional(),
|
||||||
|
nextDueDate: z.string().optional().or(z.literal('')),
|
||||||
|
findings: z.string().optional(),
|
||||||
|
result: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipRevisionSchema = z.object({
|
||||||
|
equipmentId: z.string().min(1, 'Zariadenie je povinné'),
|
||||||
|
typeId: z.string().min(1, 'Typ revízie je povinný'),
|
||||||
|
scheduledDate: z.string().min(1, 'Dátum je povinný'),
|
||||||
|
skipReason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
export const paginationSchema = z.object({
|
export const paginationSchema = z.object({
|
||||||
page: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 1)),
|
page: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 1)),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { TasksList } from '@/pages/tasks';
|
|||||||
import { EquipmentList } from '@/pages/equipment';
|
import { EquipmentList } from '@/pages/equipment';
|
||||||
import { RMAList } from '@/pages/rma';
|
import { RMAList } from '@/pages/rma';
|
||||||
import { SettingsDashboard } from '@/pages/settings';
|
import { SettingsDashboard } from '@/pages/settings';
|
||||||
|
import { RevisionsList } from '@/pages/revisions';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -87,6 +88,7 @@ function AppRoutes() {
|
|||||||
<Route path="/projects" element={<ProjectsList />} />
|
<Route path="/projects" element={<ProjectsList />} />
|
||||||
<Route path="/tasks" element={<TasksList />} />
|
<Route path="/tasks" element={<TasksList />} />
|
||||||
<Route path="/equipment" element={<EquipmentList />} />
|
<Route path="/equipment" element={<EquipmentList />} />
|
||||||
|
<Route path="/revisions" element={<RevisionsList />} />
|
||||||
<Route path="/rma" element={<RMAList />} />
|
<Route path="/rma" element={<RMAList />} />
|
||||||
<Route
|
<Route
|
||||||
path="/settings"
|
path="/settings"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CheckSquare,
|
CheckSquare,
|
||||||
Users,
|
Users,
|
||||||
Wrench,
|
Wrench,
|
||||||
|
ClipboardCheck,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -17,6 +18,7 @@ const navItems = [
|
|||||||
{ to: '/projects', icon: FolderKanban, label: 'Zákazky' },
|
{ to: '/projects', icon: FolderKanban, label: 'Zákazky' },
|
||||||
{ to: '/customers', icon: Users, label: 'Zákazníci' },
|
{ to: '/customers', icon: Users, label: 'Zákazníci' },
|
||||||
{ to: '/equipment', icon: Wrench, label: 'Zariadenia' },
|
{ to: '/equipment', icon: Wrench, label: 'Zariadenia' },
|
||||||
|
{ to: '/revisions', icon: ClipboardCheck, label: 'Revízie' },
|
||||||
{ to: '/rma', icon: RotateCcw, label: 'RMA' },
|
{ to: '/rma', icon: RotateCcw, label: 'RMA' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface ModalProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({ isOpen, onClose, title, children, className, size = 'md' }: ModalProps) {
|
export function Modal({ isOpen, onClose, title, children, className, size = 'md' }: ModalProps) {
|
||||||
@@ -37,6 +37,10 @@ export function Modal({ isOpen, onClose, title, children, className, size = 'md'
|
|||||||
md: 'max-w-md',
|
md: 'max-w-md',
|
||||||
lg: 'max-w-lg',
|
lg: 'max-w-lg',
|
||||||
xl: 'max-w-xl',
|
xl: 'max-w-xl',
|
||||||
|
'2xl': 'max-w-2xl',
|
||||||
|
'3xl': 'max-w-3xl',
|
||||||
|
'4xl': 'max-w-4xl',
|
||||||
|
'5xl': 'max-w-5xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -44,7 +48,7 @@ export function Modal({ isOpen, onClose, title, children, className, size = 'md'
|
|||||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-50 w-full rounded-lg bg-background p-6 shadow-lg',
|
'relative z-50 w-full rounded-lg bg-background p-6 shadow-lg max-h-[90vh] overflow-y-auto',
|
||||||
sizes[size],
|
sizes[size],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
102
frontend/src/hooks/useRevisionStatus.ts
Normal file
102
frontend/src/hooks/useRevisionStatus.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { settingsApi } from '@/services/settings.api';
|
||||||
|
import { AlertTriangle, Clock, CheckCircle, SkipForward, type LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface RevisionThreshold {
|
||||||
|
days: number;
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_THRESHOLDS: RevisionThreshold[] = [
|
||||||
|
{ days: 30, label: 'Blíži sa', color: '#EAB308' },
|
||||||
|
{ days: 14, label: 'Blíži sa!', color: '#F97316' },
|
||||||
|
{ days: 7, label: 'Urgentné!', color: '#EF4444' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const OVERDUE_COLOR = '#DC2626';
|
||||||
|
const OK_COLOR = '#22C55E';
|
||||||
|
const SKIP_COLOR = '#9CA3AF';
|
||||||
|
|
||||||
|
export type RevisionContext = 'schedule' | 'performed' | 'skipped';
|
||||||
|
|
||||||
|
export interface RevisionStatusResult {
|
||||||
|
label: string;
|
||||||
|
color: string | null;
|
||||||
|
icon: LucideIcon | null;
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRevisionStatus() {
|
||||||
|
const { data: settingData } = useQuery({
|
||||||
|
queryKey: ['system-setting', 'REVISION_STATUS_THRESHOLDS'],
|
||||||
|
queryFn: () => settingsApi.getSystemSetting('REVISION_STATUS_THRESHOLDS'),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const thresholds: RevisionThreshold[] = Array.isArray(settingData?.data?.value)
|
||||||
|
? (settingData.data.value as RevisionThreshold[])
|
||||||
|
: DEFAULT_THRESHOLDS;
|
||||||
|
|
||||||
|
// Zoradiť vzostupne podľa dní
|
||||||
|
const sorted = [...thresholds].sort((a, b) => a.days - b.days);
|
||||||
|
|
||||||
|
function getStatus(nextDueDate?: string | null, context?: RevisionContext | boolean): RevisionStatusResult {
|
||||||
|
// Spätná kompatibilita: boolean → context
|
||||||
|
let ctx: RevisionContext;
|
||||||
|
if (typeof context === 'boolean') {
|
||||||
|
ctx = context ? 'schedule' : 'performed';
|
||||||
|
} else {
|
||||||
|
ctx = context || 'schedule';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx === 'performed') {
|
||||||
|
return { label: 'Vykonaná', color: OK_COLOR, icon: CheckCircle, variant: 'secondary' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx === 'skipped') {
|
||||||
|
return { label: 'Vynechaná', color: SKIP_COLOR, icon: SkipForward, variant: 'secondary' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// context === 'schedule' → countdown logika
|
||||||
|
if (!nextDueDate) {
|
||||||
|
return { label: '-', color: null, icon: null, variant: 'secondary' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const due = new Date(nextDueDate);
|
||||||
|
const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Po termíne
|
||||||
|
if (diffDays < 0) {
|
||||||
|
return {
|
||||||
|
label: `Po termíne (${Math.abs(diffDays)} dní)`,
|
||||||
|
color: OVERDUE_COLOR,
|
||||||
|
icon: AlertTriangle,
|
||||||
|
variant: 'destructive',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nájsť prvý prah kde diffDays <= threshold.days
|
||||||
|
for (const threshold of sorted) {
|
||||||
|
if (diffDays <= threshold.days) {
|
||||||
|
return {
|
||||||
|
label: `${threshold.label} (${diffDays} dní)`,
|
||||||
|
color: threshold.color,
|
||||||
|
icon: Clock,
|
||||||
|
variant: 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V poriadku - ďaleko od termínu
|
||||||
|
return {
|
||||||
|
label: `Za ${diffDays} dní`,
|
||||||
|
color: OK_COLOR,
|
||||||
|
icon: CheckCircle,
|
||||||
|
variant: 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getStatus, thresholds };
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import type { Task } from '@/types';
|
|||||||
|
|
||||||
interface DashboardToday {
|
interface DashboardToday {
|
||||||
myTasks: Task[];
|
myTasks: Task[];
|
||||||
|
myTasksTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ikona podľa typu notifikácie
|
// Ikona podľa typu notifikácie
|
||||||
@@ -134,7 +135,7 @@ export function Dashboard() {
|
|||||||
}, {} as Record<string, Task[]>);
|
}, {} as Record<string, Task[]>);
|
||||||
|
|
||||||
// Štatistiky
|
// Štatistiky
|
||||||
const totalTasks = today?.myTasks?.length || 0;
|
const totalTasks = today?.myTasksTotal ?? today?.myTasks?.length ?? 0;
|
||||||
const overdueTasks = today?.myTasks?.filter(t => t.deadline && new Date(t.deadline) < new Date()) || [];
|
const overdueTasks = today?.myTasks?.filter(t => t.deadline && new Date(t.deadline) < new Date()) || [];
|
||||||
const todayTasks = today?.myTasks?.filter(t => {
|
const todayTasks = today?.myTasks?.filter(t => {
|
||||||
if (!t.deadline) return false;
|
if (!t.deadline) return false;
|
||||||
|
|||||||
318
frontend/src/pages/equipment/EquipmentDetail.tsx
Normal file
318
frontend/src/pages/equipment/EquipmentDetail.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Calendar, MapPin, Building2, Wrench, Plus, CalendarClock, ClipboardCheck, SkipForward, ChevronDown, ChevronRight, Pencil } from 'lucide-react';
|
||||||
|
import { equipmentApi } from '@/services/equipment.api';
|
||||||
|
import type { Equipment, Revision } from '@/types';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableCell,
|
||||||
|
LoadingOverlay,
|
||||||
|
Button,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import { useRevisionStatus } from '@/hooks/useRevisionStatus';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface EquipmentDetailProps {
|
||||||
|
equipment: Equipment;
|
||||||
|
onNewRevision?: (typeId?: string) => void;
|
||||||
|
onSkipRevision?: (date: string, typeId: string) => void;
|
||||||
|
onEditRevision?: (revision: Revision) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentDetail({ equipment, onNewRevision, onSkipRevision, onEditRevision }: EquipmentDetailProps) {
|
||||||
|
const { getStatus } = useRevisionStatus();
|
||||||
|
const [expandedRevision, setExpandedRevision] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['equipment-detail', equipment.id],
|
||||||
|
queryFn: () => equipmentApi.getById(equipment.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const detail = data?.data;
|
||||||
|
const revisions: Revision[] = (detail as Record<string, unknown>)?.revisions as Revision[] || [];
|
||||||
|
const revisionSchedules = (detail as Record<string, unknown>)?.revisionSchedules as Array<{
|
||||||
|
revisionType: { id: string; name: string; color?: string; intervalDays: number };
|
||||||
|
}> || [];
|
||||||
|
|
||||||
|
// Načítať schedule ak sú priradené typy revízií
|
||||||
|
const { data: scheduleData } = useQuery({
|
||||||
|
queryKey: ['equipment-schedule', equipment.id],
|
||||||
|
queryFn: () => equipmentApi.getSchedule(equipment.id),
|
||||||
|
enabled: revisionSchedules.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const schedule = scheduleData?.data;
|
||||||
|
const hasSchedules = revisionSchedules.length > 0;
|
||||||
|
|
||||||
|
const formatInterval = (days: number) => {
|
||||||
|
if (days >= 365) return `${Math.round(days / 365)} rok`;
|
||||||
|
if (days >= 90) return `štvrťročná`;
|
||||||
|
if (days >= 30) return `mesačná`;
|
||||||
|
if (days >= 14) return `${Math.round(days / 7)} týždne`;
|
||||||
|
return `${days} dní`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Info o zariadení */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Wrench className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Typ:</span>
|
||||||
|
<Badge color={equipment.type.color}>{equipment.type.name}</Badge>
|
||||||
|
</div>
|
||||||
|
{equipment.customer && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Zákazník:</span>
|
||||||
|
<span className="font-medium">{equipment.customer.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Adresa:</span>
|
||||||
|
<span>{equipment.address}</span>
|
||||||
|
</div>
|
||||||
|
{equipment.installDate && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Dátum inštalácie:</span>
|
||||||
|
<span>{formatDate(equipment.installDate)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{equipment.revisionCycleStart && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<CalendarClock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Východzia revízia:</span>
|
||||||
|
<span>{formatDate(equipment.revisionCycleStart)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{equipment.serialNumber && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Sériové číslo: </span>
|
||||||
|
<span className="font-mono">{equipment.serialNumber}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{equipment.brand && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Značka: </span>
|
||||||
|
<span>{equipment.brand}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{equipment.description && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Popis: </span>
|
||||||
|
<span>{equipment.description}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{revisionSchedules.length > 0 && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Revízne typy: </span>
|
||||||
|
<span className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{revisionSchedules.map((s) => (
|
||||||
|
<Badge key={s.revisionType.id} color={s.revisionType.color}>
|
||||||
|
{s.revisionType.name} ({formatInterval(s.revisionType.intervalDays)})
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Revízny plán - nadchádzajúce termíny */}
|
||||||
|
{schedule && schedule.upcomingDates.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
Revízny plán
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{schedule.upcomingDates.slice(0, 12).map((item, idx) => {
|
||||||
|
const isComposite = item.revisionTypes.length > 1;
|
||||||
|
const dominant = isComposite
|
||||||
|
? item.revisionTypes.reduce((a, b) => a.intervalDays >= b.intervalDays ? a : b)
|
||||||
|
: item.revisionTypes[0];
|
||||||
|
const daysUntil = Math.ceil(
|
||||||
|
(new Date(item.date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
const status = getStatus(item.date, true);
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`rounded-lg border p-3 ${
|
||||||
|
isComposite ? 'border-primary/50 bg-primary/5' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium">{formatDate(item.date)}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{StatusIcon && <StatusIcon className="h-3.5 w-3.5" style={{ color: status.color || undefined }} />}
|
||||||
|
<span className="text-xs" style={{ color: status.color || undefined }}>
|
||||||
|
{daysUntil} dní
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge color={dominant?.color} className="text-xs">
|
||||||
|
{item.label}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{onNewRevision && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2"
|
||||||
|
onClick={() => onNewRevision(dominant?.id)}
|
||||||
|
title="Vykonať revíziu"
|
||||||
|
>
|
||||||
|
<ClipboardCheck className="h-3.5 w-3.5 text-green-600" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onSkipRevision && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2"
|
||||||
|
onClick={() => onSkipRevision(item.date, dominant?.id || '')}
|
||||||
|
title="Vynechať revíziu"
|
||||||
|
>
|
||||||
|
<SkipForward className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Revízie */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-lg font-semibold">História revízií ({revisions.length})</h3>
|
||||||
|
{onNewRevision && (
|
||||||
|
<Button size="sm" onClick={() => onNewRevision()}>
|
||||||
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||||
|
Pridať záznam
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingOverlay />
|
||||||
|
) : revisions.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
|
Pre toto zariadenie zatiaľ neboli vykonané žiadne revízie.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Typ revízie</TableHead>
|
||||||
|
<TableHead>Vykonaná</TableHead>
|
||||||
|
<TableHead>Nasledujúci termín</TableHead>
|
||||||
|
<TableHead>Stav</TableHead>
|
||||||
|
<TableHead>Výsledok</TableHead>
|
||||||
|
<TableHead>Vykonal</TableHead>
|
||||||
|
<TableHead>Poznámka</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{revisions.map((revision) => {
|
||||||
|
const statusContext = revision.status === 'skipped' ? 'skipped' as const : 'performed' as const;
|
||||||
|
const status = getStatus(revision.nextDueDate, statusContext);
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
const isExpanded = expandedRevision === revision.id;
|
||||||
|
const resultText = revision.status === 'skipped' ? (revision.skipReason || '-') : (revision.result || '-');
|
||||||
|
const notesText = revision.notes || '-';
|
||||||
|
return (
|
||||||
|
<React.Fragment key={revision.id}>
|
||||||
|
<TableRow
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => setExpandedRevision(isExpanded ? null : revision.id)}
|
||||||
|
>
|
||||||
|
<TableCell className="whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isExpanded
|
||||||
|
? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
: <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
}
|
||||||
|
<Badge color={revision.type.color}>{revision.type.name}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap font-medium">{formatDate(revision.performedDate)}</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap">
|
||||||
|
{revision.nextDueDate ? formatDate(revision.nextDueDate) : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{StatusIcon && <StatusIcon className="h-3.5 w-3.5" style={{ color: status.color || undefined }} />}
|
||||||
|
<Badge color={status.color || undefined} variant={status.variant}>{status.label}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-[150px] truncate">{resultText}</TableCell>
|
||||||
|
<TableCell className="whitespace-nowrap">{revision.performedBy?.name || '-'}</TableCell>
|
||||||
|
<TableCell className="max-w-[200px] truncate">{notesText}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{isExpanded && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="bg-muted/30 py-3 px-6">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm flex-1">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Výsledok:</span>
|
||||||
|
<p className="mt-0.5 whitespace-pre-wrap">{resultText}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Zistenia:</span>
|
||||||
|
<p className="mt-0.5 whitespace-pre-wrap">{revision.findings || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="font-medium text-muted-foreground">Poznámka:</span>
|
||||||
|
<p className="mt-0.5 whitespace-pre-wrap">{notesText}</p>
|
||||||
|
</div>
|
||||||
|
{revision.status === 'skipped' && revision.skipReason && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="font-medium text-muted-foreground">Dôvod vynechania:</span>
|
||||||
|
<p className="mt-0.5 whitespace-pre-wrap">{revision.skipReason}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onEditRevision && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onEditRevision(revision); }}
|
||||||
|
>
|
||||||
|
<Pencil className="mr-1 h-3.5 w-3.5" />
|
||||||
|
Upraviť
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { customersApi } from '@/services/customers.api';
|
|||||||
import { settingsApi } from '@/services/settings.api';
|
import { settingsApi } from '@/services/settings.api';
|
||||||
import { getFiles, generateTempId } from '@/services/upload.api';
|
import { getFiles, generateTempId } from '@/services/upload.api';
|
||||||
import type { Equipment, Attachment } from '@/types';
|
import type { Equipment, Attachment } from '@/types';
|
||||||
import { Button, Input, Textarea, Select, ModalFooter, FileUpload, PendingFileUpload } from '@/components/ui';
|
import { Button, Input, Textarea, Select, ModalFooter, FileUpload, PendingFileUpload, Badge } from '@/components/ui';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const equipmentSchema = z.object({
|
const equipmentSchema = z.object({
|
||||||
@@ -22,6 +22,7 @@ const equipmentSchema = z.object({
|
|||||||
partNumber: z.string().optional(),
|
partNumber: z.string().optional(),
|
||||||
serialNumber: z.string().optional(),
|
serialNumber: z.string().optional(),
|
||||||
installDate: z.string().optional(),
|
installDate: z.string().optional(),
|
||||||
|
revisionCycleStart: z.string().optional(),
|
||||||
warrantyEnd: z.string().optional(),
|
warrantyEnd: z.string().optional(),
|
||||||
warrantyStatus: z.string().optional(),
|
warrantyStatus: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -40,6 +41,9 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEditing = !!equipment;
|
const isEditing = !!equipment;
|
||||||
const [files, setFiles] = useState<Attachment[]>([]);
|
const [files, setFiles] = useState<Attachment[]>([]);
|
||||||
|
const [selectedRevisionTypeIds, setSelectedRevisionTypeIds] = useState<string[]>(
|
||||||
|
() => equipment?.revisionSchedules?.map((s) => s.revisionType.id) || []
|
||||||
|
);
|
||||||
|
|
||||||
// Generate stable tempId for new equipment file uploads
|
// Generate stable tempId for new equipment file uploads
|
||||||
const tempId = useMemo(() => generateTempId(), []);
|
const tempId = useMemo(() => generateTempId(), []);
|
||||||
@@ -54,6 +58,11 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
queryFn: () => settingsApi.getEquipmentTypes(),
|
queryFn: () => settingsApi.getEquipmentTypes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: revisionTypesData } = useQuery({
|
||||||
|
queryKey: ['revision-types'],
|
||||||
|
queryFn: () => settingsApi.getRevisionTypes(),
|
||||||
|
});
|
||||||
|
|
||||||
// Load files when editing
|
// Load files when editing
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ['equipment-files', equipment?.id],
|
queryKey: ['equipment-files', equipment?.id],
|
||||||
@@ -84,6 +93,7 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
partNumber: equipment.partNumber || '',
|
partNumber: equipment.partNumber || '',
|
||||||
serialNumber: equipment.serialNumber || '',
|
serialNumber: equipment.serialNumber || '',
|
||||||
installDate: equipment.installDate?.split('T')[0] || '',
|
installDate: equipment.installDate?.split('T')[0] || '',
|
||||||
|
revisionCycleStart: equipment.revisionCycleStart?.split('T')[0] || '',
|
||||||
warrantyEnd: equipment.warrantyEnd?.split('T')[0] || '',
|
warrantyEnd: equipment.warrantyEnd?.split('T')[0] || '',
|
||||||
warrantyStatus: equipment.warrantyStatus || '',
|
warrantyStatus: equipment.warrantyStatus || '',
|
||||||
description: equipment.description || '',
|
description: equipment.description || '',
|
||||||
@@ -109,6 +119,7 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
mutationFn: (data: CreateEquipmentData) => equipmentApi.update(equipment!.id, data),
|
mutationFn: (data: CreateEquipmentData) => equipmentApi.update(equipment!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-detail'] });
|
||||||
toast.success('Zariadenie bolo aktualizované');
|
toast.success('Zariadenie bolo aktualizované');
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
@@ -118,11 +129,13 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: EquipmentFormData) => {
|
const onSubmit = (data: EquipmentFormData) => {
|
||||||
const cleanData = {
|
const cleanData: CreateEquipmentData = {
|
||||||
...data,
|
...data,
|
||||||
customerId: data.customerId || undefined,
|
customerId: data.customerId || undefined,
|
||||||
installDate: data.installDate || undefined,
|
installDate: data.installDate || undefined,
|
||||||
|
revisionCycleStart: data.revisionCycleStart || undefined,
|
||||||
warrantyEnd: data.warrantyEnd || undefined,
|
warrantyEnd: data.warrantyEnd || undefined,
|
||||||
|
revisionTypeIds: selectedRevisionTypeIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -133,10 +146,24 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleRevisionType = (id: string) => {
|
||||||
|
setSelectedRevisionTypeIds((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
const customerOptions = customersData?.data.map((c) => ({ value: c.id, label: c.name })) || [];
|
const customerOptions = customersData?.data.map((c) => ({ value: c.id, label: c.name })) || [];
|
||||||
const typeOptions = typesData?.data.map((t) => ({ value: t.id, label: t.name })) || [];
|
const typeOptions = typesData?.data.map((t) => ({ value: t.id, label: t.name })) || [];
|
||||||
|
const revisionTypes = revisionTypesData?.data?.filter((rt) => rt.active !== false) || [];
|
||||||
|
|
||||||
|
const formatInterval = (days: number) => {
|
||||||
|
if (days >= 365) return `${Math.round(days / 365)} rok`;
|
||||||
|
if (days >= 90) return `${Math.round(days / 90)} štvrťrok`;
|
||||||
|
if (days >= 30) return `${Math.round(days / 30)} mes.`;
|
||||||
|
return `${days} dní`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
@@ -196,13 +223,59 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) {
|
|||||||
{...register('location')}
|
{...register('location')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Input
|
<Input
|
||||||
id="installDate"
|
id="installDate"
|
||||||
type="date"
|
type="date"
|
||||||
label="Dátum inštalácie"
|
label="Dátum inštalácie"
|
||||||
{...register('installDate')}
|
{...register('installDate')}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
id="revisionCycleStart"
|
||||||
|
type="date"
|
||||||
|
label="Východzia revízia"
|
||||||
|
{...register('revisionCycleStart')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{revisionTypes.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Typy revízií
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
Ak nezadáte dátum východzej revízie, použije sa dátum inštalácie.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{revisionTypes.map((rt) => {
|
||||||
|
const isSelected = selectedRevisionTypeIds.includes(rt.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={rt.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleRevisionType(rt.id)}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'border-primary bg-primary/10 text-primary font-medium'
|
||||||
|
: 'border-border bg-background text-muted-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{rt.color && (
|
||||||
|
<span
|
||||||
|
className="h-2.5 w-2.5 rounded-full"
|
||||||
|
style={{ backgroundColor: rt.color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{rt.name}
|
||||||
|
<span className="text-xs opacity-70">({formatInterval(rt.intervalDays)})</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Input
|
<Input
|
||||||
id="warrantyEnd"
|
id="warrantyEnd"
|
||||||
type="date"
|
type="date"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Plus, Pencil, Trash2, Search } from 'lucide-react';
|
import { Plus, Pencil, Trash2, Search, Eye, Power, SkipForward } from 'lucide-react';
|
||||||
import { equipmentApi } from '@/services/equipment.api';
|
import { equipmentApi } from '@/services/equipment.api';
|
||||||
|
import { revisionsApi, type RevisionWithEquipment } from '@/services/revisions.api';
|
||||||
|
import { settingsApi } from '@/services/settings.api';
|
||||||
import type { Equipment } from '@/types';
|
import type { Equipment } from '@/types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -19,35 +21,96 @@ import {
|
|||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Modal,
|
Modal,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { EquipmentForm } from './EquipmentForm';
|
import { EquipmentForm } from './EquipmentForm';
|
||||||
|
import { EquipmentDetail } from './EquipmentDetail';
|
||||||
|
import { RevisionForm } from '../revisions/RevisionForm';
|
||||||
import { formatDate } from '@/lib/utils';
|
import { formatDate } from '@/lib/utils';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export function EquipmentList() {
|
export function EquipmentList() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [activeFilter, setActiveFilter] = useState('true');
|
||||||
|
const [typeFilter, setTypeFilter] = useState('');
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
const [editingEquipment, setEditingEquipment] = useState<Equipment | null>(null);
|
const [editingEquipment, setEditingEquipment] = useState<Equipment | null>(null);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<Equipment | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<Equipment | null>(null);
|
||||||
|
const [detailEquipment, setDetailEquipment] = useState<Equipment | null>(null);
|
||||||
|
const [revisionForEquipmentId, setRevisionForEquipmentId] = useState<string | null>(null);
|
||||||
|
const [revisionForTypeId, setRevisionForTypeId] = useState<string | undefined>(undefined);
|
||||||
|
const [editingRevision, setEditingRevision] = useState<RevisionWithEquipment | null>(null);
|
||||||
|
const [skipData, setSkipData] = useState<{ equipmentId: string; typeId: string; date: string } | null>(null);
|
||||||
|
const [skipReason, setSkipReason] = useState('');
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data: equipmentTypesData } = useQuery({
|
||||||
queryKey: ['equipment', search],
|
queryKey: ['equipment-types'],
|
||||||
queryFn: () => equipmentApi.getAll({ search, limit: 100 }),
|
queryFn: () => settingsApi.getEquipmentTypes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['equipment', search, activeFilter, typeFilter],
|
||||||
|
queryFn: () => equipmentApi.getAll({
|
||||||
|
search,
|
||||||
|
active: activeFilter ? (activeFilter === 'true') : undefined,
|
||||||
|
typeId: typeFilter || undefined,
|
||||||
|
limit: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deactivateMutation = useMutation({
|
||||||
mutationFn: (id: string) => equipmentApi.delete(id),
|
mutationFn: (id: string) => equipmentApi.delete(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
toast.success('Zariadenie bolo vymazané');
|
toast.success('Zariadenie bolo deaktivované');
|
||||||
setDeleteConfirm(null);
|
setDeleteConfirm(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error('Chyba pri mazaní zariadenia');
|
toast.error('Chyba pri deaktivácii zariadenia');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activateMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => equipmentApi.update(id, { active: true }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
toast.success('Zariadenie bolo aktivované');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri aktivácii zariadenia');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipMutation = useMutation({
|
||||||
|
mutationFn: (data: { equipmentId: string; typeId: string; scheduledDate: string; skipReason?: string }) =>
|
||||||
|
revisionsApi.skip(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-detail'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-schedule'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions'] });
|
||||||
|
toast.success('Revízia bola vynechaná');
|
||||||
|
setSkipData(null);
|
||||||
|
setSkipReason('');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri vynechaní revízie');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipmentTypeOptions = [
|
||||||
|
{ value: '', label: 'Všetky typy' },
|
||||||
|
...(equipmentTypesData?.data.map((t) => ({ value: t.id, label: t.name })) || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeFilterOptions = [
|
||||||
|
{ value: '', label: 'Všetky' },
|
||||||
|
{ value: 'true', label: 'Aktívne' },
|
||||||
|
{ value: 'false', label: 'Neaktívne' },
|
||||||
|
];
|
||||||
|
|
||||||
const handleEdit = (equipment: Equipment) => {
|
const handleEdit = (equipment: Equipment) => {
|
||||||
setEditingEquipment(equipment);
|
setEditingEquipment(equipment);
|
||||||
setIsFormOpen(true);
|
setIsFormOpen(true);
|
||||||
@@ -70,7 +133,7 @@ export function EquipmentList() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<div className="relative flex-1 max-w-sm">
|
<div className="relative flex-1 max-w-sm">
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -80,6 +143,16 @@ export function EquipmentList() {
|
|||||||
className="pl-9"
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Select
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={(e) => setTypeFilter(e.target.value)}
|
||||||
|
options={equipmentTypeOptions}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={activeFilter}
|
||||||
|
onChange={(e) => setActiveFilter(e.target.value)}
|
||||||
|
options={activeFilterOptions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -101,7 +174,11 @@ export function EquipmentList() {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.data.map((equipment) => (
|
{data?.data.map((equipment) => (
|
||||||
<TableRow key={equipment.id}>
|
<TableRow
|
||||||
|
key={equipment.id}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => setDetailEquipment(equipment)}
|
||||||
|
>
|
||||||
<TableCell className="font-medium">{equipment.name}</TableCell>
|
<TableCell className="font-medium">{equipment.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge color={equipment.type.color}>{equipment.type.name}</Badge>
|
<Badge color={equipment.type.color}>{equipment.type.name}</Badge>
|
||||||
@@ -118,12 +195,26 @@ export function EquipmentList() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(equipment)}>
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); setDetailEquipment(equipment); }} title="Detail">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); handleEdit(equipment); }} title="Upraviť">
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setDeleteConfirm(equipment)}>
|
{equipment.active ? (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); setDeleteConfirm(equipment); }} title="Deaktivovať">
|
||||||
</Button>
|
<Power className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); activateMutation.mutate(equipment.id); }}
|
||||||
|
title="Aktivovať"
|
||||||
|
>
|
||||||
|
<Power className="h-4 w-4 text-green-600" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -152,22 +243,127 @@ export function EquipmentList() {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={!!deleteConfirm}
|
isOpen={!!deleteConfirm}
|
||||||
onClose={() => setDeleteConfirm(null)}
|
onClose={() => setDeleteConfirm(null)}
|
||||||
title="Potvrdiť vymazanie"
|
title="Potvrdiť deaktiváciu"
|
||||||
>
|
>
|
||||||
<p>Naozaj chcete vymazať zariadenie "{deleteConfirm?.name}"?</p>
|
<p>
|
||||||
|
Naozaj chcete deaktivovať zariadenie "{deleteConfirm?.name}"?
|
||||||
|
Zariadenie nebude vymazané, len sa skryje zo zoznamu aktívnych zariadení.
|
||||||
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
||||||
Zrušiť
|
Zrušiť
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => deleteConfirm && deleteMutation.mutate(deleteConfirm.id)}
|
onClick={() => deleteConfirm && deactivateMutation.mutate(deleteConfirm.id)}
|
||||||
isLoading={deleteMutation.isPending}
|
isLoading={deactivateMutation.isPending}
|
||||||
>
|
>
|
||||||
Vymazať
|
Deaktivovať
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={!!detailEquipment}
|
||||||
|
onClose={() => setDetailEquipment(null)}
|
||||||
|
title={detailEquipment?.name || 'Detail zariadenia'}
|
||||||
|
size="5xl"
|
||||||
|
>
|
||||||
|
{detailEquipment && (
|
||||||
|
<EquipmentDetail
|
||||||
|
equipment={detailEquipment}
|
||||||
|
onNewRevision={(typeId) => {
|
||||||
|
setRevisionForEquipmentId(detailEquipment.id);
|
||||||
|
setRevisionForTypeId(typeId);
|
||||||
|
setDetailEquipment(null);
|
||||||
|
}}
|
||||||
|
onSkipRevision={(date, typeId) => {
|
||||||
|
setSkipData({ equipmentId: detailEquipment.id, typeId, date });
|
||||||
|
setSkipReason('');
|
||||||
|
}}
|
||||||
|
onEditRevision={(revision) => {
|
||||||
|
setEditingRevision(revision as RevisionWithEquipment);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={!!revisionForEquipmentId}
|
||||||
|
onClose={() => { setRevisionForEquipmentId(null); setRevisionForTypeId(undefined); }}
|
||||||
|
title="Pridať záznam"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{revisionForEquipmentId && (
|
||||||
|
<RevisionForm
|
||||||
|
revision={null}
|
||||||
|
onClose={() => {
|
||||||
|
setRevisionForEquipmentId(null);
|
||||||
|
setRevisionForTypeId(undefined);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
}}
|
||||||
|
preselectedEquipmentId={revisionForEquipmentId}
|
||||||
|
preselectedTypeId={revisionForTypeId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={!!skipData}
|
||||||
|
onClose={() => { setSkipData(null); setSkipReason(''); }}
|
||||||
|
title="Vynechať revíziu"
|
||||||
|
>
|
||||||
|
{skipData && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p>
|
||||||
|
Naozaj chcete vynechať revíziu s termínom{' '}
|
||||||
|
<strong>{formatDate(skipData.date)}</strong>?
|
||||||
|
</p>
|
||||||
|
<Textarea
|
||||||
|
label="Dôvod vynechania (voliteľný)"
|
||||||
|
value={skipReason}
|
||||||
|
onChange={(e) => setSkipReason(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Napr. zariadenie dočasne mimo prevádzky..."
|
||||||
|
/>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="outline" onClick={() => { setSkipData(null); setSkipReason(''); }}>
|
||||||
|
Zrušiť
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => skipMutation.mutate({
|
||||||
|
equipmentId: skipData.equipmentId,
|
||||||
|
typeId: skipData.typeId,
|
||||||
|
scheduledDate: skipData.date.split('T')[0],
|
||||||
|
skipReason: skipReason || undefined,
|
||||||
|
})}
|
||||||
|
isLoading={skipMutation.isPending}
|
||||||
|
>
|
||||||
|
<SkipForward className="mr-2 h-4 w-4" />
|
||||||
|
Vynechať
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={!!editingRevision}
|
||||||
|
onClose={() => setEditingRevision(null)}
|
||||||
|
title="Upraviť záznam"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{editingRevision && (
|
||||||
|
<RevisionForm
|
||||||
|
revision={editingRevision}
|
||||||
|
onClose={() => {
|
||||||
|
setEditingRevision(null);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
}}
|
||||||
|
preselectedEquipmentId={editingRevision.equipmentId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
318
frontend/src/pages/revisions/RevisionForm.tsx
Normal file
318
frontend/src/pages/revisions/RevisionForm.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||||
|
import { revisionsApi, type CreateRevisionData, type RevisionWithEquipment } from '@/services/revisions.api';
|
||||||
|
import { equipmentApi } from '@/services/equipment.api';
|
||||||
|
import { settingsApi } from '@/services/settings.api';
|
||||||
|
import { Button, Input, Textarea, ModalFooter, Badge } from '@/components/ui';
|
||||||
|
import { SearchableSelect } from '@/components/ui/SearchableSelect';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const revisionFormSchema = z.object({
|
||||||
|
equipmentId: z.string().min(1, 'Zariadenie je povinné'),
|
||||||
|
typeId: z.string().min(1, 'Typ revízie je povinný'),
|
||||||
|
performedDate: z.string().min(1, 'Dátum vykonania je povinný'),
|
||||||
|
nextDueDate: z.string().optional(),
|
||||||
|
findings: z.string().optional(),
|
||||||
|
result: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type RevisionFormData = z.input<typeof revisionFormSchema>;
|
||||||
|
|
||||||
|
interface RevisionFormProps {
|
||||||
|
revision: RevisionWithEquipment | null;
|
||||||
|
onClose: () => void;
|
||||||
|
preselectedEquipmentId?: string;
|
||||||
|
preselectedTypeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RevisionForm({ revision, onClose, preselectedEquipmentId, preselectedTypeId }: RevisionFormProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isEditing = !!revision;
|
||||||
|
|
||||||
|
const { data: equipmentData } = useQuery({
|
||||||
|
queryKey: ['equipment-select'],
|
||||||
|
queryFn: () => equipmentApi.getAll({ active: true, limit: 1000 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: revisionTypesData } = useQuery({
|
||||||
|
queryKey: ['revision-types'],
|
||||||
|
queryFn: () => settingsApi.getRevisionTypes(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<RevisionFormData>({
|
||||||
|
resolver: zodResolver(revisionFormSchema),
|
||||||
|
defaultValues: revision
|
||||||
|
? {
|
||||||
|
equipmentId: revision.equipmentId,
|
||||||
|
typeId: revision.typeId,
|
||||||
|
performedDate: revision.performedDate?.split('T')[0] || '',
|
||||||
|
nextDueDate: revision.nextDueDate?.split('T')[0] || '',
|
||||||
|
findings: revision.findings || '',
|
||||||
|
result: revision.result || '',
|
||||||
|
notes: revision.notes || '',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
equipmentId: preselectedEquipmentId || '',
|
||||||
|
typeId: preselectedTypeId || '',
|
||||||
|
performedDate: new Date().toISOString().split('T')[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedEquipmentId = watch('equipmentId');
|
||||||
|
const watchedTypeId = watch('typeId');
|
||||||
|
|
||||||
|
// Zistiť priradené typy revízií pre vybrané zariadenie
|
||||||
|
const selectedEquipment = useMemo(
|
||||||
|
() => equipmentData?.data.find((e) => e.id === watchedEquipmentId),
|
||||||
|
[equipmentData, watchedEquipmentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const assignedSchedules = selectedEquipment?.revisionSchedules || [];
|
||||||
|
const hasSchedules = assignedSchedules.length > 0;
|
||||||
|
|
||||||
|
// Ak je preselectedTypeId, nastaviť ho; ak zariadenie má 1 revízny typ, auto-select
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing) return;
|
||||||
|
if (preselectedTypeId && !watchedTypeId) {
|
||||||
|
setValue('typeId', preselectedTypeId, { shouldValidate: true });
|
||||||
|
} else if (hasSchedules && !watchedTypeId && assignedSchedules.length === 1) {
|
||||||
|
setValue('typeId', assignedSchedules[0].revisionType.id, { shouldValidate: true });
|
||||||
|
}
|
||||||
|
}, [hasSchedules, assignedSchedules, isEditing, watchedTypeId, preselectedTypeId, setValue]);
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data: CreateRevisionData) => revisionsApi.create(data),
|
||||||
|
onSuccess: (response) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-detail'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-schedule'] });
|
||||||
|
|
||||||
|
const skippedCycles = (response.data as Record<string, unknown>)?.skippedCycles as Date[] | undefined;
|
||||||
|
if (skippedCycles && skippedCycles.length > 0) {
|
||||||
|
toast.success('Revízia bola vytvorená');
|
||||||
|
toast(`Upozornenie: ${skippedCycles.length} revízny(ch) cyklus(ov) bolo preskočených!`, {
|
||||||
|
icon: '\u26A0\uFE0F',
|
||||||
|
duration: 6000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success('Revízia bola vytvorená');
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri vytváraní revízie');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: (data: CreateRevisionData) => revisionsApi.update(revision!.id, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
toast.success('Revízia bola aktualizovaná');
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri aktualizácii revízie');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: RevisionFormData) => {
|
||||||
|
const cleanData: CreateRevisionData = {
|
||||||
|
equipmentId: data.equipmentId,
|
||||||
|
typeId: data.typeId,
|
||||||
|
performedDate: data.performedDate,
|
||||||
|
nextDueDate: data.nextDueDate || undefined,
|
||||||
|
findings: data.findings || undefined,
|
||||||
|
result: data.result || undefined,
|
||||||
|
notes: data.notes || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
updateMutation.mutate(cleanData);
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(cleanData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
|
const equipmentOptions = equipmentData?.data.map((e) => ({
|
||||||
|
value: e.id,
|
||||||
|
label: `${e.name} - ${e.address}${e.customer ? ` (${e.customer.name})` : ''}`,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const revisionTypeOptions = revisionTypesData?.data.map((t) => ({
|
||||||
|
value: t.id,
|
||||||
|
label: `${t.name} (${t.intervalDays} dní)`,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const formatInterval = (days: number) => {
|
||||||
|
if (days >= 365) return `${Math.round(days / 365)} rok`;
|
||||||
|
if (days >= 90) return `štvrťročná`;
|
||||||
|
if (days >= 30) return `mesačná`;
|
||||||
|
return `${days} dní`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<SearchableSelect
|
||||||
|
label="Zariadenie *"
|
||||||
|
options={equipmentOptions}
|
||||||
|
value={watchedEquipmentId}
|
||||||
|
onChange={(val) => {
|
||||||
|
setValue('equipmentId', val, { shouldValidate: true });
|
||||||
|
// Reset typeId pri zmene zariadenia
|
||||||
|
if (!isEditing) {
|
||||||
|
setValue('typeId', '', { shouldValidate: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Vyberte zariadenie..."
|
||||||
|
error={errors.equipmentId?.message}
|
||||||
|
disabled={isEditing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Typ revízie: podľa toho či je preselected, zariadenie má plány, alebo voľný výber */}
|
||||||
|
{preselectedTypeId && !isEditing ? (
|
||||||
|
// Predvybraný typ z plánu - read-only zobrazenie
|
||||||
|
(() => {
|
||||||
|
const preselectedType = assignedSchedules.find((s) => s.revisionType.id === preselectedTypeId)?.revisionType
|
||||||
|
|| revisionTypesData?.data.find((t) => t.id === preselectedTypeId);
|
||||||
|
return preselectedType ? (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Typ revízie
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2 rounded-md border px-3 py-2 bg-muted/30">
|
||||||
|
<Badge color={preselectedType.color}>
|
||||||
|
{preselectedType.name}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
({formatInterval(preselectedType.intervalDays)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()
|
||||||
|
) : hasSchedules && !isEditing ? (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Typ revízie *
|
||||||
|
</label>
|
||||||
|
{assignedSchedules.length === 1 ? (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border px-3 py-2 bg-muted/30">
|
||||||
|
<Badge color={assignedSchedules[0].revisionType.color}>
|
||||||
|
{assignedSchedules[0].revisionType.name}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
({formatInterval(assignedSchedules[0].revisionType.intervalDays)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignedSchedules.map((s) => {
|
||||||
|
const isSelected = watchedTypeId === s.revisionType.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={s.revisionType.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setValue('typeId', s.revisionType.id, { shouldValidate: true })}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'border-primary bg-primary/10 text-primary font-medium'
|
||||||
|
: 'border-border bg-background text-muted-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{s.revisionType.color && (
|
||||||
|
<span
|
||||||
|
className="h-2.5 w-2.5 rounded-full"
|
||||||
|
style={{ backgroundColor: s.revisionType.color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{s.revisionType.name}
|
||||||
|
<span className="text-xs opacity-70">({formatInterval(s.revisionType.intervalDays)})</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errors.typeId?.message && (
|
||||||
|
<p className="text-sm text-destructive mt-1">{errors.typeId.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SearchableSelect
|
||||||
|
label="Typ revízie *"
|
||||||
|
options={revisionTypeOptions}
|
||||||
|
value={watchedTypeId}
|
||||||
|
onChange={(val) => setValue('typeId', val, { shouldValidate: true })}
|
||||||
|
placeholder="Vyberte typ revízie..."
|
||||||
|
error={errors.typeId?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Input
|
||||||
|
id="performedDate"
|
||||||
|
type="date"
|
||||||
|
label="Dátum vykonania *"
|
||||||
|
error={errors.performedDate?.message}
|
||||||
|
{...register('performedDate')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="nextDueDate"
|
||||||
|
type="date"
|
||||||
|
label="Nasledujúci termín"
|
||||||
|
{...register('nextDueDate')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground -mt-2">
|
||||||
|
Ak nevyplníte nasledujúci termín, vypočíta sa automaticky podľa intervalu.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="result"
|
||||||
|
label="Výsledok"
|
||||||
|
placeholder="napr. OK, Vyhovuje, Nevyhovuje"
|
||||||
|
{...register('result')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="findings"
|
||||||
|
label="Zistenia"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Zistenia z revízie..."
|
||||||
|
{...register('findings')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="notes"
|
||||||
|
label="Poznámky"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Ďalšie poznámky..."
|
||||||
|
{...register('notes')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
|
Zrušiť
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isPending}>
|
||||||
|
{isEditing ? 'Uložiť' : 'Vytvoriť'}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
658
frontend/src/pages/revisions/RevisionsList.tsx
Normal file
658
frontend/src/pages/revisions/RevisionsList.tsx
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { Plus, Pencil, Trash2, Search, ClipboardCheck, SkipForward, CalendarClock, AlertTriangle, List } from 'lucide-react';
|
||||||
|
import { revisionsApi, type RevisionWithEquipment } from '@/services/revisions.api';
|
||||||
|
import { equipmentApi } from '@/services/equipment.api';
|
||||||
|
import { settingsApi } from '@/services/settings.api';
|
||||||
|
import type { Equipment, RevisionScheduleItem } from '@/types';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardContent,
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableCell,
|
||||||
|
Badge,
|
||||||
|
LoadingOverlay,
|
||||||
|
Modal,
|
||||||
|
ModalFooter,
|
||||||
|
Textarea,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import { RevisionForm } from './RevisionForm';
|
||||||
|
import { EquipmentDetail } from '../equipment/EquipmentDetail';
|
||||||
|
import { useRevisionStatus } from '@/hooks/useRevisionStatus';
|
||||||
|
import { formatDate } from '@/lib/utils';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
type FilterTab = 'upcoming' | 'overdue' | 'skipped';
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
|
export function RevisionsList() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { getStatus } = useRevisionStatus();
|
||||||
|
|
||||||
|
const [selectedFilters, setSelectedFilters] = useState<Set<FilterTab>>(new Set(['upcoming']));
|
||||||
|
const [activeTypeFilter, setActiveTypeFilter] = useState('');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
// Form modal
|
||||||
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
|
const [editingRevision, setEditingRevision] = useState<RevisionWithEquipment | null>(null);
|
||||||
|
const [preselectedEquipmentId, setPreselectedEquipmentId] = useState<string>();
|
||||||
|
const [preselectedTypeId, setPreselectedTypeId] = useState<string>();
|
||||||
|
|
||||||
|
// Equipment detail modal
|
||||||
|
const [detailEquipment, setDetailEquipment] = useState<Equipment | null>(null);
|
||||||
|
|
||||||
|
// Delete modal
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<RevisionWithEquipment | null>(null);
|
||||||
|
|
||||||
|
// Skip modal (z equipment detail)
|
||||||
|
const [skipData, setSkipData] = useState<{ equipmentId: string; equipmentName: string; typeId: string; date: string } | null>(null);
|
||||||
|
const [skipReason, setSkipReason] = useState('');
|
||||||
|
|
||||||
|
// Derived state
|
||||||
|
const hasUpcoming = selectedFilters.has('upcoming');
|
||||||
|
const hasOverdue = selectedFilters.has('overdue');
|
||||||
|
const hasSkipped = selectedFilters.has('skipped');
|
||||||
|
const isAllMode = selectedFilters.size === 0;
|
||||||
|
const needsSchedule = !isAllMode && (hasUpcoming || hasOverdue);
|
||||||
|
const needsRevisions = isAllMode || hasSkipped;
|
||||||
|
const isMixedView = needsSchedule && needsRevisions;
|
||||||
|
|
||||||
|
const handleFilterToggle = (filter: FilterTab) => {
|
||||||
|
setSelectedFilters(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(filter)) {
|
||||||
|
next.delete(filter);
|
||||||
|
} else {
|
||||||
|
next.add(filter);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAllClick = () => {
|
||||||
|
setSelectedFilters(new Set());
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: revisionTypesData } = useQuery({
|
||||||
|
queryKey: ['revision-types'],
|
||||||
|
queryFn: () => settingsApi.getRevisionTypes(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: statsData } = useQuery({
|
||||||
|
queryKey: ['revision-stats'],
|
||||||
|
queryFn: () => revisionsApi.getStats(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule queries - vždy bežia, React Query cachuje
|
||||||
|
const scheduleQueryFilters = {
|
||||||
|
typeId: activeTypeFilter || undefined,
|
||||||
|
search: search || undefined,
|
||||||
|
limit: 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: upcomingData, isLoading: upcomingLoading } = useQuery({
|
||||||
|
queryKey: ['revisions-schedule', 'upcoming', activeTypeFilter, search],
|
||||||
|
queryFn: () => revisionsApi.getSchedule({ view: 'upcoming', ...scheduleQueryFilters }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: overdueData, isLoading: overdueLoading } = useQuery({
|
||||||
|
queryKey: ['revisions-schedule', 'overdue', activeTypeFilter, search],
|
||||||
|
queryFn: () => revisionsApi.getSchedule({ view: 'overdue', ...scheduleQueryFilters }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revisions query (skipped / all)
|
||||||
|
const { data: revisionsData, isLoading: revisionsLoading } = useQuery({
|
||||||
|
queryKey: ['revisions', isAllMode ? 'all' : 'skipped', activeTypeFilter, search, page],
|
||||||
|
queryFn: () =>
|
||||||
|
revisionsApi.getAll({
|
||||||
|
status: hasSkipped && !isAllMode ? 'skipped' : undefined,
|
||||||
|
typeId: activeTypeFilter || undefined,
|
||||||
|
search: search || undefined,
|
||||||
|
page,
|
||||||
|
limit: ITEMS_PER_PAGE,
|
||||||
|
}),
|
||||||
|
enabled: needsRevisions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge schedule items podľa vybraných filtrov
|
||||||
|
const mergedScheduleItems = useMemo(() => {
|
||||||
|
if (!needsSchedule) return [];
|
||||||
|
const items: RevisionScheduleItem[] = [
|
||||||
|
...(hasOverdue ? (overdueData?.data || []) : []),
|
||||||
|
...(hasUpcoming ? (upcomingData?.data || []) : []),
|
||||||
|
];
|
||||||
|
items.sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
|
||||||
|
return items;
|
||||||
|
}, [needsSchedule, hasUpcoming, hasOverdue, upcomingData, overdueData]);
|
||||||
|
|
||||||
|
// Frontend stránkovanie pre schedule
|
||||||
|
const scheduleTotal = mergedScheduleItems.length;
|
||||||
|
const scheduleTotalPages = Math.ceil(scheduleTotal / ITEMS_PER_PAGE);
|
||||||
|
const displayedScheduleItems = isMixedView
|
||||||
|
? mergedScheduleItems // mixed → bez stránkovania
|
||||||
|
: mergedScheduleItems.slice((page - 1) * ITEMS_PER_PAGE, page * ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => revisionsApi.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions-schedule'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revision-stats'] });
|
||||||
|
toast.success('Revízia bola vymazaná');
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri mazaní revízie');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipMutation = useMutation({
|
||||||
|
mutationFn: (data: { equipmentId: string; typeId: string; scheduledDate: string; skipReason?: string }) =>
|
||||||
|
revisionsApi.skip(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revisions-schedule'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['revision-stats'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-detail'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['equipment-schedule'] });
|
||||||
|
toast.success('Revízia bola vynechaná');
|
||||||
|
setSkipData(null);
|
||||||
|
setSkipReason('');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Chyba pri vynechaní revízie');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTypeFilter = (typeId: string) => {
|
||||||
|
setActiveTypeFilter(typeId === activeTypeFilter ? '' : typeId);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePerformFromSchedule = (item: RevisionScheduleItem) => {
|
||||||
|
setPreselectedEquipmentId(item.equipmentId);
|
||||||
|
setPreselectedTypeId(item.revisionType.id);
|
||||||
|
setEditingRevision(null);
|
||||||
|
setIsFormOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (revision: RevisionWithEquipment) => {
|
||||||
|
setPreselectedEquipmentId(undefined);
|
||||||
|
setPreselectedTypeId(undefined);
|
||||||
|
setEditingRevision(revision);
|
||||||
|
setIsFormOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseForm = () => {
|
||||||
|
setIsFormOpen(false);
|
||||||
|
setEditingRevision(null);
|
||||||
|
setPreselectedEquipmentId(undefined);
|
||||||
|
setPreselectedTypeId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Otvoriť detail zariadenia
|
||||||
|
const openEquipmentDetail = async (equipmentId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await equipmentApi.getById(equipmentId);
|
||||||
|
setDetailEquipment(response.data as Equipment);
|
||||||
|
} catch {
|
||||||
|
toast.error('Chyba pri načítaní zariadenia');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = statsData?.data;
|
||||||
|
const allCount = stats ? stats.performed + stats.skipped : undefined;
|
||||||
|
|
||||||
|
const scheduleLoading = needsSchedule && ((hasUpcoming && upcomingLoading) || (hasOverdue && overdueLoading));
|
||||||
|
const revsLoading = needsRevisions && revisionsLoading;
|
||||||
|
const isLoading = scheduleLoading || revsLoading;
|
||||||
|
|
||||||
|
const filterTabs: { key: FilterTab; label: string; count?: number; icon: typeof CalendarClock; color: string }[] = [
|
||||||
|
{ key: 'upcoming', label: 'Nadchádzajúce', count: stats?.upcoming, icon: CalendarClock, color: 'text-blue-600' },
|
||||||
|
{ key: 'overdue', label: 'Po termíne', count: stats?.overdue, icon: AlertTriangle, color: 'text-destructive' },
|
||||||
|
{ key: 'skipped', label: 'Vynechané', count: stats?.skipped, icon: SkipForward, color: 'text-muted-foreground' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Stránkovanie pre schedule (len keď nie je mixed)
|
||||||
|
const schedulePagination = !isMixedView && needsSchedule && !needsRevisions && scheduleTotalPages > 1
|
||||||
|
? { page, total: scheduleTotal, totalPages: scheduleTotalPages, limit: ITEMS_PER_PAGE, hasPrev: page > 1, hasNext: page < scheduleTotalPages }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Stránkovanie pre revisions (server-side)
|
||||||
|
const revisionsPagination = !isMixedView && needsRevisions && !needsSchedule
|
||||||
|
? revisionsData?.pagination
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Revízie</h1>
|
||||||
|
<Button onClick={() => { setEditingRevision(null); setPreselectedEquipmentId(undefined); setPreselectedTypeId(undefined); setIsFormOpen(true); }}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Pridať záznam
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Štatistiky - multi-select filtrovacie tlačidlá */}
|
||||||
|
<div className="grid gap-3 grid-cols-2 md:grid-cols-4">
|
||||||
|
{filterTabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
const isActive = selectedFilters.has(tab.key);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => handleFilterToggle(tab.key)}
|
||||||
|
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
||||||
|
: 'border-border bg-card hover:bg-muted/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`h-5 w-5 ${isActive ? 'text-primary' : tab.color}`} />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">{tab.count ?? '-'}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{tab.label}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
onClick={handleAllClick}
|
||||||
|
className={`flex items-center gap-3 rounded-lg border p-4 text-left transition-colors ${
|
||||||
|
isAllMode
|
||||||
|
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
||||||
|
: 'border-border bg-card hover:bg-muted/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<List className={`h-5 w-5 ${isAllMode ? 'text-primary' : 'text-muted-foreground'}`} />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">{allCount ?? '-'}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Všetky</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
|
{/* Vyhľadávanie */}
|
||||||
|
<div className="relative flex-1 max-w-sm">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Hľadať (zariadenie, adresa, poznámky)..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Typ filtre - toggle tlačidlá */}
|
||||||
|
{revisionTypesData?.data && revisionTypesData.data.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{revisionTypesData.data.map((rt) => {
|
||||||
|
const isSelected = activeTypeFilter === rt.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={rt.id}
|
||||||
|
onClick={() => handleTypeFilter(rt.id)}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'border-primary bg-primary/10 text-primary font-medium'
|
||||||
|
: 'border-border bg-background text-muted-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{rt.color && (
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{ backgroundColor: rt.color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{rt.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingOverlay />
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Schedule tabuľka (upcoming / overdue) */}
|
||||||
|
{needsSchedule && displayedScheduleItems.length > 0 && (
|
||||||
|
<div>
|
||||||
|
{isMixedView && (
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
||||||
|
Plánované revízie ({scheduleTotal})
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Zariadenie</TableHead>
|
||||||
|
<TableHead>Zákazník</TableHead>
|
||||||
|
<TableHead>Typ revízie</TableHead>
|
||||||
|
<TableHead>Termín</TableHead>
|
||||||
|
<TableHead>Stav</TableHead>
|
||||||
|
<TableHead>Posledná revízia</TableHead>
|
||||||
|
<TableHead className="text-right">Akcie</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{displayedScheduleItems.map((item, idx) => {
|
||||||
|
const status = getStatus(item.dueDate, 'schedule');
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={`${item.equipmentId}-${item.revisionType.id}-${idx}`}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => openEquipmentDetail(item.equipmentId)}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<div className="font-medium">{item.equipmentName}</div>
|
||||||
|
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||||
|
{item.equipmentAddress}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{item.customer?.name || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge color={item.revisionType.color}>{item.revisionType.name}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{formatDate(item.dueDate)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{StatusIcon && <StatusIcon className="h-3.5 w-3.5" style={{ color: status.color || undefined }} />}
|
||||||
|
<Badge color={status.color || undefined} variant={status.variant}>{status.label}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.lastPerformedDate ? formatDate(item.lastPerformedDate) : '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handlePerformFromSchedule(item); }}
|
||||||
|
title="Vykonať revíziu"
|
||||||
|
>
|
||||||
|
<ClipboardCheck className="h-4 w-4 text-green-600" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{schedulePagination && renderPagination(schedulePagination)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Prázdny stav pre schedule */}
|
||||||
|
{needsSchedule && !needsRevisions && displayedScheduleItems.length === 0 && !isLoading && (
|
||||||
|
<p className="text-center text-muted-foreground py-8">
|
||||||
|
{hasUpcoming && hasOverdue
|
||||||
|
? 'Žiadne plánované revízie'
|
||||||
|
: hasUpcoming
|
||||||
|
? 'Žiadne nadchádzajúce revízie'
|
||||||
|
: 'Žiadne revízie po termíne'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Revisions tabuľka (skipped / all) */}
|
||||||
|
{needsRevisions && (
|
||||||
|
<div>
|
||||||
|
{isMixedView && (
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
|
||||||
|
Vynechané revízie ({revisionsData?.pagination?.total || 0})
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{revisionsData?.data && revisionsData.data.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Zariadenie</TableHead>
|
||||||
|
<TableHead>Zákazník</TableHead>
|
||||||
|
<TableHead>Typ revízie</TableHead>
|
||||||
|
<TableHead>Dátum</TableHead>
|
||||||
|
<TableHead>Stav</TableHead>
|
||||||
|
{isAllMode && <TableHead>Výsledok</TableHead>}
|
||||||
|
{hasSkipped && !isAllMode && <TableHead>Dôvod</TableHead>}
|
||||||
|
<TableHead>Vykonal</TableHead>
|
||||||
|
<TableHead>Poznámka</TableHead>
|
||||||
|
<TableHead className="text-right">Akcie</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{revisionsData.data.map((revision) => {
|
||||||
|
const status = getStatus(revision.nextDueDate, revision.status === 'skipped' ? 'skipped' : 'performed');
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={revision.id}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => openEquipmentDetail(revision.equipmentId)}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<div className="font-medium">{revision.equipment.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||||
|
{revision.equipment.address}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{revision.equipment.customer?.name || '-'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge color={revision.type.color}>{revision.type.name}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDate(revision.performedDate)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{StatusIcon && <StatusIcon className="h-3.5 w-3.5" style={{ color: status.color || undefined }} />}
|
||||||
|
<Badge color={status.color || undefined} variant={status.variant}>{status.label}</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{isAllMode && (
|
||||||
|
<TableCell>{revision.status === 'skipped' ? (revision.skipReason || '-') : (revision.result || '-')}</TableCell>
|
||||||
|
)}
|
||||||
|
{hasSkipped && !isAllMode && (
|
||||||
|
<TableCell className="max-w-[200px] truncate">
|
||||||
|
{revision.skipReason || '-'}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell>{revision.performedBy?.name || '-'}</TableCell>
|
||||||
|
<TableCell className="max-w-[150px] truncate">
|
||||||
|
{revision.notes || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{revision.status === 'performed' && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); handleEdit(revision); }}>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); setDeleteConfirm(revision); }}>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{revisionsPagination && renderPagination(revisionsPagination)}
|
||||||
|
</>
|
||||||
|
) : !isMixedView ? (
|
||||||
|
<p className="text-center text-muted-foreground py-8">
|
||||||
|
{isAllMode ? 'Žiadne revízie' : 'Žiadne vynechané revízie'}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Equipment Detail Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={!!detailEquipment}
|
||||||
|
onClose={() => setDetailEquipment(null)}
|
||||||
|
title={detailEquipment?.name || 'Detail zariadenia'}
|
||||||
|
size="5xl"
|
||||||
|
>
|
||||||
|
{detailEquipment && (
|
||||||
|
<EquipmentDetail
|
||||||
|
equipment={detailEquipment}
|
||||||
|
onNewRevision={(typeId) => {
|
||||||
|
setPreselectedEquipmentId(detailEquipment.id);
|
||||||
|
setPreselectedTypeId(typeId);
|
||||||
|
setEditingRevision(null);
|
||||||
|
setDetailEquipment(null);
|
||||||
|
setIsFormOpen(true);
|
||||||
|
}}
|
||||||
|
onSkipRevision={(date, typeId) => {
|
||||||
|
setSkipData({
|
||||||
|
equipmentId: detailEquipment.id,
|
||||||
|
equipmentName: detailEquipment.name,
|
||||||
|
typeId,
|
||||||
|
date,
|
||||||
|
});
|
||||||
|
setSkipReason('');
|
||||||
|
}}
|
||||||
|
onEditRevision={(revision) => {
|
||||||
|
setEditingRevision(revision as RevisionWithEquipment);
|
||||||
|
setDetailEquipment(null);
|
||||||
|
setIsFormOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isFormOpen}
|
||||||
|
onClose={handleCloseForm}
|
||||||
|
title={editingRevision ? 'Upraviť záznam' : 'Pridať záznam'}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<RevisionForm
|
||||||
|
revision={editingRevision}
|
||||||
|
onClose={handleCloseForm}
|
||||||
|
preselectedEquipmentId={preselectedEquipmentId}
|
||||||
|
preselectedTypeId={preselectedTypeId}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Delete Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={!!deleteConfirm}
|
||||||
|
onClose={() => setDeleteConfirm(null)}
|
||||||
|
title="Potvrdiť vymazanie"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Naozaj chcete vymazať revíziu pre "{deleteConfirm?.equipment.name}" zo dňa{' '}
|
||||||
|
{deleteConfirm?.performedDate ? formatDate(deleteConfirm.performedDate) : ''}?
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
||||||
|
Zrušiť
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => deleteConfirm && deleteMutation.mutate(deleteConfirm.id)}
|
||||||
|
isLoading={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
Vymazať
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Skip Modal (z equipment detail) */}
|
||||||
|
<Modal
|
||||||
|
isOpen={!!skipData}
|
||||||
|
onClose={() => { setSkipData(null); setSkipReason(''); }}
|
||||||
|
title="Vynechať revíziu"
|
||||||
|
>
|
||||||
|
{skipData && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p>
|
||||||
|
Naozaj chcete vynechať revíziu pre zariadenie{' '}
|
||||||
|
<strong>{skipData.equipmentName}</strong> s termínom{' '}
|
||||||
|
<strong>{formatDate(skipData.date)}</strong>?
|
||||||
|
</p>
|
||||||
|
<Textarea
|
||||||
|
label="Dôvod vynechania (voliteľný)"
|
||||||
|
value={skipReason}
|
||||||
|
onChange={(e) => setSkipReason(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Napr. zariadenie dočasne mimo prevádzky..."
|
||||||
|
/>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="outline" onClick={() => { setSkipData(null); setSkipReason(''); }}>
|
||||||
|
Zrušiť
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => skipMutation.mutate({
|
||||||
|
equipmentId: skipData.equipmentId,
|
||||||
|
typeId: skipData.typeId,
|
||||||
|
scheduledDate: skipData.date.split('T')[0],
|
||||||
|
skipReason: skipReason || undefined,
|
||||||
|
})}
|
||||||
|
isLoading={skipMutation.isPending}
|
||||||
|
>
|
||||||
|
<SkipForward className="mr-2 h-4 w-4" />
|
||||||
|
Vynechať
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderPagination(pagination: { page: number; total: number; totalPages: number; limit: number; hasPrev: boolean; hasNext: boolean } | null | undefined) {
|
||||||
|
if (!pagination || pagination.totalPages <= 1) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between border-t pt-4 mt-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Zobrazujem {(pagination.page - 1) * pagination.limit + 1} -{' '}
|
||||||
|
{Math.min(pagination.page * pagination.limit, pagination.total)} z{' '}
|
||||||
|
{pagination.total}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
|
disabled={!pagination.hasPrev}
|
||||||
|
>
|
||||||
|
Predchádzajúca
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
disabled={!pagination.hasNext}
|
||||||
|
>
|
||||||
|
Nasledujúca
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/src/pages/revisions/index.ts
Normal file
1
frontend/src/pages/revisions/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { RevisionsList } from './RevisionsList';
|
||||||
@@ -21,14 +21,39 @@ export interface CreateEquipmentData {
|
|||||||
partNumber?: string;
|
partNumber?: string;
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
installDate?: string;
|
installDate?: string;
|
||||||
|
revisionCycleStart?: string;
|
||||||
warrantyEnd?: string;
|
warrantyEnd?: string;
|
||||||
warrantyStatus?: string;
|
warrantyStatus?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
revisionTypeIds?: string[];
|
||||||
tempId?: string; // For pending file uploads
|
tempId?: string; // For pending file uploads
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EquipmentScheduleItem {
|
||||||
|
revisionType: { id: string; name: string; color?: string; intervalDays: number };
|
||||||
|
lastPerformed: string | null;
|
||||||
|
nextDueDate: string | null;
|
||||||
|
upcomingCycles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EquipmentSchedule {
|
||||||
|
cycleAnchor: string;
|
||||||
|
schedules: EquipmentScheduleItem[];
|
||||||
|
upcomingDates: Array<{
|
||||||
|
date: string;
|
||||||
|
label: string;
|
||||||
|
revisionTypes: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
intervalDays: number;
|
||||||
|
cycleNumber: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateEquipmentData = Partial<CreateEquipmentData>;
|
export type UpdateEquipmentData = Partial<CreateEquipmentData>;
|
||||||
|
|
||||||
export interface CreateRevisionData {
|
export interface CreateRevisionData {
|
||||||
@@ -73,4 +98,8 @@ export const equipmentApi = {
|
|||||||
|
|
||||||
createRevision: (equipmentId: string, data: CreateRevisionData) =>
|
createRevision: (equipmentId: string, data: CreateRevisionData) =>
|
||||||
post<Revision>(`/equipment/${equipmentId}/revisions`, data),
|
post<Revision>(`/equipment/${equipmentId}/revisions`, data),
|
||||||
|
|
||||||
|
// Schedule
|
||||||
|
getSchedule: (equipmentId: string, days?: number) =>
|
||||||
|
get<EquipmentSchedule>(`/equipment/${equipmentId}/schedule${days ? `?days=${days}` : ''}`),
|
||||||
};
|
};
|
||||||
|
|||||||
111
frontend/src/services/revisions.api.ts
Normal file
111
frontend/src/services/revisions.api.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { get, getPaginated, post, put, del } from './api';
|
||||||
|
import type { Revision, RevisionScheduleItem } from '@/types';
|
||||||
|
|
||||||
|
export interface RevisionFilters {
|
||||||
|
search?: string;
|
||||||
|
equipmentId?: string;
|
||||||
|
typeId?: string;
|
||||||
|
customerId?: string;
|
||||||
|
status?: string;
|
||||||
|
dueSoon?: string;
|
||||||
|
overdue?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleFilters {
|
||||||
|
view?: 'upcoming' | 'overdue';
|
||||||
|
typeId?: string;
|
||||||
|
customerId?: string;
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRevisionData {
|
||||||
|
equipmentId: string;
|
||||||
|
typeId: string;
|
||||||
|
performedDate: string;
|
||||||
|
nextDueDate?: string;
|
||||||
|
findings?: string;
|
||||||
|
result?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkipRevisionData {
|
||||||
|
equipmentId: string;
|
||||||
|
typeId: string;
|
||||||
|
scheduledDate: string;
|
||||||
|
skipReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateRevisionData = Partial<Omit<CreateRevisionData, 'equipmentId'>>;
|
||||||
|
|
||||||
|
export interface RevisionStats {
|
||||||
|
upcoming: number;
|
||||||
|
overdue: number;
|
||||||
|
performed: number;
|
||||||
|
skipped: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryString(filters: RevisionFilters): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.search) params.append('search', filters.search);
|
||||||
|
if (filters.equipmentId) params.append('equipmentId', filters.equipmentId);
|
||||||
|
if (filters.typeId) params.append('typeId', filters.typeId);
|
||||||
|
if (filters.customerId) params.append('customerId', filters.customerId);
|
||||||
|
if (filters.status) params.append('status', filters.status);
|
||||||
|
if (filters.dueSoon) params.append('dueSoon', filters.dueSoon);
|
||||||
|
if (filters.overdue) params.append('overdue', filters.overdue);
|
||||||
|
if (filters.page) params.append('page', String(filters.page));
|
||||||
|
if (filters.limit) params.append('limit', String(filters.limit));
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScheduleQueryString(filters: ScheduleFilters): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.view) params.append('view', filters.view);
|
||||||
|
if (filters.typeId) params.append('typeId', filters.typeId);
|
||||||
|
if (filters.customerId) params.append('customerId', filters.customerId);
|
||||||
|
if (filters.search) params.append('search', filters.search);
|
||||||
|
if (filters.page) params.append('page', String(filters.page));
|
||||||
|
if (filters.limit) params.append('limit', String(filters.limit));
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RevisionWithEquipment extends Revision {
|
||||||
|
isLatest?: boolean;
|
||||||
|
equipment: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
type: { id: string; name: string; color?: string };
|
||||||
|
customer?: { id: string; name: string } | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revisionsApi = {
|
||||||
|
getAll: (filters: RevisionFilters = {}) =>
|
||||||
|
getPaginated<RevisionWithEquipment>(`/revisions?${buildQueryString(filters)}`),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
get<RevisionWithEquipment>(`/revisions/${id}`),
|
||||||
|
|
||||||
|
create: (data: CreateRevisionData) =>
|
||||||
|
post<RevisionWithEquipment>('/revisions', data),
|
||||||
|
|
||||||
|
update: (id: string, data: UpdateRevisionData) =>
|
||||||
|
put<RevisionWithEquipment>(`/revisions/${id}`, data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
del<void>(`/revisions/${id}`),
|
||||||
|
|
||||||
|
getStats: () =>
|
||||||
|
get<RevisionStats>('/revisions/stats'),
|
||||||
|
|
||||||
|
getSchedule: (filters: ScheduleFilters = {}) =>
|
||||||
|
getPaginated<RevisionScheduleItem>(`/revisions/schedule?${buildScheduleQueryString(filters)}`),
|
||||||
|
|
||||||
|
skip: (data: SkipRevisionData) =>
|
||||||
|
post<RevisionWithEquipment>('/revisions/skip', data),
|
||||||
|
};
|
||||||
@@ -64,6 +64,7 @@ export const settingsApi = {
|
|||||||
|
|
||||||
// System Settings
|
// System Settings
|
||||||
getSystemSettings: () => get<SystemSetting[]>('/settings/system'),
|
getSystemSettings: () => get<SystemSetting[]>('/settings/system'),
|
||||||
|
getSystemSetting: (key: string) => get<SystemSetting>(`/settings/system/${key}`),
|
||||||
updateSystemSetting: (key: string, value: unknown) => put<SystemSetting>(`/settings/system/${key}`, { value }),
|
updateSystemSetting: (key: string, value: unknown) => put<SystemSetting>(`/settings/system/${key}`, { value }),
|
||||||
|
|
||||||
// Users (admin)
|
// Users (admin)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export interface Equipment {
|
|||||||
partNumber?: string;
|
partNumber?: string;
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
installDate?: string;
|
installDate?: string;
|
||||||
|
revisionCycleStart?: string;
|
||||||
warrantyEnd?: string;
|
warrantyEnd?: string;
|
||||||
warrantyStatus?: string;
|
warrantyStatus?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -128,6 +129,7 @@ export interface Equipment {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
revisionSchedules?: Array<{ revisionType: RevisionType }>;
|
||||||
_count?: {
|
_count?: {
|
||||||
revisions: number;
|
revisions: number;
|
||||||
};
|
};
|
||||||
@@ -138,6 +140,7 @@ export interface Revision {
|
|||||||
equipmentId: string;
|
equipmentId: string;
|
||||||
typeId: string;
|
typeId: string;
|
||||||
type: RevisionType;
|
type: RevisionType;
|
||||||
|
status: 'performed' | 'skipped';
|
||||||
performedDate: string;
|
performedDate: string;
|
||||||
nextDueDate?: string;
|
nextDueDate?: string;
|
||||||
performedById: string;
|
performedById: string;
|
||||||
@@ -145,9 +148,22 @@ export interface Revision {
|
|||||||
findings?: string;
|
findings?: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
skipReason?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RevisionScheduleItem {
|
||||||
|
equipmentId: string;
|
||||||
|
equipmentName: string;
|
||||||
|
equipmentAddress: string;
|
||||||
|
customer: { id: string; name: string } | null;
|
||||||
|
revisionType: { id: string; name: string; color?: string; intervalDays: number };
|
||||||
|
dueDate: string;
|
||||||
|
daysUntil: number;
|
||||||
|
lastPerformedDate: string | null;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
// RMA
|
// RMA
|
||||||
export interface RMA {
|
export interface RMA {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user