From da265ff0975a0a19b71a6a93f387a68a9c544944 Mon Sep 17 00:00:00 2001 From: pettrop Date: Mon, 23 Feb 2026 21:59:23 +0100 Subject: [PATCH] =?UTF-8?q?Rev=C3=ADzny=20syst=C3=A9m=20-=20kompletn=C3=A1?= =?UTF-8?q?=20implement=C3=A1cia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- HELPDESK_INIT_V2.md | 247 +++-- backend/prisma/schema.prisma | 37 +- backend/prisma/seed-data.ts | 849 ++++++++++++++++++ backend/prisma/seed.ts | 505 ++++++++++- .../src/controllers/dashboard.controller.ts | 18 +- .../src/controllers/equipment.controller.ts | 294 +++++- .../src/controllers/revisions.controller.ts | 615 +++++++++++++ backend/src/controllers/tasks.controller.ts | 50 +- backend/src/routes/equipment.routes.ts | 3 + backend/src/routes/index.ts | 2 + backend/src/routes/revisions.routes.ts | 23 + backend/src/utils/revisionSchedule.ts | 184 ++++ backend/src/utils/validators.ts | 29 + frontend/src/App.tsx | 2 + frontend/src/components/layout/Sidebar.tsx | 2 + frontend/src/components/ui/Modal.tsx | 8 +- frontend/src/hooks/useRevisionStatus.ts | 102 +++ frontend/src/pages/Dashboard.tsx | 3 +- .../src/pages/equipment/EquipmentDetail.tsx | 318 +++++++ .../src/pages/equipment/EquipmentForm.tsx | 79 +- .../src/pages/equipment/EquipmentList.tsx | 232 ++++- frontend/src/pages/revisions/RevisionForm.tsx | 318 +++++++ .../src/pages/revisions/RevisionsList.tsx | 658 ++++++++++++++ frontend/src/pages/revisions/index.ts | 1 + frontend/src/services/equipment.api.ts | 29 + frontend/src/services/revisions.api.ts | 111 +++ frontend/src/services/settings.api.ts | 1 + frontend/src/types/index.ts | 16 + 28 files changed, 4587 insertions(+), 149 deletions(-) create mode 100644 backend/prisma/seed-data.ts create mode 100644 backend/src/controllers/revisions.controller.ts create mode 100644 backend/src/routes/revisions.routes.ts create mode 100644 backend/src/utils/revisionSchedule.ts create mode 100644 frontend/src/hooks/useRevisionStatus.ts create mode 100644 frontend/src/pages/equipment/EquipmentDetail.tsx create mode 100644 frontend/src/pages/revisions/RevisionForm.tsx create mode 100644 frontend/src/pages/revisions/RevisionsList.tsx create mode 100644 frontend/src/pages/revisions/index.ts create mode 100644 frontend/src/services/revisions.api.ts diff --git a/HELPDESK_INIT_V2.md b/HELPDESK_INIT_V2.md index df5de6f..0addb14 100644 --- a/HELPDESK_INIT_V2.md +++ b/HELPDESK_INIT_V2.md @@ -6,7 +6,7 @@ **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ť **Jazyk UI:** Slovenčina -**Dátum:** 02.02.2026 +**Dátum:** 23.02.2026 --- @@ -232,12 +232,13 @@ model RevisionType { description String? order Int @default(0) active Boolean @default(true) - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - revisions Revision[] - + + revisions Revision[] + equipmentSchedules EquipmentRevisionSchedule[] + @@index([active]) @@index([order]) } @@ -646,24 +647,26 @@ model Equipment { partNumber String? // PN serialNumber String? // SN - installDate DateTime? - warrantyEnd DateTime? - warrantyStatus String? // "ACTIVE", "EXPIRED", "EXTENDED" - + installDate DateTime? + revisionCycleStart DateTime? // Anchor pre výpočet revíznych cyklov (default = installDate) + warrantyEnd DateTime? + warrantyStatus String? // "ACTIVE", "EXPIRED", "EXTENDED" + description String? @db.Text notes String? @db.Text - + active Boolean @default(true) - + createdById String createdBy User @relation("EquipmentCreator", fields: [createdById], references: [id]) - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - revisions Revision[] - attachments EquipmentAttachment[] - tags EquipmentTag[] + + revisions Revision[] + revisionSchedules EquipmentRevisionSchedule[] // Priradené typy revízií + attachments EquipmentAttachment[] + tags EquipmentTag[] @@index([typeId]) @@index([customerId]) @@ -672,37 +675,57 @@ model Equipment { @@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 { id String @id @default(cuid()) - + equipmentId String equipment Equipment @relation(fields: [equipmentId], references: [id], onDelete: Cascade) - - // Type relation (namiesto enum) + typeId String type RevisionType @relation(fields: [typeId], references: [id]) - + + status String @default("performed") // "performed" | "skipped" + performedDate DateTime - nextDueDate DateTime? // Auto-calculated: performedDate + type.intervalDays - + nextDueDate DateTime? // Auto-calculated z cyklového anchoru + performedById String performedBy User @relation(fields: [performedById], references: [id]) - + findings String? @db.Text result String? // "OK", "MINOR_ISSUES", "CRITICAL" notes String? @db.Text - + skipReason String? // Dôvod preskočenia (ak status = "skipped") + reminderSent Boolean @default(false) reminderDate DateTime? // Auto-calculated: nextDueDate - type.reminderDays - + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + @@index([equipmentId]) @@index([typeId]) @@index([performedById]) @@index([nextDueDate]) @@index([reminderDate]) + @@index([status]) } model EquipmentAttachment { @@ -1008,25 +1031,43 @@ POST /api/customers/import // Import z externej DB ### **🆕 Equipment** ``` -GET /api/equipment -POST /api/equipment -GET /api/equipment/:id -PUT /api/equipment/:id -DELETE /api/equipment/:id -GET /api/equipment/:id/revisions -POST /api/equipment/:id/revisions -POST /api/equipment/:id/attachments -GET /api/equipment/reminders // Upcoming revisions +GET /api/equipment // Zoznam zariadení (stránkovaný, search, filtre) +POST /api/equipment // Vytvorenie zariadenia + revisionSchedules +GET /api/equipment/reminders // Upcoming revisions (PRED /:id!) +GET /api/equipment/:id // Detail zariadenia +PUT /api/equipment/:id // Úprava zariadenia + revisionSchedules +DELETE /api/equipment/:id // Soft delete (deaktivácia) +GET /api/equipment/:id/schedule // Revízny plán zariadenia (nadchádzajúce dátumy) +GET /api/equipment/:id/revisions // História revízií zariadenia +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** ``` -GET /api/revisions -GET /api/revisions/upcoming -GET /api/revisions/overdue -PUT /api/revisions/:id -DELETE /api/revisions/:id +GET /api/revisions // Zoznam revízií (stránkovaný, status filter, search) +GET /api/revisions/stats // Štatistiky: { upcoming, overdue, performed, skipped } +GET /api/revisions/schedule // Agregovaný plán zo VŠETKÝCH zariadení (view=upcoming|overdue) +POST /api/revisions/skip // Preskočenie plánovanej revízie (s dôvodom) +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** @@ -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 ### Rozšírená Štruktúra @@ -1192,11 +1297,13 @@ src/ │ │ │ ├── equipment/ # NEW │ │ ├── EquipmentList.tsx -│ │ ├── EquipmentDetail.tsx -│ │ ├── EquipmentForm.tsx -│ │ ├── RevisionForm.tsx -│ │ ├── RevisionCalendar.tsx -│ │ └── EquipmentCard.tsx +│ │ ├── EquipmentDetail.tsx # Detail s revíznym plánom, históriou, prílohami +│ │ ├── EquipmentForm.tsx # Formulár s revisionSchedules (typ revízií) +│ │ └── RevisionForm.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 │ │ ├── RMAList.tsx @@ -1233,6 +1340,19 @@ src/ │ │ └── Toast.tsx │ │ │ └── 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 Comments (s kontrolou oprávnení - len autor/priradený) - [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] **Settings API** (CRUD pre všetky config tables) - [x] **Dashboard API** (`/dashboard`, `/dashboard/today`, `/dashboard/stats`) @@ -1295,7 +1415,7 @@ src/ ✅ Priradenie používateľov na úlohy (multi-select) ✅ Dashboard s mojimi úlohami a projektmi + urgentné úlohy ✅ Zákazníci -✅ Zariadenia (bez revízií) +✅ Zariadenia (s revíznymi plánmi) ✅ RMA (bez workflow) ✅ ROOT môže konfigurovať všetko cez Settings ✅ Žiadne hardcoded ENUMs @@ -1329,10 +1449,15 @@ cd backend && npx prisma db seed **Cieľ:** Swimlanes, revízie, RMA workflow, reminders, notifikácie **Backend:** -- [ ] **Revision system** - - [ ] Create revision endpoint - - [ ] Auto-calculate nextDueDate - - [ ] Reminder scheduler +- [x] **Revision system** ✅ + - [x] CRUD endpoints (create, read, update, delete) + - [x] Auto-calculate nextDueDate (cyklový anchor, pozičné labeling) + - [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** - [ ] Status transitions validation - [ ] Approval flow (customer RMAs) @@ -1358,10 +1483,19 @@ cd backend && npx prisma db seed - [ ] Drag & Drop - [ ] Collapse/expand - [ ] Progress indicators -- [ ] **Equipment Management** - - [ ] Revision form - - [ ] Revision calendar view - - [ ] Reminder notifications +- [x] **Equipment Management** ✅ + - [x] EquipmentList - zoznam zariadení s filtrami + - [x] EquipmentForm - formulár s revisionSchedules + - [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** - [ ] Status change UI - [ ] Approval buttons (admin) @@ -1387,7 +1521,8 @@ cd backend && npx prisma db seed ``` ✅ Všetko z Fázy 1 + ⏳ 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 ⏳ Email notifikácie ⏳ Live updates (WebSocket) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index cde999a..354c1fb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -109,7 +109,8 @@ model RevisionType { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - revisions Revision[] + revisions Revision[] + equipmentSchedules EquipmentRevisionSchedule[] @@index([active]) @@index([order]) @@ -492,9 +493,10 @@ model Equipment { partNumber String? serialNumber String? - installDate DateTime? - warrantyEnd DateTime? - warrantyStatus String? + installDate DateTime? + revisionCycleStart DateTime? + warrantyEnd DateTime? + warrantyStatus String? description String? notes String? @@ -507,9 +509,10 @@ model Equipment { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - revisions Revision[] - attachments EquipmentAttachment[] - tags EquipmentTag[] + revisions Revision[] + revisionSchedules EquipmentRevisionSchedule[] + attachments EquipmentAttachment[] + tags EquipmentTag[] @@index([typeId]) @@index([customerId]) @@ -518,6 +521,22 @@ model Equipment { @@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 { id String @id @default(cuid()) @@ -527,6 +546,8 @@ model Revision { typeId String type RevisionType @relation(fields: [typeId], references: [id]) + status String @default("performed") // "performed" | "skipped" + performedDate DateTime nextDueDate DateTime? @@ -536,6 +557,7 @@ model Revision { findings String? result String? notes String? + skipReason String? reminderSent Boolean @default(false) reminderDate DateTime? @@ -548,6 +570,7 @@ model Revision { @@index([performedById]) @@index([nextDueDate]) @@index([reminderDate]) + @@index([status]) } model EquipmentAttachment { diff --git a/backend/prisma/seed-data.ts b/backend/prisma/seed-data.ts new file mode 100644 index 0000000..8dfeb23 --- /dev/null +++ b/backend/prisma/seed-data.ts @@ -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 = { + 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 = { + 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 = { + 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.'], +]; diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 5cd4ae0..7a2fe5e 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -1,8 +1,44 @@ import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcryptjs'; +import { + AUDIT_MAP, + PERIOD_TO_TYPE, + ADDITIONAL_EQUIP_TYPES, + CUSTOMERS, + EQUIPMENT, + SERVICE_RECORDS, +} from './seed-data'; 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() { console.log('Seeding database...'); @@ -88,10 +124,17 @@ async function seed() { skipDuplicates: true, data: [ { 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: 'ACCESS', name: 'Prístupový systém', color: '#F59E0B', order: 4 }, { 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({ skipDuplicates: true, data: [ - { code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 1 }, - { code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 2 }, - { code: 'ANNUAL', name: 'Ročná revízia', intervalDays: 365, reminderDays: 30, color: '#DC2626', order: 3 }, - { code: 'EMERGENCY', name: 'Mimoriadna revízia', intervalDays: 0, reminderDays: 0, color: '#DC2626', order: 4 }, + { code: 'MONTHLY', name: 'Mesačná revízia', intervalDays: 30, reminderDays: 7, color: '#6366F1', order: 1 }, + { code: 'QUARTERLY', name: 'Štvrťročná revízia', intervalDays: 90, reminderDays: 14, color: '#FFA500', order: 2 }, + { code: 'BIANNUAL', name: 'Polročná revízia', intervalDays: 180, reminderDays: 21, color: '#FBBF24', order: 3 }, + { 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: '1 hodina', minutes: 60 }, { 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', 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".', 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 ===== - console.log('Creating demo users...'); + // ===== USERS (demo + real from old DB) ===== + console.log('Creating users...'); const rootRole = roles.find(r => r.code === 'ROOT'); const adminRole = roles.find(r => r.code === 'ADMIN'); const userRole = roles.find(r => r.code === 'USER'); + const customerRole = roles.find(r => r.code === 'CUSTOMER'); - if (rootRole && adminRole && userRole) { - await prisma.user.upsert({ - where: { email: 'root@helpdesk.sk' }, - update: { password: await bcrypt.hash('root123', 10) }, + if (!rootRole || !adminRole || !userRole || !customerRole) { + throw new Error('Required roles not found'); + } + + 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: { - email: 'root@helpdesk.sk', - password: await bcrypt.hash('root123', 10), - name: 'Root Admin', + email: 'root@root.sk', + password: hashedRoot, + name: 'Root Používateľ', roleId: rootRole.id, }, - }); - - await prisma.user.upsert({ - where: { email: 'admin@helpdesk.sk' }, - update: { password: await bcrypt.hash('admin123', 10) }, + }), + prisma.user.upsert({ + where: { email: 'admin@admin.sk' }, + update: {}, create: { - email: 'admin@helpdesk.sk', - password: await bcrypt.hash('admin123', 10), - name: 'Peter Admin', + email: 'admin@admin.sk', + password: hashedAdmin, + name: 'Admin Používateľ', roleId: adminRole.id, }, - }); - - await prisma.user.upsert({ - where: { email: 'user@helpdesk.sk' }, - update: { password: await bcrypt.hash('user123', 10) }, + }), + prisma.user.upsert({ + where: { email: 'user1@user.sk' }, + update: {}, create: { - email: 'user@helpdesk.sk', - password: await bcrypt.hash('user123', 10), - name: 'Martin Používateľ', + email: 'user1@user.sk', + password: hashedUser, + name: 'Ján Technik', 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 = {}; + + 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; + vychodziaDate: string | null; + } + + const equipMeta: Map = 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 = {}; + for (const t of equipTypes) { + equipTypeMap[t.code] = t.id; + } + + const equipmentIdMap: Record = {}; // 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 = {}; + 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(); + 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 => 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!'); } diff --git a/backend/src/controllers/dashboard.controller.ts b/backend/src/controllers/dashboard.controller.ts index 9f11b9a..cee0e7f 100644 --- a/backend/src/controllers/dashboard.controller.ts +++ b/backend/src/controllers/dashboard.controller.ts @@ -81,13 +81,15 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis const userId = req.user!.userId; - const [myTasks, myProjects, recentRMAs] = await Promise.all([ - // Tasks assigned to me that are not completed + const myTasksWhere = { + 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({ - where: { - assignees: { some: { userId } }, - status: { isFinal: false }, - }, + where: myTasksWhere, include: { status: true, priority: true, @@ -99,6 +101,9 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis take: 10, }), + // Total count of my active tasks (without limit) + prisma.task.count({ where: myTasksWhere }), + // My active projects prisma.project.findMany({ where: { @@ -133,6 +138,7 @@ export const getDashboardToday = async (req: AuthRequest, res: Response): Promis successResponse(res, { myTasks, + myTasksTotal, myProjects, recentRMAs, }); diff --git a/backend/src/controllers/equipment.controller.ts b/backend/src/controllers/equipment.controller.ts index cbd9494..412dd54 100644 --- a/backend/src/controllers/equipment.controller.ts +++ b/backend/src/controllers/equipment.controller.ts @@ -3,6 +3,13 @@ import prisma from '../config/database'; import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers'; import { AuthRequest } from '../middleware/auth.middleware'; 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 => { try { @@ -14,19 +21,23 @@ export const getEquipment = async (req: AuthRequest, res: Response): Promise = { ...(active !== undefined && { active: active === 'true' }), ...(typeId && { typeId }), ...(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>` + 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([ prisma.equipment.findMany({ where, @@ -37,6 +48,7 @@ export const getEquipment = async (req: AuthRequest, res: Response): Promise => { try { + const revisionTypeIds: string[] = req.body.revisionTypeIds || []; + const equipment = await prisma.equipment.create({ data: { name: req.body.name, @@ -101,15 +118,22 @@ export const createEquipment = async (req: AuthRequest, res: Response): Promise< partNumber: req.body.partNumber, serialNumber: req.body.serialNumber, 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, warrantyStatus: req.body.warrantyStatus, description: req.body.description, notes: req.body.notes, createdById: req.user!.userId, + ...(revisionTypeIds.length > 0 && { + revisionSchedules: { + create: revisionTypeIds.map((rtId: string) => ({ revisionTypeId: rtId })), + }, + }), }, include: { type: 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) { 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) { 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({ where: { id }, data: updateData, include: { type: 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 nextDueDate = new Date(performedDate); - nextDueDate.setDate(nextDueDate.getDate() + revisionType.intervalDays); + let nextDueDate: Date; - const reminderDate = new Date(nextDueDate); - reminderDate.setDate(reminderDate.getDate() - revisionType.reminderDays); + const cycleAnchor = equipment.revisionCycleStart || equipment.installDate; + + 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({ 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) { console.error('Error creating revision:', error); 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); } }; + +/** + * 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 => { + 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); + } +}; diff --git a/backend/src/controllers/revisions.controller.ts b/backend/src/controllers/revisions.controller.ts new file mode 100644 index 0000000..933ae51 --- /dev/null +++ b/backend/src/controllers/revisions.controller.ts @@ -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 => { + 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 = { + ...(status && { status }), + ...(equipmentId && { equipmentId }), + ...(typeId && { typeId }), + ...(customerId && { equipment: { customerId } }), + }; + + // Vyhľadávanie bez diakritiky pomocou unaccent + if (search) { + const matchingIds = await prisma.$queryRaw>` + 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>` + 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>` + 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 => { + 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 => { + 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 => { + 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 = {}; + + 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 => { + 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> { + const equipmentWhere: Record = { + 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>` + 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>` + 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(); + 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 => { + 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 => { + 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 => { + 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); + } +}; diff --git a/backend/src/controllers/tasks.controller.ts b/backend/src/controllers/tasks.controller.ts index dac0a70..e7cbd51 100644 --- a/backend/src/controllers/tasks.controller.ts +++ b/backend/src/controllers/tasks.controller.ts @@ -18,21 +18,36 @@ export const getTasks = async (req: AuthRequest, res: Response): Promise = const createdById = getQueryString(req, 'createdById'); const assigneeId = getQueryString(req, 'assigneeId'); - const where = { - ...(projectId && { projectId }), - ...(statusId && { statusId }), - ...(priorityId && { priorityId }), - ...(createdById && { createdById }), - ...(assigneeId && { - assignees: { some: { userId: assigneeId } }, - }), - ...(search && { + // ROOT a ADMIN vidia všetky úlohy, ostatní len svoje (priradené alebo vytvorené) + const userId = req.user!.userId; + const roleCode = req.user!.roleCode; + const isAdmin = roleCode === 'ROOT' || roleCode === 'ADMIN'; + + const conditions: object[] = []; + + if (!isAdmin) { + 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: [ { title: { 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([ prisma.task.findMany({ @@ -95,6 +110,19 @@ export const getTask = async (req: AuthRequest, res: Response): Promise => 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); } catch (error) { console.error('Error fetching task:', error); diff --git a/backend/src/routes/equipment.routes.ts b/backend/src/routes/equipment.routes.ts index 0dea964..d042b10 100644 --- a/backend/src/routes/equipment.routes.ts +++ b/backend/src/routes/equipment.routes.ts @@ -20,6 +20,9 @@ router.get('/:id', canRead('equipment'), equipmentController.getEquipmentById); router.put('/:id', canUpdate('equipment'), equipmentController.updateEquipment); router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipment); +// Schedule +router.get('/:id/schedule', canRead('equipment'), equipmentController.getEquipmentSchedule); + // Revisions router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions); router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index cbd70d7..ecfed6d 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -11,6 +11,7 @@ import dashboardRoutes from './dashboard.routes'; import uploadRoutes from './upload.routes'; import zakazkyRoutes from './zakazky.routes'; import notificationRoutes from './notification.routes'; +import revisionsRoutes from './revisions.routes'; const router = Router(); @@ -20,6 +21,7 @@ router.use('/customers', customersRoutes); router.use('/projects', projectsRoutes); router.use('/tasks', tasksRoutes); router.use('/equipment', equipmentRoutes); +router.use('/revisions', revisionsRoutes); router.use('/rma', rmaRoutes); router.use('/settings', settingsRoutes); router.use('/dashboard', dashboardRoutes); diff --git a/backend/src/routes/revisions.routes.ts b/backend/src/routes/revisions.routes.ts new file mode 100644 index 0000000..9f8a817 --- /dev/null +++ b/backend/src/routes/revisions.routes.ts @@ -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; diff --git a/backend/src/utils/revisionSchedule.ts b/backend/src/utils/revisionSchedule.ts new file mode 100644 index 0000000..cddb2ae --- /dev/null +++ b/backend/src/utils/revisionSchedule.ts @@ -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 { + 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( + 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; + }); +} diff --git a/backend/src/utils/validators.ts b/backend/src/utils/validators.ts index 8916493..3e7248f 100644 --- a/backend/src/utils/validators.ts +++ b/backend/src/utils/validators.ts @@ -84,10 +84,12 @@ export const equipmentSchema = z.object({ partNumber: z.string().optional(), serialNumber: z.string().optional(), installDate: z.string().datetime().optional().or(z.literal('')), + revisionCycleStart: z.string().datetime().optional().or(z.literal('')), warrantyEnd: z.string().datetime().optional().or(z.literal('')), warrantyStatus: z.string().optional(), description: z.string().optional(), notes: z.string().optional(), + revisionTypeIds: z.array(z.string()).optional(), }); // RMA validators @@ -187,6 +189,33 @@ export const tagSchema = z.object({ 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 export const paginationSchema = z.object({ page: z.string().optional().transform((val) => (val ? parseInt(val, 10) : 1)), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9e06ec6..adfebfa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import { TasksList } from '@/pages/tasks'; import { EquipmentList } from '@/pages/equipment'; import { RMAList } from '@/pages/rma'; import { SettingsDashboard } from '@/pages/settings'; +import { RevisionsList } from '@/pages/revisions'; const queryClient = new QueryClient({ defaultOptions: { @@ -87,6 +88,7 @@ function AppRoutes() { } /> } /> } /> + } /> } />
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 }; +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 6692e8b..e3feef0 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -31,6 +31,7 @@ import type { Task } from '@/types'; interface DashboardToday { myTasks: Task[]; + myTasksTotal: number; } // Ikona podľa typu notifikácie @@ -134,7 +135,7 @@ export function Dashboard() { }, {} as Record); // Š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 todayTasks = today?.myTasks?.filter(t => { if (!t.deadline) return false; diff --git a/frontend/src/pages/equipment/EquipmentDetail.tsx b/frontend/src/pages/equipment/EquipmentDetail.tsx new file mode 100644 index 0000000..dd62684 --- /dev/null +++ b/frontend/src/pages/equipment/EquipmentDetail.tsx @@ -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(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)?.revisions as Revision[] || []; + const revisionSchedules = (detail as Record)?.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 ( +
+ {/* Info o zariadení */} +
+
+
+ + Typ: + {equipment.type.name} +
+ {equipment.customer && ( +
+ + Zákazník: + {equipment.customer.name} +
+ )} +
+ + Adresa: + {equipment.address} +
+ {equipment.installDate && ( +
+ + Dátum inštalácie: + {formatDate(equipment.installDate)} +
+ )} + {equipment.revisionCycleStart && ( +
+ + Východzia revízia: + {formatDate(equipment.revisionCycleStart)} +
+ )} +
+
+ {equipment.serialNumber && ( +
+ Sériové číslo: + {equipment.serialNumber} +
+ )} + {equipment.brand && ( +
+ Značka: + {equipment.brand} +
+ )} + {equipment.description && ( +
+ Popis: + {equipment.description} +
+ )} + {revisionSchedules.length > 0 && ( +
+ Revízne typy: + + {revisionSchedules.map((s) => ( + + {s.revisionType.name} ({formatInterval(s.revisionType.intervalDays)}) + + ))} + +
+ )} +
+
+ + {/* Revízny plán - nadchádzajúce termíny */} + {schedule && schedule.upcomingDates.length > 0 && ( +
+

+ Revízny plán +

+
+ {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 ( +
+
+ {formatDate(item.date)} +
+ {StatusIcon && } + + {daysUntil} dní + +
+
+
+
+ + {item.label} + +
+
+ {onNewRevision && ( + + )} + {onSkipRevision && ( + + )} +
+
+
+ ); + })} +
+
+ )} + + {/* Revízie */} +
+
+

