diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.ar.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.ar.resx
index c57e1e43..ff30b709 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.ar.resx
@@ -678,4 +678,17 @@
نوع الهدف
المحطة / المجموعة
قوائم التحقق معطلة لهذا القسم.
+إنشاء أمر عمل Readiness Pro عند الإخفاق
+تبقى قائمة التحقق مجانية. تُعاد محاولة المتابعة المدفوعة عند توافر Readiness Pro؛ وتتطلب حظورات السلامة رفعًا مستقلاً منفصلاً.
+أوامر العمل المرتبطة
+الأولوية
+منخفضة
+عادية
+عالية
+طارئة
+إخراج الوحدة من الخدمة
+إخراج المعدة للإصلاح
+تخزين الصيانة غير متاح.
+ليس لديك إذن لتنفيذ هذا الإجراء.
+اختر هدفًا متاحًا في قسمك.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.de.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.de.resx
index 6ad6d747..61f4f4bd 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.de.resx
@@ -678,4 +678,17 @@
Zieltyp
Wache / Gruppe
Checklisten sind für diese Abteilung deaktiviert.
+Bei Fehler einen Readiness Pro-Arbeitsauftrag erstellen
+Die Prüfliste bleibt kostenlos. Kostenpflichtige Folgeschritte werden bei verfügbarem Readiness Pro erneut versucht; Sicherheitssperren erfordern eine separate unabhängige Freigabe.
+Verknüpfte Arbeitsaufträge
+Priorität
+Niedrig
+Normale Priorität
+Hoch
+Notfall
+Einheit außer Dienst setzen
+Gerät zur Reparatur sperren
+Wartungsspeicher nicht verfügbar.
+Sie haben keine Berechtigung für diese Aktion.
+Wählen Sie ein verfügbares Ziel in Ihrer Organisation.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.el.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.el.resx
index 9621352c..883236b8 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.el.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.el.resx
@@ -678,4 +678,17 @@
Τύπος στόχου
Σταθμός / ομάδα
Οι λίστες ελέγχου είναι απενεργοποιημένες για αυτό το τμήμα.
+Δημιουργία εντολής εργασίας Readiness Pro σε αποτυχία
+Η λίστα ελέγχου παραμένει δωρεάν. Η επί πληρωμή συνέχεια επαναλαμβάνεται όταν διατίθεται το Readiness Pro· οι δεσμεύσεις ασφαλείας απαιτούν χωριστή ανεξάρτητη άρση.
+Σχετικές εντολές εργασίας
+Προτεραιότητα
+Χαμηλή
+Κανονική
+Υψηλή
+Έκτακτη ανάγκη
+Θέση μονάδας εκτός υπηρεσίας
+Θέση εξοπλισμού εκτός χρήσης για επισκευή
+Ο χώρος αποθήκευσης συντήρησης δεν είναι διαθέσιμος.
+Δεν έχετε άδεια για αυτή την ενέργεια.
+Επιλέξτε έναν διαθέσιμο στόχο στο τμήμα σας.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.en.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.en.resx
index 590f1350..bed28f45 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.en.resx
@@ -678,4 +678,17 @@
Target type
Station / group
Checklists are disabled for this department.
+Create a Readiness Pro work order on failure
+The checklist remains free. Paid follow-up retries when Readiness Pro is available; configured safety holds require a separate independent release.
+Related work orders
+Priority
+Low
+Normal
+High
+Emergency
+Place unit out of service
+Place equipment out for repair
+Maintenance storage is unavailable.
+You do not have permission to perform this action.
+Select an available target in your department.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.es.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.es.resx
index 46c81bcc..4bdb95d7 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.es.resx
@@ -678,4 +678,17 @@
Tipo de objetivo
Estación / grupo
Las listas de verificación están deshabilitadas para este departamento.
+Crear una orden Readiness Pro al fallar
+La lista sigue siendo gratuita. El seguimiento de pago se reintenta cuando Readiness Pro está disponible; los bloqueos configurados requieren una liberación independiente separada.
+Órdenes de trabajo relacionadas
+Prioridad
+Baja
+Prioridad normal
+Alta
+Emergencia
+Poner la unidad fuera de servicio
+Retirar el equipo para reparación
+El almacenamiento de mantenimiento no está disponible.
+No tiene permiso para realizar esta acción.
+Seleccione un destino disponible en su departamento.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.fr.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.fr.resx
index f82a6564..57f21287 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.fr.resx
@@ -678,4 +678,17 @@
Type de cible
Caserne / groupe
Les listes de contrôle sont désactivées pour ce département.
+Créer un ordre Readiness Pro en cas d'échec
+La liste reste gratuite. Le suivi payant est réessayé lorsque Readiness Pro est disponible ; les blocages configurés exigent une levée indépendante distincte.
+Ordres de travail associés
+Priorité
+Basse
+Normale
+Haute
+Urgence
+Mettre l'unité hors service
+Retirer le matériel pour réparation
+Le stockage de maintenance est indisponible.
+Vous n’avez pas l’autorisation d’effectuer cette action.
+Sélectionnez une cible disponible dans votre service.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.it.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.it.resx
index d5936258..cc6fc7b3 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.it.resx
@@ -678,4 +678,17 @@
Tipo di destinatario
Stazione / gruppo
Le liste di controllo sono disabilitate per questo reparto.
+Crea un ordine Readiness Pro in caso di esito negativo
+La lista rimane gratuita. Il seguito a pagamento viene ritentato quando Readiness Pro è disponibile; i blocchi richiedono un rilascio indipendente separato.
+Ordini di lavoro correlati
+Priorità
+Bassa
+Normale
+Alta
+Emergenza
+Metti l'unità fuori servizio
+Metti l'attrezzatura fuori uso per riparazione
+L'archivio di manutenzione non è disponibile.
+Non hai l’autorizzazione per eseguire questa azione.
+Seleziona un destinatario disponibile nel tuo dipartimento.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.pl.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.pl.resx
index 0d635ffd..8c6241a6 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.pl.resx
@@ -678,4 +678,17 @@
Typ celu
Stacja / grupa
Listy kontrolne są wyłączone dla tego działu.
+Utwórz zlecenie Readiness Pro przy niepowodzeniu
+Lista pozostaje bezpłatna. Płatne działania są ponawiane, gdy Readiness Pro jest dostępny; blokady wymagają oddzielnego niezależnego zwolnienia.
+Powiązane zlecenia
+Priorytet
+Niski
+Normalny
+Wysoki
+Nagły wypadek
+Wyłącz jednostkę ze służby
+Wyłącz sprzęt do naprawy
+Magazyn danych konserwacji jest niedostępny.
+Nie masz uprawnień do wykonania tej czynności.
+Wybierz dostępny obiekt w swoim dziale.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.resx
index 590f1350..bed28f45 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.resx
@@ -678,4 +678,17 @@
Target type
Station / group
Checklists are disabled for this department.
+Create a Readiness Pro work order on failure
+The checklist remains free. Paid follow-up retries when Readiness Pro is available; configured safety holds require a separate independent release.
+Related work orders
+Priority
+Low
+Normal
+High
+Emergency
+Place unit out of service
+Place equipment out for repair
+Maintenance storage is unavailable.
+You do not have permission to perform this action.
+Select an available target in your department.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.sv.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.sv.resx
index fd9f460d..56410f8b 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.sv.resx
@@ -678,4 +678,17 @@
Måltyp
Station / grupp
Checklistor är inaktiverade för den här avdelningen.
+Skapa en Readiness Pro-arbetsorder vid fel
+Checklistan förblir kostnadsfri. Betald uppföljning försöks igen när Readiness Pro är tillgängligt; spärrar kräver en separat oberoende frigivning.
+Relaterade arbetsorder
+Prioritet
+Låg
+Normal prioritet
+Hög
+Nödläge
+Ta enheten ur tjänst
+Ta utrustning ur drift för reparation
+Underhållslagringen är inte tillgänglig.
+Du har inte behörighet att utföra denna åtgärd.
+Välj ett tillgängligt mål inom din avdelning.
diff --git a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.uk.resx b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.uk.resx
index c456c362..39c980e1 100644
--- a/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Checklists/Checklists.uk.resx
@@ -678,4 +678,17 @@
Тип цілі
Станція / група
Контрольні списки вимкнено для цього підрозділу.
+Створити наряд Readiness Pro у разі невдачі
+Контрольний список залишається безкоштовним. Платні дії повторюються за доступності Readiness Pro; налаштовані блокування потребують окремого незалежного зняття.
+Пов’язані наряди
+Пріоритет
+Низький
+Звичайний
+Високий
+Надзвичайний
+Вивести підрозділ зі служби
+Вивести обладнання з експлуатації для ремонту
+Сховище технічного обслуговування недоступне.
+Ви не маєте дозволу на цю дію.
+Виберіть доступний об’єкт у своєму підрозділі.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
index ecf8d657..6ee19442 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
@@ -523,4 +523,7 @@
تم إرجاع المخزون
تغيرت حالة أصل المخزون
تم تسجيل مادة خاضعة للرقابة
+ سجل هذه القطعة أو اعكسها من خلال أمر العمل المرتبط.
+ يمنع حظر سلامة الصيانة إعادة هذه المعدة إلى الخدمة.
+ يلزم Readiness Pro لأعمال الصيانة الجديدة.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
index 7b8ed13c..047f3dcc 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
@@ -647,4 +647,7 @@
Bestand zurückgegeben
Status des Inventargegenstands geändert
Kontrollierte Substanz erfasst
+ Dieses Teil über seinen Arbeitsauftrag buchen oder stornieren.
+ Eine Wartungssicherheitssperre verhindert die Wiederinbetriebnahme dieses Geräts.
+ Für neue Wartungsarbeiten ist Readiness Pro erforderlich.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
index 535a9d5c..9d534911 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
@@ -706,4 +706,7 @@
Το απόθεμα επιστράφηκε
Η κατάσταση του εξοπλισμού άλλαξε
Η ελεγχόμενη ουσία καταγράφηκε
+ Καταχωρίστε ή αντιλογίστε το ανταλλακτικό μέσω της εντολής εργασίας του.
+ Δέσμευση ασφαλείας συντήρησης εμποδίζει την επαναφορά του εξοπλισμού.
+ Απαιτείται Readiness Pro για νέες εργασίες συντήρησης.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
index 70991670..aa1fa5fa 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
@@ -706,4 +706,7 @@
Inventory returned
Inventory asset status changed
Controlled substance recorded
+ Post or reverse this part through its work order.
+ A maintenance safety hold prevents returning this equipment to service.
+ Readiness Pro is required for new maintenance work.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
index 98ce8ab4..abe8f78e 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
@@ -647,4 +647,7 @@
Inventario devuelto
Estado del activo de inventario actualizado
Sustancia controlada registrada
+ Registre o revierta esta pieza mediante su orden de trabajo.
+ Un bloqueo de seguridad de mantenimiento impide devolver este equipo al servicio.
+ Readiness Pro es necesario para nuevos trabajos de mantenimiento.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
index cc15c2db..f2668a83 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
@@ -647,4 +647,7 @@
Stock retourné
Statut du bien en stock modifié
Substance contrôlée enregistrée
+ Comptabilisez ou annulez cette pièce depuis son ordre de travail.
+ Un blocage de sécurité de maintenance empêche la remise en service de ce matériel.
+ Readiness Pro est requis pour les nouveaux travaux de maintenance.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
index 8e5289d8..74c36471 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
@@ -647,4 +647,7 @@
Inventario restituito
Stato del bene inventariato modificato
Sostanza controllata registrata
+ Registra o storna questo ricambio tramite il relativo ordine di lavoro.
+ Un blocco di sicurezza di manutenzione impedisce il ritorno in servizio dell'attrezzatura.
+ Readiness Pro è necessario per nuovi lavori di manutenzione.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
index 93831026..00136901 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
@@ -647,4 +647,7 @@
Zwrócono zapasy
Zmieniono status składnika wyposażenia
Zarejestrowano substancję kontrolowaną
+ Zaksięguj lub odwróć tę część przez jej zlecenie.
+ Blokada bezpieczeństwa konserwacji uniemożliwia przywrócenie sprzętu do użytku.
+ Readiness Pro jest wymagany do nowych prac konserwacyjnych.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
index 20d2ce2f..74adfce3 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
@@ -647,4 +647,7 @@
Lager återlämnat
Status för inventariet ändrad
Kontrollerad substans registrerad
+ Bokför eller återför delen via dess arbetsorder.
+ En säkerhetsspärr för underhåll hindrar att utrustningen återtas i drift.
+ Readiness Pro krävs för nya underhållsarbeten.
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
index f30c4e65..410437e0 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
@@ -647,4 +647,7 @@
Запаси повернуто
Стан інвентарного активу змінено
Контрольовану речовину зареєстровано
+ Проведіть або сторнуйте цю деталь через її наряд.
+ Блокування безпеки обслуговування перешкоджає поверненню обладнання в експлуатацію.
+ Для нових робіт з обслуговування потрібен Readiness Pro.
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.ar.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.ar.resx
index 1a85f85f..a5d3a642 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.ar.resx
@@ -75,4 +75,7 @@
إرسال بريد إلكتروني لإعادة تعيين كلمة المرور
إرسال بريد إلكتروني لإعادة تعيين كلمة المرور
سيرسل Resgrid إلى هذا المستخدم رابطًا قصير الصلاحية يُستخدم مرة واحدة إلى عنوان بريده الإلكتروني المؤكد. لن يختار المسؤول كلمة المرور الجديدة ولن يراها.
+
+ تعذّر تحديث الجدول. حدّث الصفحة وحاول مرة أخرى.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.de.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.de.resx
index 4c5c906b..f23de716 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.de.resx
@@ -254,4 +254,7 @@
E-Mail zum Zurücksetzen des Passworts senden
E-Mail zum Zurücksetzen des Passworts senden
Resgrid sendet diesem Benutzer einen kurzlebigen, einmal verwendbaren Link an seine bestätigte E-Mail-Adresse. Der Administrator wählt oder sieht das neue Passwort nicht.
+
+ Der Zeitplan konnte nicht aktualisiert werden. Laden Sie die Seite neu und versuchen Sie es erneut.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.el.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.el.resx
index 0edd3105..acd04958 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.el.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.el.resx
@@ -303,4 +303,7 @@
Αποστολή email επαναφοράς κωδικού πρόσβασης
Αποστολή email επαναφοράς κωδικού πρόσβασης
Το Resgrid θα στείλει σε αυτόν τον χρήστη έναν σύνδεσμο σύντομης διάρκειας και μίας χρήσης στην επιβεβαιωμένη διεύθυνση email του. Ο διαχειριστής δεν θα επιλέξει ούτε θα δει τον νέο κωδικό πρόσβασης.
+
+ Δεν ήταν δυνατή η ενημέρωση του προγράμματος. Ανανεώστε τη σελίδα και δοκιμάστε ξανά.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx
index 2abe6c26..d3665b3a 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx
@@ -303,4 +303,7 @@
Send Password Reset Email
Send Password Reset Email
Resgrid will send this user a short-lived, single-use link at their confirmed email address. The administrator will not choose or see the new password.
+
+ The schedule could not be updated. Refresh the page and try again.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx
index 0aabbe91..ece0af80 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx
@@ -303,4 +303,7 @@
Enviar correo de restablecimiento de contraseña
Enviar correo de restablecimiento de contraseña
Resgrid enviará a este usuario un enlace de corta duración y de un solo uso a su dirección de correo confirmada. El administrador no elegirá ni verá la nueva contraseña.
+
+ No se pudo actualizar la programación. Actualice la página e inténtelo de nuevo.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.fr.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.fr.resx
index 038059d5..4f230809 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.fr.resx
@@ -254,4 +254,7 @@
Envoyer l’e-mail de réinitialisation du mot de passe
Envoyer l’e-mail de réinitialisation du mot de passe
Resgrid enverra à cet utilisateur un lien à courte durée de vie et à usage unique à son adresse e-mail confirmée. L’administrateur ne choisira ni ne verra le nouveau mot de passe.
+
+ Le planning n’a pas pu être mis à jour. Actualisez la page et réessayez.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.it.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.it.resx
index d5ad592a..3c661109 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.it.resx
@@ -254,4 +254,7 @@
Invia email di reimpostazione password
Invia email di reimpostazione password
Resgrid invierà a questo utente un link di breve durata e monouso al suo indirizzo email confermato. L’amministratore non sceglierà né vedrà la nuova password.
+
+ Impossibile aggiornare la pianificazione. Aggiorna la pagina e riprova.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.pl.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.pl.resx
index fdb8f5f0..8308202e 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.pl.resx
@@ -254,4 +254,7 @@
Wyślij wiadomość e-mail do resetowania hasła
Wyślij wiadomość e-mail do resetowania hasła
Resgrid wyśle temu użytkownikowi krótkotrwały, jednorazowy link na potwierdzony adres e-mail. Administrator nie wybierze ani nie zobaczy nowego hasła.
+
+ Nie udało się zaktualizować harmonogramu. Odśwież stronę i spróbuj ponownie.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.sv.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.sv.resx
index b7d74729..12549101 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.sv.resx
@@ -254,4 +254,7 @@
Skicka e-post för lösenordsåterställning
Skicka e-post för lösenordsåterställning
Resgrid skickar en kortlivad engångslänk till användarens bekräftade e-postadress. Administratören väljer eller ser inte det nya lösenordet.
+
+ Schemat kunde inte uppdateras. Uppdatera sidan och försök igen.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.uk.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.uk.resx
index 51086b99..35de6fdc 100644
--- a/Core/Resgrid.Localization/Areas/User/Profile/Profile.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.uk.resx
@@ -254,4 +254,7 @@
Надіслати електронний лист для скидання пароля
Надіслати електронний лист для скидання пароля
Resgrid надішле цьому користувачеві короткочасне одноразове посилання на підтверджену електронну адресу. Адміністратор не вибиратиме й не бачитиме новий пароль.
+
+ Не вдалося оновити розклад. Оновіть сторінку та спробуйте ще раз.
+
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.ar.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.ar.resx
index 599b4358..b1f5e8fb 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.ar.resx
@@ -154,4 +154,110 @@
تم إبطال قطعة
تمت إضافة ملف
تم سحب ملف
+جدول نشط
+تم تأجيل الموعد
+تصعيد التأخير
+تم فرض حظر السلامة
+تم رفع حظر السلامة
+فرض حظر السلامة
+الموافقة على التأجيل
+حظر سلامة المعدة
+الإسناد والتصعيد
+بداية فترة الاستبعاد
+نهاية فترة الاستبعاد
+تكرار التقويم
+يومي
+شهري
+بلا محفز تقويمي
+ربع سنوي
+أسبوعي
+سنوي
+حساب الفترة التالية من الإغلاق المعتمد
+محفز الحالة
+عند العتبة أو أعلى منها
+عند العتبة أو أقل منها
+بلا محفز حالة
+عتبة الحالة
+وحدة قياس الحالة
+قراءة الحالة
+سبب التأجيل
+تعديل الجدول
+تاريخ الانتهاء
+التصعيد بعد عدد دقائق التأخير
+دور مستلم التصعيد
+أول موعد استحقاق بالتوقيت المحلي
+متابعة خلل قائمة التحقق
+إخراج المعدة للإصلاح
+إخراج الوحدة من الخدمة
+يجب أن يسجل مراجع مستقل ومخول مؤهلاته وأدلة رفع الحظر. تتطلب استعادة الخدمة رفع آخر حظر وعدم تغير الحالة؛ ولا يكفي الإغلاق وحده.
+يجب أن يرفع هذا الحظر مراجع مخول آخر.
+فترة الجدولة
+تعذر إكمال عملية المخزون. راجع التوافر والصلاحيات.
+اختر صنف مخزون وموقع مصدر صالحين.
+قطع مرتبطة بالمخزون
+اترك الصنف فارغًا لقطعة بنص حر. تستخدم قطع المخزون تكاليف السجل وقد تتطلب شاهدًا مستقلاً في المخزون.
+يلزم إذن المخزون أو التحقق من البيانات المحمية.
+شاهد المخزون قيد الانتظار؛ أكمل الشهادة قبل إغلاق أمر العمل.
+المهلة بالأيام
+تحميل مزيد من الخيارات
+تخزين الصيانة غير متاح.
+خط أساس فترة العداد
+دورات التشغيل
+جيل العداد
+ساعات التشغيل
+فترة العداد
+كيلومترات
+الأميال المقطوعة
+بلا محفز عداد
+وحدة قياس العداد
+قراءة العداد
+جدول وقائي جديد
+موعد الاستحقاق التالي
+وقت الرصد
+موعد الاستحقاق الأصلي
+جدول متوقف
+الصيانة الوقائية
+تحقق من القراءة ووقت الرصد ومتطلبات إعادة ضبط العداد.
+مصدر القراءة
+سجل القراءات
+تسجيل القراءة
+تُستخدم محفزات التقويم والعداد والحالة بحسب الأسبق. يُسمح بأمر معلق واحد لكل جدول. يؤدي إلغاء أمر مُنشأ إلى إيقاف الجدول للمراجعة.
+تحقق من الجدول والمنطقة الزمنية ونافذة الخدمة وإعدادات المحفزات.
+أكمل الأمر المعلق أو ألغِه قبل تغيير الهدف أو خط أساس العداد.
+أدلة التحقق لرفع الحظر
+رفع حظر السلامة
+مؤهل المراجع
+تم استبدال العداد أو إعادة ضبطه (السبب مطلوب)
+استعادة الحالة السابقة فقط إذا لم تتغير ولم تبقَ حظورات أخرى
+موعد الاستحقاق المعدل
+حظورات السلامة
+جدول الصيانة
+نهاية نافذة الخدمة
+بداية نافذة الخدمة
+نافذة الخدمة
+إرسال قائمة التحقق المصدر
+تم الحفاظ على الحالة الحالية
+تمت استعادة الحالة السابقة
+هدف الصيانة
+المنطقة الزمنية
+حظر سلامة الوحدة
+الاستخدام والحالة
+استخدم إجراء التأجيل المعتمد لتغيير موعد الاستحقاق.
+سجل إعادة ضبط موثقة لتغيير خط الأساس؛ واستخدم جدولًا جديدًا لوحدات قياس مختلفة.
+صنف المخزون
+موقع المخزون المصدر
+دفعة المخزون
+تم تغيير الجدول الوقائي
+تم بلوغ عتبة الصيانة
+تم تغيير قطعة أمر العمل
+تم إعداد الجدول
+تم تسجيل القراءة
+تم تسجيل إعادة ضبط العداد
+تم تأجيل الموعد
+تم إنشاء أمر العمل
+تصعيد التأخير
+تصعيد التأخير
+تم فرض حظر السلامة
+تم رفع حظر السلامة
+تم تأجيل الموعد
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.de.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.de.resx
index 20388893..8229f257 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.de.resx
@@ -154,4 +154,110 @@
Teil storniert
Datei hinzugefügt
Datei zurückgezogen
+Aktiver Zeitplan
+Fälligkeit verschoben
+Überfälligkeit eskaliert
+Sicherheitssperre gesetzt
+Sicherheitssperre aufgehoben
+Sicherheitssperre setzen
+Verschiebung genehmigen
+Gerätesicherheitssperre
+Zuweisung und Eskalation
+Sperrzeit ab
+Sperrzeit bis
+Kalenderhäufigkeit
+Täglich
+Monatlich
+Kein Kalenderauslöser
+Vierteljährlich
+Wöchentlich
+Jährlich
+Nächstes Intervall ab geprüftem Abschluss berechnen
+Zustandsauslöser
+Ab dem Schwellenwert
+Bis zum Schwellenwert
+Kein Zustandsauslöser
+Zustandsschwellenwert
+Maßeinheit des Zustands
+Zustandsmesswert
+Grund der Verschiebung
+Zeitplan bearbeiten
+Enddatum
+Eskalation nach überfälligen Minuten
+Empfängerrolle für Eskalationen
+Erste Fälligkeit in Ortszeit
+Nachbearbeitung eines Prüflistenmangels
+Gerät zur Reparatur sperren
+Einheit außer Dienst setzen
+Ein unabhängiger berechtigter Prüfer muss Qualifikation und Freigabenachweis dokumentieren. Wiederherstellung erfordert die letzte Sperre und einen unveränderten Zustand; der Abschluss allein stellt den Dienst nicht wieder her.
+Ein anderer berechtigter Prüfer muss diese Sperre aufheben.
+Planungsintervall
+Der Bestandsvorgang konnte nicht abgeschlossen werden. Verfügbarkeit und Berechtigungen prüfen.
+Gültigen Bestandsartikel und Quellort auswählen.
+Mit dem Bestand verknüpfte Teile
+Artikel für eine Freitextposition leer lassen. Bestandsteile verwenden Buchungskosten und können einen unabhängigen Zeugen im Bestand erfordern.
+Bestandsberechtigung oder Prüfung geschützter Daten erforderlich.
+Bestandszeuge ausstehend; Bezeugung vor dem Abschluss dieses Arbeitsauftrags abschließen.
+Vorlauf in Tagen
+Weitere Auswahl laden
+Wartungsspeicher nicht verfügbar.
+Zählerintervall-Basiswert
+Betriebszyklen
+Zählergeneration
+Betriebsstunden
+Zählerintervall
+Kilometer
+Gefahrene Meilen
+Kein Zählerauslöser
+Zählermaßeinheit
+Zählerstand
+Neuer vorbeugender Wartungsplan
+Nächste Fälligkeit
+Beobachtungszeit
+Ursprüngliche Fälligkeit
+Pausierter Zeitplan
+Vorbeugende Wartung
+Messwert, Beobachtungszeit und Anforderungen zum Zählerwechsel prüfen.
+Quelle des Messwerts
+Messwerthistorie
+Messwert erfassen
+Kalender, Zähler und Zustand lösen beim zuerst erreichten Grenzwert aus. Pro Zeitplan ist ein offener Auftrag erlaubt. Das Stornieren eines erzeugten Auftrags pausiert den Zeitplan zur Prüfung.
+Zeitplan, Zeitzone, Servicefenster und Auslöser prüfen.
+Offenen Auftrag abschließen oder stornieren, bevor Ziel oder Zählerbasis geändert werden.
+Prüfnachweis zur Freigabe
+Sicherheitssperre aufheben
+Qualifikation des Prüfers
+Zähler ersetzt oder zurückgesetzt (Grund erforderlich)
+Vorherigen Zustand nur bei unverändertem Zustand und ohne weitere Sperren herstellen
+Geänderte Fälligkeit
+Sicherheitssperren
+Wartungsplan
+Ende des Servicefensters
+Beginn des Servicefensters
+Servicefenster
+Ursprüngliche Prüflisteneinreichung
+Aktueller Zustand beibehalten
+Vorheriger Zustand wiederhergestellt
+Wartungsziel
+Zeitzone
+Einheitssicherheitssperre
+Nutzung und Zustand
+Zum Ändern der Fälligkeit die genehmigte Verschiebung verwenden.
+Basiswert durch dokumentiertes Zurücksetzen ändern; für andere Maßeinheiten einen neuen Zeitplan verwenden.
+Bestandsartikel
+Quelllagerort
+Bestandscharge
+Vorbeugender Wartungsplan geändert
+Wartungsschwellenwert erreicht
+Arbeitsauftragsteil geändert
+Zeitplan eingerichtet
+Messwert erfasst
+Zählerrücksetzung erfasst
+Fälligkeit verschoben
+Arbeitsauftrag erstellt
+Überfälligkeit eskaliert
+Überfälligkeit eskaliert
+Sicherheitssperre gesetzt
+Sicherheitssperre aufgehoben
+Fälligkeit verschoben
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.el.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.el.resx
index 13962c81..768aa7db 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.el.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.el.resx
@@ -154,4 +154,110 @@
Ακυρώθηκε ανταλλακτικό
Προστέθηκε αρχείο
Αποσύρθηκε αρχείο
+Ενεργό πρόγραμμα
+Αναβολή προθεσμίας
+Κλιμάκωση καθυστέρησης
+Επιβολή δέσμευσης ασφαλείας
+Άρση δέσμευσης ασφαλείας
+Επιβολή δέσμευσης ασφαλείας
+Έγκριση αναβολής
+Δέσμευση ασφαλείας εξοπλισμού
+Ανάθεση και κλιμάκωση
+Έναρξη αποκλεισμού
+Λήξη αποκλεισμού
+Ημερολογιακή συχνότητα
+Καθημερινά
+Μηνιαία
+Χωρίς ημερολογιακό έναυσμα
+Τριμηνιαία
+Εβδομαδιαία
+Ετησίως
+Υπολογισμός επόμενου διαστήματος από την επαληθευμένη ολοκλήρωση
+Έναυσμα κατάστασης
+Στο όριο ή υψηλότερα
+Στο όριο ή χαμηλότερα
+Χωρίς έναυσμα κατάστασης
+Όριο κατάστασης
+Μονάδα μέτρησης κατάστασης
+Ένδειξη κατάστασης
+Αιτία αναβολής
+Επεξεργασία προγράμματος
+Ημερομηνία λήξης
+Κλιμάκωση μετά από λεπτά καθυστέρησης
+Ρόλος παραλήπτη κλιμάκωσης
+Πρώτη προθεσμία σε τοπική ώρα
+Παρακολούθηση ελαττώματος λίστας ελέγχου
+Θέση εξοπλισμού εκτός χρήσης για επισκευή
+Θέση μονάδας εκτός υπηρεσίας
+Ανεξάρτητος εξουσιοδοτημένος ελεγκτής καταγράφει προσόντα και τεκμήρια άρσης. Η επαναφορά απαιτεί την τελευταία δέσμευση και αμετάβλητη κατάσταση· η ολοκλήρωση μόνη της δεν επαναφέρει τη λειτουργία.
+Άλλος εξουσιοδοτημένος ελεγκτής πρέπει να άρει τη δέσμευση.
+Διάστημα προγραμματισμού
+Η πράξη αποθέματος δεν ολοκληρώθηκε. Ελέγξτε διαθεσιμότητα και δικαιώματα.
+Επιλέξτε έγκυρο είδος αποθέματος και θέση προέλευσης.
+Ανταλλακτικά συνδεδεμένα με απόθεμα
+Αφήστε κενό το είδος για ελεύθερο κείμενο. Τα ανταλλακτικά αποθέματος χρησιμοποιούν κόστος καθολικού και ενδέχεται να απαιτούν ανεξάρτητο μάρτυρα στο απόθεμα.
+Απαιτείται δικαίωμα αποθέματος ή επαλήθευση προστατευμένων δεδομένων.
+Εκκρεμεί μάρτυρας αποθέματος· ολοκληρώστε την επιβεβαίωση πριν κλείσετε την εντολή εργασίας.
+Χρόνος προειδοποίησης σε ημέρες
+Φόρτωση περισσότερων επιλογών
+Ο χώρος αποθήκευσης συντήρησης δεν είναι διαθέσιμος.
+Βασική τιμή διαστήματος μετρητή
+Κύκλοι λειτουργίας
+Γενιά μετρητή
+Ώρες λειτουργίας
+Διάστημα μετρητή
+Χιλιόμετρα
+Διανυθέντα μίλια
+Χωρίς έναυσμα μετρητή
+Μονάδα μέτρησης μετρητή
+Ένδειξη μετρητή
+Νέο πρόγραμμα προληπτικής συντήρησης
+Επόμενη προθεσμία
+Χρόνος παρατήρησης
+Αρχική προθεσμία
+Πρόγραμμα σε παύση
+Προληπτική συντήρηση
+Ελέγξτε την ένδειξη, τον χρόνο παρατήρησης και τις απαιτήσεις μηδενισμού μετρητή.
+Πηγή ένδειξης
+Ιστορικό ενδείξεων
+Καταγραφή ένδειξης
+Τα εναύσματα ημερολογίου, μετρητή και κατάστασης ενεργοποιούνται με όποιο επιτευχθεί πρώτο. Επιτρέπεται μία εκκρεμής εντολή ανά πρόγραμμα. Η ακύρωση παραγόμενης εντολής θέτει το πρόγραμμα σε παύση για έλεγχο.
+Ελέγξτε πρόγραμμα, ζώνη ώρας, παράθυρο συντήρησης και εναύσματα.
+Ολοκληρώστε ή ακυρώστε την εκκρεμή εντολή πριν αλλάξετε στόχο ή βάση μετρητή.
+Τεκμήρια επαλήθευσης άρσης
+Άρση δέσμευσης ασφαλείας
+Προσόντα ελεγκτή
+Αντικατάσταση ή μηδενισμός μετρητή (απαιτείται αιτία)
+Επαναφορά προηγούμενης κατάστασης μόνο αν παραμένει αμετάβλητη και δεν υπάρχουν άλλες δεσμεύσεις
+Αναθεωρημένη προθεσμία
+Δεσμεύσεις ασφαλείας
+Πρόγραμμα συντήρησης
+Λήξη παραθύρου συντήρησης
+Έναρξη παραθύρου συντήρησης
+Παράθυρο συντήρησης
+Αρχική υποβολή λίστας ελέγχου
+Διατήρηση τρέχουσας κατάστασης
+Επαναφορά προηγούμενης κατάστασης
+Στόχος συντήρησης
+Ζώνη ώρας
+Δέσμευση ασφαλείας μονάδας
+Χρήση και κατάσταση
+Χρησιμοποιήστε την εγκεκριμένη αναβολή για αλλαγή προθεσμίας.
+Καταγράψτε ελεγχόμενο μηδενισμό για αλλαγή βάσης· χρησιμοποιήστε νέο πρόγραμμα για άλλες μονάδες.
+Είδος αποθέματος
+Θέση αποθέματος προέλευσης
+Παρτίδα αποθέματος
+Αλλαγή προγράμματος προληπτικής συντήρησης
+Επίτευξη ορίου συντήρησης
+Αλλαγή ανταλλακτικού εντολής εργασίας
+Ρύθμιση προγράμματος
+Καταγραφή ένδειξης
+Καταγραφή μηδενισμού μετρητή
+Αναβολή προθεσμίας
+Δημιουργήθηκε εντολή εργασίας
+Κλιμάκωση καθυστέρησης
+Κλιμάκωση καθυστέρησης
+Επιβολή δέσμευσης ασφαλείας
+Άρση δέσμευσης ασφαλείας
+Αναβολή προθεσμίας
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.en.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.en.resx
index a5cdf33c..e3441bfe 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.en.resx
@@ -154,4 +154,110 @@
Part voided
File added
File withdrawn
+Active schedule
+Due date deferred
+Overdue escalation
+Safety hold applied
+Safety hold released
+Apply safety hold
+Approve deferral
+Equipment safety hold
+Assignment and escalation
+Blackout starts
+Blackout ends
+Calendar frequency
+Daily
+Monthly
+No calendar trigger
+Quarterly
+Weekly
+Yearly
+Measure the next interval from verified closure
+Condition trigger
+At or above threshold
+At or below threshold
+No condition trigger
+Condition threshold
+Condition measurement unit
+Condition reading
+Reason for deferral
+Edit schedule
+End date
+Escalate after overdue minutes
+Escalation recipient role
+First due date and local time
+Checklist defect follow-up
+Place equipment out for repair
+Place unit out of service
+An independent authorized reviewer must record qualifications and release evidence. Restoration requires the last hold and an unchanged state; closure alone never restores service.
+A different authorized reviewer must release this hold.
+Scheduling interval
+The inventory operation could not be completed. Review inventory availability and permissions.
+Choose a valid inventory item and source location.
+Inventory-linked parts
+Leave the item blank for a free-text part. Inventory parts use ledger costs and may require an independent witness in Inventory.
+Inventory permission or protected-data verification is required.
+Inventory witness pending; complete witnessing before closing this work order.
+Lead time in days
+Load more choices
+Maintenance storage is unavailable.
+Meter interval baseline
+Operating cycles
+Meter generation
+Operating hours
+Meter interval
+Kilometers
+Miles travelled
+No meter trigger
+Meter measurement unit
+Meter reading
+New preventive schedule
+Next due date
+Observation time
+Original due date
+Paused schedule
+Preventive maintenance
+Check the reading, observation time and meter reset requirements.
+Reading source
+Reading history
+Record reading
+Calendar, meter and condition triggers use whichever is reached first. One pending order is allowed per schedule. Cancelling a generated order pauses the schedule for review.
+Check the schedule, time zone, service window and trigger settings.
+Complete or cancel the pending order before changing this target or meter baseline.
+Release verification evidence
+Release safety hold
+Reviewer qualification
+Meter replaced or reset (reason required)
+Restore previous state only if unchanged and no other holds remain
+Revised due date
+Safety holds
+Maintenance schedule
+Service window ends
+Service window starts
+Service window
+Source checklist submission
+Current state preserved
+Previous state restored
+Maintenance target
+Time zone
+Unit safety hold
+Usage and condition
+Use the approved deferral action to change the due date.
+Record an audited meter reset to change the baseline; use a new schedule for different measurement units.
+Inventory item
+Source stock location
+Inventory lot
+Preventive schedule changed
+Maintenance threshold reached
+Work order part changed
+Schedule configured
+Reading recorded
+Meter reset recorded
+Due date deferred
+Work order created
+Overdue escalation
+Overdue escalation
+Safety hold applied
+Safety hold released
+Due date deferred
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.es.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.es.resx
index ccf5b396..08e439a1 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.es.resx
@@ -154,4 +154,110 @@
Pieza anulada
Archivo añadido
Archivo retirado
+Programa activo
+Fecha aplazada
+Escalamiento por retraso
+Bloqueo de seguridad aplicado
+Bloqueo de seguridad liberado
+Aplicar bloqueo de seguridad
+Aprobar aplazamiento
+Bloqueo de seguridad del equipo
+Asignación y escalamiento
+Inicio del período excluido
+Fin del período excluido
+Frecuencia del calendario
+Diaria
+Mensual
+Sin activación por calendario
+Trimestral
+Semanal
+Anual
+Calcular el siguiente intervalo desde el cierre verificado
+Activación por condición
+Igual o superior al umbral
+Igual o inferior al umbral
+Sin activación por condición
+Umbral de condición
+Unidad de medida de condición
+Lectura de condición
+Motivo del aplazamiento
+Editar programa
+Fecha de finalización
+Escalar tras los minutos de retraso
+Rol destinatario del escalamiento
+Primera fecha y hora local de vencimiento
+Seguimiento de defecto de lista de verificación
+Retirar el equipo para reparación
+Poner la unidad fuera de servicio
+Un revisor independiente autorizado debe registrar su cualificación y las pruebas de liberación. La restauración exige que sea el último bloqueo y que el estado no haya cambiado; el cierre por sí solo no restablece el servicio.
+Otro revisor autorizado debe liberar este bloqueo.
+Intervalo de programación
+No se pudo completar la operación de inventario. Revise disponibilidad y permisos.
+Seleccione un artículo y una ubicación de origen válidos.
+Piezas vinculadas al inventario
+Deje el artículo vacío para una pieza de texto libre. Las piezas de inventario usan costes del registro y pueden requerir un testigo independiente en Inventario.
+Se requiere permiso de inventario o verificación de datos protegidos.
+Testigo de inventario pendiente; complete la atestación antes de cerrar esta orden de trabajo.
+Antelación en días
+Cargar más opciones
+El almacenamiento de mantenimiento no está disponible.
+Valor base del intervalo del contador
+Ciclos de funcionamiento
+Generación del contador
+Horas de funcionamiento
+Intervalo del contador
+Kilómetros
+Millas recorridas
+Sin activación por contador
+Unidad de medida del contador
+Lectura del contador
+Nuevo programa preventivo
+Próximo vencimiento
+Hora de observación
+Vencimiento original
+Programa pausado
+Mantenimiento preventivo
+Revise la lectura, la hora y los requisitos de reinicio del contador.
+Fuente de la lectura
+Historial de lecturas
+Registrar lectura
+Se activa el primer criterio alcanzado de calendario, contador o condición. Se permite una orden pendiente por programa. Cancelar una orden generada pausa el programa para revisión.
+Revise el programa, la zona horaria, la ventana de servicio y los criterios.
+Complete o cancele la orden pendiente antes de cambiar el objetivo o la base del contador.
+Pruebas de verificación de liberación
+Liberar bloqueo de seguridad
+Cualificación del revisor
+Contador reemplazado o reiniciado (motivo obligatorio)
+Restaurar el estado anterior solo si no ha cambiado y no quedan otros bloqueos
+Vencimiento revisado
+Bloqueos de seguridad
+Programa de mantenimiento
+Fin de la ventana de servicio
+Inicio de la ventana de servicio
+Ventana de servicio
+Envío de la lista de verificación de origen
+Estado actual conservado
+Estado anterior restaurado
+Objetivo de mantenimiento
+Zona horaria
+Bloqueo de seguridad de la unidad
+Uso y condición
+Use la acción de aplazamiento aprobado para cambiar el vencimiento.
+Registre un reinicio auditado para cambiar la base; use un programa nuevo para unidades diferentes.
+Artículo de inventario
+Ubicación del stock de origen
+Lote de inventario
+Programa preventivo modificado
+Umbral de mantenimiento alcanzado
+Pieza de orden de trabajo modificada
+Programa configurado
+Lectura registrada
+Reinicio del contador registrado
+Fecha aplazada
+Orden de trabajo creada
+Escalamiento por retraso
+Escalamiento por retraso
+Bloqueo de seguridad aplicado
+Bloqueo de seguridad liberado
+Fecha aplazada
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.fr.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.fr.resx
index 6ef23cd7..b310c4ad 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.fr.resx
@@ -154,4 +154,110 @@
Pièce annulée
Fichier ajouté
Fichier retiré
+Programme actif
+Échéance reportée
+Signalement du retard
+Blocage de sécurité appliqué
+Blocage de sécurité levé
+Appliquer un blocage de sécurité
+Approuver le report
+Blocage de sécurité du matériel
+Affectation et signalement
+Début de la période exclue
+Fin de la période exclue
+Fréquence calendaire
+Quotidienne
+Mensuelle
+Sans déclenchement calendaire
+Trimestrielle
+Hebdomadaire
+Annuelle
+Calculer le prochain intervalle depuis la clôture vérifiée
+Déclenchement selon l'état
+Au seuil ou au-dessus
+Au seuil ou en dessous
+Sans déclenchement selon l'état
+Seuil d'état
+Unité de mesure de l'état
+Mesure de l'état
+Motif du report
+Modifier le programme
+Date de fin
+Signaler après le nombre de minutes de retard
+Rôle destinataire du signalement
+Première échéance en heure locale
+Suivi d'un défaut de liste de contrôle
+Retirer le matériel pour réparation
+Mettre l'unité hors service
+Un contrôleur indépendant autorisé consigne ses qualifications et les preuves de levée. Le rétablissement exige le dernier blocage et un état inchangé ; la clôture seule ne remet jamais en service.
+Un autre contrôleur autorisé doit lever ce blocage.
+Intervalle de planification
+L'opération de stock n'a pas abouti. Vérifiez la disponibilité et les autorisations.
+Choisissez un article et un emplacement source valides.
+Pièces liées au stock
+Laissez l'article vide pour une pièce en texte libre. Les pièces de stock utilisent les coûts du registre et peuvent nécessiter un témoin indépendant dans le stock.
+Une autorisation de stock ou une vérification des données protégées est requise.
+Témoin de stock en attente ; terminez l'attestation avant de clôturer cet ordre de travail.
+Délai d'anticipation en jours
+Charger davantage de choix
+Le stockage de maintenance est indisponible.
+Valeur de référence de l'intervalle du compteur
+Cycles de fonctionnement
+Génération du compteur
+Heures de fonctionnement
+Intervalle du compteur
+Kilomètres
+Milles parcourus
+Sans déclenchement par compteur
+Unité de mesure du compteur
+Relevé du compteur
+Nouveau programme préventif
+Prochaine échéance
+Heure d'observation
+Échéance initiale
+Programme suspendu
+Maintenance préventive
+Vérifiez le relevé, l'heure et les conditions de remise à zéro du compteur.
+Source du relevé
+Historique des relevés
+Enregistrer le relevé
+Le premier critère atteint parmi calendrier, compteur et état déclenche l'ordre. Un seul ordre en attente est permis par programme. Annuler un ordre généré suspend le programme pour révision.
+Vérifiez le programme, le fuseau horaire, la plage de service et les critères.
+Terminez ou annulez l'ordre en attente avant de modifier la cible ou la référence du compteur.
+Preuves de vérification de la levée
+Lever le blocage de sécurité
+Qualification du contrôleur
+Compteur remplacé ou remis à zéro (motif requis)
+Rétablir l'état précédent uniquement s'il est inchangé et sans autre blocage
+Échéance révisée
+Blocages de sécurité
+Programme de maintenance
+Fin de la plage de service
+Début de la plage de service
+Plage de service
+Soumission de liste de contrôle source
+État actuel conservé
+État précédent rétabli
+Cible de maintenance
+Fuseau horaire
+Blocage de sécurité de l'unité
+Utilisation et état
+Utilisez l'action de report approuvé pour modifier l'échéance.
+Consignez une remise à zéro auditée pour modifier la référence ; utilisez un nouveau programme pour d'autres unités.
+Article en stock
+Emplacement du stock source
+Lot en stock
+Programme préventif modifié
+Seuil de maintenance atteint
+Pièce d'ordre de travail modifiée
+Programme configuré
+Relevé enregistré
+Remise à zéro du compteur enregistrée
+Échéance reportée
+Ordre de travail créé
+Signalement du retard
+Signalement du retard
+Blocage de sécurité appliqué
+Blocage de sécurité levé
+Échéance reportée
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.it.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.it.resx
index 5d251f3d..e70ba4a1 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.it.resx
@@ -154,4 +154,110 @@
Ricambio annullato
File aggiunto
File ritirato
+Programma attivo
+Scadenza rinviata
+Segnalazione del ritardo
+Blocco di sicurezza applicato
+Blocco di sicurezza rimosso
+Applicare un blocco di sicurezza
+Approvare il rinvio
+Blocco di sicurezza dell'attrezzatura
+Assegnazione e segnalazione
+Inizio del periodo escluso
+Fine del periodo escluso
+Frequenza del calendario
+Giornaliera
+Mensile
+Senza attivazione da calendario
+Trimestrale
+Settimanale
+Annuale
+Calcolare l'intervallo successivo dalla chiusura verificata
+Attivazione in base alla condizione
+Alla soglia o al di sopra
+Alla soglia o al di sotto
+Senza attivazione per condizione
+Soglia di condizione
+Unità di misura della condizione
+Lettura della condizione
+Motivo del rinvio
+Modifica programma
+Data di fine
+Segnalare dopo i minuti di ritardo
+Ruolo destinatario della segnalazione
+Prima scadenza in ora locale
+Verifica del difetto della lista di controllo
+Metti l'attrezzatura fuori uso per riparazione
+Metti l'unità fuori servizio
+Un revisore indipendente autorizzato deve registrare qualifiche e prove di rilascio. Il ripristino richiede l'ultimo blocco e uno stato invariato; la sola chiusura non ripristina il servizio.
+Un altro revisore autorizzato deve rimuovere questo blocco.
+Intervallo di pianificazione
+Impossibile completare l'operazione di magazzino. Verificare disponibilità e permessi.
+Selezionare un articolo e un'ubicazione di origine validi.
+Ricambi collegati al magazzino
+Lasciare l'articolo vuoto per un ricambio a testo libero. I ricambi di magazzino usano i costi del registro e possono richiedere un testimone indipendente nel magazzino.
+È richiesta l'autorizzazione al magazzino o la verifica dei dati protetti.
+Testimone di magazzino in attesa; completare l'attestazione prima di chiudere l'ordine di lavoro.
+Anticipo in giorni
+Carica altre opzioni
+L'archivio di manutenzione non è disponibile.
+Valore di riferimento dell'intervallo del contatore
+Cicli operativi
+Generazione del contatore
+Ore di funzionamento
+Intervallo del contatore
+Chilometri
+Miglia percorse
+Senza attivazione da contatore
+Unità di misura del contatore
+Lettura del contatore
+Nuovo programma preventivo
+Prossima scadenza
+Ora di osservazione
+Scadenza originale
+Programma sospeso
+Manutenzione preventiva
+Verificare lettura, ora di osservazione e requisiti di azzeramento del contatore.
+Fonte della lettura
+Cronologia delle letture
+Registra lettura
+Si attiva il primo criterio raggiunto tra calendario, contatore e condizione. È consentito un ordine in sospeso per programma. L'annullamento di un ordine generato sospende il programma per revisione.
+Verificare programma, fuso orario, finestra di servizio e criteri.
+Completare o annullare l'ordine in sospeso prima di modificare destinazione o base del contatore.
+Prove di verifica del rilascio
+Rimuovi blocco di sicurezza
+Qualifica del revisore
+Contatore sostituito o azzerato (motivo obbligatorio)
+Ripristina lo stato precedente solo se invariato e senza altri blocchi
+Scadenza modificata
+Blocchi di sicurezza
+Programma di manutenzione
+Fine della finestra di servizio
+Inizio della finestra di servizio
+Finestra di servizio
+Invio della lista di controllo di origine
+Stato attuale conservato
+Stato precedente ripristinato
+Destinazione della manutenzione
+Fuso orario
+Blocco di sicurezza dell'unità
+Utilizzo e condizione
+Utilizzare l'azione di rinvio approvato per modificare la scadenza.
+Registrare un azzeramento tracciato per cambiare la base; usare un nuovo programma per unità diverse.
+Articolo di magazzino
+Ubicazione di magazzino di origine
+Lotto di magazzino
+Programma preventivo modificato
+Soglia di manutenzione raggiunta
+Ricambio dell'ordine di lavoro modificato
+Programma configurato
+Lettura registrata
+Azzeramento del contatore registrato
+Scadenza rinviata
+Ordine di lavoro creato
+Segnalazione del ritardo
+Segnalazione del ritardo
+Blocco di sicurezza applicato
+Blocco di sicurezza rimosso
+Scadenza rinviata
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.pl.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.pl.resx
index 8d625361..c8838187 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.pl.resx
@@ -154,4 +154,110 @@
Unieważniono część
Dodano plik
Wycofano plik
+Aktywny harmonogram
+Termin odroczony
+Eskalacja opóźnienia
+Nałożono blokadę bezpieczeństwa
+Zwolniono blokadę bezpieczeństwa
+Nałóż blokadę bezpieczeństwa
+Zatwierdź odroczenie
+Blokada bezpieczeństwa sprzętu
+Przypisanie i eskalacja
+Początek okresu wyłączenia
+Koniec okresu wyłączenia
+Częstotliwość kalendarzowa
+Codziennie
+Co miesiąc
+Bez wyzwalacza kalendarzowego
+Co kwartał
+Co tydzień
+Co rok
+Licz następny okres od zweryfikowanego zamknięcia
+Wyzwalacz stanu
+Na progu lub powyżej
+Na progu lub poniżej
+Bez wyzwalacza stanu
+Próg stanu
+Jednostka pomiaru stanu
+Odczyt stanu
+Powód odroczenia
+Edytuj harmonogram
+Data końcowa
+Eskaluj po upływie minut opóźnienia
+Rola odbiorcy eskalacji
+Pierwszy termin w czasie lokalnym
+Działania po usterce z listy kontrolnej
+Wyłącz sprzęt do naprawy
+Wyłącz jednostkę ze służby
+Niezależny uprawniony kontroler musi zapisać kwalifikacje i dowody zwolnienia. Przywrócenie wymaga ostatniej blokady i niezmienionego stanu; samo zamknięcie nie przywraca gotowości.
+Inny uprawniony kontroler musi zwolnić tę blokadę.
+Odstęp harmonogramu
+Nie udało się zakończyć operacji magazynowej. Sprawdź dostępność i uprawnienia.
+Wybierz prawidłowy artykuł i lokalizację źródłową.
+Części powiązane z magazynem
+Pozostaw artykuł pusty, aby opisać część dowolnym tekstem. Części magazynowe używają kosztów ewidencji i mogą wymagać niezależnego świadka w magazynie.
+Wymagane uprawnienie magazynowe lub weryfikacja danych chronionych.
+Oczekiwanie na świadka magazynowego; zakończ poświadczenie przed zamknięciem zlecenia.
+Wyprzedzenie w dniach
+Wczytaj więcej opcji
+Magazyn danych konserwacji jest niedostępny.
+Baza okresu licznika
+Cykle pracy
+Generacja licznika
+Godziny pracy
+Okres licznika
+Kilometry
+Przejechane mile
+Bez wyzwalacza licznika
+Jednostka licznika
+Odczyt licznika
+Nowy harmonogram prewencyjny
+Następny termin
+Czas obserwacji
+Pierwotny termin
+Wstrzymany harmonogram
+Konserwacja zapobiegawcza
+Sprawdź odczyt, czas obserwacji i wymagania zerowania licznika.
+Źródło odczytu
+Historia odczytów
+Zapisz odczyt
+Wyzwala pierwszy osiągnięty warunek kalendarza, licznika lub stanu. Dozwolone jest jedno oczekujące zlecenie na harmonogram. Anulowanie wygenerowanego zlecenia wstrzymuje harmonogram do przeglądu.
+Sprawdź harmonogram, strefę czasową, okno serwisowe i wyzwalacze.
+Zakończ lub anuluj oczekujące zlecenie przed zmianą celu lub bazy licznika.
+Dowody weryfikacji zwolnienia
+Zwolnij blokadę bezpieczeństwa
+Kwalifikacje kontrolera
+Licznik wymieniony lub wyzerowany (wymagany powód)
+Przywróć poprzedni stan tylko bez zmian i innych blokad
+Zmieniony termin
+Blokady bezpieczeństwa
+Harmonogram konserwacji
+Koniec okna serwisowego
+Początek okna serwisowego
+Okno serwisowe
+Źródłowe zgłoszenie listy kontrolnej
+Zachowano bieżący stan
+Przywrócono poprzedni stan
+Cel konserwacji
+Strefa czasowa
+Blokada bezpieczeństwa jednostki
+Użytkowanie i stan
+Użyj zatwierdzonego odroczenia, aby zmienić termin.
+Zapisz audytowane zerowanie, aby zmienić bazę; dla innych jednostek utwórz nowy harmonogram.
+Artykuł magazynowy
+Źródłowa lokalizacja magazynowa
+Partia magazynowa
+Zmieniono harmonogram prewencyjny
+Osiągnięto próg konserwacji
+Zmieniono część zlecenia
+Skonfigurowano harmonogram
+Zapisano odczyt
+Zapisano zerowanie licznika
+Termin odroczony
+Utworzono zlecenie pracy
+Eskalacja opóźnienia
+Eskalacja opóźnienia
+Nałożono blokadę bezpieczeństwa
+Zwolniono blokadę bezpieczeństwa
+Termin odroczony
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.resx
index a5cdf33c..e3441bfe 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.resx
@@ -154,4 +154,110 @@
Part voided
File added
File withdrawn
+Active schedule
+Due date deferred
+Overdue escalation
+Safety hold applied
+Safety hold released
+Apply safety hold
+Approve deferral
+Equipment safety hold
+Assignment and escalation
+Blackout starts
+Blackout ends
+Calendar frequency
+Daily
+Monthly
+No calendar trigger
+Quarterly
+Weekly
+Yearly
+Measure the next interval from verified closure
+Condition trigger
+At or above threshold
+At or below threshold
+No condition trigger
+Condition threshold
+Condition measurement unit
+Condition reading
+Reason for deferral
+Edit schedule
+End date
+Escalate after overdue minutes
+Escalation recipient role
+First due date and local time
+Checklist defect follow-up
+Place equipment out for repair
+Place unit out of service
+An independent authorized reviewer must record qualifications and release evidence. Restoration requires the last hold and an unchanged state; closure alone never restores service.
+A different authorized reviewer must release this hold.
+Scheduling interval
+The inventory operation could not be completed. Review inventory availability and permissions.
+Choose a valid inventory item and source location.
+Inventory-linked parts
+Leave the item blank for a free-text part. Inventory parts use ledger costs and may require an independent witness in Inventory.
+Inventory permission or protected-data verification is required.
+Inventory witness pending; complete witnessing before closing this work order.
+Lead time in days
+Load more choices
+Maintenance storage is unavailable.
+Meter interval baseline
+Operating cycles
+Meter generation
+Operating hours
+Meter interval
+Kilometers
+Miles travelled
+No meter trigger
+Meter measurement unit
+Meter reading
+New preventive schedule
+Next due date
+Observation time
+Original due date
+Paused schedule
+Preventive maintenance
+Check the reading, observation time and meter reset requirements.
+Reading source
+Reading history
+Record reading
+Calendar, meter and condition triggers use whichever is reached first. One pending order is allowed per schedule. Cancelling a generated order pauses the schedule for review.
+Check the schedule, time zone, service window and trigger settings.
+Complete or cancel the pending order before changing this target or meter baseline.
+Release verification evidence
+Release safety hold
+Reviewer qualification
+Meter replaced or reset (reason required)
+Restore previous state only if unchanged and no other holds remain
+Revised due date
+Safety holds
+Maintenance schedule
+Service window ends
+Service window starts
+Service window
+Source checklist submission
+Current state preserved
+Previous state restored
+Maintenance target
+Time zone
+Unit safety hold
+Usage and condition
+Use the approved deferral action to change the due date.
+Record an audited meter reset to change the baseline; use a new schedule for different measurement units.
+Inventory item
+Source stock location
+Inventory lot
+Preventive schedule changed
+Maintenance threshold reached
+Work order part changed
+Schedule configured
+Reading recorded
+Meter reset recorded
+Due date deferred
+Work order created
+Overdue escalation
+Overdue escalation
+Safety hold applied
+Safety hold released
+Due date deferred
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.sv.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.sv.resx
index 26b7823b..1b1afcd9 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.sv.resx
@@ -154,4 +154,110 @@
Del makulerad
Fil tillagd
Fil återkallad
+Aktivt schema
+Förfallodatum uppskjutet
+Försening eskalerad
+Säkerhetsspärr införd
+Säkerhetsspärr hävd
+Inför säkerhetsspärr
+Godkänn uppskjutning
+Säkerhetsspärr för utrustning
+Tilldelning och eskalering
+Spärrperiodens början
+Spärrperiodens slut
+Kalenderfrekvens
+Dagligen
+Månadsvis
+Ingen kalenderutlösare
+Kvartalsvis
+Veckovis
+Årligen
+Beräkna nästa intervall från verifierad stängning
+Tillståndsutlösare
+Vid eller över tröskeln
+Vid eller under tröskeln
+Ingen tillståndsutlösare
+Tillståndströskel
+Mätenhet för tillstånd
+Tillståndsmätvärde
+Orsak till uppskjutning
+Redigera schema
+Slutdatum
+Eskalera efter antal försenade minuter
+Mottagarroll för eskalering
+Första förfallodatum och lokal tid
+Uppföljning av checklistfel
+Ta utrustning ur drift för reparation
+Ta enheten ur tjänst
+En oberoende behörig granskare måste registrera kvalifikationer och frigivningsunderlag. Återställning kräver den sista spärren och oförändrat tillstånd; enbart stängning återställer aldrig drift.
+En annan behörig granskare måste häva spärren.
+Schemaläggningsintervall
+Lageråtgärden kunde inte slutföras. Kontrollera tillgänglighet och behörigheter.
+Välj en giltig lagerartikel och källplats.
+Lagerkopplade delar
+Lämna artikeln tom för en del i fritext. Lagerdelar använder bokförda kostnader och kan kräva ett oberoende vittne i lagret.
+Lagerbehörighet eller verifiering av skyddade data krävs.
+Lagervittne inväntas; slutför bevittningen innan arbetsordern stängs.
+Framförhållning i dagar
+Läs in fler alternativ
+Underhållslagringen är inte tillgänglig.
+Basvärde för mätarintervall
+Driftcykler
+Mätargeneration
+Drifttimmar
+Mätarintervall
+Kilometer
+Körda engelska mil
+Ingen mätarutlösare
+Mätarens måttenhet
+Mätarställning
+Nytt förebyggande schema
+Nästa förfallodatum
+Observationstid
+Ursprungligt förfallodatum
+Pausat schema
+Förebyggande underhåll
+Kontrollera mätvärde, observationstid och krav för nollställning.
+Mätvärdets källa
+Mätvärdeshistorik
+Registrera mätvärde
+Det första uppnådda villkoret för kalender, mätare eller tillstånd utlöser ordern. En väntande order tillåts per schema. Avbruten genererad order pausar schemat för granskning.
+Kontrollera schema, tidszon, servicefönster och utlösare.
+Slutför eller avbryt väntande order innan mål eller mätarbas ändras.
+Verifieringsunderlag för frigivning
+Häv säkerhetsspärr
+Granskarens kvalifikation
+Mätare bytt eller nollställd (orsak krävs)
+Återställ tidigare tillstånd endast om det är oförändrat och inga andra spärrar finns
+Ändrat förfallodatum
+Säkerhetsspärrar
+Underhållsschema
+Servicefönstrets slut
+Servicefönstrets början
+Servicefönster
+Ursprunglig checklistinlämning
+Aktuellt tillstånd bevarat
+Tidigare tillstånd återställt
+Underhållsmål
+Tidszon
+Säkerhetsspärr för enhet
+Användning och tillstånd
+Använd godkänd uppskjutning för att ändra förfallodatum.
+Registrera en spårbar nollställning för att ändra basvärdet; använd ett nytt schema för andra måttenheter.
+Lagerartikel
+Källplats i lagret
+Lagerparti
+Förebyggande schema ändrat
+Underhållströskel uppnådd
+Arbetsorderdel ändrad
+Schema konfigurerat
+Mätvärde registrerat
+Mätarnollställning registrerad
+Förfallodatum uppskjutet
+Arbetsorder skapad
+Försening eskalerad
+Försening eskalerad
+Säkerhetsspärr införd
+Säkerhetsspärr hävd
+Förfallodatum uppskjutet
diff --git a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.uk.resx b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.uk.resx
index b7fa630d..237d37c5 100644
--- a/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/WorkOrders/WorkOrders.uk.resx
@@ -154,4 +154,110 @@
Анульовано деталь
Додано файл
Відкликано файл
+Активний розклад
+Термін відкладено
+Ескалація прострочення
+Накладено блокування безпеки
+Блокування безпеки знято
+Накласти блокування безпеки
+Затвердити відкладення
+Блокування безпеки обладнання
+Призначення та ескалація
+Початок періоду виключення
+Кінець періоду виключення
+Календарна періодичність
+Щодня
+Щомісяця
+Без календарного тригера
+Щокварталу
+Щотижня
+Щороку
+Відраховувати наступний інтервал від підтвердженого закриття
+Тригер за станом
+На рівні порога або вище
+На рівні порога або нижче
+Без тригера за станом
+Порогове значення стану
+Одиниця вимірювання стану
+Показник стану
+Причина відкладення
+Редагувати розклад
+Дата завершення
+Ескалювати через вказану кількість хвилин прострочення
+Роль одержувача ескалації
+Перший термін за місцевим часом
+Усунення дефекту з контрольного списку
+Вивести обладнання з експлуатації для ремонту
+Вивести підрозділ зі служби
+Незалежний уповноважений перевіряльник має записати кваліфікацію та докази зняття блокування. Відновлення можливе лише для останнього блокування за незмінного стану; саме закриття не відновлює експлуатацію.
+Це блокування має зняти інший уповноважений перевіряльник.
+Інтервал планування
+Не вдалося завершити складську операцію. Перевірте наявність та дозволи.
+Виберіть коректну складську позицію та місце походження.
+Деталі, пов’язані зі складом
+Залиште позицію порожньою для довільного опису деталі. Складські деталі використовують витрати з обліку та можуть потребувати незалежного свідка в розділі складу.
+Потрібен дозвіл на склад або перевірка захищених даних.
+Очікується складський свідок; завершіть засвідчення перед закриттям наряду.
+Випередження в днях
+Завантажити більше варіантів
+Сховище технічного обслуговування недоступне.
+Базове значення інтервалу лічильника
+Робочі цикли
+Покоління лічильника
+Мотогодини
+Інтервал лічильника
+Кілометри
+Пройдені милі
+Без тригера лічильника
+Одиниця вимірювання лічильника
+Показник лічильника
+Новий профілактичний розклад
+Наступний термін
+Час спостереження
+Первісний термін
+Призупинений розклад
+Профілактичне обслуговування
+Перевірте показник, час спостереження та вимоги скидання лічильника.
+Джерело показника
+Історія показників
+Записати показник
+Спрацьовує перший досягнутий критерій календаря, лічильника або стану. Дозволено один незавершений наряд на розклад. Скасування створеного наряду призупиняє розклад для перегляду.
+Перевірте розклад, часовий пояс, вікно обслуговування та тригери.
+Завершіть або скасуйте незавершений наряд перед зміною цілі чи бази лічильника.
+Докази перевірки для зняття блокування
+Зняти блокування безпеки
+Кваліфікація перевіряльника
+Лічильник замінено або скинуто (потрібна причина)
+Відновити попередній стан лише за відсутності змін та інших блокувань
+Змінений термін
+Блокування безпеки
+Розклад обслуговування
+Кінець вікна обслуговування
+Початок вікна обслуговування
+Вікно обслуговування
+Вихідне подання контрольного списку
+Поточний стан збережено
+Попередній стан відновлено
+Об’єкт обслуговування
+Часовий пояс
+Блокування безпеки підрозділу
+Використання та стан
+Щоб змінити термін, скористайтеся затвердженим відкладенням.
+Запишіть контрольоване скидання для зміни бази; для інших одиниць вимірювання створіть новий розклад.
+Складська позиція
+Вихідне місце зберігання
+Складська партія
+Профілактичний розклад змінено
+Досягнуто порога обслуговування
+Деталь наряду змінено
+Розклад налаштовано
+Показник записано
+Скидання лічильника записано
+Термін відкладено
+Створено наряд на роботи
+Ескалація прострочення
+Ескалація прострочення
+Накладено блокування безпеки
+Блокування безпеки знято
+Термін відкладено
diff --git a/Core/Resgrid.Model/Checklists/ChecklistContracts.cs b/Core/Resgrid.Model/Checklists/ChecklistContracts.cs
index 698a3d09..77fd5a6f 100644
--- a/Core/Resgrid.Model/Checklists/ChecklistContracts.cs
+++ b/Core/Resgrid.Model/Checklists/ChecklistContracts.cs
@@ -57,6 +57,10 @@ public sealed class ChecklistItem
public bool AllowNotApplicable { get; set; }
public bool RequireNoteOnFail { get; set; } = true;
public bool RequirePhotoOnFail { get; set; }
+ public bool CreateWorkOrderOnFail { get; set; }
+ public Resgrid.Model.WorkOrders.WorkOrderPriority WorkOrderPriority { get; set; } = Resgrid.Model.WorkOrders.WorkOrderPriority.Normal;
+ public bool SetUnitStateOnFail { get; set; }
+ public bool HoldAssetOnFail { get; set; }
public decimal Weight { get; set; } = 1;
public string Units { get; set; }
public decimal? Minimum { get; set; }
diff --git a/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs b/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
index e764ddb4..46abfbc9 100644
--- a/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
+++ b/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
@@ -16,7 +16,7 @@ public static class ChecklistWorkflowPayload
(int)WorkflowTriggerEventType.ChecklistMissed, (int)WorkflowTriggerEventType.WorkOrderCreated,
(int)WorkflowTriggerEventType.WorkOrderStatusChanged, (int)WorkflowTriggerEventType.WorkOrderAssigned,
(int)WorkflowTriggerEventType.ChecklistScheduleChanged, (int)WorkflowTriggerEventType.ChecklistOccurrenceSkipped
- }.Concat(Resgrid.Model.Inventories.InventoryWorkflowPayload.Triggers).ToArray());
+ }.Concat(Resgrid.Model.WorkOrders.WorkOrderWorkflowPayload.Triggers).Distinct().Concat(Resgrid.Model.Inventories.InventoryWorkflowPayload.Triggers).ToArray());
private static readonly string[] Identifiers = { "CompletionId", "DefinitionId", "VersionId", "ItemId", "ScheduleId", "OccurrenceId" };
private static bool IsStructuralTarget(int type, string target) =>
type >= 0 && type <= 2 && int.TryParse(target, out var numeric) && numeric > 0
@@ -32,7 +32,7 @@ public static string Routing(string payloadJson, string aggregateId)
{
var source = Resgrid.Model.Inventories.InventoryWorkflowPayload.Parse(payloadJson);
if (Resgrid.Model.Inventories.InventoryWorkflowPayload.IsInventory(source)) return Resgrid.Model.Inventories.InventoryWorkflowPayload.Routing(source);
- if (source["WorkOrderId"] != null) return Resgrid.Model.WorkOrders.WorkOrderWorkflowPayload.Routing(source);
+ if (source["WorkOrderId"] != null || source["RecurrenceId"] != null) return Resgrid.Model.WorkOrders.WorkOrderWorkflowPayload.Routing(source);
var safe = new JObject();
foreach (var name in Identifiers)
if (source[name]?.Type == JTokenType.String && Guid.TryParse(source[name].Value(), out var id)) safe[name] = id.ToString("D");
@@ -57,7 +57,7 @@ public static async Task ProjectAsync(int departmentId, object value, IP
var source = value as JObject ?? (value == null ? new JObject() : JObject.FromObject(value));
var payload = wrapped ? source["Payload"] as JObject ?? new JObject() : source;
if (Resgrid.Model.Inventories.InventoryWorkflowPayload.IsInventory(payload)) return await Resgrid.Model.Inventories.InventoryWorkflowPayload.ProjectAsync(departmentId, source, protection, wrapped);
- if (payload["WorkOrderId"] != null) return await Resgrid.Model.WorkOrders.WorkOrderWorkflowPayload.ProjectAsync(departmentId, payload, protection, wrapped);
+ if (payload["WorkOrderId"] != null || payload["RecurrenceId"] != null) return await Resgrid.Model.WorkOrders.WorkOrderWorkflowPayload.ProjectAsync(departmentId, payload, protection, wrapped);
var safe = new JObject();
foreach (var name in Identifiers)
if (Guid.TryParse(payload[name]?.Value(), out var id)) safe[name] = id.ToString("D");
diff --git a/Core/Resgrid.Model/Inventories/InventoryContracts.cs b/Core/Resgrid.Model/Inventories/InventoryContracts.cs
index 9617f682..f21048af 100644
--- a/Core/Resgrid.Model/Inventories/InventoryContracts.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryContracts.cs
@@ -39,6 +39,7 @@ public sealed class InventoryPosting
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public InventoryUsageType? UsageType { get; set; }
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string PurchaseOrderItemId { get; set; }
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string CountItemId { get; set; }
+ [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public int? WorkOrderPartId { get; set; }
public string Note { get; set; }
public decimal? UnitCost { get; set; }
}
diff --git a/Core/Resgrid.Model/Inventories/InventoryModels.cs b/Core/Resgrid.Model/Inventories/InventoryModels.cs
index 06ec1e87..0c349217 100644
--- a/Core/Resgrid.Model/Inventories/InventoryModels.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryModels.cs
@@ -94,6 +94,7 @@ public sealed class InventoryTransaction : InventoryRow
public string IssuanceId { get; set; }
public string PurchaseOrderItemId { get; set; }
public string CountItemId { get; set; }
+ public int? WorkOrderPartId { get; set; }
public int? LegacyInventoryId { get; set; }
public DateTime OccurredOn { get; set; }
}
diff --git a/Core/Resgrid.Model/Repositories/IInventoryStore.cs b/Core/Resgrid.Model/Repositories/IInventoryStore.cs
index 27d63619..7bd34f86 100644
--- a/Core/Resgrid.Model/Repositories/IInventoryStore.cs
+++ b/Core/Resgrid.Model/Repositories/IInventoryStore.cs
@@ -13,10 +13,12 @@ public interface IInventoryStore
Task OpenAlertAsync(int departmentId, string dedupKey);
Task AlertDeliveryAsync(int departmentId, string alertId, string userId);
Task> OpenAlertsAsync(int departmentId, int skip = 0);
+ Task> ClaimableAlertsAsync(int departmentId, string userId, System.DateTime now, int skip = 0);
Task GetAsync(int departmentId, string id) where T : InventoryRow;
Task> ListAsync(int departmentId, int skip = 0) where T : InventoryRow;
Task> QueryAsync(int departmentId, InventoryQuery filter, int skip = 0) where T : InventoryRow;
Task> RelatedAsync(int departmentId, string column, string id) where T : InventoryRow;
+ Task> RelatedManyAsync(int departmentId, string column, IReadOnlyCollection ids) where T : InventoryRow;
Task InsertAsync(T row) where T : InventoryRow;
Task UpdateAsync(T row, int expectedRevision) where T : InventoryRow;
Task RequestAsync(int departmentId, string requestId);
diff --git a/Core/Resgrid.Model/Repositories/IWorkOrderMaintenanceRepository.cs b/Core/Resgrid.Model/Repositories/IWorkOrderMaintenanceRepository.cs
new file mode 100644
index 00000000..1d31da0f
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/IWorkOrderMaintenanceRepository.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface IWorkOrderMaintenanceRepository
+ {
+ Task> QueryMaintenanceAsync(int departmentId, string field = null, object value = null, int skip = 0, bool pendingOnly = false) where T : WorkOrderRow;
+ Task> MaintenanceDepartmentsAsync(int afterDepartmentId);
+ Task> OverdueAsync(int departmentId, DateTime now);
+ Task FailureAsync(int departmentId, string completionId, string itemId);
+ Task> ActiveHoldsAsync(int departmentId, int? unitId, string assetId);
+ Task LockUnitAsync(int departmentId, int unitId);
+ Task LatestUnitStateAsync(int departmentId, int unitId);
+ Task LastAppendedUnitStateIdAsync(int departmentId, int unitId);
+ Task AppendUnitStateAsync(int departmentId, int unitId, int state, DateTime now);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IInventoryModernizationService.cs b/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
index 8f2e30e8..cb34fb40 100644
--- a/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
+++ b/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
@@ -14,6 +14,7 @@ public interface IInventoryCatalogService
Task SaveLotAsync(InventoryActor actor, InventoryLot lot, InventoryLotContent details);
Task ArchiveAsync(InventoryActor actor, string id, int revision) where T : InventoryMutableRow;
Task GetAsync(InventoryActor actor, string id) where T : InventoryRow;
+ Task> GetManyAsync(InventoryActor actor, IReadOnlyCollection ids) where T : InventoryRow;
Task GetLegacyTransactionAsync(InventoryActor actor, int inventoryId);
Task> ListAsync(InventoryActor actor, int page = 0) where T : InventoryRow;
Task> QueryAsync(InventoryActor actor, InventoryQuery filter, int page = 0) where T : InventoryRow;
diff --git a/Core/Resgrid.Model/Services/IWorkOrdersService.cs b/Core/Resgrid.Model/Services/IWorkOrdersService.cs
index a9ecaed8..30ba39c8 100644
--- a/Core/Resgrid.Model/Services/IWorkOrdersService.cs
+++ b/Core/Resgrid.Model/Services/IWorkOrdersService.cs
@@ -47,6 +47,7 @@ public sealed class WorkOrderException : Exception
}
public sealed class WorkOrderFilter
{
+ public string ChecklistCompletionId { get; set; }
public int Page { get; set; }
public WorkOrderStatus? Status { get; set; }
public WorkOrderPriority? Priority { get; set; }
@@ -88,7 +89,7 @@ public sealed class WorkOrderTransition
}
public sealed class WorkOrderAssignment { public int Revision { get; set; } public string UserId { get; set; } public int? RoleId { get; set; } }
public sealed class WorkOrderLaborInput { public int Revision { get; set; } public string UserId { get; set; } public DateTime WorkDate { get; set; } public WorkOrderLaborContent Content { get; set; } = new WorkOrderLaborContent(); }
- public sealed class WorkOrderPartInput { public int Revision { get; set; } public WorkOrderPartContent Content { get; set; } = new WorkOrderPartContent(); }
+ public sealed class WorkOrderPartInput { public string RequestId { get; set; } public string InventoryItemId { get; set; } public string InventoryAssetId { get; set; } public string InventoryLotId { get; set; } public string InventoryLocationId { get; set; } public int Revision { get; set; } public WorkOrderPartContent Content { get; set; } = new WorkOrderPartContent(); }
public sealed class WorkOrderSummary
{
public WorkOrderStatus? QuickStatus { get; set; }
@@ -122,14 +123,19 @@ public sealed class WorkOrderDetail
public string VerifiedBy { get; set; }
public DateTime? CompletedOn { get; set; }
public DateTime? ClosedOn { get; set; }
+ public string SourceChecklistCompletionId { get; set; }
+ public string SourceChecklistItemId { get; set; }
+ public string RecurrenceId { get; set; }
+ public DateTime? OriginalDueOn { get; set; }
+ public DateTime? EscalatedOn { get; set; }
public List Activities { get; set; } = new List();
public List Labor { get; set; } = new List();
public List Parts { get; set; } = new List();
public List Files { get; set; } = new List();
}
- public sealed class WorkOrderActivityView { public WorkOrderContent Snapshot { get; set; } public string AssignedToUserId { get; set; } public int? AssignedToRoleId { get; set; } public int Id { get; set; } public WorkOrderActivityType Type { get; set; } public string UserId { get; set; } public DateTime CreatedOn { get; set; } public string Note { get; set; } public int? OldStatus { get; set; } public int? NewStatus { get; set; } }
+ public sealed class WorkOrderActivityView { public DateTime? OriginalDueOn { get; set; } public DateTime? RevisedDueOn { get; set; } public WorkOrderContent Snapshot { get; set; } public string AssignedToUserId { get; set; } public int? AssignedToRoleId { get; set; } public int Id { get; set; } public WorkOrderActivityType Type { get; set; } public string UserId { get; set; } public DateTime CreatedOn { get; set; } public string Note { get; set; } public int? OldStatus { get; set; } public int? NewStatus { get; set; } }
public sealed class WorkOrderLaborView { public int Id { get; set; } public string UserId { get; set; } public DateTime WorkDate { get; set; } public WorkOrderLaborContent Content { get; set; } }
- public sealed class WorkOrderPartView { public int Id { get; set; } public DateTime? VoidedOn { get; set; } public WorkOrderPartContent Content { get; set; } }
+ public sealed class WorkOrderPartView { public string InventoryWitnessRequestId { get; set; } public string InventoryItemId { get; set; } public string InventoryTransactionId { get; set; } public string InventoryOperationId { get; set; } public bool AwaitingWitness { get; set; } public int Id { get; set; } public DateTime? VoidedOn { get; set; } public WorkOrderPartContent Content { get; set; } }
public sealed class WorkOrderFileView { public int Id { get; set; } public string Name { get; set; } public string ContentType { get; set; } public int Size { get; set; } public DateTime? WithdrawnOn { get; set; } }
public sealed class WorkOrderChoice { public string Id { get; set; } public string Name { get; set; } }
public sealed class WorkOrderChoices
diff --git a/Core/Resgrid.Model/WorkOrders/WorkOrderMaintenance.cs b/Core/Resgrid.Model/WorkOrders/WorkOrderMaintenance.cs
new file mode 100644
index 00000000..15cbc548
--- /dev/null
+++ b/Core/Resgrid.Model/WorkOrders/WorkOrderMaintenance.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Model.WorkOrders
+{
+ public enum MaintenanceCalendar { None = 0, Daily = 1, Weekly = 2, Monthly = 3, Quarterly = 4, Yearly = 5 }
+ public enum MaintenanceMeterUnit { None = 0, Hours = 1, Kilometers = 2, Miles = 3, Cycles = 4 }
+ public enum MaintenanceCondition { None = 0, AtOrAbove = 1, AtOrBelow = 2 }
+ public enum MaintenanceChangeType { Configured = 0, Reading = 1, MeterReset = 2, Deferred = 3, Generated = 4, Overdue = 5 }
+
+ /// Routing only. Never persist findings, answers or a grant in an unattended intent.
+ public sealed class WorkOrderFailureIntent : WorkOrderRow
+ {
+ public string CompletionId { get; set; }
+ public string ItemId { get; set; }
+ public string VersionId { get; set; }
+ public string OccurrenceId { get; set; }
+ public string AuthorizedBy { get; set; }
+ public int TargetType { get; set; }
+ public string TargetId { get; set; }
+ public int? TargetGroupId { get; set; }
+ public int Priority { get; set; }
+ public bool HoldUnit { get; set; }
+ public bool HoldAsset { get; set; }
+ public DateTime? ProcessedOn { get; set; }
+ }
+ public sealed class WorkOrderSafetyHold : WorkOrderRow
+ {
+ public int? UnitId { get; set; }
+ public string AssetId { get; set; }
+ public int? PreviousState { get; set; }
+ public int? AppliedStateId { get; set; }
+ public int? AppliedAssetRevision { get; set; }
+ public DateTime? ReleasedOn { get; set; }
+ public string ReleasedBy { get; set; }
+ public bool StateRestored { get; set; }
+ }
+ public sealed class WorkOrderHoldContent { public string Reason { get; set; } public string ReleaseEvidence { get; set; } public string Qualification { get; set; } }
+ public sealed class WorkOrderHoldInput { public int Revision { get; set; } public bool Unit { get; set; } public bool Asset { get; set; } public string Reason { get; set; } }
+ public sealed class WorkOrderReleaseInput { public int Revision { get; set; } public string Evidence { get; set; } public string Qualification { get; set; } public bool RestoreState { get; set; } }
+ public sealed class WorkOrderHoldView { public WorkOrderSafetyHold Hold { get; set; } public WorkOrderHoldContent Content { get; set; } }
+
+ public sealed class WorkOrderRecurrence : WorkOrderRow
+ {
+ public string RequestId { get; set; }
+ public int CurrentVersionId { get; set; }
+ public int? TargetUnitId { get; set; }
+ public int? TargetGroupId { get; set; }
+ public string InventoryAssetId { get; set; }
+ public string AssignedToUserId { get; set; }
+ public int? AssignedToRoleId { get; set; }
+ public int Priority { get; set; }
+ public bool IsActive { get; set; }
+ public int Calendar { get; set; }
+ public int Interval { get; set; }
+ public string TimeZoneId { get; set; }
+ public DateTime AnchorLocal { get; set; }
+ public DateTime? EndOn { get; set; }
+ public int LeadDays { get; set; }
+ public bool CompletionBased { get; set; }
+ public int ServiceWeekdays { get; set; }
+ public int ServiceStartMinute { get; set; }
+ public int ServiceEndMinute { get; set; }
+ public DateTime? BlackoutFrom { get; set; }
+ public DateTime? BlackoutUntil { get; set; }
+ public DateTime? NextDueOn { get; set; }
+ public long Cycle { get; set; }
+ public int? PendingWorkOrderId { get; set; }
+ public bool ReadingDue { get; set; }
+ public DateTime? ReadingDueOn { get; set; }
+ public int MeterUnit { get; set; }
+ public int MeterEpoch { get; set; }
+ public decimal? MeterInterval { get; set; }
+ public decimal? MeterBaseline { get; set; }
+ public decimal? LastMeterValue { get; set; }
+ public DateTime? LastReadingOn { get; set; }
+ public int Condition { get; set; }
+ public decimal? ConditionThreshold { get; set; }
+ public bool ConditionLatched { get; set; }
+ public int EscalateAfterMinutes { get; set; }
+ public int? EscalationRoleId { get; set; }
+ }
+ /// Immutable protected instructions/configuration; generated orders retain this identity.
+ public sealed class WorkOrderRecurrenceVersion : WorkOrderRow { public int RecurrenceId { get; set; } }
+ public sealed class WorkOrderMeterReading : WorkOrderRow
+ {
+ public int RecurrenceId { get; set; }
+ public string RequestId { get; set; }
+ public int MeterEpoch { get; set; }
+ public DateTime ObservedOn { get; set; }
+ }
+ public sealed class WorkOrderRecurrenceChange : WorkOrderRow { public int RecurrenceId { get; set; } public int ChangeType { get; set; } public DateTime? OriginalDueOn { get; set; } public DateTime? RevisedDueOn { get; set; } }
+ public sealed class WorkOrderRecurrenceInput
+ {
+ public int Id { get; set; }
+ public int Revision { get; set; }
+ public bool IsActive { get; set; } = true;
+ public WorkOrderInput Template { get; set; } = new();
+ public string AssignedToUserId { get; set; }
+ public int? AssignedToRoleId { get; set; }
+ public MaintenanceCalendar Calendar { get; set; } = MaintenanceCalendar.Monthly;
+ public int Interval { get; set; } = 1;
+ public string TimeZoneId { get; set; } = "UTC";
+ public DateTime AnchorLocal { get; set; }
+ public DateTime? EndOn { get; set; }
+ public int LeadDays { get; set; } = 3;
+ public bool CompletionBased { get; set; }
+ public int ServiceWeekdays { get; set; } = 127;
+ public int ServiceStartMinute { get; set; }
+ public int ServiceEndMinute { get; set; } = 1439;
+ public DateTime? BlackoutFrom { get; set; }
+ public DateTime? BlackoutUntil { get; set; }
+ public MaintenanceMeterUnit MeterUnit { get; set; }
+ public decimal? MeterInterval { get; set; }
+ public decimal? MeterBaseline { get; set; }
+ public MaintenanceCondition Condition { get; set; }
+ public decimal? ConditionThreshold { get; set; }
+ public string ConditionUnit { get; set; }
+ public int EscalateAfterMinutes { get; set; }
+ public int? EscalationRoleId { get; set; }
+ public string Reason { get; set; }
+ }
+ public sealed class WorkOrderReadingInput { public int Revision { get; set; } public string RequestId { get; set; } public DateTime ObservedOn { get; set; } public decimal? MeterValue { get; set; } public decimal? ConditionValue { get; set; } public bool ResetMeter { get; set; } public string Source { get; set; } public string Note { get; set; } }
+ public sealed class WorkOrderDeferralInput { public int Revision { get; set; } public DateTime DueOn { get; set; } public string Reason { get; set; } }
+ public sealed class WorkOrderRecurrenceView { public int HistoryPage { get; set; } public bool HasMoreHistory { get; set; } public WorkOrderRecurrence Schedule { get; set; } public WorkOrderRecurrenceInput Settings { get; set; } public List Changes { get; set; } = new(); public List Readings { get; set; } = new(); }
+ public sealed class WorkOrderMaintenanceSweep { public int Generated { get; set; } public int Held { get; set; } public int Escalated { get; set; } public int Deferred { get; set; } public int Errors { get; set; } }
+ public sealed class WorkOrderAssetState { public List OutboxIds { get; set; } = new(); public int State { get; set; } public int Revision { get; set; } }
+}
+
+namespace Resgrid.Model.Services
+{
+ using Resgrid.Model.WorkOrders;
+ public interface IWorkOrderMaintenanceService
+ {
+ Task> InventoryChoicesAsync(ChecklistActor actor, string kind, string itemId = null, int page = 0);
+ Task> HoldsAsync(ChecklistActor actor, int orderId);
+ Task CancelPartWitnessAsync(ChecklistActor actor, int orderId, int partId, int revision, string reason);
+ Task AddHoldAsync(ChecklistActor actor, int orderId, WorkOrderHoldInput input);
+ Task ReleaseHoldAsync(ChecklistActor actor, int holdId, WorkOrderReleaseInput input);
+ Task> RecurrencesAsync(ChecklistActor actor, int page = 0);
+ Task RecurrenceAsync(ChecklistActor actor, int id, int historyPage = 0);
+ Task SaveRecurrenceAsync(ChecklistActor actor, WorkOrderRecurrenceInput input);
+ Task RecordReadingAsync(ChecklistActor actor, int id, WorkOrderReadingInput input);
+ Task DeferAsync(ChecklistActor actor, int orderId, WorkOrderDeferralInput input);
+ Task GenerateMaintenanceAsync(int departmentId);
+ Task EscalateMaintenanceAsync(int departmentId);
+ Task ValidateFailureOptionsAsync(ChecklistActor actor, ChecklistForm form);
+ Task RecordFailureIntentAsync(ChecklistActor actor, ChecklistCompletion completion, ChecklistDefinitionVersion version, ChecklistItem item);
+ Task CompleteInventoryPartAsync(InventoryActor actor, int partId, string transactionId, bool reversal, List events);
+ }
+ /// Inventory owns its permissions, controlled witnesses, ledger and asset state. Caller owns the shared transaction.
+ public interface IInventoryWorkOrderAdapter
+ {
+ Task PostPartAsync(InventoryActor actor, int partId, InventoryCommand command);
+ Task CancelPendingPartAsync(InventoryActor actor, int partId, string operationId);
+ Task ApplyHoldAsync(InventoryActor actor, int orderId, string assetId, int? restoreState = null, int? expectedRevision = null, bool safetyRelease = false);
+ }
+}
diff --git a/Core/Resgrid.Model/WorkOrders/WorkOrderModels.cs b/Core/Resgrid.Model/WorkOrders/WorkOrderModels.cs
index da2a1480..7b8d02a7 100644
--- a/Core/Resgrid.Model/WorkOrders/WorkOrderModels.cs
+++ b/Core/Resgrid.Model/WorkOrders/WorkOrderModels.cs
@@ -8,7 +8,7 @@ namespace Resgrid.Model.WorkOrders
public enum WorkOrderStatus { Requested = 0, Accepted = 1, Assigned = 2, InProgress = 3, OnHold = 4, Completed = 5, Closed = 6, Rejected = 7, Duplicate = 8, Cancelled = 9 }
public enum WorkOrderPriority { Low = 0, Normal = 1, High = 2, Emergency = 3 }
public enum WorkOrderType { Corrective = 0, Preventive = 1, Inspection = 2, Facility = 3, Other = 4 }
- public enum WorkOrderActivityType { Created = 0, Updated = 1, StatusChanged = 2, Assigned = 3, AssignmentAccepted = 4, Comment = 5, LaborAdded = 6, PartAdded = 7, PartVoided = 8, FileAdded = 9, FileWithdrawn = 10 }
+ public enum WorkOrderActivityType { Created = 0, Updated = 1, StatusChanged = 2, Assigned = 3, AssignmentAccepted = 4, Comment = 5, LaborAdded = 6, PartAdded = 7, PartVoided = 8, FileAdded = 9, FileWithdrawn = 10, SafetyHold = 11, SafetyReleased = 12, Deferred = 13, Escalated = 14 }
/// Free text, monetary details and file names live only in cataloged Content. Numeric identities are allocated with empty content before sealing.
public abstract class WorkOrderRow : IEntity
{
@@ -59,6 +59,12 @@ public sealed class WorkOrder : WorkOrderRow
public bool RestoreUnitStateOnClose { get; set; }
public int? PreviousUnitStateType { get; set; }
public string WorkOrderRecurrenceId { get; set; }
+ public int? RecurrenceVersionId { get; set; }
+ public long? RecurrenceCycle { get; set; }
+ public DateTime? OriginalDueOn { get; set; }
+ public DateTime? EscalatedOn { get; set; }
+ public int EscalateAfterMinutes { get; set; }
+ public int? EscalationRoleId { get; set; }
public bool IsDeleted { get; set; }
}
public sealed class WorkOrderActivity : WorkOrderRow
@@ -72,6 +78,9 @@ public sealed class WorkOrderPart : WorkOrderRow
{
public string InventoryItemId { get; set; }
public string InventoryTransactionId { get; set; }
+ public string InventoryOperationId { get; set; }
+ public string InventoryReversalId { get; set; }
+ public string InventoryRequestId { get; set; }
public DateTime? VoidedOn { get; set; }
}
public sealed class WorkOrderFile : WorkOrderRow
@@ -108,14 +117,20 @@ public sealed class WorkOrderContent
}
public sealed class WorkOrderTaskStep { public string Text { get; set; } public bool Completed { get; set; } }
public sealed class WorkOrderLaborContent { public decimal Hours { get; set; } public decimal? RatePerHour { get; set; } public string Note { get; set; } }
- public sealed class WorkOrderPartContent { public string Description { get; set; } public decimal Quantity { get; set; } public decimal? UnitCost { get; set; } public string VoidReason { get; set; } }
+ public sealed class WorkOrderPartContent { [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string RequestFingerprint { get; set; } public string Currency { get; set; } public string Description { get; set; } public decimal Quantity { get; set; } public decimal? UnitCost { get; set; } public string VoidReason { get; set; } }
public static class WorkOrderTables
{
public const int CatalogVersion = 18;
+ public const int IntegrationCatalogVersion = 23;
+ public const int RecurrenceCatalogVersion = 24;
+ public static int VersionFor(string table) => table is "WorkOrderSafetyHolds" or "WorkOrderFailureIntents" ? IntegrationCatalogVersion : table is "WorkOrderRecurrences" or "WorkOrderRecurrenceVersions" or "WorkOrderMeterReadings" or "WorkOrderRecurrenceChanges" ? RecurrenceCatalogVersion : CatalogVersion;
public static readonly IReadOnlyDictionary All = new Dictionary
{
[typeof(WorkOrder)] = "WorkOrders", [typeof(WorkOrderActivity)] = "WorkOrderActivities", [typeof(WorkOrderLabor)] = "WorkOrderLabors",
- [typeof(WorkOrderPart)] = "WorkOrderParts", [typeof(WorkOrderFile)] = "WorkOrderFiles"
+ [typeof(WorkOrderPart)] = "WorkOrderParts", [typeof(WorkOrderFile)] = "WorkOrderFiles",
+ [typeof(WorkOrderFailureIntent)] = "WorkOrderFailureIntents", [typeof(WorkOrderSafetyHold)] = "WorkOrderSafetyHolds",
+ [typeof(WorkOrderRecurrence)] = "WorkOrderRecurrences", [typeof(WorkOrderRecurrenceVersion)] = "WorkOrderRecurrenceVersions",
+ [typeof(WorkOrderMeterReading)] = "WorkOrderMeterReadings", [typeof(WorkOrderRecurrenceChange)] = "WorkOrderRecurrenceChanges"
};
public static IReadOnlyDictionary Get, Action Set)> Fields() where T : WorkOrderRow =>
new Dictionary, Action)> { [All[typeof(T)].ToLowerInvariant() + ".content"] = (x => x.Content, (x, v) => x.Content = v) };
diff --git a/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs b/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
index 9ddf6a8b..9d8099f8 100644
--- a/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
+++ b/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
@@ -9,12 +9,13 @@ namespace Resgrid.Model.WorkOrders
/// Workflow and notification routing never contains work-order content, personnel IDs, file names or grants.
public static class WorkOrderWorkflowPayload
{
- public static readonly (string Variable, string Property)[] Variables = { ("id", "WorkOrderId"), ("revision", "Revision"), ("status", "Status"), ("priority", "Priority"), ("unit_id", "TargetUnitId"), ("group_id", "TargetGroupId"), ("asset_id", "InventoryAssetId"), ("role_id", "AssignedToRoleId"), ("due_on", "DueOn"), ("title", "Title") };
- public static bool IsWorkOrder(int trigger) => trigger is (int)WorkflowTriggerEventType.WorkOrderCreated or (int)WorkflowTriggerEventType.WorkOrderStatusChanged or (int)WorkflowTriggerEventType.WorkOrderAssigned;
+ public static readonly (string Variable, string Property)[] Variables = { ("id", "WorkOrderId"), ("revision", "Revision"), ("status", "Status"), ("priority", "Priority"), ("unit_id", "TargetUnitId"), ("group_id", "TargetGroupId"), ("asset_id", "InventoryAssetId"), ("role_id", "AssignedToRoleId"), ("due_on", "DueOn"), ("title", "Title"), ("recurrence_id", "RecurrenceId"), ("hold_id", "HoldId"), ("old_status", "OldStatus") };
+ public static readonly int[] Triggers = { 70, 71, 72, 73, 167, 168, 169, 170, 171, 172 };
+ public static bool IsWorkOrder(int trigger) => Array.IndexOf(Triggers, trigger) >= 0;
public static string Routing(JObject payload)
{
var safe = new JObject();
- foreach (var field in new[] { "WorkOrderId", "Revision", "Status", "Priority", "TargetUnitId", "TargetGroupId", "AssignedToRoleId" })
+ foreach (var field in new[] { "WorkOrderId", "Revision", "Status", "Priority", "TargetUnitId", "TargetGroupId", "AssignedToRoleId", "RecurrenceId", "HoldId", "OldStatus" })
if (payload[field]?.Type == JTokenType.Integer && payload[field].Value() >= 0 && payload[field].Value() <= int.MaxValue) safe[field] = payload[field].DeepClone();
if (payload["InventoryAssetId"]?.Type == JTokenType.String && Guid.TryParseExact(payload["InventoryAssetId"].Value(), "D", out var asset)) safe["InventoryAssetId"] = asset.ToString("D");
if (payload["DueOn"] is JValue { Type: JTokenType.Date } date)
@@ -25,7 +26,7 @@ public static string Routing(JObject payload)
safe["DueOn"] = utc.ToString("O");
}
else if (payload["DueOn"]?.Type == JTokenType.String && DateTimeOffset.TryParse(payload["DueOn"].Value(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var due)) safe["DueOn"] = due.UtcDateTime.ToString("O");
- safe["Title"] = ProtectedDataEnvelope.RedactionValue; safe["is_redacted"] = true; safe["redacted_fields"] = new JArray("Title"); safe["catalog_version"] = WorkOrderTables.CatalogVersion;
+ safe["Title"] = ProtectedDataEnvelope.RedactionValue; safe["is_redacted"] = true; safe["redacted_fields"] = new JArray("Title"); safe["catalog_version"] = WorkOrderTables.RecurrenceCatalogVersion;
return safe.ToString(Formatting.None);
}
public static async Task ProjectAsync(int departmentId, JObject payload, IProtectedProjectionService protection, bool wrapped)
diff --git a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
index ac372acb..6c2b0142 100644
--- a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
+++ b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
@@ -389,6 +389,13 @@ public static IReadOnlyList GetVariableCatalog(Workf
case WorkflowTriggerEventType.WorkOrderCreated:
case WorkflowTriggerEventType.WorkOrderStatusChanged:
case WorkflowTriggerEventType.WorkOrderAssigned:
+ case WorkflowTriggerEventType.WorkOrderOverdue:
+ case WorkflowTriggerEventType.WorkOrderSafetyHoldApplied:
+ case WorkflowTriggerEventType.WorkOrderSafetyHoldReleased:
+ case WorkflowTriggerEventType.WorkOrderRecurrenceChanged:
+ case WorkflowTriggerEventType.WorkOrderThresholdReached:
+ case WorkflowTriggerEventType.WorkOrderDeferred:
+ case WorkflowTriggerEventType.WorkOrderPartChanged:
foreach (var pair in WorkOrders.WorkOrderWorkflowPayload.Variables)
list.Add(new TemplateVariableDescriptor("work_order." + pair.Variable, "Work order " + pair.Variable.Replace('_', ' ') + (pair.Variable == "title" ? "; always REDACTED. Do not compare or render this value." : ""), pair.Variable is "asset_id" or "due_on" or "title" ? "string" : "int", false));
list.Add(new TemplateVariableDescriptor("work_order.url", "Authenticated work-order link", "string", false));
diff --git a/Core/Resgrid.Model/WorkflowTriggerEventType.cs b/Core/Resgrid.Model/WorkflowTriggerEventType.cs
index 1f962201..9b0ec643 100644
--- a/Core/Resgrid.Model/WorkflowTriggerEventType.cs
+++ b/Core/Resgrid.Model/WorkflowTriggerEventType.cs
@@ -171,6 +171,13 @@ public enum WorkflowTriggerEventType
WorkOrderCreated = 70,
WorkOrderStatusChanged = 71,
WorkOrderAssigned = 72,
+ WorkOrderOverdue = 73,
+ WorkOrderSafetyHoldApplied = 167,
+ WorkOrderSafetyHoldReleased = 168,
+ WorkOrderRecurrenceChanged = 169,
+ WorkOrderThresholdReached = 170,
+ WorkOrderDeferred = 171,
+ WorkOrderPartChanged = 172,
// Inventory modernization: persisted registry allocations; InventoryAdjusted retains value 22.
InventoryTransferCompleted = 58,
diff --git a/Core/Resgrid.Services/ChecklistsService.cs b/Core/Resgrid.Services/ChecklistsService.cs
index 00249c89..d6e987f5 100644
--- a/Core/Resgrid.Services/ChecklistsService.cs
+++ b/Core/Resgrid.Services/ChecklistsService.cs
@@ -35,13 +35,21 @@ public partial class ChecklistsService : IChecklistsService
private readonly Lazy _reportCalls;
private readonly Lazy _reportAuthorization;
private readonly IChecklistHistoricalAssetSource _historicalAssets;
+ private readonly Lazy _failureMaintenance;
public ChecklistsService(IChecklistRepository store, IChecklistAuthorizationService authorization, IReadinessAccessService access,
IUnitOfWork uow, IAuditLogsRepository audit, IDomainEventOutboxService outbox, Lazy read,
Lazy write, IRecordAttachmentScanner scanner, TimeProvider clock = null, IChecklistAssignmentService assignments = null, IChecklistAssetSource assets = null,
- Lazy reportCalls = null, Lazy reportAuthorization = null, IChecklistHistoricalAssetSource historicalAssets = null)
- { _store = store; _authorization = authorization; _access = access; _uow = uow; _audit = audit; _outbox = outbox; _read = read; _write = write; _scanner = scanner; _clock = clock ?? TimeProvider.System; _assignments = assignments; _assets = assets; _reportCalls = reportCalls; _reportAuthorization = reportAuthorization; _historicalAssets = historicalAssets; }
+ Lazy reportCalls = null, Lazy reportAuthorization = null, IChecklistHistoricalAssetSource historicalAssets = null, Lazy failureMaintenance = null)
+ { _store = store; _authorization = authorization; _access = access; _uow = uow; _audit = audit; _outbox = outbox; _read = read; _write = write; _scanner = scanner; _clock = clock ?? TimeProvider.System; _assignments = assignments; _assets = assets; _reportCalls = reportCalls; _reportAuthorization = reportAuthorization; _historicalAssets = historicalAssets; _failureMaintenance = failureMaintenance; }
- public Task CanManageAsync(ChecklistActor actor) => _authorization.CanManageAsync(actor);
+ private async Task ValidateMaintenanceOptionsAsync(ChecklistActor actor, ChecklistForm form)
+ {
+ if (!form.Sections.SelectMany(s => s.Items).Any(i => i.CreateWorkOrderOnFail || i.SetUnitStateOnFail || i.HoldAssetOnFail)) return;
+ if (_failureMaintenance == null) throw new ChecklistException(503, "MaintenanceUnavailable");
+ try { await _failureMaintenance.Value.ValidateFailureOptionsAsync(actor, form); }
+ catch (Resgrid.Model.WorkOrders.WorkOrderException ex) { throw new ChecklistException(ex.StatusCode, ex.Code); }
+ }
+ public Task CanManageAsync(ChecklistActor actor) => _authorization.CanManageAsync(actor);
private async Task RequireWriteAsync(ChecklistActor actor, bool manage = false)
{
await _authorization.RequireMemberAsync(actor);
@@ -146,7 +154,7 @@ public async Task GetDefinitionAsync(ChecklistActor act
}
public async Task SaveDefinitionAsync(ChecklistActor actor, string id, int revision, ChecklistForm form)
{
- await RequireWriteAsync(actor, true); Valid(ChecklistValidation.Validate(form)); if (id != null) Id(id);
+ await RequireWriteAsync(actor, true); Valid(ChecklistValidation.Validate(form)); await ValidateMaintenanceOptionsAsync(actor, form); if (id != null) Id(id);
if (form.TargetType == ChecklistTargetType.InventoryAsset && !await AssetTargetsAvailableAsync(actor)) throw new ChecklistException(400, "AssetUnavailable");
foreach (var section in form.Sections)
{
@@ -176,7 +184,7 @@ await TransactionAsync(actor, async events =>
{
var row = await RevealAsync(actor, await _store.GetAsync(actor.DepartmentId, id)); Revision(row, revision);
if (row.DeletedOn.HasValue) throw new ChecklistException(409, "Deleted definitions cannot be published.");
- Valid(ChecklistValidation.Validate(Decode(row.Content)));
+ Valid(ChecklistValidation.Validate(Decode(row.Content))); await ValidateMaintenanceOptionsAsync(actor, Decode(row.Content));
var version = New(actor, row.Id); version.Version = row.PublishedVersion + 1; version.Content = row.Content;
await PersistAsync(actor, version, true);
row.CurrentVersionId = version.Id; row.PublishedVersion = version.Version; row.Retired = false; row.Revision++;
@@ -333,7 +341,16 @@ public async Task SaveRunAsync(ChecklistActor actor, string id, ChecklistRu
row.SubmittedOn = DateTime.UtcNow; row.Score = evaluation.Score; row.Passed = evaluation.Passed; row.SubmissionHash = hash;
row.State = (int)(view.Form.RequiresIndependentWitness ? ChecklistRunState.AwaitingWitness : ChecklistRunState.Submitted);
await AdvanceOccurrenceAsync(actor, row);
- foreach (var failed in evaluation.FailedItemIds) await EventAsync(actor, row, WorkflowTriggerEventType.ChecklistFailed, failed, events);
+ foreach (var failed in evaluation.FailedItemIds)
+ {
+ await EventAsync(actor, row, WorkflowTriggerEventType.ChecklistFailed, failed, events);
+ var option = view.Form.Sections.SelectMany(s => s.Items).Single(i => i.Id == failed);
+ if (option.CreateWorkOrderOnFail)
+ {
+ if (_failureMaintenance == null) throw new ChecklistException(503, "MaintenanceUnavailable");
+ await _failureMaintenance.Value.RecordFailureIntentAsync(actor, row, await _store.GetAsync(actor.DepartmentId, row.VersionId), option);
+ }
+ }
if (row.State == (int)ChecklistRunState.Submitted) await EventAsync(actor, row, WorkflowTriggerEventType.ChecklistCompleted, null, events);
}
await PersistAsync(actor, row, false); await AuditAsync(actor, row, submit ? AuditLogTypes.ChecklistCompletionSubmitted : AuditLogTypes.ChecklistProgressSaved,
diff --git a/Core/Resgrid.Services/GdprDataExportService.cs b/Core/Resgrid.Services/GdprDataExportService.cs
index df557ca8..91f1191e 100644
--- a/Core/Resgrid.Services/GdprDataExportService.cs
+++ b/Core/Resgrid.Services/GdprDataExportService.cs
@@ -53,10 +53,11 @@ public GdprDataExportService(
ICertificationService certificationService,
ITrainingService trainingService,
IShiftsService shiftsService,
- IEmailService emailService, IChecklistRepository checklists, Lazy checklistProtection, IChecklistReminderRepository checklistReminders, IWorkOrderRepository workOrders, IInventoryStore inventoryStore)
+ IEmailService emailService, IChecklistRepository checklists, Lazy checklistProtection, IChecklistReminderRepository checklistReminders, IWorkOrderRepository workOrders, IInventoryStore inventoryStore, IWorkOrderMaintenanceRepository maintenance)
{
_repository = repository;
- _workOrders = workOrders ?? throw new ArgumentNullException(nameof(workOrders));
+ _workOrderMaintenance = maintenance ?? throw new ArgumentNullException(nameof(maintenance));
+ _workOrders = workOrders ?? throw new ArgumentNullException(nameof(workOrders));
_inventoryStore = inventoryStore ?? throw new ArgumentNullException(nameof(inventoryStore));
_checklistReminders = checklistReminders ?? throw new ArgumentNullException(nameof(checklistReminders));
_userProfileService = userProfileService;
@@ -190,6 +191,7 @@ private async Task BuildExportZipAsync(string userId, int departmentId)
await AddJsonEntry(archive, "shifts.json", await BuildShiftsDataAsync(userId), ledger);
await AddJsonEntry(archive, "checklists.json", await BuildChecklistDataAsync(userId, departmentId), ledger);
await AddJsonEntry(archive, "workorders.json", await BuildWorkOrderDataAsync(userId, departmentId), ledger);
+ await AddJsonEntry(archive, "maintenance.json", await BuildMaintenanceDataAsync(userId, departmentId), ledger);
await AddJsonEntry(archive, "inventory.json", await BuildInventoryDataAsync(userId, departmentId), ledger);
// Written last, so it can report what every other entry withheld. Only present when
diff --git a/Core/Resgrid.Services/InventoryAlerts.cs b/Core/Resgrid.Services/InventoryAlerts.cs
index f50b71ab..84219edc 100644
--- a/Core/Resgrid.Services/InventoryAlerts.cs
+++ b/Core/Resgrid.Services/InventoryAlerts.cs
@@ -45,13 +45,13 @@ private async Task RequireAlertAccessAsync(InventoryActor actor, InventoryAlert
if (item == null) throw new InventoryException(404, "Unavailable");
if (item.IsControlledSubstance) await _auth.RequireAsync(actor, false, PermissionTypes.ManageControlledSubstances);
}
- private async Task SetAlertAsync(int departmentId, InventoryAlertType type, string itemId, string locationId, string lotId, string assetId, string issuanceId, bool active, DateTime? due, decimal? quantity, List events)
+ private async Task SetAlertAsync(int departmentId, InventoryAlertType type, string itemId, string locationId, string lotId, string assetId, string issuanceId, bool active, DateTime? due, decimal? quantity, List events, Dictionary openAlerts = null)
{
var key = Fingerprint(new { type, itemId, locationId, lotId, assetId, issuanceId });
- var current = await _store.OpenAlertAsync(departmentId, key);
+ var current = openAlerts == null ? await _store.OpenAlertAsync(departmentId, key) : openAlerts.GetValueOrDefault(key);
if (!active)
{
- if (current != null) { current.Status = 1; current.ResolvedOn = Now; await UpdateAlertMetadataAsync(current); }
+ if (current != null) { current.Status = 1; current.ResolvedOn = Now; await UpdateAlertMetadataAsync(current); openAlerts?.Remove(key); }
return;
}
if (current != null)
@@ -62,6 +62,7 @@ private async Task SetAlertAsync(int departmentId, InventoryAlertType type, stri
var row = New(new InventoryActor { DepartmentId = departmentId });
row.AlertType = (int)type; row.DedupKey = key; row.ItemId = itemId; row.LocationId = locationId; row.LotId = lotId; row.AssetId = assetId; row.IssuanceId = issuanceId; row.OpenedOn = Now; row.DueOn = due; row.Quantity = quantity;
await _store.InsertAsync(row);
+ if (openAlerts != null) openAlerts[key] = row;
var trigger = type == InventoryAlertType.LowStock ? WorkflowTriggerEventType.InventoryLowStock : type == InventoryAlertType.OverdueReturn ? WorkflowTriggerEventType.InventoryReturnOverdue : WorkflowTriggerEventType.InventoryExpiring;
var entry = await _outbox.EnqueueAsync(departmentId, "Inventory", new DomainEventEnvelope { EventName = trigger.ToString(), Trigger = trigger, SchemaVersion = 1,
AggregateType = "InventoryAlert", AggregateId = row.Id, OccurredOn = Now,
@@ -110,10 +111,37 @@ private async Task RefreshPostingAlertsAsync(InventoryActor actor, InventoryComm
}
foreach (var item in items) await RefreshLowStockAsync(actor, item, events);
}
+ private async Task RefreshAllLowStockAsync(InventoryActor actor, List events)
+ {
+ // One department snapshot under the posting lock; reuse holder liveness across items and lots.
+ var openAlerts = (await AllOpenAlertsAsync(actor.DepartmentId)).ToDictionary(a => a.DedupKey);
+ var stocks = (await AllAsync(actor.DepartmentId)).Where(s => !s.IsDeleted).ToLookup(s => s.ItemId);
+ var assets = (await AllAsync(actor.DepartmentId)).Where(a => !a.IsDeleted && a.Status is 0 or 1 or 2 or 3).ToLookup(a => a.ItemId);
+ var locations = new Dictionary();
+ async Task Live(string id)
+ {
+ if (id == null) return false;
+ if (!locations.TryGetValue(id, out var live)) locations[id] = live = await LiveAlertLocationAsync(actor.DepartmentId, id);
+ return live;
+ }
+ foreach (var row in await AllAsync(actor.DepartmentId))
+ {
+ await AuthorizeRowAsync(actor, row);
+ var item = await RevealAsync(actor, row); var content = Decode(item); decimal quantity = 0;
+ if (item.TrackingMode == 0)
+ {
+ foreach (var stock in stocks[item.Id]) if (await Live(stock.LocationId)) quantity += stock.Quantity;
+ }
+ else foreach (var asset in assets[item.Id]) if (await Live(asset.CurrentLocationId)) quantity++;
+ var threshold = content.ReorderPoint ?? content.MinLevel;
+ await SetAlertAsync(actor.DepartmentId, InventoryAlertType.LowStock, item.Id, null, null, null, null,
+ !item.IsDeleted && item.IsActive && threshold.HasValue && quantity <= threshold.Value, null, quantity, events, openAlerts);
+ }
+ }
public Task RefreshAlertsAsync(InventoryActor actor) => TransactionAsync(actor, async events =>
{
await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
- foreach (var item in await AllAsync(actor.DepartmentId)) await RefreshLowStockAsync(actor, item.Id, events);
+ await RefreshAllLowStockAsync(actor, events);
await EvaluateDatedAlertsAsync(actor.DepartmentId, events); return true;
});
public Task SweepAlertsAsync(int departmentId) => AlertTransactionAsync(departmentId, async events => { await EvaluateDatedAlertsAsync(departmentId, events); return true; });
@@ -188,20 +216,25 @@ public async Task CanReceiveAlertAsync(int departmentId, string userId, st
}
public Task ClaimAlertAsync(int departmentId, string userId) => AlertTransactionAsync(departmentId, async events =>
{
- foreach (var alert in await AllOpenAlertsAsync(departmentId))
+ for (var skip = 0; skip <= 100000; skip += 500)
{
- var delivery = await _store.AlertDeliveryAsync(departmentId, alert.Id, userId);
- if (delivery != null && (delivery.State is 2 or 3 || delivery.NextAttemptOn > Now || delivery.LeaseUntil > Now)) continue;
- if (!await CanReceiveAlertAsync(departmentId, userId, alert.Id)) continue;
- if (delivery == null)
+ var candidates = await _store.ClaimableAlertsAsync(departmentId, userId, Now, skip);
+ foreach (var alert in candidates.Take(500))
{
- delivery = New(new InventoryActor { DepartmentId = departmentId }); delivery.AlertId = alert.Id; delivery.UserId = userId; delivery.NextAttemptOn = Now;
- await _store.InsertAsync(delivery);
+ if (!await CanReceiveAlertAsync(departmentId, userId, alert.Id)) continue;
+ var delivery = await _store.AlertDeliveryAsync(departmentId, alert.Id, userId);
+ if (delivery != null && (delivery.State is 2 or 3 || delivery.NextAttemptOn > Now || delivery.LeaseUntil > Now)) continue;
+ if (delivery == null)
+ {
+ delivery = New(new InventoryActor { DepartmentId = departmentId }); delivery.AlertId = alert.Id; delivery.UserId = userId; delivery.NextAttemptOn = Now;
+ await _store.InsertAsync(delivery);
+ }
+ delivery.State = 1; delivery.ClaimToken = Guid.NewGuid().ToString("D"); delivery.LeaseUntil = Now.AddMinutes(30); delivery.AttemptCount++;
+ await UpdateAlertMetadataAsync(delivery); return delivery;
}
- delivery.State = 1; delivery.ClaimToken = Guid.NewGuid().ToString("D"); delivery.LeaseUntil = Now.AddMinutes(30); delivery.AttemptCount++;
- await UpdateAlertMetadataAsync(delivery); return delivery;
+ if (candidates.Count <= 500) return null;
}
- return null;
+ throw new InventoryException(409, "InventoryTooLarge");
});
private async Task> AllOpenAlertsAsync(int departmentId)
{
diff --git a/Core/Resgrid.Services/InventoryCatalog.cs b/Core/Resgrid.Services/InventoryCatalog.cs
index 0ee7bb30..67f94fa9 100644
--- a/Core/Resgrid.Services/InventoryCatalog.cs
+++ b/Core/Resgrid.Services/InventoryCatalog.cs
@@ -110,7 +110,7 @@ public Task ArchiveAsync(InventoryActor actor, string id, int revision) where
});
public Task RebuildStocksAsync(InventoryActor actor) => TransactionAsync(actor, async events => {
await _auth.RequireAsync(actor, true); await _store.RebuildStocksAsync(actor.DepartmentId);
- foreach (var item in await AllAsync(actor.DepartmentId)) await RefreshLowStockAsync(actor, item.Id, events);
+ await RefreshAllLowStockAsync(actor, events);
return true;
});
public async Task> GetByReferenceAsync(InventoryActor actor, InventoryReferenceType type, string id)
diff --git a/Core/Resgrid.Services/InventoryModernizationService.cs b/Core/Resgrid.Services/InventoryModernizationService.cs
index aa958043..19aaa55f 100644
--- a/Core/Resgrid.Services/InventoryModernizationService.cs
+++ b/Core/Resgrid.Services/InventoryModernizationService.cs
@@ -15,7 +15,7 @@
namespace Resgrid.Services
{
- public sealed partial class InventoryModernizationService : IInventoryCatalogService, IInventoryStockService, IInventoryTransferService, IInventoryIssuanceService, IInventoryMigrationService, IInventoryPurchasingService, IInventoryOperationsService, IInventoryAlertService, IChecklistAssetSource, IChecklistHistoricalAssetSource
+ public sealed partial class InventoryModernizationService : IInventoryCatalogService, IInventoryStockService, IInventoryTransferService, IInventoryIssuanceService, IInventoryMigrationService, IInventoryPurchasingService, IInventoryOperationsService, IInventoryAlertService, IInventoryWorkOrderAdapter, IChecklistAssetSource, IChecklistHistoricalAssetSource
{
private readonly IInventoryStore _store;
private readonly IInventoryAuthorizationService _auth;
@@ -34,12 +34,15 @@ public sealed partial class InventoryModernizationService : IInventoryCatalogSer
private readonly Lazy _recordsAuthorization;
private readonly Lazy _recordUsage;
private readonly IContactsService _contacts;
+ private readonly IWorkOrderMaintenanceRepository _maintenanceOrders;
+ private readonly Lazy _workOrderMaintenance;
+ private readonly IReadinessAccessService _readinessAccess;
public InventoryModernizationService(IInventoryStore store, IInventoryAuthorizationService auth, IUnitOfWork uow, IProtectedReadService read,
IProtectedWriteService write, IDomainEventOutboxService outbox, IAuditLogsRepository audit, IUnitsService units, IDepartmentGroupsService groups, TimeProvider clock = null,
IInventoryRepository legacyInventory = null, IInventoryTypesRepository legacyTypes = null,
IWorkOrderRepository workOrders = null, Lazy workOrderAuthorization = null,
- Lazy recordsAuthorization = null, Lazy recordUsage = null, IContactsService contacts = null)
- { _store = store; _auth = auth; _uow = uow; _read = read; _write = write; _outbox = outbox; _audit = audit; _units = units; _groups = groups; _clock = clock ?? TimeProvider.System; _legacyInventory = legacyInventory; _legacyTypes = legacyTypes; _workOrders = workOrders; _workOrderAuthorization = workOrderAuthorization; _recordsAuthorization = recordsAuthorization; _recordUsage = recordUsage; _contacts = contacts; }
+ Lazy recordsAuthorization = null, Lazy recordUsage = null, IContactsService contacts = null, IWorkOrderMaintenanceRepository maintenanceOrders = null, Lazy workOrderMaintenance = null, IReadinessAccessService readinessAccess = null)
+ { _store = store; _auth = auth; _uow = uow; _read = read; _write = write; _outbox = outbox; _audit = audit; _units = units; _groups = groups; _clock = clock ?? TimeProvider.System; _legacyInventory = legacyInventory; _legacyTypes = legacyTypes; _workOrders = workOrders; _workOrderAuthorization = workOrderAuthorization; _recordsAuthorization = recordsAuthorization; _recordUsage = recordUsage; _contacts = contacts; _maintenanceOrders = maintenanceOrders; _workOrderMaintenance = workOrderMaintenance; _readinessAccess = readinessAccess; }
public Task IsMigratedAsync(int departmentId) => _store.HasLegacyMigrationAsync(departmentId);
private DateTime Now => _clock.GetUtcNow().UtcDateTime;
private static void Id(string id) { if (!Guid.TryParseExact(id, "D", out var value) || value == Guid.Empty) throw new InventoryException(400, "InvalidIdentifier"); }
diff --git a/Core/Resgrid.Services/InventoryPosting.cs b/Core/Resgrid.Services/InventoryPosting.cs
index 8968d48e..960a5e22 100644
--- a/Core/Resgrid.Services/InventoryPosting.cs
+++ b/Core/Resgrid.Services/InventoryPosting.cs
@@ -36,6 +36,8 @@ private async Task ValidateCommandAsync(InventoryActor actor, InventoryCommand c
if (command?.Lines == null || command.Lines.Count is < 1 or > 100) throw new InventoryException(400, "InvalidLines"); Id(command.RequestId);
foreach (var line in command.Lines)
{
+ if (line?.WorkOrderPartId != null && (!joined || line.ReferenceType != InventoryReferenceType.WorkOrder)) throw new InventoryException(400, "WorkOrderPostingRequired");
+ if (line?.WorkOrderPartId != null) await RequireWorkOrderPartAsync(actor, line);
if (line?.CountItemId != null && !counting) throw new InventoryException(400, "CountCompletionRequired");
if (line?.PurchaseOrderItemId != null && !purchasing) throw new InventoryException(400, "PurchaseReceiptRequired");
if (line != null && (line.UsageId != null || line.UsageType.HasValue))
@@ -207,7 +209,16 @@ public Task WitnessAsync(InventoryActor actor, string requestId
await RequirePurchaseReceiptAccessAsync(performer, receipt.PendingPurchaseReceipt);
result = await CompletePurchaseReceiptAsync(actor, operation, receipt.PendingPurchaseReceipt, events, performer.UserId, actor.UserId, attestation);
break;
- case "Post":
+ case "WorkOrder":
+ if (_workOrderMaintenance == null || receipt.PendingCommand?.Lines?.Count != 1) throw new InventoryException(409, "ReferenceUnavailable");
+ await RequireCommandAccessAsync(performer, receipt.PendingCommand, joined: true);
+ await RequireWorkOrderPartAsync(performer, receipt.PendingCommand.Lines[0]);
+ await RequireCommandAccessAsync(actor, receipt.PendingCommand, joined: true);
+ await ValidateCommandAsync(actor, receipt.PendingCommand, joined: true);
+ result = await PostLinesAsync(actor, operation, receipt.PendingCommand, events, performer.UserId, actor.UserId, attestation);
+ await _workOrderMaintenance.Value.CompleteInventoryPartAsync(actor, receipt.PendingCommand.Lines[0].WorkOrderPartId.Value, result.TransactionIds.Single(), receipt.PendingCommand.Lines[0].ReversesTransactionId != null, events);
+ break;
+ case "Post":
case "Transfer":
case "Receive":
await RequireCommandAccessAsync(performer, receipt.PendingCommand);
@@ -247,11 +258,12 @@ private async Task PostLinesAsync(InventoryActor actor, Invento
transaction.FromLocationId = line.FromLocationId; transaction.ToLocationId = line.ToLocationId; transaction.TransactionType = (int)line.Type;
transaction.ReferenceType = (int)line.ReferenceType; transaction.ReferenceId = line.ReferenceId; transaction.OccurredOn = Now;
transaction.ReversesTransactionId = line.ReversesTransactionId; transaction.IssuanceId = line.IssuanceId; transaction.PurchaseOrderItemId = line.PurchaseOrderItemId;
- transaction.CountItemId = line.CountItemId;
+ transaction.CountItemId = line.CountItemId; transaction.WorkOrderPartId = line.WorkOrderPartId;
if (line.ReversesTransactionId != null)
{
var original = await GetAsync(actor, line.ReversesTransactionId);
if (original.PurchaseOrderItemId != null) throw new InventoryException(409, "PurchaseReceiptImmutable");
+ if (original.WorkOrderPartId.HasValue && original.WorkOrderPartId != line.WorkOrderPartId) throw new InventoryException(409, "WorkOrderPostingRequired");
if (original.CountItemId != null) throw new InventoryException(409, "CountReceiptImmutable");
if (original.ReferenceType == (int)InventoryReferenceType.RmsRecord && (line.ReferenceType != InventoryReferenceType.RmsRecord || line.ReferenceId != original.ReferenceId)) throw new InventoryException(409, "RecordUsageCorrectionRequired");
if (original.ReversesTransactionId != null || original.ItemId != item.Id || original.AssetId != line.AssetId || original.LotId != line.LotId || original.Quantity != line.Quantity
@@ -319,7 +331,8 @@ private async Task MoveAssetAsync(InventoryActor actor, InventoryPosting line, I
if (issuance != null && line.Type is not (InventoryTransactionType.Return or InventoryTransactionType.StatusChange) && !(line.Type == InventoryTransactionType.Issue && issuance.Id == line.IssuanceId)) throw new InventoryException(409, "ReturnAssetFirst");
var status = line.Type switch { InventoryTransactionType.Issue => InventoryAssetStatus.Issued, InventoryTransactionType.Consume => InventoryAssetStatus.Consumed,
InventoryTransactionType.WriteOff or InventoryTransactionType.Count => InventoryAssetStatus.Lost, InventoryTransactionType.Return or InventoryTransactionType.StatusChange => line.Status ?? InventoryAssetStatus.InService, _ => (InventoryAssetStatus)asset.Status };
- if (line.Type == InventoryTransactionType.StatusChange)
+ if (status is InventoryAssetStatus.InService or InventoryAssetStatus.Issued && _maintenanceOrders != null && (await _maintenanceOrders.ActiveHoldsAsync(actor.DepartmentId, null, asset.Id)).Count > 0) throw new InventoryException(409, "AssetSafetyHold");
+ if (line.Type == InventoryTransactionType.StatusChange)
{
if (asset.Status == (int)status) throw new InventoryException(409, "StatusUnchanged");
if (status == InventoryAssetStatus.Issued && issuance == null || status == InventoryAssetStatus.InService && issuance != null) throw new InventoryException(409, "IssuanceStatusConflict");
diff --git a/Core/Resgrid.Services/InventoryPurchasing.cs b/Core/Resgrid.Services/InventoryPurchasing.cs
index dfc99b2f..751d08a2 100644
--- a/Core/Resgrid.Services/InventoryPurchasing.cs
+++ b/Core/Resgrid.Services/InventoryPurchasing.cs
@@ -40,11 +40,15 @@ public async Task> GetVendorContactsAsync(InventoryA
{
await RequirePurchasingAccessAsync(actor, false);
if (_contacts == null) throw new InventoryException(404, "VendorContactUnavailable");
- var contacts = (await _contacts.GetAllContactsForDepartmentAsync(actor.DepartmentId)).Where(c => c.DepartmentId == actor.DepartmentId && c.ContactType == 1 && !c.IsDeleted).ToList();
+ var contacts = (await _contacts.GetAllContactsForDepartmentAsync(actor.DepartmentId)).Where(c => c.DepartmentId == actor.DepartmentId && c.ContactType == 1 && !c.IsDeleted)
+ .Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))).ToList();
if (contacts.Count > 5000) throw new InventoryException(409, "InventoryTooLarge");
- var result = new List();
- foreach (var contact in contacts) result.Add(new() { Id = contact.ContactId, Name = (await VendorContactAsync(actor, contact.ContactId, true)).CompanyName });
- return result;
+ if (contacts.Count == 0) return new List();
+ var wasPlain = contacts.Any(c => !string.IsNullOrEmpty(c.CompanyName) && !ProtectedDataEnvelope.HasEnvelopePrefix(c.CompanyName));
+ var result = await _read.ResolveContactsForReadAsync(actor.DepartmentId, contacts, actor.GrantToken, actor.UserId);
+ if (result == null || result.RedactedFields.Count > 0 || result.IsProtected && wasPlain || contacts.Any(c => ProtectedDataEnvelope.HasEnvelopePrefix(c.CompanyName)))
+ throw new InventoryException(403, "ProtectedDataRequired");
+ return contacts.Select(c => new InventoryVendorChoice { Id = c.ContactId, Name = c.CompanyName }).ToList();
}
public Task SaveVendorAsync(InventoryActor actor, InventoryVendorInput input) => TransactionAsync(actor, async events =>
{
diff --git a/Core/Resgrid.Services/InventoryQueries.cs b/Core/Resgrid.Services/InventoryQueries.cs
index 1d580a6b..5f599ab3 100644
--- a/Core/Resgrid.Services/InventoryQueries.cs
+++ b/Core/Resgrid.Services/InventoryQueries.cs
@@ -6,6 +6,24 @@ namespace Resgrid.Services
{
public sealed partial class InventoryModernizationService
{
+ public async Task> GetManyAsync(InventoryActor actor, System.Collections.Generic.IReadOnlyCollection ids) where T : InventoryRow
+ {
+ await _auth.RequireAsync(actor);
+ if (ids == null || ids.Count > 5000) throw new InventoryException(400, "InvalidPage");
+ foreach (var id in ids) Id(id);
+ var result = new System.Collections.Generic.Dictionary();
+ foreach (var batch in ids.Distinct().Chunk(500))
+ {
+ var rows = await _store.RelatedManyAsync(actor.DepartmentId, "Id", batch);
+ if (rows.Count != batch.Length) throw new InventoryException(404, "Unavailable");
+ foreach (var row in rows)
+ {
+ await AuthorizeRowAsync(actor, row);
+ result.Add(row.Id, await RevealAsync(actor, row));
+ }
+ }
+ return result;
+ }
public async Task GetLegacyTransactionAsync(InventoryActor actor, int inventoryId)
{
await _auth.RequireAsync(actor);
diff --git a/Core/Resgrid.Services/InventoryWorkOrders.cs b/Core/Resgrid.Services/InventoryWorkOrders.cs
new file mode 100644
index 00000000..227d7ab4
--- /dev/null
+++ b/Core/Resgrid.Services/InventoryWorkOrders.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Resgrid.Model;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Inventories;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Services
+{
+ public sealed partial class InventoryModernizationService
+ {
+ public async Task CancelPendingPartAsync(InventoryActor actor, int partId, string operationId)
+ {
+ if (_uow.Transaction == null) throw new InvalidOperationException("The source transaction owns cancellation.");
+ await _store.LockDepartmentAsync(actor.DepartmentId);
+ var operation = await GetAsync(actor, operationId);
+ var receipt = Decode(operation);
+ if (operation.State != 1 || receipt.PendingKind != "WorkOrder" || receipt.PendingCommand?.Lines?.Count != 1 || receipt.PendingCommand.Lines[0].WorkOrderPartId != partId)
+ throw new InventoryException(409, "ReferenceUnavailable");
+ await RequireWorkOrderPartAsync(actor, receipt.PendingCommand.Lines[0]);
+ await RequireCommandAccessAsync(actor, receipt.PendingCommand, joined: true);
+ operation.State = 3; await SaveAsync(actor, operation, false);
+ }
+ public async Task PostPartAsync(InventoryActor actor, int partId, InventoryCommand command)
+ {
+ if (_uow.Transaction == null || _workOrders == null || _maintenanceOrders == null || _readinessAccess == null) throw new InvalidOperationException("Work-order inventory requires its source transaction.");
+ await _store.LockDepartmentAsync(actor.DepartmentId);
+ if (!await _store.HasLegacyMigrationAsync(actor.DepartmentId) || !await _auth.IsEnabledAsync(actor.DepartmentId)) throw new InventoryException(409, "InventoryDisabled");
+ if (!await _readinessAccess.CanUseMaintenanceAsync(actor.DepartmentId)) throw new InventoryException(402, "ReadinessProRequired");
+ if (command?.Lines?.Count != 1 || command.Lines[0].WorkOrderPartId != partId) throw new InventoryException(400, "ReferenceUnavailable");
+ var preflight = await _write.PreflightWriteAsync(actor.DepartmentId, actor.GrantToken, actor.UserId, false);
+ if (preflight?.Success != true) throw new InventoryException(403, "ProtectedDataRequired");
+ await RequireCommandAccessAsync(actor, command, joined: true);
+ var events = new System.Collections.Generic.List();
+ return await OperationAsync(actor, command.RequestId, new { Kind = "WorkOrder", command.Lines }, async (operation, pending) =>
+ {
+ await ValidateCommandAsync(actor, command, joined: true);
+ if (await RequiresWitnessAsync(actor, command.Lines.Select(l => l.ItemId))) return await AwaitWitnessAsync(actor, operation, "WorkOrder", command);
+ return await PostLinesAsync(actor, operation, command, pending);
+ }, events);
+ }
+ private async Task RequireWorkOrderPartAsync(InventoryActor actor, InventoryPosting line)
+ {
+ if (!line.WorkOrderPartId.HasValue || _workOrders == null || _workOrderAuthorization == null || _readinessAccess == null) throw new InventoryException(400, "ReferenceUnavailable");
+ if (!await _readinessAccess.CanUseMaintenanceAsync(actor.DepartmentId)) throw new InventoryException(402, "ReadinessProRequired");
+ var part = await _workOrders.GetAsync(actor.DepartmentId, line.WorkOrderPartId.Value);
+ if (part?.WorkOrderId?.ToString(System.Globalization.CultureInfo.InvariantCulture) != line.ReferenceId || part.InventoryItemId != line.ItemId || part.VoidedOn.HasValue) throw new InventoryException(409, "ReferenceUnavailable");
+ if (line.ReversesTransactionId == null && part.InventoryTransactionId != null || line.ReversesTransactionId != null && part.InventoryTransactionId != line.ReversesTransactionId) throw new InventoryException(409, "ReferenceUnavailable");
+ var order = await _workOrders.GetAsync(actor.DepartmentId, part.WorkOrderId.Value);
+ var principal = new ChecklistActor { DepartmentId = actor.DepartmentId, UserId = actor.UserId, GrantToken = actor.GrantToken };
+ if (order == null || order.Status >= 5 || !await _workOrderAuthorization.Value.CanContributeAsync(principal, order)) throw new InventoryException(409, "ReferenceClosed");
+ }
+ public async Task ApplyHoldAsync(InventoryActor actor, int orderId, string assetId, int? restoreState = null, int? expectedRevision = null, bool safetyRelease = false)
+ {
+ if (_uow.Transaction == null || _maintenanceOrders == null || _workOrders == null || _workOrderAuthorization == null) throw new InvalidOperationException("Safety changes require the work-order transaction.");
+ await _store.LockDepartmentAsync(actor.DepartmentId);
+ if (!await _store.HasLegacyMigrationAsync(actor.DepartmentId)) throw new InventoryException(409, "MigrationRequired");
+ var asset = await _store.GetAsync(actor.DepartmentId, assetId);
+ if (asset == null || asset.IsDeleted || asset.Status is 4 or 5 or 6 || asset.CurrentLocationId == null) { if (safetyRelease) return null; throw new InventoryException(409, "AssetNotAvailable"); }
+ var location = await LocationAsync(actor, asset.CurrentLocationId, write: !safetyRelease, permission: PermissionTypes.AdjustInventory);
+ var item = await _store.GetAsync(actor.DepartmentId, asset.ItemId);
+ if (item?.IsControlledSubstance == true) await _auth.RequireAsync(actor, !safetyRelease, PermissionTypes.ManageControlledSubstances);
+ var order = await _workOrders.GetAsync(actor.DepartmentId, orderId);
+ var principal = new ChecklistActor { DepartmentId = actor.DepartmentId, UserId = actor.UserId, GrantToken = actor.GrantToken };
+ if (order?.InventoryAssetId != assetId || !await _workOrderAuthorization.Value.CanManageAsync(principal, order.TargetGroupId)) throw new InventoryException(403, "PermissionRequired");
+ var old = asset.Status;
+ if (safetyRelease)
+ {
+ if (!restoreState.HasValue || restoreState is < 0 or > 3 || asset.Revision != expectedRevision || asset.Status != (int)InventoryAssetStatus.OutForRepair) return null;
+ var active = await _maintenanceOrders.ActiveHoldsAsync(actor.DepartmentId, null, assetId);
+ if (active.Count != 1 || active[0].WorkOrderId != orderId) return null;
+ var issuance = (await _store.RelatedAsync(actor.DepartmentId, "AssetId", assetId)).SingleOrDefault(i => i.Status is 0 or 2);
+ if (restoreState == (int)InventoryAssetStatus.InService && issuance != null || restoreState == (int)InventoryAssetStatus.Issued && issuance == null) return null;
+ asset.Status = restoreState.Value;
+ }
+ else
+ {
+ if (_readinessAccess == null || !await _readinessAccess.CanUseMaintenanceAsync(actor.DepartmentId)) throw new InventoryException(402, "ReadinessProRequired");
+ if (asset.Status == (int)InventoryAssetStatus.OutForRepair) return new WorkOrderAssetState { State = old, Revision = asset.Revision };
+ asset.Status = (int)InventoryAssetStatus.OutForRepair;
+ }
+ // Only reviewed state metadata changes. Protected Content is preserved byte-for-byte; no worker read grant exists.
+ var expected = asset.Revision++; asset.ModifiedOn = Now; await _store.UpdateAsync(asset, expected);
+ var ledger = New(actor); ledger.ItemId = asset.ItemId; ledger.AssetId = asset.Id; ledger.LotId = asset.LotId;
+ ledger.TransactionType = (int)InventoryTransactionType.StatusChange; ledger.OldStatus = old; ledger.NewStatus = asset.Status;
+ ledger.FromLocationId = asset.CurrentLocationId; ledger.ToLocationId = asset.CurrentLocationId; ledger.OccurredOn = Now;
+ ledger.ReferenceType = (int)InventoryReferenceType.WorkOrder; ledger.ReferenceId = orderId.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ await _store.InsertAsync(ledger);
+ var events = new System.Collections.Generic.List(); await EventAsync(ledger, WorkflowTriggerEventType.InventoryAssetStatusChanged, events);
+ return new WorkOrderAssetState { State = old, Revision = asset.Revision, OutboxIds = events };
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/ProtectedFieldCatalog.cs b/Core/Resgrid.Services/ProtectedFieldCatalog.cs
index 161b9f17..f89baf70 100644
--- a/Core/Resgrid.Services/ProtectedFieldCatalog.cs
+++ b/Core/Resgrid.Services/ProtectedFieldCatalog.cs
@@ -700,7 +700,7 @@ void Prevention(string table, string column, ProtectedFieldClassification classi
ProtectedFieldClassification.Sensitive, PermissionTypes.ViewProtectedOperationalData, PermissionTypes.EditProtectedCallData, ChecklistContentCatalogVersion));
foreach (var table in Resgrid.Model.WorkOrders.WorkOrderTables.All.Values)
list.Add(new ProtectedFieldDefinition(table.ToLowerInvariant() + ".content", OperationalFamily, table, "Content", ProtectedFieldStorageKind.Text,
- ProtectedFieldClassification.Sensitive, PermissionTypes.ViewProtectedOperationalData, PermissionTypes.EditProtectedCallData, Resgrid.Model.WorkOrders.WorkOrderTables.CatalogVersion));
+ ProtectedFieldClassification.Sensitive, PermissionTypes.ViewProtectedOperationalData, PermissionTypes.EditProtectedCallData, Resgrid.Model.WorkOrders.WorkOrderTables.VersionFor(table)));
list.Add(new ProtectedFieldDefinition("workorderfiles.data", OperationalFamily, "WorkOrderFiles", "Data", ProtectedFieldStorageKind.Binary,
ProtectedFieldClassification.Sensitive, PermissionTypes.ViewProtectedOperationalData, PermissionTypes.EditProtectedCallData, Resgrid.Model.WorkOrders.WorkOrderTables.CatalogVersion));
foreach (var table in Resgrid.Model.Inventories.InventoryTables.All.Values)
diff --git a/Core/Resgrid.Services/Records/RecordInventoryUsageService.cs b/Core/Resgrid.Services/Records/RecordInventoryUsageService.cs
index a61bda4d..f25ec45e 100644
--- a/Core/Resgrid.Services/Records/RecordInventoryUsageService.cs
+++ b/Core/Resgrid.Services/Records/RecordInventoryUsageService.cs
@@ -274,22 +274,32 @@ async Task Authorize()
// This endpoint must not reveal a copied legacy snapshot without an Inventory protection boundary.
legacy.Note = legacy.ItemName = legacy.UnitOfMeasure = ProtectedDataEnvelope.RedactionValue;
}
- foreach (var usage in result.Where(x => x.TransactionId != null))
+ var modern = result.Where(x => x.TransactionId != null).ToList();
+ var transactions = modern.Count == 0 ? new Dictionary()
+ : await _modernCatalog.GetManyAsync(actor, modern.Select(x => x.TransactionId).Distinct().ToArray());
+ var correctionIds = modern.Where(x => x.ReversesUsageId != null).Select(x => x.UsageId).Distinct().ToArray();
+ var corrections = correctionIds.Length == 0 ? new Dictionary()
+ : await _modernCatalog.GetManyAsync(actor, correctionIds);
+ var reversals = new List();
+ foreach (var batch in modern.Where(x => x.ReversesUsageId == null).Select(x => x.TransactionId).Distinct().Chunk(500))
+ reversals.AddRange(await _modernStore.RelatedManyAsync(actor.DepartmentId, "ReversesTransactionId", batch));
+ var reversalsByTransaction = reversals.ToLookup(t => t.ReversesTransactionId);
+ var attachedCorrections = result.Where(x => x.ReversesUsageId != null).Select(x => (x.ReversesUsageId, x.TransactionId)).ToHashSet();
+ foreach (var usage in modern)
{
- var transaction = await _modernCatalog.GetAsync(actor, usage.TransactionId);
+ var transaction = transactions[usage.TransactionId];
await AuthorizeModernSourceAsync(actor, transaction.FromLocationId ?? transaction.ToLocationId, historical: true);
var details = JObject.Parse(transaction.Content ?? "{}");
usage.Note = details.Value("Note"); usage.ItemName = details.Value("ItemName"); usage.UnitOfMeasure = details.Value("UnitOfMeasure");
usage.AssetId = transaction.AssetId; usage.LotId = transaction.LotId; usage.LocationId = transaction.FromLocationId ?? transaction.ToLocationId;
if (usage.ReversesUsageId != null)
{
- var row = await _modernCatalog.GetAsync(actor, usage.UsageId);
+ var row = corrections[usage.UsageId];
usage.Note = JsonConvert.DeserializeObject(row.Content ?? "{}")?.Note;
}
else
{
- var reversedTransactions = await _modernStore.RelatedAsync(actor.DepartmentId, "ReversesTransactionId", usage.TransactionId);
- usage.PendingReversalTransactionId = reversedTransactions.FirstOrDefault(t => !result.Any(r => r.ReversesUsageId == usage.UsageId && r.TransactionId == t.Id))?.Id;
+ usage.PendingReversalTransactionId = reversalsByTransaction[usage.TransactionId].FirstOrDefault(t => !attachedCorrections.Contains((usage.UsageId, t.Id)))?.Id;
}
}
var reversed = result.Where(x => x.ReversesUsageId != null).Select(x => x.ReversesUsageId).ToHashSet();
diff --git a/Core/Resgrid.Services/Records/RecordsEvidenceService.cs b/Core/Resgrid.Services/Records/RecordsEvidenceService.cs
index 23e65ae9..75630121 100644
--- a/Core/Resgrid.Services/Records/RecordsEvidenceService.cs
+++ b/Core/Resgrid.Services/Records/RecordsEvidenceService.cs
@@ -41,9 +41,9 @@ public class RecordsEvidenceService : IRecordsEvidenceService
public RecordsEvidenceService(IRmsEvidenceArtifactsRepository artifacts, IRmsOperationalRecordsRepository records,
IRmsIncidentReportsRepository incidentReports, IRmsAccessAuditsRepository audits, IUnitOfWork unitOfWork,
IEnumerable adapters, IRecordsAuthorizationService authorization, ICallsService calls, IRmsExternalReferencesRepository references,
- IRecordsProtectionService protection, IDomainEventOutboxService outbox, IInventoryStore inventoryStore = null)
+ IRecordsProtectionService protection, IDomainEventOutboxService outbox, IInventoryStore inventoryStore)
{
- _inventoryStore = inventoryStore;
+ _inventoryStore = inventoryStore ?? throw new ArgumentNullException(nameof(inventoryStore));
_protection = protection;
_outbox = outbox;
_artifacts = artifacts;
@@ -67,7 +67,6 @@ public async Task RequireInventoryCoverageAsync(int departmentId, string recordI
// Never finalize an apparently balanced evidence snapshot while that source movement is unaccounted for.
foreach (var reference in references.Where(r => r.SourceEntityType == "RecordInventoryUsage"))
{
- if (_inventoryStore == null) throw new InvalidOperationException("Inventory evidence verification is unavailable.");
var usage = await _inventoryStore.GetAsync(departmentId, reference.SourceEntityId);
if (usage == null || usage.SourceId != recordId || usage.RecordKind != reference.RecordKind) throw new InvalidOperationException("Inventory usage provenance is unavailable.");
if (usage.ReversesUsageId != null) continue;
diff --git a/Core/Resgrid.Services/ServicesModule.cs b/Core/Resgrid.Services/ServicesModule.cs
index 01059dde..a8637bc0 100644
--- a/Core/Resgrid.Services/ServicesModule.cs
+++ b/Core/Resgrid.Services/ServicesModule.cs
@@ -16,7 +16,7 @@ public class ServicesModule : Module
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType().As().InstancePerLifetimeScope();
- builder.RegisterType().AsSelf().As().As().As()
+ builder.RegisterType().AsSelf().As().As().As().As()
.As().As().As().As().As().As().As().InstancePerLifetimeScope();
builder.RegisterType().AsSelf().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
@@ -31,7 +31,7 @@ protected override void Load(ContainerBuilder builder)
var scope = context.Resolve();
return (Func)(() => scope.ResolveNamed("readiness-billing-client"));
}).InstancePerLifetimeScope();
- builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Core/Resgrid.Services/WorkOrderInventoryChoices.cs b/Core/Resgrid.Services/WorkOrderInventoryChoices.cs
new file mode 100644
index 00000000..b8a5482d
--- /dev/null
+++ b/Core/Resgrid.Services/WorkOrderInventoryChoices.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Inventories;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Services
+{
+ public sealed partial class WorkOrdersService
+ {
+ public async Task> InventoryChoicesAsync(ChecklistActor actor, string kind, string itemId = null, int page = 0)
+ {
+ await _authorization.RequireMemberAsync(actor);
+ if (_inventoryCatalog == null || page < 0 || page > 10000 || kind is not ("item" or "location" or "lot" or "asset") || (kind is "lot" or "asset") && !Guid.TryParseExact(itemId, "D", out _))
+ throw new WorkOrderException(400, "InventoryPartInvalid");
+ try
+ {
+ return kind switch
+ {
+ "item" => await InventoryChoicesPageAsync(actor, new(), page, "Name"),
+ "location" => await InventoryChoicesPageAsync(actor, new(), page, "Name"),
+ "lot" => await InventoryChoicesPageAsync(actor, new() { ItemId = itemId }, page, "LotNumber"),
+ _ => await InventoryChoicesPageAsync(actor, new() { ItemId = itemId }, page, "SerialNumber")
+ };
+ }
+ catch (InventoryException ex) { throw new WorkOrderException(ex.StatusCode, ex.StatusCode == 403 ? "InventoryPermissionRequired" : "InventoryOperationFailed"); }
+ }
+ private async Task> InventoryChoicesPageAsync(ChecklistActor actor, InventoryQuery query, int page, string label) where T : InventoryRow
+ {
+ var result = await _inventoryCatalog.Value.QueryAsync(InventoryActor(actor), query, page);
+ return new InventoryPage { HasMore = result.HasMore, Items = result.Items.Where(row => row is not InventoryMutableRow mutable || !mutable.IsDeleted)
+ .Select(row => new WorkOrderChoice { Id = row.Id, Name = JObject.Parse(row.Content ?? "{}").Value(label) ?? row.Id }).ToList() };
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/WorkOrderInventoryParts.cs b/Core/Resgrid.Services/WorkOrderInventoryParts.cs
new file mode 100644
index 00000000..e0764f0a
--- /dev/null
+++ b/Core/Resgrid.Services/WorkOrderInventoryParts.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Resgrid.Model;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Inventories;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Services
+{
+ public sealed partial class WorkOrdersService
+ {
+ public async Task CancelPartWitnessAsync(ChecklistActor actor, int orderId, int partId, int revision, string reason)
+ {
+ Text(reason, 4000, true);
+ await TransactionAsync(actor, async events =>
+ {
+ var order = await ContributionAsync(actor, orderId, revision);
+ var part = await _store.GetAsync(actor.DepartmentId, partId);
+ if (part?.WorkOrderId != orderId || part.InventoryOperationId == null || part.VoidedOn.HasValue || _inventoryMaintenance == null)
+ throw new WorkOrderException(409, "Unavailable");
+ await RevealAsync(actor, part); var content = Decode(part.Content);
+ if (part.InventoryTransactionId != null && (content.VoidReason == null || part.InventoryReversalId != null)) throw new WorkOrderException(409, "Conflict");
+ await _inventoryMaintenance.Value.CancelPendingPartAsync(InventoryActor(actor), part.Id, part.InventoryOperationId);
+ if (part.InventoryTransactionId == null) { part.VoidedOn = Now; content.VoidReason = reason; }
+ else { content.VoidReason = null; part.InventoryOperationId = (await _inventoryCatalog.Value.GetAsync(InventoryActor(actor), part.InventoryTransactionId)).OperationId; }
+ part.Content = JsonConvert.SerializeObject(content); part.Revision++; await SaveAsync(actor, part);
+ await ChangedAsync(actor, order, WorkOrderActivityType.PartVoided, events, reason, trigger: WorkflowTriggerEventType.WorkOrderPartChanged); return true;
+ });
+ }
+ private async Task RequireNoPendingPartsAsync(ChecklistActor actor, int id)
+ {
+ foreach (var part in await ChildrenAsync(actor, id))
+ if (part.InventoryOperationId != null && !part.VoidedOn.HasValue && (part.InventoryTransactionId == null || Decode(part.Content).VoidReason != null && part.InventoryReversalId == null))
+ throw new WorkOrderException(409, "InventoryWitnessPending");
+ }
+ public async Task AddPartAsync(ChecklistActor actor, int id, WorkOrderPartInput input)
+ {
+ if (input?.Content == null || input.Content.Quantity <= 0 || input.Content.Quantity > 100000 || decimal.Round(input.Content.Quantity, 6) != input.Content.Quantity) throw new WorkOrderException(400, "InvalidInput");
+ Text(input.Content.Description, 2000, true); Money(input.Content.UnitCost); input.Content.VoidReason = null;
+ var linked = !string.IsNullOrWhiteSpace(input.InventoryItemId);
+ var fingerprint = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { input.Content.Description, input.Content.Quantity, input.Content.UnitCost, input.InventoryItemId, input.InventoryAssetId, input.InventoryLotId, input.InventoryLocationId }))));
+ if (linked && (!Guid.TryParseExact(input.RequestId, "D", out _) || !Guid.TryParseExact(input.InventoryItemId, "D", out _) || !Guid.TryParseExact(input.InventoryLocationId, "D", out _) || _inventoryMaintenance == null)) throw new WorkOrderException(400, "InventoryPartInvalid");
+ await TransactionAsync(actor, async events =>
+ {
+ if (linked)
+ {
+ RequireMaintenanceStore();
+ var prior = (await _maintenance.QueryMaintenanceAsync(actor.DepartmentId, "InventoryRequestId", input.RequestId)).SingleOrDefault();
+ if (prior != null)
+ {
+ await ReadOrderAsync(actor, id); await RevealAsync(actor, prior);
+ var content = Decode(prior.Content);
+ if (prior.WorkOrderId != id || prior.CreatedBy != actor.UserId || prior.InventoryItemId != input.InventoryItemId || content.Quantity != input.Content.Quantity || content.RequestFingerprint != fingerprint) throw new WorkOrderException(409, "Conflict");
+ return true;
+ }
+ }
+ var row = await ContributionAsync(actor, id, input.Revision);
+ var part = New(actor, id); part.InventoryItemId = linked ? input.InventoryItemId : null; part.InventoryRequestId = linked ? input.RequestId : null;
+ input.Content.Currency = Decode(row.Content).Fields.Currency; input.Content.RequestFingerprint = linked ? fingerprint : null;
+ part.Content = JsonConvert.SerializeObject(input.Content); await SaveAsync(actor, part, true);
+ if (linked)
+ {
+ var command = new InventoryCommand { RequestId = input.RequestId, Lines = new() { new InventoryPosting { ItemId = input.InventoryItemId, AssetId = input.InventoryAssetId, LotId = input.InventoryLotId, FromLocationId = input.InventoryLocationId,
+ Quantity = input.Content.Quantity, Type = InventoryTransactionType.Consume, ReferenceType = InventoryReferenceType.WorkOrder, ReferenceId = id.ToString(CultureInfo.InvariantCulture), WorkOrderPartId = part.Id } } };
+ var result = await _inventoryMaintenance.Value.PostPartAsync(InventoryActor(actor), part.Id, command);
+ events.AddRange(result.OutboxIds); part.InventoryOperationId = result.OperationId; part.Revision++;
+ await RevealAsync(actor, part); await SaveAsync(actor, part);
+ if (!result.AwaitingWitness) { await CompleteInventoryPartAsync(InventoryActor(actor), part.Id, result.TransactionIds.Single(), false, events); return true; }
+ }
+ await ChangedAsync(actor, row, WorkOrderActivityType.PartAdded, events, trigger: WorkflowTriggerEventType.WorkOrderPartChanged); return true;
+ });
+ }
+ public async Task VoidPartAsync(ChecklistActor actor, int id, int partId, int revision, string reason)
+ {
+ Text(reason, 4000, true);
+ await TransactionAsync(actor, async events =>
+ {
+ var row = await ContributionAsync(actor, id, revision); var part = await _store.GetAsync(actor.DepartmentId, partId);
+ if (part?.WorkOrderId != id || part.VoidedOn.HasValue) throw new WorkOrderException(409, "Unavailable");
+ await RevealAsync(actor, part); var content = Decode(part.Content);
+ if (content.VoidReason != null) throw new WorkOrderException(409, "InventoryWitnessPending");
+ content.VoidReason = reason; part.Content = JsonConvert.SerializeObject(content); part.Revision++;
+ if (part.InventoryItemId != null)
+ {
+ if (part.InventoryTransactionId == null || _inventoryCatalog == null || _inventoryMaintenance == null) throw new WorkOrderException(409, "InventoryWitnessPending");
+ var original = await _inventoryCatalog.Value.GetAsync(InventoryActor(actor), part.InventoryTransactionId);
+ if (original.WorkOrderPartId != partId || original.ReversesTransactionId != null) throw new WorkOrderException(409, "InventoryPartInvalid");
+ await SaveAsync(actor, part);
+ var command = new InventoryCommand { RequestId = MaintenanceIdentity("part-reversal:" + actor.DepartmentId + ":" + part.Id + ":" + part.Revision), Lines = new() { new InventoryPosting {
+ ItemId = original.ItemId, AssetId = original.AssetId, LotId = original.LotId, ToLocationId = original.FromLocationId, Quantity = original.Quantity, Type = InventoryTransactionType.Adjust,
+ ReferenceType = InventoryReferenceType.WorkOrder, ReferenceId = id.ToString(CultureInfo.InvariantCulture), WorkOrderPartId = partId, ReversesTransactionId = original.Id, Note = reason } } };
+ var result = await _inventoryMaintenance.Value.PostPartAsync(InventoryActor(actor), partId, command); events.AddRange(result.OutboxIds);
+ if (!result.AwaitingWitness) { await CompleteInventoryPartAsync(InventoryActor(actor), partId, result.TransactionIds.Single(), true, events); return true; }
+ part.InventoryOperationId = result.OperationId; await RevealAsync(actor, part); await SaveAsync(actor, part);
+ }
+ else { part.VoidedOn = Now; await SaveAsync(actor, part); }
+ await ChangedAsync(actor, row, WorkOrderActivityType.PartVoided, events, reason, trigger: WorkflowTriggerEventType.WorkOrderPartChanged); return true;
+ });
+ }
+ public async Task CompleteInventoryPartAsync(InventoryActor actor, int partId, string transactionId, bool reversal, List events)
+ {
+ if (_uow.Transaction == null || _inventoryCatalog == null) throw new InvalidOperationException("The inventory posting transaction owns completion.");
+ var principal = new ChecklistActor { DepartmentId = actor.DepartmentId, UserId = actor.UserId, GrantToken = actor.GrantToken };
+ await RequireWriteAsync(principal);
+ var part = await _store.GetAsync(actor.DepartmentId, partId);
+ if (part?.WorkOrderId == null) throw new WorkOrderException(404, "Unavailable");
+ var row = await ReadOrderAsync(principal, part.WorkOrderId.Value);
+ if (row.Status >= 5 || !await _authorization.CanContributeAsync(principal, row)) throw new WorkOrderException(403, "PermissionRequired");
+ await RevealAsync(principal, part);
+ var ledger = await _inventoryCatalog.Value.GetAsync(actor, transactionId);
+ if (ledger.WorkOrderPartId != partId || ledger.ReferenceType != (int)InventoryReferenceType.WorkOrder || ledger.ReferenceId != row.Id.ToString(CultureInfo.InvariantCulture) || ledger.ItemId != part.InventoryItemId) throw new WorkOrderException(409, "InventoryPartInvalid");
+ var content = Decode(part.Content);
+ if (ledger.Quantity != content.Quantity || reversal && ledger.ReversesTransactionId != part.InventoryTransactionId || !reversal && ledger.ReversesTransactionId != null) throw new WorkOrderException(409, "InventoryPartInvalid");
+ if (reversal) { if (part.InventoryReversalId != null) throw new WorkOrderException(409, "Conflict"); part.InventoryReversalId = ledger.Id; part.VoidedOn = Now; }
+ else
+ {
+ if (part.InventoryTransactionId != null) throw new WorkOrderException(409, "Conflict");
+ part.InventoryTransactionId = ledger.Id;
+ var details = JObject.Parse(ledger.Content ?? "{}"); content.UnitCost = details["UnitCost"]?.Value(); content.Currency = details["CurrencyCode"]?.Value();
+ part.Content = JsonConvert.SerializeObject(content);
+ }
+ part.Revision++; await SaveAsync(principal, part);
+ await ChangedAsync(principal, row, reversal ? WorkOrderActivityType.PartVoided : WorkOrderActivityType.PartAdded, events, trigger: WorkflowTriggerEventType.WorkOrderPartChanged);
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/WorkOrderMaintenanceCore.cs b/Core/Resgrid.Services/WorkOrderMaintenanceCore.cs
new file mode 100644
index 00000000..182d3646
--- /dev/null
+++ b/Core/Resgrid.Services/WorkOrderMaintenanceCore.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Resources;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Resgrid.Model;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Inventories;
+using Resgrid.Model.Services;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Services
+{
+ public sealed partial class WorkOrdersService
+ {
+ private static string MaintenanceText(string key) => new ResourceManager("Resgrid.Localization.Areas.User.WorkOrders.WorkOrders", typeof(Resgrid.Localization.SupportedLocales).Assembly).GetString(key, CultureInfo.CurrentUICulture) ?? key;
+ private void RequireMaintenanceStore() { if (_maintenance == null) throw new WorkOrderException(503, "MaintenanceUnavailable"); }
+ private static InventoryActor InventoryActor(ChecklistActor actor) => new() { DepartmentId = actor.DepartmentId, UserId = actor.UserId, GrantToken = actor.GrantToken };
+ private static string MaintenanceIdentity(string source) => new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(source)).Take(16).ToArray()).ToString("D");
+ private async Task RevealOrderAsync(ChecklistActor actor, WorkOrder row)
+ {
+ await RevealAsync(actor, row);
+ if (!string.IsNullOrEmpty(row.Content)) return row;
+ WorkOrderContent fields;
+ if (row.SourceType == 2 && row.RecurrenceVersionId.HasValue)
+ {
+ var version = await RevealAsync(actor, await _store.GetAsync(actor.DepartmentId, row.RecurrenceVersionId.Value));
+ var settings = Decode(version.Content);
+ fields = settings?.Template?.Content ?? throw new WorkOrderException(409, "MaintenanceUnavailable");
+ }
+ else if (row.SourceType == 1 && row.SourceChecklistCompletionId != null) fields = new WorkOrderContent { Title = MaintenanceText("GeneratedFailureTitle"), SafetyCritical = true };
+ else throw new WorkOrderException(409, "MaintenanceUnavailable");
+ // An attended projection only. First mutation seals a row-specific copy with its numeric AAD.
+ row.Content = JsonConvert.SerializeObject(new StoredContent { Fields = fields }); return row;
+ }
+ public async Task ValidateFailureOptionsAsync(ChecklistActor actor, ChecklistForm form)
+ {
+ var items = form.Sections.SelectMany(s => s.Items).ToList();
+ if (items.Any(i => !Enum.IsDefined(i.WorkOrderPriority) || !i.CreateWorkOrderOnFail && (i.SetUnitStateOnFail || i.HoldAssetOnFail))) throw new WorkOrderException(400, "InvalidInput");
+ if (!items.Any(i => i.CreateWorkOrderOnFail)) return;
+ await _authorization.RequireMemberAsync(actor);
+ if (!await _authorization.CanManageAsync(actor, null)) throw new WorkOrderException(403, "PermissionRequired");
+ if (items.Any(i => i.SetUnitStateOnFail) && form.TargetType is not (ChecklistTargetType.Unit or ChecklistTargetType.InventoryAsset)
+ || items.Any(i => i.HoldAssetOnFail) && form.TargetType != ChecklistTargetType.InventoryAsset) throw new WorkOrderException(400, "TargetUnavailable");
+ // Configuring the failure routing does not call billing from free Checklists.
+ }
+ public async Task RecordFailureIntentAsync(ChecklistActor actor, ChecklistCompletion completion, ChecklistDefinitionVersion version, ChecklistItem item)
+ {
+ if (!item.CreateWorkOrderOnFail) return;
+ RequireMaintenanceStore();
+ if (_uow.Transaction == null || completion.DepartmentId != actor.DepartmentId || version.DepartmentId != actor.DepartmentId || version.Id != completion.VersionId) throw new InvalidOperationException("A checklist failure intent requires its source transaction.");
+ if (await _maintenance.FailureAsync(actor.DepartmentId, completion.Id, item.Id) != null) return;
+ var intent = New(actor);
+ intent.CompletionId = completion.Id; intent.ItemId = item.Id; intent.VersionId = version.Id; intent.OccurrenceId = completion.OccurrenceId;
+ intent.AuthorizedBy = version.CreatedBy; intent.TargetType = completion.TargetType; intent.TargetId = completion.TargetId; intent.TargetGroupId = completion.TargetGroupId;
+ intent.Priority = (int)item.WorkOrderPriority; intent.HoldUnit = item.SetUnitStateOnFail; intent.HoldAsset = item.HoldAssetOnFail;
+ await _store.AllocateAsync(intent); // No answers, findings, copied text, billing lookup or paid side effect.
+ }
+ private async Task AutomatedTargetAsync(ChecklistActor owner, int? unitId, int? groupId, string assetId)
+ {
+ await _authorization.RequireMemberAsync(owner);
+ var target = new WorkOrderInput { TargetUnitId = unitId, TargetGroupId = groupId, InventoryAssetId = assetId };
+ if (assetId != null)
+ {
+ var asset = _maintenanceAssets == null ? null : await _maintenanceAssets.RoutingAsync(owner.DepartmentId, assetId);
+ if (asset?.DepartmentId != owner.DepartmentId || !await _maintenanceAssets.CanReceiveReminderAsync(owner.DepartmentId, owner.UserId, assetId)) throw new WorkOrderException(404, "TargetUnavailable");
+ target.TargetUnitId = asset.UnitId; target.TargetGroupId = asset.GroupId;
+ }
+ else if (unitId.HasValue)
+ {
+ var unit = _maintenanceUnits == null ? null : await _maintenanceUnits.GetUnitByIdAsync(unitId.Value);
+ if (unit?.DepartmentId != owner.DepartmentId) throw new WorkOrderException(404, "TargetUnavailable");
+ target.TargetGroupId = unit.StationGroupId;
+ }
+ if (!await _authorization.CanManageAsync(owner, target.TargetGroupId)) throw new WorkOrderException(403, "PermissionRequired");
+ return target;
+ }
+ private async Task> WorkerTransactionAsync(int departmentId, Func, Task> work)
+ {
+ RequireMaintenanceStore();
+ if (_uow.Transaction != null) throw new InvalidOperationException("Maintenance workers own their transaction.");
+ var events = new List();
+ try
+ {
+ await _uow.CreateOrGetConnectionAsync(CancellationToken.None); await _store.LockDepartmentAsync(departmentId);
+ if (await _access.CanUseMaintenanceAsync(departmentId)) await work(events);
+ _uow.CommitChanges();
+ }
+ catch { _uow.DiscardChanges(); throw; }
+ await _outbox.DispatchAfterCommitAsync(events); return events;
+ }
+ private async Task ProcessFailureAsync(int departmentId, int intentId, WorkOrderMaintenanceSweep result)
+ {
+ await WorkerTransactionAsync(departmentId, async events =>
+ {
+ var intent = await _store.GetAsync(departmentId, intentId);
+ if (intent == null || intent.ProcessedOn.HasValue) return;
+ var completion = _checklists == null ? null : await _checklists.GetAsync(departmentId, intent.CompletionId);
+ // A submitted failure requires prompt restriction even while its independent checklist witness is pending.
+ if ((completion == null || completion.State != (int)ChecklistRunState.Submitted && completion.State != (int)ChecklistRunState.AwaitingWitness) || completion.VersionId != intent.VersionId) { result.Deferred++; return; }
+ var owner = new ChecklistActor { DepartmentId = departmentId, UserId = intent.AuthorizedBy };
+ var unit = intent.TargetType == (int)ChecklistTargetType.Unit && int.TryParse(intent.TargetId, out var u) ? (int?)u : null;
+ var target = await AutomatedTargetAsync(owner, unit, intent.TargetGroupId, intent.TargetType == (int)ChecklistTargetType.InventoryAsset ? intent.TargetId : null);
+ var request = MaintenanceIdentity("failure:" + departmentId + ":" + intent.CompletionId + ":" + intent.ItemId);
+ var order = await _store.RequestAsync(departmentId, request);
+ if (order == null)
+ {
+ order = New(new ChecklistActor { DepartmentId = departmentId, UserId = completion.CreatedBy });
+ order.RequestId = request; order.NumberYear = Now.Year; order.NumberSequence = await _store.NextNumberAsync(departmentId, order.NumberYear);
+ order.SourceType = 1; order.SourceChecklistCompletionId = intent.CompletionId; order.SourceChecklistItemId = intent.ItemId; order.SourceOccurrenceId = intent.OccurrenceId;
+ order.Priority = intent.Priority; order.TargetUnitId = target.TargetUnitId; order.TargetGroupId = target.TargetGroupId; order.InventoryAssetId = target.InventoryAssetId;
+ await _store.AllocateAsync(order); await EventAsync(order, WorkflowTriggerEventType.WorkOrderCreated, events); result.Generated++;
+ }
+ if (intent.HoldUnit && order.TargetUnitId.HasValue) { await CreateHoldAsync(owner, order, true, null, events, true); result.Held++; }
+ if (intent.HoldAsset && order.InventoryAssetId != null) { await CreateHoldAsync(owner, order, false, null, events, true); result.Held++; }
+ intent.WorkOrderId = order.Id; intent.ProcessedOn = Now; intent.UpdatedOn = Now; intent.Revision++; await _store.WriteAsync(intent);
+ });
+ }
+ public async Task> HoldsAsync(ChecklistActor actor, int orderId)
+ {
+ RequireMaintenanceStore(); await ReadOrderAsync(actor, orderId);
+ return (await ChildrenAsync(actor, orderId)).Select(h => new WorkOrderHoldView { Hold = h, Content = Decode(h.Content) }).ToList();
+ }
+ public async Task AddHoldAsync(ChecklistActor actor, int orderId, WorkOrderHoldInput input)
+ {
+ if (input == null || !input.Unit && !input.Asset) throw new WorkOrderException(400, "InvalidInput");
+ Text(input.Reason, 4000, true); RequireMaintenanceStore();
+ await TransactionAsync(actor, async events =>
+ {
+ var order = await ReadOrderAsync(actor, orderId); Revision(order, input.Revision);
+ if (Terminal(order) || !await _authorization.CanManageAsync(actor, order.TargetGroupId)) throw new WorkOrderException(403, "PermissionRequired");
+ if (input.Unit) await CreateHoldAsync(actor, order, true, input.Reason, events, false);
+ if (input.Asset) await CreateHoldAsync(actor, order, false, input.Reason, events, false);
+ await ChangedAsync(actor, order, WorkOrderActivityType.SafetyHold, events, input.Reason); return true;
+ });
+ }
+ private async Task CreateHoldAsync(ChecklistActor actor, WorkOrder order, bool unit, string reason, List events, bool unattended)
+ {
+ if (unit && !order.TargetUnitId.HasValue || !unit && order.InventoryAssetId == null) throw new WorkOrderException(400, "TargetUnavailable");
+ var holds = await _maintenance.ActiveHoldsAsync(actor.DepartmentId, unit ? order.TargetUnitId : null, unit ? null : order.InventoryAssetId);
+ if (holds.Any(h => h.WorkOrderId == order.Id)) return;
+ var hold = New(actor, order.Id);
+ hold.UnitId = unit ? order.TargetUnitId : null; hold.AssetId = unit ? null : order.InventoryAssetId;
+ if (unit)
+ {
+ await AutomatedTargetAsync(actor, order.TargetUnitId, order.TargetGroupId, null);
+ var previous = await _maintenance.LatestUnitStateAsync(actor.DepartmentId, order.TargetUnitId.Value);
+ var inherited = holds.FirstOrDefault(h => h.AppliedStateId == previous?.UnitStateId);
+ hold.PreviousState = inherited?.PreviousState ?? previous?.State;
+ if (previous?.State == (int)UnitStateTypes.OutOfService) hold.AppliedStateId = inherited?.AppliedStateId; // Never take ownership of a manual restriction.
+ else hold.AppliedStateId = await _maintenance.AppendUnitStateAsync(actor.DepartmentId, order.TargetUnitId.Value, (int)UnitStateTypes.OutOfService, Now);
+ }
+ else
+ {
+ if (_inventoryMaintenance == null) throw new WorkOrderException(503, "MaintenanceUnavailable");
+ var applied = await _inventoryMaintenance.Value.ApplyHoldAsync(InventoryActor(actor), order.Id, order.InventoryAssetId);
+ events.AddRange(applied.OutboxIds);
+ var inherited = holds.FirstOrDefault(h => h.AppliedAssetRevision == applied.Revision);
+ hold.PreviousState = inherited?.PreviousState ?? applied.State;
+ hold.AppliedAssetRevision = applied.State == (int)InventoryAssetStatus.OutForRepair ? inherited?.AppliedAssetRevision : applied.Revision;
+ }
+ if (unattended) await _store.AllocateAsync(hold);
+ else { hold.Content = JsonConvert.SerializeObject(new WorkOrderHoldContent { Reason = reason }); await SaveAsync(actor, hold, true); }
+ await MaintenanceEventAsync(order, WorkflowTriggerEventType.WorkOrderSafetyHoldApplied, events, holdId: hold.Id);
+ }
+ public async Task ReleaseHoldAsync(ChecklistActor actor, int holdId, WorkOrderReleaseInput input)
+ {
+ if (input == null) throw new WorkOrderException(400, "InvalidInput");
+ Text(input.Evidence, 4000, true); Text(input.Qualification, 2000, true); RequireMaintenanceStore();
+ await TransactionAsync(actor, async events =>
+ {
+ var hold = await _store.GetAsync(actor.DepartmentId, holdId);
+ if (hold?.WorkOrderId == null) throw new WorkOrderException(404, "Unavailable");
+ var order = await ReadOrderAsync(actor, hold.WorkOrderId.Value); await RevealAsync(actor, hold); Revision(hold, input.Revision);
+ if (hold.ReleasedOn.HasValue || !await _authorization.CanManageAsync(actor, order.TargetGroupId)) throw new WorkOrderException(409, "PermissionRequired");
+ if (actor.UserId == order.CompletedBy || actor.UserId == hold.CreatedBy) throw new WorkOrderException(403, "IndependentReleaseRequired");
+ var others = (await _maintenance.ActiveHoldsAsync(actor.DepartmentId, hold.UnitId, hold.AssetId)).Where(h => h.Id != hold.Id).ToList();
+ if (input.RestoreState && others.Count == 0 && hold.PreviousState.HasValue)
+ {
+ if (hold.UnitId.HasValue && hold.AppliedStateId.HasValue)
+ {
+ var current = await _maintenance.LatestUnitStateAsync(actor.DepartmentId, hold.UnitId.Value);
+ if (current?.UnitStateId == hold.AppliedStateId && current.State == (int)UnitStateTypes.OutOfService && await _maintenance.LastAppendedUnitStateIdAsync(actor.DepartmentId, hold.UnitId.Value) == hold.AppliedStateId)
+ { await AutomatedTargetAsync(actor, hold.UnitId, order.TargetGroupId, null); await _maintenance.AppendUnitStateAsync(actor.DepartmentId, hold.UnitId.Value, hold.PreviousState.Value, Now); hold.StateRestored = true; }
+ }
+ else if (hold.AssetId != null && hold.AppliedAssetRevision.HasValue && _inventoryMaintenance != null)
+ {
+ var restored = await _inventoryMaintenance.Value.ApplyHoldAsync(InventoryActor(actor), order.Id, hold.AssetId, hold.PreviousState, hold.AppliedAssetRevision, true);
+ hold.StateRestored = restored != null; if (restored != null) events.AddRange(restored.OutboxIds);
+ }
+ }
+ var content = Decode(hold.Content); content.ReleaseEvidence = input.Evidence; content.Qualification = input.Qualification;
+ hold.Content = JsonConvert.SerializeObject(content); hold.ReleasedOn = Now; hold.ReleasedBy = actor.UserId; hold.Revision++; await SaveAsync(actor, hold);
+ await ChangedAsync(actor, order, WorkOrderActivityType.SafetyReleased, events, input.Evidence);
+ await MaintenanceEventAsync(order, WorkflowTriggerEventType.WorkOrderSafetyHoldReleased, events, holdId: hold.Id); return true;
+ }, safetyRelease: true);
+ }
+ private async Task MaintenanceEventAsync(WorkOrder order, WorkflowTriggerEventType trigger, List events, int? holdId = null, int? recurrenceId = null)
+ {
+ var entry = await _outbox.EnqueueAsync(order.DepartmentId, "WorkOrders", new DomainEventEnvelope { EventName = trigger.ToString(), AggregateType = "WorkOrder", AggregateId = Key(order), AggregateVersion = order.Revision, Trigger = trigger, OccurredOn = Now,
+ Payload = new { WorkOrderId = order.Id, order.Revision, order.Status, order.Priority, order.TargetUnitId, order.TargetGroupId, order.InventoryAssetId, order.AssignedToRoleId, order.DueOn, HoldId = holdId, RecurrenceId = recurrenceId } });
+ events.Add(entry.DomainEventOutboxId);
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/WorkOrderMaintenanceGdprExport.cs b/Core/Resgrid.Services/WorkOrderMaintenanceGdprExport.cs
new file mode 100644
index 00000000..8756c990
--- /dev/null
+++ b/Core/Resgrid.Services/WorkOrderMaintenanceGdprExport.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.WorkOrders;
+
+namespace Resgrid.Services
+{
+ public partial class GdprDataExportService
+ {
+ private readonly IWorkOrderMaintenanceRepository _workOrderMaintenance;
+ private async Task