História revízií ({revisions.length})

+ {onNewRevision && ( + + )} +
+ + {isLoading ? ( + + ) : revisions.length === 0 ? ( +

+ Pre toto zariadenie zatiaľ neboli vykonané žiadne revízie. +

+ ) : ( + + + + Typ revízie + Vykonaná + Nasledujúci termín + Stav + Výsledok + Vykonal + Poznámka + + + + {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 ( + + setExpandedRevision(isExpanded ? null : revision.id)} + > + +
+ {isExpanded + ? + : + } + {revision.type.name} +
+
+ {formatDate(revision.performedDate)} + + {revision.nextDueDate ? formatDate(revision.nextDueDate) : '-'} + + +
+ {StatusIcon && } + {status.label} +
+
+ {resultText} + {revision.performedBy?.name || '-'} + {notesText} +
+ {isExpanded && ( + + +
+
+
+ Výsledok: +

{resultText}

+
+
+ Zistenia: +

{revision.findings || '-'}

+
+
+ Poznámka: +

{notesText}

+
+ {revision.status === 'skipped' && revision.skipReason && ( +
+ Dôvod vynechania: +

{revision.skipReason}

+
+ )} +
+ {onEditRevision && ( + + )} +
+
+
+ )} +
+ ); + })} +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/equipment/EquipmentForm.tsx b/frontend/src/pages/equipment/EquipmentForm.tsx index 0b8fb03..e99468c 100644 --- a/frontend/src/pages/equipment/EquipmentForm.tsx +++ b/frontend/src/pages/equipment/EquipmentForm.tsx @@ -8,7 +8,7 @@ import { customersApi } from '@/services/customers.api'; import { settingsApi } from '@/services/settings.api'; import { getFiles, generateTempId } from '@/services/upload.api'; 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'; const equipmentSchema = z.object({ @@ -22,6 +22,7 @@ const equipmentSchema = z.object({ partNumber: z.string().optional(), serialNumber: z.string().optional(), installDate: z.string().optional(), + revisionCycleStart: z.string().optional(), warrantyEnd: z.string().optional(), warrantyStatus: z.string().optional(), description: z.string().optional(), @@ -40,6 +41,9 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { const queryClient = useQueryClient(); const isEditing = !!equipment; const [files, setFiles] = useState([]); + const [selectedRevisionTypeIds, setSelectedRevisionTypeIds] = useState( + () => equipment?.revisionSchedules?.map((s) => s.revisionType.id) || [] + ); // Generate stable tempId for new equipment file uploads const tempId = useMemo(() => generateTempId(), []); @@ -54,6 +58,11 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { queryFn: () => settingsApi.getEquipmentTypes(), }); + const { data: revisionTypesData } = useQuery({ + queryKey: ['revision-types'], + queryFn: () => settingsApi.getRevisionTypes(), + }); + // Load files when editing useQuery({ queryKey: ['equipment-files', equipment?.id], @@ -84,6 +93,7 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { partNumber: equipment.partNumber || '', serialNumber: equipment.serialNumber || '', installDate: equipment.installDate?.split('T')[0] || '', + revisionCycleStart: equipment.revisionCycleStart?.split('T')[0] || '', warrantyEnd: equipment.warrantyEnd?.split('T')[0] || '', warrantyStatus: equipment.warrantyStatus || '', description: equipment.description || '', @@ -109,6 +119,7 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { mutationFn: (data: CreateEquipmentData) => equipmentApi.update(equipment!.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['equipment'] }); + queryClient.invalidateQueries({ queryKey: ['equipment-detail'] }); toast.success('Zariadenie bolo aktualizované'); onClose(); }, @@ -118,11 +129,13 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { }); const onSubmit = (data: EquipmentFormData) => { - const cleanData = { + const cleanData: CreateEquipmentData = { ...data, customerId: data.customerId || undefined, installDate: data.installDate || undefined, + revisionCycleStart: data.revisionCycleStart || undefined, warrantyEnd: data.warrantyEnd || undefined, + revisionTypeIds: selectedRevisionTypeIds, }; 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 customerOptions = customersData?.data.map((c) => ({ value: c.id, label: c.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 (
@@ -196,13 +223,59 @@ export function EquipmentForm({ equipment, onClose }: EquipmentFormProps) { {...register('location')} /> -
+
+ +
+ + {revisionTypes.length > 0 && ( +
+ +

+ Ak nezadáte dátum východzej revízie, použije sa dátum inštalácie. +

+
+ {revisionTypes.map((rt) => { + const isSelected = selectedRevisionTypeIds.includes(rt.id); + return ( + + ); + })} +
+
+ )} + +
(null); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [detailEquipment, setDetailEquipment] = useState(null); + const [revisionForEquipmentId, setRevisionForEquipmentId] = useState(null); + const [revisionForTypeId, setRevisionForTypeId] = useState(undefined); + const [editingRevision, setEditingRevision] = useState(null); + const [skipData, setSkipData] = useState<{ equipmentId: string; typeId: string; date: string } | null>(null); + const [skipReason, setSkipReason] = useState(''); - const { data, isLoading } = useQuery({ - queryKey: ['equipment', search], - queryFn: () => equipmentApi.getAll({ search, limit: 100 }), + const { data: equipmentTypesData } = useQuery({ + queryKey: ['equipment-types'], + 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), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['equipment'] }); - toast.success('Zariadenie bolo vymazané'); + toast.success('Zariadenie bolo deaktivované'); setDeleteConfirm(null); }, 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) => { setEditingEquipment(equipment); setIsFormOpen(true); @@ -70,7 +133,7 @@ export function EquipmentList() { -
+
+ setActiveFilter(e.target.value)} + options={activeFilterOptions} + />
@@ -101,7 +174,11 @@ export function EquipmentList() { {data?.data.map((equipment) => ( - + setDetailEquipment(equipment)} + > {equipment.name} {equipment.type.name} @@ -118,12 +195,26 @@ export function EquipmentList() { - + - + {equipment.active ? ( + + ) : ( + + )} ))} @@ -152,22 +243,127 @@ export function EquipmentList() { setDeleteConfirm(null)} - title="Potvrdiť vymazanie" + title="Potvrdiť deaktiváciu" > -

Naozaj chcete vymazať zariadenie "{deleteConfirm?.name}"?

+

+ Naozaj chcete deaktivovať zariadenie "{deleteConfirm?.name}"? + Zariadenie nebude vymazané, len sa skryje zo zoznamu aktívnych zariadení. +

+ + setDetailEquipment(null)} + title={detailEquipment?.name || 'Detail zariadenia'} + size="5xl" + > + {detailEquipment && ( + { + setRevisionForEquipmentId(detailEquipment.id); + setRevisionForTypeId(typeId); + setDetailEquipment(null); + }} + onSkipRevision={(date, typeId) => { + setSkipData({ equipmentId: detailEquipment.id, typeId, date }); + setSkipReason(''); + }} + onEditRevision={(revision) => { + setEditingRevision(revision as RevisionWithEquipment); + }} + /> + )} + + + { setRevisionForEquipmentId(null); setRevisionForTypeId(undefined); }} + title="Pridať záznam" + size="lg" + > + {revisionForEquipmentId && ( + { + setRevisionForEquipmentId(null); + setRevisionForTypeId(undefined); + queryClient.invalidateQueries({ queryKey: ['equipment'] }); + }} + preselectedEquipmentId={revisionForEquipmentId} + preselectedTypeId={revisionForTypeId} + /> + )} + + + { setSkipData(null); setSkipReason(''); }} + title="Vynechať revíziu" + > + {skipData && ( +
+

+ Naozaj chcete vynechať revíziu s termínom{' '} + {formatDate(skipData.date)}? +

+