diff --git a/.gitignore b/.gitignore
index 2d0321e8..0d0bebd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -273,3 +273,4 @@ Web/Resgrid.WebCore/wwwroot/lib/*
/Web/Resgrid.Web/wwwroot/lib
.dual-graph/
.claude/settings.local.json
+.claude/settings.local.json
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.ar.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.ar.resx
index 9ddf38b6..842540f4 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.ar.resx
@@ -300,4 +300,106 @@
الأنواع
+
+ تسجيل الحضور
+
+
+ تسجيل الانصراف
+
+
+ وقت الحضور
+
+
+ وقت الانصراف
+
+
+ المدة
+
+
+ تم تسجيل الحضور بنجاح
+
+
+ تم تسجيل الانصراف بنجاح
+
+
+ تعديل أوقات التسجيل
+
+
+ تعديل يدوي
+
+
+ تسجيل حضور بواسطة المسؤول
+
+
+ تسجيل حضور نيابة عن شخص آخر
+
+
+ تقرير حضور الفعاليات
+
+
+ إجمالي الفعاليات المحضورة
+
+
+ إجمالي الساعات
+
+
+ لم يتم العثور على سجل حضور
+
+
+ تم تسجيل الحضور مسبقاً
+
+
+ لم يتم تسجيل الحضور
+
+
+ Not checked out
+
+
+ تسجيل متأخر
+
+
+ أوقات مخصصة
+
+
+ غير مصرح
+
+
+ تم حذف سجل الحضور
+
+
+ ملاحظة الحضور
+
+
+ ملاحظة الانصراف
+
+
+ سُجّل الحضور بواسطة
+
+
+ سُجّل الانصراف بواسطة
+
+
+ معطّل
+
+
+ تسجيل ذاتي للحضور/الانصراف
+
+
+ المنشئ والمسؤولون فقط
+
+
+ يُفتح تسجيل الحضور قبل 15 دقيقة من بدء الفعالية.
+
+
+ الحاضرون
+
+
+ ستتم إضافة المجموعات/الأفراد المحددين كحاضرين وإخطارهم مباشرة.
+
+
+ سيتم إخطار المجموعات/الأفراد المحددين بهذه الفعالية.
+
+
+ طباعة ورقة التسجيل
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.de.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.de.resx
index f1233824..2e483357 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.de.resx
@@ -251,4 +251,106 @@
Typen
+
+ Einchecken
+
+
+ Auschecken
+
+
+ Eincheckzeit
+
+
+ Auscheckzeit
+
+
+ Dauer
+
+
+ Einchecken erfolgreich
+
+
+ Auschecken erfolgreich
+
+
+ Eincheckzeiten bearbeiten
+
+
+ Manuelle Anpassung
+
+
+ Einchecken durch Administrator
+
+
+ Stellvertretend einchecken
+
+
+ Anwesenheitsbericht für Veranstaltungen
+
+
+ Gesamtzahl besuchter Veranstaltungen
+
+
+ Gesamtstunden
+
+
+ Kein Eincheckdatensatz gefunden
+
+
+ Bereits eingecheckt
+
+
+ Nicht eingecheckt
+
+
+ Not checked out
+
+
+ Verspätete Anmeldung
+
+
+ Benutzerdefinierte Zeiten
+
+
+ Nicht autorisiert
+
+
+ Eincheckdatensatz gelöscht
+
+
+ Eincheck-Notiz
+
+
+ Auscheck-Notiz
+
+
+ Eingecheckt von
+
+
+ Ausgecheckt von
+
+
+ Deaktiviert
+
+
+ Selbst Ein-/Auschecken
+
+
+ Nur Ersteller & Administratoren
+
+
+ Das Einchecken öffnet 15 Minuten vor Veranstaltungsbeginn.
+
+
+ Teilnehmer
+
+
+ Ausgewählte Gruppen/Mitarbeiter werden als Teilnehmer hinzugefügt und direkt benachrichtigt.
+
+
+ Ausgewählte Gruppen/Mitarbeiter werden über diese Veranstaltung benachrichtigt.
+
+
+ Anmeldeliste drucken
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx
index 3725c2c0..b8ebdc3a 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx
@@ -300,4 +300,115 @@
Types
+
+ Check In
+
+
+ Check Out
+
+
+ Check-In Time
+
+
+ Check-Out Time
+
+
+ Duration
+
+
+ Check-in successful
+
+
+ Check-out successful
+
+
+ Edit Check-In Times
+
+
+ Manual Override
+
+
+ Admin Check-In
+
+
+ Check In on Behalf
+
+
+ Event Attendance Report
+
+
+ Total Events Attended
+
+
+ Total Hours
+
+
+ No check-in record found
+
+
+ Already checked in
+
+
+ Not checked in
+
+
+ Not checked out
+
+
+ Late RSVP
+
+
+ Custom Times
+
+
+ Unauthorized
+
+
+ Check-in deleted
+
+
+ Check-In Note
+
+
+ Check-Out Note
+
+
+ Checked In By
+
+
+ Checked Out By
+
+
+ Disabled
+
+
+ Self Check-In/Out
+
+
+ Creator & Admins Only
+
+
+ Check-in opens 15 minutes before the event starts.
+
+
+ Attendees
+
+
+ Selected groups/personnel will be added as attendees and notified directly.
+
+
+ Selected groups/personnel will be notified about this event.
+
+
+ Print Signup Sheet
+
+
+ Check-in is no longer available for this event.
+
+
+ Checked in by {0}
+
+
+ Checked out by {0}
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx
index 534936fa..536ad93d 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx
@@ -297,4 +297,106 @@
Tipos
+
+ Registrar Entrada
+
+
+ Registrar Salida
+
+
+ Hora de Entrada
+
+
+ Hora de Salida
+
+
+ Duración
+
+
+ Registro de entrada exitoso
+
+
+ Registro de salida exitoso
+
+
+ Editar Horarios de Registro
+
+
+ Ajuste Manual
+
+
+ Registro de Entrada por Administrador
+
+
+ Registrar Entrada en Nombre de Otro
+
+
+ Reporte de Asistencia a Eventos
+
+
+ Total de Eventos Asistidos
+
+
+ Total de Horas
+
+
+ No se encontró registro de entrada
+
+
+ Ya se registró la entrada
+
+
+ Sin registro de entrada
+
+
+ Not checked out
+
+
+ RSVP Tardío
+
+
+ Horarios Personalizados
+
+
+ No Autorizado
+
+
+ Registro de entrada eliminado
+
+
+ Nota de Entrada
+
+
+ Nota de Salida
+
+
+ Entrada Registrada Por
+
+
+ Salida Registrada Por
+
+
+ Deshabilitado
+
+
+ Auto Registro de Entrada/Salida
+
+
+ Solo Creador y Administradores
+
+
+ El registro de entrada se abre 15 minutos antes del inicio del evento.
+
+
+ Asistentes
+
+
+ Los grupos/personal seleccionados se agregarán como asistentes y serán notificados directamente.
+
+
+ Los grupos/personal seleccionados serán notificados sobre este evento.
+
+
+ Imprimir Hoja de Registro
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.fr.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.fr.resx
index fca632a2..57ba01fc 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.fr.resx
@@ -251,4 +251,106 @@
Types
+
+ Pointer l'arrivée
+
+
+ Pointer le départ
+
+
+ Heure d'arrivée
+
+
+ Heure de départ
+
+
+ Durée
+
+
+ Pointage d'arrivée réussi
+
+
+ Pointage de départ réussi
+
+
+ Modifier les horaires de pointage
+
+
+ Ajustement manuel
+
+
+ Pointage par l'administrateur
+
+
+ Pointer pour le compte d'un autre
+
+
+ Rapport de présence aux événements
+
+
+ Total des événements assistés
+
+
+ Total des heures
+
+
+ Aucun enregistrement de pointage trouvé
+
+
+ Déjà pointé
+
+
+ Non pointé
+
+
+ Not checked out
+
+
+ Inscription tardive
+
+
+ Horaires personnalisés
+
+
+ Non autorisé
+
+
+ Pointage supprimé
+
+
+ Note d'arrivée
+
+
+ Note de départ
+
+
+ Arrivée enregistrée par
+
+
+ Départ enregistré par
+
+
+ Désactivé
+
+
+ Auto-pointage arrivée/départ
+
+
+ Créateur et administrateurs uniquement
+
+
+ Le pointage ouvre 15 minutes avant le début de l'événement.
+
+
+ Participants
+
+
+ Les groupes/personnels sélectionnés seront ajoutés comme participants et notifiés directement.
+
+
+ Les groupes/personnels sélectionnés seront notifiés de cet événement.
+
+
+ Imprimer la feuille d'émargement
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.it.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.it.resx
index 3f506649..556a7b5b 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.it.resx
@@ -251,4 +251,106 @@
Tipi
+
+ Registra Entrata
+
+
+ Registra Uscita
+
+
+ Ora di Entrata
+
+
+ Ora di Uscita
+
+
+ Durata
+
+
+ Registrazione entrata riuscita
+
+
+ Registrazione uscita riuscita
+
+
+ Modifica Orari di Registrazione
+
+
+ Modifica Manuale
+
+
+ Registrazione Entrata da Amministratore
+
+
+ Registra Entrata per Conto di
+
+
+ Report Presenze Eventi
+
+
+ Totale Eventi Partecipati
+
+
+ Ore Totali
+
+
+ Nessun record di entrata trovato
+
+
+ Entrata già registrata
+
+
+ Entrata non registrata
+
+
+ Not checked out
+
+
+ RSVP in Ritardo
+
+
+ Orari Personalizzati
+
+
+ Non Autorizzato
+
+
+ Registrazione entrata eliminata
+
+
+ Nota di Entrata
+
+
+ Nota di Uscita
+
+
+ Entrata Registrata Da
+
+
+ Uscita Registrata Da
+
+
+ Disabilitato
+
+
+ Auto Registrazione Entrata/Uscita
+
+
+ Solo Creatore e Amministratori
+
+
+ La registrazione entrata si apre 15 minuti prima dell'inizio dell'evento.
+
+
+ Partecipanti
+
+
+ I gruppi/personale selezionati verranno aggiunti come partecipanti e notificati direttamente.
+
+
+ I gruppi/personale selezionati verranno notificati di questo evento.
+
+
+ Stampa Foglio Presenze
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.pl.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.pl.resx
index f16062b1..06914a15 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.pl.resx
@@ -251,4 +251,106 @@
Typy
+
+ Zamelduj się
+
+
+ Wymelduj się
+
+
+ Czas zameldowania
+
+
+ Czas wymeldowania
+
+
+ Czas trwania
+
+
+ Zameldowanie zakończone sukcesem
+
+
+ Wymeldowanie zakończone sukcesem
+
+
+ Edytuj czasy zameldowania
+
+
+ Ręczna korekta
+
+
+ Zameldowanie przez administratora
+
+
+ Zamelduj w imieniu innej osoby
+
+
+ Raport obecności na wydarzeniach
+
+
+ Łączna liczba wydarzeń
+
+
+ Łączna liczba godzin
+
+
+ Nie znaleziono rekordu zameldowania
+
+
+ Już zameldowany
+
+
+ Niezameldowany
+
+
+ Not checked out
+
+
+ Późna rejestracja
+
+
+ Niestandardowe czasy
+
+
+ Brak autoryzacji
+
+
+ Rekord zameldowania usunięty
+
+
+ Notatka zameldowania
+
+
+ Notatka wymeldowania
+
+
+ Zameldowany przez
+
+
+ Wymeldowany przez
+
+
+ Wyłączone
+
+
+ Samodzielne zameldowanie/wymeldowanie
+
+
+ Tylko twórca i administratorzy
+
+
+ Zameldowanie otwiera się 15 minut przed rozpoczęciem wydarzenia.
+
+
+ Uczestnicy
+
+
+ Wybrane grupy/personel zostaną dodani jako uczestnicy i powiadomieni bezpośrednio.
+
+
+ Wybrane grupy/personel zostaną powiadomieni o tym wydarzeniu.
+
+
+ Drukuj listę obecności
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.sv.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.sv.resx
index 71d452ab..dd55cb74 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.sv.resx
@@ -251,4 +251,106 @@
Typer
+
+ Checka in
+
+
+ Checka ut
+
+
+ Incheckningstid
+
+
+ Utcheckningstid
+
+
+ Varaktighet
+
+
+ Incheckning lyckades
+
+
+ Utcheckning lyckades
+
+
+ Redigera incheckningstider
+
+
+ Manuell justering
+
+
+ Administratörsincheckning
+
+
+ Checka in åt annan person
+
+
+ Närvarorapport för evenemang
+
+
+ Totalt antal besökta evenemang
+
+
+ Totala timmar
+
+
+ Ingen incheckningspost hittades
+
+
+ Redan incheckad
+
+
+ Inte incheckad
+
+
+ Not checked out
+
+
+ Sen anmälan
+
+
+ Anpassade tider
+
+
+ Ej behörig
+
+
+ Incheckningspost raderad
+
+
+ Incheckningsanteckning
+
+
+ Utcheckningsanteckning
+
+
+ Incheckad av
+
+
+ Utcheckad av
+
+
+ Inaktiverad
+
+
+ Själv in-/utcheckning
+
+
+ Endast skapare och administratörer
+
+
+ Incheckning öppnar 15 minuter före evenemangets start.
+
+
+ Deltagare
+
+
+ Valda grupper/personal läggs till som deltagare och meddelas direkt.
+
+
+ Valda grupper/personal meddelas om detta evenemang.
+
+
+ Skriv ut närvarolista
+
diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.uk.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.uk.resx
index ae25612b..32666144 100644
--- a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.uk.resx
@@ -251,4 +251,106 @@
Типи
+
+ Реєстрація входу
+
+
+ Реєстрація виходу
+
+
+ Час входу
+
+
+ Час виходу
+
+
+ Тривалість
+
+
+ Реєстрацію входу виконано
+
+
+ Реєстрацію виходу виконано
+
+
+ Редагувати час реєстрації
+
+
+ Ручне коригування
+
+
+ Реєстрація адміністратором
+
+
+ Зареєструвати від імені іншого
+
+
+ Звіт про відвідуваність подій
+
+
+ Загальна кількість відвіданих подій
+
+
+ Загальна кількість годин
+
+
+ Запис реєстрації не знайдено
+
+
+ Вже зареєстровано
+
+
+ Не зареєстровано
+
+
+ Not checked out
+
+
+ Пізня реєстрація
+
+
+ Власний час
+
+
+ Не авторизовано
+
+
+ Запис реєстрації видалено
+
+
+ Примітка до входу
+
+
+ Примітка до виходу
+
+
+ Вхід зареєстровано
+
+
+ Вихід зареєстровано
+
+
+ Вимкнено
+
+
+ Самостійна реєстрація входу/виходу
+
+
+ Тільки автор та адміністратори
+
+
+ Реєстрація відкривається за 15 хвилин до початку події.
+
+
+ Учасники
+
+
+ Обрані групи/персонал будуть додані як учасники та сповіщені безпосередньо.
+
+
+ Обрані групи/персонал будуть сповіщені про цю подію.
+
+
+ Друк листа реєстрації
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.ar.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.ar.resx
index c3cd0b00..0cc0d142 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.ar.resx
@@ -507,4 +507,133 @@
عند إرسال وحدة لحالة وكان المستخدمون معيّنون على أدوار، ستُعيَّن حالة المستخدمين إلى "على الوحدة".
+
+ إعدادات المكالمات والإرسال
+
+
+ المكالمات والإرسال
+
+
+ تم حفظ إعدادات المكالمات والإرسال بنجاح.
+
+
+ إعدادات مؤقت تسجيل الوصول
+
+
+ الإعدادات العامة
+
+
+ تفعيل مؤقتات تسجيل الوصول تلقائياً للمكالمات الجديدة
+
+
+ عند التفعيل، ستحصل جميع المكالمات الجديدة تلقائياً على مؤقتات تسجيل الوصول.
+
+
+ تكوينات المؤقت الافتراضية
+
+
+ تحدد هذه فترات تسجيل الوصول الافتراضية لكل نوع هدف.
+
+
+ التجاوزات حسب نوع المكالمة / الأولوية
+
+
+ التجاوزات لها الأولوية على المؤقتات الافتراضية عندما تتطابق المكالمة مع النوع و/أو الأولوية المحددة.
+
+
+ نوع الهدف
+
+
+ نوع الوحدة
+
+
+ المدة (دقائق)
+
+
+ حد التحذير (دقائق)
+
+
+ مفعل
+
+
+ معرف نوع المكالمة
+
+
+ أولوية المكالمة
+
+
+ إضافة تكوين مؤقت
+
+
+ إضافة تجاوز
+
+
+ إضافة مؤقت افتراضي جديد
+
+
+ إضافة تجاوز جديد
+
+
+ -- لا شيء --
+
+
+ -- أي --
+
+
+ الأفراد
+
+
+ نوع الوحدة
+
+
+ قائد الحادث
+
+
+ مساءلة الأفراد
+
+
+ التعرض للمواد الخطرة
+
+
+ تدوير القطاع
+
+
+ إعادة التأهيل
+
+
+ اتركه فارغاً لأي
+
+
+ نعم
+
+
+ لا
+
+
+ حذف تكوين المؤقت هذا؟
+
+
+ حذف هذا التجاوز؟
+
+
+ افتراضي
+
+
+ تجاوز
+
+
+ منخفضة
+
+
+ متوسطة
+
+
+ عالية
+
+
+ طوارئ
+
+
+ حفظ الإعدادات العامة
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx
index 31936537..b9e830c3 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.de.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Einsatz- & Alarmierungseinstellungen
+
+
+ Einsatz & Alarmierung
+
+
+ Einsatz- & Alarmierungseinstellungen erfolgreich gespeichert.
+
+
+ Check-In-Timer-Einstellungen
+
+
+ Allgemeine Einstellungen
+
+
+ Check-In-Timer für neue Einsätze automatisch aktivieren
+
+
+ Wenn aktiviert, werden alle neuen Einsätze automatisch mit Check-In-Timern versehen.
+
+
+ Standard-Timer-Konfigurationen
+
+
+ Diese definieren die Standard-Check-In-Intervalle für jeden Zieltyp.
+
+
+ Überschreibungen nach Einsatztyp / Priorität
+
+
+ Überschreibungen haben Vorrang vor Standard-Timern wenn ein Einsatz dem angegebenen Typ und/oder Priorität entspricht.
+
+
+ Zieltyp
+
+
+ Einheitentyp
+
+
+ Dauer (Minuten)
+
+
+ Warnschwelle (Minuten)
+
+
+ Aktiviert
+
+
+ Einsatztyp-ID
+
+
+ Einsatzpriorität
+
+
+ Timer-Konfiguration hinzufügen
+
+
+ Überschreibung hinzufügen
+
+
+ Neuen Standard-Timer hinzufügen
+
+
+ Neue Überschreibung hinzufügen
+
+
+ -- Keine --
+
+
+ -- Beliebig --
+
+
+ Personal
+
+
+ Einheitentyp
+
+
+ EL (Einsatzleiter)
+
+
+ PAR (Personalverantwortung)
+
+
+ Gefahrstoffexposition
+
+
+ Sektorrotation
+
+
+ Rehabilitation
+
+
+ leer lassen für beliebig
+
+
+ Ja
+
+
+ Nein
+
+
+ Diese Timer-Konfiguration löschen?
+
+
+ Diese Überschreibung löschen?
+
+
+ Standard
+
+
+ Überschreibung
+
+
+ Niedrig
+
+
+ Mittel
+
+
+ Hoch
+
+
+ Notfall
+
+
+ Allgemeine Einstellungen speichern
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx
index 5fc6146b..42505b69 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx
@@ -507,4 +507,142 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Call & Dispatch Settings
+
+
+ Call & Dispatch Settings
+
+
+ Successfully saved Call & Dispatch Settings.
+
+
+ Check-In Timer Settings
+
+
+ General Settings
+
+
+ Auto-Enable Check-In Timers for New Calls
+
+
+ When enabled, all new calls will automatically have check-in timers turned on.
+
+
+ Default Timer Configurations
+
+
+ These define the default check-in intervals for each target type. They apply to all calls unless overridden by call type/priority rules below.
+
+
+ Call Type / Priority Overrides
+
+
+ Overrides take precedence over default timers when a call matches the specified type and/or priority.
+
+
+ Target Type
+
+
+ Unit Type
+
+
+ Duration (minutes)
+
+
+ Warning Threshold (minutes)
+
+
+ Enabled
+
+
+ Call Type ID
+
+
+ Call Priority
+
+
+ Add Timer Config
+
+
+ Add Override
+
+
+ Add New Default Timer
+
+
+ Add New Override
+
+
+ -- None --
+
+
+ -- Any --
+
+
+ Personnel
+
+
+ Unit Type
+
+
+ IC (Incident Commander)
+
+
+ PAR (Personnel Accountability)
+
+
+ Hazmat Exposure
+
+
+ Sector Rotation
+
+
+ Rehab
+
+
+ leave blank for any
+
+
+ Yes
+
+
+ No
+
+
+ Delete this timer configuration?
+
+
+ Delete this override?
+
+
+ Default
+
+
+ Override
+
+
+ Low
+
+
+ Medium
+
+
+ High
+
+
+ Emergency
+
+
+ Save General Settings
+
+
+ Active States
+
+
+ Leave empty for "All States".
+
+
+ All
+
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx
index abfee4b0..14fa14c7 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx
@@ -393,4 +393,133 @@
+
+ Configuraciones de Llamadas y Despacho
+
+
+ Llamadas y Despacho
+
+
+ Configuraciones de llamadas y despacho guardadas exitosamente.
+
+
+ Configuraciones de Temporizador de Registro
+
+
+ Configuraciones Generales
+
+
+ Habilitar Automáticamente los Temporizadores de Registro para Nuevas Llamadas
+
+
+ Cuando está habilitado, todas las llamadas nuevas tendrán automáticamente los temporizadores de registro activados.
+
+
+ Configuraciones Predeterminadas de Temporizador
+
+
+ Estos definen los intervalos predeterminados de registro para cada tipo de objetivo.
+
+
+ Anulaciones por Tipo de Llamada / Prioridad
+
+
+ Las anulaciones tienen prioridad sobre los temporizadores predeterminados cuando una llamada coincide con el tipo y/o prioridad especificados.
+
+
+ Tipo de Objetivo
+
+
+ Tipo de Unidad
+
+
+ Duración (minutos)
+
+
+ Umbral de Advertencia (minutos)
+
+
+ Habilitado
+
+
+ ID de Tipo de Llamada
+
+
+ Prioridad de Llamada
+
+
+ Agregar Configuración de Temporizador
+
+
+ Agregar Anulación
+
+
+ Agregar Nuevo Temporizador Predeterminado
+
+
+ Agregar Nueva Anulación
+
+
+ -- Ninguno --
+
+
+ -- Cualquiera --
+
+
+ Personal
+
+
+ Tipo de Unidad
+
+
+ CI (Comandante de Incidente)
+
+
+ PAR (Rendición de Cuentas del Personal)
+
+
+ Exposición a Materiales Peligrosos
+
+
+ Rotación de Sector
+
+
+ Rehabilitación
+
+
+ dejar en blanco para cualquiera
+
+
+ Sí
+
+
+ No
+
+
+ ¿Eliminar esta configuración de temporizador?
+
+
+ ¿Eliminar esta anulación?
+
+
+ Predeterminado
+
+
+ Anulación
+
+
+ Baja
+
+
+ Media
+
+
+ Alta
+
+
+ Emergencia
+
+
+ Guardar Configuraciones Generales
+
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx
index 4296c6cb..1b9373ee 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.fr.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Paramètres d'Appels et de Dispatch
+
+
+ Appels et Dispatch
+
+
+ Paramètres d'appels et de dispatch enregistrés avec succès.
+
+
+ Paramètres des Minuteries de Pointage
+
+
+ Paramètres Généraux
+
+
+ Activer automatiquement les minuteries de pointage pour les nouveaux appels
+
+
+ Lorsqu'activé, tous les nouveaux appels auront automatiquement les minuteries de pointage activées.
+
+
+ Configurations de Minuterie par Défaut
+
+
+ Ceux-ci définissent les intervalles de pointage par défaut pour chaque type de cible.
+
+
+ Remplacements par Type d'Appel / Priorité
+
+
+ Les remplacements ont priorité sur les minuteries par défaut lorsqu'un appel correspond au type et/ou à la priorité spécifiés.
+
+
+ Type de Cible
+
+
+ Type d'Unité
+
+
+ Durée (minutes)
+
+
+ Seuil d'Avertissement (minutes)
+
+
+ Activé
+
+
+ ID du Type d'Appel
+
+
+ Priorité d'Appel
+
+
+ Ajouter une Configuration de Minuterie
+
+
+ Ajouter un Remplacement
+
+
+ Ajouter une Nouvelle Minuterie par Défaut
+
+
+ Ajouter un Nouveau Remplacement
+
+
+ -- Aucun --
+
+
+ -- N'importe quel --
+
+
+ Personnel
+
+
+ Type d'Unité
+
+
+ CI (Commandant d'Intervention)
+
+
+ PAR (Responsabilité du Personnel)
+
+
+ Exposition aux Matières Dangereuses
+
+
+ Rotation de Secteur
+
+
+ Réhabilitation
+
+
+ laisser vide pour n'importe quel
+
+
+ Oui
+
+
+ Non
+
+
+ Supprimer cette configuration de minuterie?
+
+
+ Supprimer ce remplacement?
+
+
+ Par Défaut
+
+
+ Remplacement
+
+
+ Basse
+
+
+ Moyenne
+
+
+ Haute
+
+
+ Urgence
+
+
+ Enregistrer les Paramètres Généraux
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx
index 2533ad15..ba6767fc 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.it.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Impostazioni Chiamate e Invio
+
+
+ Chiamate e Invio
+
+
+ Impostazioni chiamate e invio salvate con successo.
+
+
+ Impostazioni Timer di Registrazione
+
+
+ Impostazioni Generali
+
+
+ Abilita automaticamente i timer di registrazione per le nuove chiamate
+
+
+ Quando abilitato, tutte le nuove chiamate avranno automaticamente i timer di registrazione attivati.
+
+
+ Configurazioni Timer Predefinite
+
+
+ Questi definiscono gli intervalli di registrazione predefiniti per ogni tipo di obiettivo.
+
+
+ Sovrascritture per Tipo di Chiamata / Priorità
+
+
+ Le sovrascritture hanno la precedenza sui timer predefiniti quando una chiamata corrisponde al tipo e/o alla priorità specificati.
+
+
+ Tipo di Obiettivo
+
+
+ Tipo di Unità
+
+
+ Durata (minuti)
+
+
+ Soglia di Avviso (minuti)
+
+
+ Abilitato
+
+
+ ID Tipo di Chiamata
+
+
+ Priorità di Chiamata
+
+
+ Aggiungi Configurazione Timer
+
+
+ Aggiungi Sovrascrittura
+
+
+ Aggiungi Nuovo Timer Predefinito
+
+
+ Aggiungi Nuova Sovrascrittura
+
+
+ -- Nessuno --
+
+
+ -- Qualsiasi --
+
+
+ Personale
+
+
+ Tipo di Unità
+
+
+ CI (Comandante dell'Incidente)
+
+
+ PAR (Responsabilità del Personale)
+
+
+ Esposizione a Materiali Pericolosi
+
+
+ Rotazione del Settore
+
+
+ Riabilitazione
+
+
+ lasciare vuoto per qualsiasi
+
+
+ Sì
+
+
+ No
+
+
+ Eliminare questa configurazione del timer?
+
+
+ Eliminare questa sovrascrittura?
+
+
+ Predefinito
+
+
+ Sovrascrittura
+
+
+ Bassa
+
+
+ Media
+
+
+ Alta
+
+
+ Emergenza
+
+
+ Salva Impostazioni Generali
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx
index a2d579da..a631b278 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.pl.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Ustawienia Wezwań i Dyspozycji
+
+
+ Wezwania i Dyspozycja
+
+
+ Pomyślnie zapisano ustawienia wezwań i dyspozycji.
+
+
+ Ustawienia Czasomierza Meldowania
+
+
+ Ustawienia Ogólne
+
+
+ Automatycznie włącz czasomierze meldowania dla nowych wezwań
+
+
+ Po włączeniu wszystkie nowe wezwania będą automatycznie miały włączone czasomierze meldowania.
+
+
+ Domyślne Konfiguracje Czasomierza
+
+
+ Definiują domyślne interwały meldowania dla każdego typu celu.
+
+
+ Nadpisania według Typu Wezwania / Priorytetu
+
+
+ Nadpisania mają pierwszeństwo przed domyślnymi czasomierzami gdy wezwanie pasuje do określonego typu i/lub priorytetu.
+
+
+ Typ Celu
+
+
+ Typ Jednostki
+
+
+ Czas trwania (minuty)
+
+
+ Próg Ostrzeżenia (minuty)
+
+
+ Włączony
+
+
+ ID Typu Wezwania
+
+
+ Priorytet Wezwania
+
+
+ Dodaj Konfigurację Czasomierza
+
+
+ Dodaj Nadpisanie
+
+
+ Dodaj Nowy Domyślny Czasomierz
+
+
+ Dodaj Nowe Nadpisanie
+
+
+ -- Brak --
+
+
+ -- Dowolny --
+
+
+ Personel
+
+
+ Typ Jednostki
+
+
+ KD (Kierownik Działań)
+
+
+ PAR (Odpowiedzialność Personelu)
+
+
+ Narażenie na Materiały Niebezpieczne
+
+
+ Rotacja Sektora
+
+
+ Rehabilitacja
+
+
+ pozostaw puste dla dowolnego
+
+
+ Tak
+
+
+ Nie
+
+
+ Usunąć tę konfigurację czasomierza?
+
+
+ Usunąć to nadpisanie?
+
+
+ Domyślny
+
+
+ Nadpisanie
+
+
+ Niski
+
+
+ Średni
+
+
+ Wysoki
+
+
+ Nagły
+
+
+ Zapisz Ustawienia Ogólne
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.resx
index 4eb30502..768fe3d7 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.resx
@@ -192,4 +192,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx
index c253c7ea..ba2beaa2 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.sv.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Larm- & Utskicksinställningar
+
+
+ Larm & Utskick
+
+
+ Larm- och utskicksinställningar sparade.
+
+
+ Inställningar för Incheckningstimer
+
+
+ Allmänna Inställningar
+
+
+ Aktivera incheckningstimers automatiskt för nya larm
+
+
+ När aktiverat kommer alla nya larm automatiskt att ha incheckningstimers påslagna.
+
+
+ Standardkonfigurationer för Timer
+
+
+ Dessa definierar standardintervaller för incheckning för varje måltyp.
+
+
+ Åsidosättningar per Larmtyp / Prioritet
+
+
+ Åsidosättningar har företräde framför standardtimers när ett larm matchar angiven typ och/eller prioritet.
+
+
+ Måltyp
+
+
+ Enhetstyp
+
+
+ Varaktighet (minuter)
+
+
+ Varningströskel (minuter)
+
+
+ Aktiverad
+
+
+ Larmtyp-ID
+
+
+ Larmprioritet
+
+
+ Lägg till Timerkonfiguration
+
+
+ Lägg till Åsidosättning
+
+
+ Lägg till Ny Standardtimer
+
+
+ Lägg till Ny Åsidosättning
+
+
+ -- Ingen --
+
+
+ -- Valfri --
+
+
+ Personal
+
+
+ Enhetstyp
+
+
+ IL (Insatsledare)
+
+
+ PAR (Personalredovisning)
+
+
+ Farligt Gods-exponering
+
+
+ Sektorrotation
+
+
+ Rehabilitering
+
+
+ lämna tomt för valfri
+
+
+ Ja
+
+
+ Nej
+
+
+ Ta bort denna timerkonfiguration?
+
+
+ Ta bort denna åsidosättning?
+
+
+ Standard
+
+
+ Åsidosättning
+
+
+ Låg
+
+
+ Medel
+
+
+ Hög
+
+
+ Nödsituation
+
+
+ Spara Allmänna Inställningar
+
diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx
index ae03d8e9..c62c3489 100644
--- a/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Department/Department.uk.resx
@@ -458,4 +458,133 @@
When a unit submits a status and users are setup on roles, the users status will be set to On Unit.
+
+ Налаштування Викликів та Диспетчеризації
+
+
+ Виклики та Диспетчеризація
+
+
+ Налаштування викликів та диспетчеризації успішно збережено.
+
+
+ Налаштування Таймера Реєстрації
+
+
+ Загальні Налаштування
+
+
+ Автоматично вмикати таймери реєстрації для нових викликів
+
+
+ Коли увімкнено, всі нові виклики автоматично матимуть увімкнені таймери реєстрації.
+
+
+ Стандартні Конфігурації Таймера
+
+
+ Вони визначають стандартні інтервали реєстрації для кожного типу цілі.
+
+
+ Перевизначення за Типом Виклику / Пріоритетом
+
+
+ Перевизначення мають пріоритет над стандартними таймерами коли виклик відповідає вказаному типу та/або пріоритету.
+
+
+ Тип Цілі
+
+
+ Тип Підрозділу
+
+
+ Тривалість (хвилини)
+
+
+ Поріг Попередження (хвилини)
+
+
+ Увімкнено
+
+
+ ID Типу Виклику
+
+
+ Пріоритет Виклику
+
+
+ Додати Конфігурацію Таймера
+
+
+ Додати Перевизначення
+
+
+ Додати Новий Стандартний Таймер
+
+
+ Додати Нове Перевизначення
+
+
+ -- Немає --
+
+
+ -- Будь-який --
+
+
+ Персонал
+
+
+ Тип Підрозділу
+
+
+ КІ (Командир Інциденту)
+
+
+ PAR (Відповідальність Персоналу)
+
+
+ Вплив Небезпечних Матеріалів
+
+
+ Ротація Сектора
+
+
+ Реабілітація
+
+
+ залиште порожнім для будь-якого
+
+
+ Так
+
+
+ Ні
+
+
+ Видалити цю конфігурацію таймера?
+
+
+ Видалити це перевизначення?
+
+
+ Стандартний
+
+
+ Перевизначення
+
+
+ Низький
+
+
+ Середній
+
+
+ Високий
+
+
+ Надзвичайний
+
+
+ Зберегти Загальні Налаштування
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
index 09a2f23f..c50c8844 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
@@ -135,4 +135,26 @@
عرض البلاغ
عرض النص
هذا عنوان what3words.
+ مؤقتات تسجيل الوصول
+ تفعيل مؤقتات تسجيل الوصول
+ مؤقتات تسجيل الوصول
+ الهدف
+ النوع
+ آخر تسجيل وصول
+ الوقت المتبقي
+ الحالة
+ الإجراءات
+ تسجيل الوصول
+ عرض سجل تسجيل الوصول
+ الطابع الزمني
+ من
+ النوع
+ الوحدة
+ الموقع
+ ملاحظة
+ تكوين مؤقت تسجيل الوصول
+ سجل تسجيل الوصول
+ المدة (دقائق)
+ حد التحذير (دقائق)
+ المصدر
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
index 12366fca..7851beef 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Check-In-Timer
+
+
+ Check-In-Timer aktivieren
+
+
+ Check-In-Timer
+
+
+ Ziel
+
+
+ Typ
+
+
+ Letzter Check-In
+
+
+ Verbleibende Zeit
+
+
+ Status
+
+
+ Aktionen
+
+
+ Einchecken
+
+
+ Check-In-Verlauf anzeigen
+
+
+ Zeitstempel
+
+
+ Wer
+
+
+ Typ
+
+
+ Einheit
+
+
+ Standort
+
+
+ Notiz
+
+
+ Check-In-Timer-Konfiguration
+
+
+ Check-In-Protokoll
+
+
+ Dauer (Min)
+
+
+ Warnschwelle (Min)
+
+
+ Quelle
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
index 30e59a0b..edf9c74c 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
@@ -507,4 +507,70 @@
This is a what3words address.
+
+ Check-In Timers
+
+
+ Enable Check-In Timers
+
+
+ Check-In Timers
+
+
+ Target
+
+
+ Type
+
+
+ Last Check-In
+
+
+ Time Remaining
+
+
+ Status
+
+
+ Actions
+
+
+ Check In
+
+
+ Show Check-In History
+
+
+ Timestamp
+
+
+ Who
+
+
+ Type
+
+
+ Unit
+
+
+ Location
+
+
+ Note
+
+
+ Check-In Timer Configuration
+
+
+ Check-In Log
+
+
+ Duration (min)
+
+
+ Warning Threshold (min)
+
+
+ Source
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
index 7b2db68b..2e8bbc40 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
@@ -507,4 +507,70 @@
Esta es una dirección de what3words.
+
+ Temporizadores de Registro
+
+
+ Habilitar Temporizadores de Registro
+
+
+ Temporizadores de Registro
+
+
+ Objetivo
+
+
+ Tipo
+
+
+ Último Registro
+
+
+ Tiempo Restante
+
+
+ Estado
+
+
+ Acciones
+
+
+ Registrarse
+
+
+ Mostrar Historial de Registros
+
+
+ Marca de Tiempo
+
+
+ Quién
+
+
+ Tipo
+
+
+ Unidad
+
+
+ Ubicación
+
+
+ Nota
+
+
+ Configuración del Temporizador de Registro
+
+
+ Registro de Check-In
+
+
+ Duración (min)
+
+
+ Umbral de Advertencia (min)
+
+
+ Fuente
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
index aa590699..8b4ee8f6 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Minuteries de Pointage
+
+
+ Activer les Minuteries de Pointage
+
+
+ Minuteries de Pointage
+
+
+ Cible
+
+
+ Type
+
+
+ Dernier Pointage
+
+
+ Temps Restant
+
+
+ Statut
+
+
+ Actions
+
+
+ Pointer
+
+
+ Afficher l'Historique des Pointages
+
+
+ Horodatage
+
+
+ Qui
+
+
+ Type
+
+
+ Unité
+
+
+ Emplacement
+
+
+ Note
+
+
+ Configuration des Minuteries de Pointage
+
+
+ Journal de Pointage
+
+
+ Durée (min)
+
+
+ Seuil d'Avertissement (min)
+
+
+ Source
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
index a990af88..12f4f3d9 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Timer di Registrazione
+
+
+ Abilita Timer di Registrazione
+
+
+ Timer di Registrazione
+
+
+ Obiettivo
+
+
+ Tipo
+
+
+ Ultima Registrazione
+
+
+ Tempo Rimanente
+
+
+ Stato
+
+
+ Azioni
+
+
+ Registrarsi
+
+
+ Mostra Cronologia Registrazioni
+
+
+ Marca Temporale
+
+
+ Chi
+
+
+ Tipo
+
+
+ Unità
+
+
+ Posizione
+
+
+ Nota
+
+
+ Configurazione Timer di Registrazione
+
+
+ Registro Check-In
+
+
+ Durata (min)
+
+
+ Soglia di Avviso (min)
+
+
+ Fonte
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
index 03880aa1..56b2c540 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Czasomierze Meldowania
+
+
+ Włącz Czasomierze Meldowania
+
+
+ Czasomierze Meldowania
+
+
+ Cel
+
+
+ Typ
+
+
+ Ostatnie Meldowanie
+
+
+ Pozostały Czas
+
+
+ Status
+
+
+ Akcje
+
+
+ Zamelduj się
+
+
+ Pokaż Historię Meldowań
+
+
+ Znacznik Czasu
+
+
+ Kto
+
+
+ Typ
+
+
+ Jednostka
+
+
+ Lokalizacja
+
+
+ Notatka
+
+
+ Konfiguracja Czasomierza Meldowania
+
+
+ Dziennik Meldowań
+
+
+ Czas trwania (min)
+
+
+ Próg Ostrzeżenia (min)
+
+
+ Źródło
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
index abc0b7e9..99ddf435 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Incheckningstimers
+
+
+ Aktivera Incheckningstimers
+
+
+ Incheckningstimers
+
+
+ Mål
+
+
+ Typ
+
+
+ Senaste Incheckning
+
+
+ Återstående Tid
+
+
+ Status
+
+
+ Åtgärder
+
+
+ Checka In
+
+
+ Visa Incheckningshistorik
+
+
+ Tidsstämpel
+
+
+ Vem
+
+
+ Typ
+
+
+ Enhet
+
+
+ Plats
+
+
+ Anteckning
+
+
+ Incheckningstimer-konfiguration
+
+
+ Incheckningslogg
+
+
+ Varaktighet (min)
+
+
+ Varningströskel (min)
+
+
+ Källa
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
index e1e4dbce..a513d5c1 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
@@ -458,4 +458,70 @@
This is a what3words address.
+
+ Таймери Реєстрації
+
+
+ Увімкнути Таймери Реєстрації
+
+
+ Таймери Реєстрації
+
+
+ Ціль
+
+
+ Тип
+
+
+ Остання Реєстрація
+
+
+ Залишок Часу
+
+
+ Статус
+
+
+ Дії
+
+
+ Зареєструватися
+
+
+ Показати Історію Реєстрацій
+
+
+ Мітка Часу
+
+
+ Хто
+
+
+ Тип
+
+
+ Підрозділ
+
+
+ Місцезнаходження
+
+
+ Примітка
+
+
+ Конфігурація Таймера Реєстрації
+
+
+ Журнал Реєстрацій
+
+
+ Тривалість (хв)
+
+
+ Поріг Попередження (хв)
+
+
+ Джерело
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.ar.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.ar.resx
index fc5bd3fb..ca04e7e2 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.ar.resx
@@ -246,4 +246,67 @@
معرف المرجع
لا شيء
رد اتصال
+
+ تقرير حضور الفعاليات
+
+
+ يعرض ساعات الحضور لكل شخص مع تتبع تسجيل الحضور والانصراف.
+
+
+ تقرير حضور الفعاليات
+
+
+ ساعات الحضور
+
+
+ الفعاليات المحضورة
+
+
+ الفعاليات الفائتة
+
+
+ إجمالي الساعات
+
+
+ التفاصيل
+
+
+ تفاصيل الحضور
+
+
+ الفعالية
+
+
+ وقت الحضور
+
+
+ وقت الانصراف
+
+
+ المدة
+
+
+ ملاحظة الحضور
+
+
+ ملاحظة الانصراف
+
+
+ سُجّل الحضور بواسطة
+
+
+ سُجّل الانصراف بواسطة
+
+
+ موقع الحضور
+
+
+ موقع الانصراف
+
+
+ لم يتم تسجيل الانصراف
+
+
+ الوقت الإجمالي
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.de.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.de.resx
index 7bd26a34..3fbef089 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.de.resx
@@ -246,4 +246,67 @@
Referenz-Id
Keine
Rückruf
+
+ Anwesenheitsbericht für Veranstaltungen
+
+
+ Zeigt die Anwesenheitsstunden pro Person mit Ein-/Auscheckprotokoll.
+
+
+ Anwesenheitsbericht für Veranstaltungen
+
+
+ Anwesenheitsstunden
+
+
+ Besuchte Veranstaltungen
+
+
+ Versäumte Veranstaltungen
+
+
+ Gesamtstunden
+
+
+ Details
+
+
+ Anwesenheitsdetails
+
+
+ Veranstaltung
+
+
+ Eincheckzeit
+
+
+ Auscheckzeit
+
+
+ Dauer
+
+
+ Eincheck-Notiz
+
+
+ Auscheck-Notiz
+
+
+ Eingecheckt von
+
+
+ Ausgecheckt von
+
+
+ Eincheck-Standort
+
+
+ Auscheck-Standort
+
+
+ Nicht ausgecheckt
+
+
+ Gesamtzeit
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.en.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.en.resx
index 8613df63..905083d8 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.en.resx
@@ -675,4 +675,69 @@
Callback
+
+
+
+ Event Attendance Report
+
+
+ Shows volunteer/event attendance hours per person with check-in/check-out tracking.
+
+
+ Event Attendance Report
+
+
+ Event Attendance Hours
+
+
+ Events Attended
+
+
+ Events Missed
+
+
+ Total Hours
+
+
+ Details
+
+
+ Event Attendance Detail
+
+
+ Event
+
+
+ Check-In Time
+
+
+ Check-Out Time
+
+
+ Duration
+
+
+ Check-In Note
+
+
+ Check-Out Note
+
+
+ Checked In By
+
+
+ Checked Out By
+
+
+ Check-In Location
+
+
+ Check-Out Location
+
+
+ Not checked out
+
+
+ Total Time
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.es.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.es.resx
index 71683acc..08fef471 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.es.resx
@@ -246,4 +246,67 @@
Id de Referencia
Ninguno
Devolución de Llamada
+
+ Reporte de Asistencia a Eventos
+
+
+ Muestra las horas de asistencia voluntaria/evento por persona con seguimiento de entrada/salida.
+
+
+ Reporte de Asistencia a Eventos
+
+
+ Horas de Asistencia a Eventos
+
+
+ Eventos Asistidos
+
+
+ Eventos Perdidos
+
+
+ Total de Horas
+
+
+ Detalles
+
+
+ Detalle de Asistencia a Eventos
+
+
+ Evento
+
+
+ Hora de Entrada
+
+
+ Hora de Salida
+
+
+ Duración
+
+
+ Nota de Entrada
+
+
+ Nota de Salida
+
+
+ Entrada Registrada Por
+
+
+ Salida Registrada Por
+
+
+ Ubicación de Entrada
+
+
+ Ubicación de Salida
+
+
+ Sin registro de salida
+
+
+ Tiempo Total
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.fr.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.fr.resx
index 13798105..6c91627e 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.fr.resx
@@ -246,4 +246,67 @@
Id de Référence
Aucun
Rappel
+
+ Rapport de présence aux événements
+
+
+ Affiche les heures de présence par personne avec suivi des pointages.
+
+
+ Rapport de présence aux événements
+
+
+ Heures de présence
+
+
+ Événements assistés
+
+
+ Événements manqués
+
+
+ Total des heures
+
+
+ Détails
+
+
+ Détail de présence
+
+
+ Événement
+
+
+ Heure d'arrivée
+
+
+ Heure de départ
+
+
+ Durée
+
+
+ Note d'arrivée
+
+
+ Note de départ
+
+
+ Arrivée enregistrée par
+
+
+ Départ enregistré par
+
+
+ Lieu d'arrivée
+
+
+ Lieu de départ
+
+
+ Non pointé au départ
+
+
+ Temps total
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.it.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.it.resx
index 35199347..45f79b02 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.it.resx
@@ -246,4 +246,67 @@
Id Riferimento
Nessuno
Richiamata
+
+ Report Presenze Eventi
+
+
+ Mostra le ore di presenza per persona con tracciamento entrata/uscita.
+
+
+ Report Presenze Eventi
+
+
+ Ore di Presenza
+
+
+ Eventi Partecipati
+
+
+ Eventi Mancati
+
+
+ Ore Totali
+
+
+ Dettagli
+
+
+ Dettaglio Presenze
+
+
+ Evento
+
+
+ Ora di Entrata
+
+
+ Ora di Uscita
+
+
+ Durata
+
+
+ Nota di Entrata
+
+
+ Nota di Uscita
+
+
+ Entrata Registrata Da
+
+
+ Uscita Registrata Da
+
+
+ Posizione Entrata
+
+
+ Posizione Uscita
+
+
+ Uscita non registrata
+
+
+ Tempo Totale
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.pl.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.pl.resx
index c305cff9..9ea01849 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.pl.resx
@@ -246,4 +246,67 @@
Id Referencji
Brak
Oddzwonienie
+
+ Raport obecności na wydarzeniach
+
+
+ Pokazuje godziny obecności na osobę ze śledzeniem zameldowań/wymeldowań.
+
+
+ Raport obecności na wydarzeniach
+
+
+ Godziny obecności
+
+
+ Wydarzenia z obecnością
+
+
+ Wydarzenia nieobecne
+
+
+ Łączna liczba godzin
+
+
+ Szczegóły
+
+
+ Szczegóły obecności
+
+
+ Wydarzenie
+
+
+ Czas zameldowania
+
+
+ Czas wymeldowania
+
+
+ Czas trwania
+
+
+ Notatka zameldowania
+
+
+ Notatka wymeldowania
+
+
+ Zameldowany przez
+
+
+ Wymeldowany przez
+
+
+ Lokalizacja zameldowania
+
+
+ Lokalizacja wymeldowania
+
+
+ Nie wymeldowany
+
+
+ Łączny czas
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.sv.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.sv.resx
index 774a62ca..bd0b657b 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.sv.resx
@@ -246,4 +246,67 @@
Referens-Id
Ingen
Återuppringning
+
+ Närvarorapport för evenemang
+
+
+ Visar närvarotimmar per person med in-/utcheckningsspårning.
+
+
+ Närvarorapport för evenemang
+
+
+ Närvarotimmar
+
+
+ Besökta evenemang
+
+
+ Missade evenemang
+
+
+ Totala timmar
+
+
+ Detaljer
+
+
+ Närvarodetaljer
+
+
+ Evenemang
+
+
+ Incheckningstid
+
+
+ Utcheckningstid
+
+
+ Varaktighet
+
+
+ Incheckningsanteckning
+
+
+ Utcheckningsanteckning
+
+
+ Incheckad av
+
+
+ Utcheckad av
+
+
+ Incheckningsplats
+
+
+ Utcheckningsplats
+
+
+ Inte utcheckad
+
+
+ Total tid
+
diff --git a/Core/Resgrid.Localization/Areas/User/Reports/Reports.uk.resx b/Core/Resgrid.Localization/Areas/User/Reports/Reports.uk.resx
index 8ee37b40..41555ced 100644
--- a/Core/Resgrid.Localization/Areas/User/Reports/Reports.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Reports/Reports.uk.resx
@@ -246,4 +246,67 @@
Id Посилання
Немає
Зворотній Дзвінок
+
+ Звіт про відвідуваність подій
+
+
+ Показує години присутності на особу з відстеженням входу/виходу.
+
+
+ Звіт про відвідуваність подій
+
+
+ Години присутності
+
+
+ Відвідані події
+
+
+ Пропущені події
+
+
+ Загальна кількість годин
+
+
+ Деталі
+
+
+ Деталі присутності
+
+
+ Подія
+
+
+ Час входу
+
+
+ Час виходу
+
+
+ Тривалість
+
+
+ Примітка до входу
+
+
+ Примітка до виходу
+
+
+ Вхід зареєстровано
+
+
+ Вихід зареєстровано
+
+
+ Місце входу
+
+
+ Місце виходу
+
+
+ Вихід не зареєстровано
+
+
+ Загальний час
+
diff --git a/Core/Resgrid.Localization/Areas/User/Security/Security.en.resx b/Core/Resgrid.Localization/Areas/User/Security/Security.en.resx
index 6cbda9ac..568a926a 100644
--- a/Core/Resgrid.Localization/Areas/User/Security/Security.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Security/Security.en.resx
@@ -370,6 +370,8 @@
Password must contain at least one lowercase letter.
Password must be at least {0} characters long.
Minimum password length cannot be less than the system default of 8 characters.
+ Delete Log Entries
+ Who in your department is allowed to delete log entries
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.ar.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.ar.resx
index 833ac212..e0b144b0 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.ar.resx
@@ -168,4 +168,7 @@
قالب البلاغ السريع
+
+ تفعيل مؤقتات تسجيل الوصول
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.de.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.de.resx
index 050aa6fe..741b29be 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.de.resx
@@ -119,4 +119,7 @@
Anruf-Schnellvorlage
+
+ Check-In-Timer aktivieren
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.en.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.en.resx
index b8f14b90..e69505c5 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.en.resx
@@ -168,4 +168,7 @@
Call Quick Template
+
+ Enable Check-In Timers
+
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.es.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.es.resx
index b942b06f..43671284 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.es.resx
@@ -115,4 +115,5 @@
Agregar plantilla
Nota de llamada
Plantilla rápida de llamada
+ Habilitar Temporizadores de Registro
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.fr.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.fr.resx
index 10ada4f4..89e75e88 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.fr.resx
@@ -119,4 +119,7 @@
Modèle rapide d'appel
+
+ Activer les Minuteries de Pointage
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.it.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.it.resx
index d98f37d3..abdc19f2 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.it.resx
@@ -119,4 +119,7 @@
Modello rapido chiamata
+
+ Abilita Timer di Registrazione
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.pl.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.pl.resx
index f2b18943..3c199f56 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.pl.resx
@@ -119,4 +119,7 @@
Szablon szybkiego zgłoszenia
+
+ Włącz Czasomierze Meldowania
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.sv.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.sv.resx
index 4af9eaf4..1390103f 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.sv.resx
@@ -119,4 +119,7 @@
Snabbmall för samtal
+
+ Aktivera Incheckningstimers
+
diff --git a/Core/Resgrid.Localization/Areas/User/Templates/Templates.uk.resx b/Core/Resgrid.Localization/Areas/User/Templates/Templates.uk.resx
index db1a3c10..4d0daff1 100644
--- a/Core/Resgrid.Localization/Areas/User/Templates/Templates.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Templates/Templates.uk.resx
@@ -119,4 +119,7 @@
Шаблон швидкого виклику
+
+ Увімкнути Таймери Реєстрації
+
diff --git a/Core/Resgrid.Model/AuditLogTypes.cs b/Core/Resgrid.Model/AuditLogTypes.cs
index 7c93dc27..9241f42f 100644
--- a/Core/Resgrid.Model/AuditLogTypes.cs
+++ b/Core/Resgrid.Model/AuditLogTypes.cs
@@ -124,6 +124,25 @@ public enum AuditLogTypes
RouteStopCheckedOut,
RouteStopSkipped,
RouteDeviationDetected,
- RouteDeviationAcknowledged
+ RouteDeviationAcknowledged,
+ // Check-In Timers
+ CheckInTimerConfigCreated,
+ CheckInTimerConfigUpdated,
+ CheckInTimerConfigDeleted,
+ CheckInTimerOverrideCreated,
+ CheckInTimerOverrideUpdated,
+ CheckInTimerOverrideDeleted,
+ CheckInPerformed,
+ CheckInTimerEnabledOnCall,
+ CheckInTimerDisabledOnCall,
+ // Calendar Check-In Attendance
+ CalendarCheckInPerformed,
+ CalendarCheckOutPerformed,
+ CalendarCheckInUpdated,
+ CalendarCheckInDeleted,
+ CalendarAdminCheckInPerformed,
+ // Log operations
+ LogCreated,
+ LogDeleted
}
}
diff --git a/Core/Resgrid.Model/CalendarItem.cs b/Core/Resgrid.Model/CalendarItem.cs
index 5e0e39d4..02e7340b 100644
--- a/Core/Resgrid.Model/CalendarItem.cs
+++ b/Core/Resgrid.Model/CalendarItem.cs
@@ -52,6 +52,8 @@ public class CalendarItem: IEntity
public int SignupType { get; set; }
+ public int CheckInType { get; set; }
+
public int Reminder { get; set; }
public bool LockEditing { get; set; }
@@ -171,6 +173,7 @@ public CalendarItem CreateRecurranceItem(DateTime start, DateTime end, string ti
item.IsAllDay = IsAllDay;
item.ItemType = ItemType;
item.SignupType = SignupType;
+ item.CheckInType = CheckInType;
item.Public = Public;
item.StartTimezone = timeZone;
item.EndTimezone = timeZone;
diff --git a/Core/Resgrid.Model/CalendarItemCheckIn.cs b/Core/Resgrid.Model/CalendarItemCheckIn.cs
new file mode 100644
index 00000000..39365117
--- /dev/null
+++ b/Core/Resgrid.Model/CalendarItemCheckIn.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ public class CalendarItemCheckIn : IEntity
+ {
+ public string CalendarItemCheckInId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ public int CalendarItemId { get; set; }
+
+ public string UserId { get; set; }
+
+ public DateTime CheckInTime { get; set; }
+
+ public DateTime? CheckOutTime { get; set; }
+
+ public string CheckInByUserId { get; set; }
+
+ public string CheckOutByUserId { get; set; }
+
+ public bool IsManualOverride { get; set; }
+
+ public string CheckInNote { get; set; }
+
+ public string CheckOutNote { get; set; }
+
+ public string CheckInLatitude { get; set; }
+
+ public string CheckInLongitude { get; set; }
+
+ public string CheckOutLatitude { get; set; }
+
+ public string CheckOutLongitude { get; set; }
+
+ public DateTime Timestamp { get; set; }
+
+ [NotMapped]
+ public string TableName => "CalendarItemCheckIns";
+
+ [NotMapped]
+ public string IdName => "CalendarItemCheckInId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CalendarItemCheckInId; }
+ set { CalendarItemCheckInId = (string)value; }
+ }
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" };
+
+ public TimeSpan? GetDuration()
+ {
+ if (CheckOutTime.HasValue)
+ return CheckOutTime.Value - CheckInTime;
+
+ return null;
+ }
+ }
+}
diff --git a/Core/Resgrid.Model/CalendarItemCheckInTypes.cs b/Core/Resgrid.Model/CalendarItemCheckInTypes.cs
new file mode 100644
index 00000000..7ea6d8ba
--- /dev/null
+++ b/Core/Resgrid.Model/CalendarItemCheckInTypes.cs
@@ -0,0 +1,20 @@
+namespace Resgrid.Model
+{
+ public enum CalendarItemCheckInTypes
+ {
+ ///
+ /// Check-in is disabled for this event
+ ///
+ Disabled = 0,
+
+ ///
+ /// Users can self check-in and check-out
+ ///
+ SelfCheckIn = 1,
+
+ ///
+ /// Only the event creator, department admins, or group admins can check in/out users
+ ///
+ AdminOnly = 2
+ }
+}
diff --git a/Core/Resgrid.Model/Call.cs b/Core/Resgrid.Model/Call.cs
index b55e8b5e..7a843eb3 100644
--- a/Core/Resgrid.Model/Call.cs
+++ b/Core/Resgrid.Model/Call.cs
@@ -179,6 +179,8 @@ public class Call : IEntity
public string IndoorMapFloorId { get; set; }
+ public bool CheckInTimersEnabled { get; set; }
+
[NotMapped]
[JsonIgnore]
public object IdValue
diff --git a/Core/Resgrid.Model/CallQuickTemplate.cs b/Core/Resgrid.Model/CallQuickTemplate.cs
index 80f94956..99ec9a05 100644
--- a/Core/Resgrid.Model/CallQuickTemplate.cs
+++ b/Core/Resgrid.Model/CallQuickTemplate.cs
@@ -38,6 +38,8 @@ public class CallQuickTemplate : IEntity
public DateTime CreatedOn { get; set; }
+ public bool? CheckInTimersEnabled { get; set; }
+
[NotMapped]
[JsonIgnore]
public object IdValue
diff --git a/Core/Resgrid.Model/CheckInRecord.cs b/Core/Resgrid.Model/CheckInRecord.cs
new file mode 100644
index 00000000..ccea2fdb
--- /dev/null
+++ b/Core/Resgrid.Model/CheckInRecord.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ public class CheckInRecord : IEntity
+ {
+ public string CheckInRecordId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ public int CallId { get; set; }
+
+ public int CheckInType { get; set; }
+
+ public string UserId { get; set; }
+
+ public int? UnitId { get; set; }
+
+ public string Latitude { get; set; }
+
+ public string Longitude { get; set; }
+
+ public DateTime Timestamp { get; set; }
+
+ public string Note { get; set; }
+
+ [NotMapped]
+ public string TableName => "CheckInRecords";
+
+ [NotMapped]
+ public string IdName => "CheckInRecordId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CheckInRecordId; }
+ set { CheckInRecordId = (string)value; }
+ }
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" };
+ }
+}
diff --git a/Core/Resgrid.Model/CheckInTimerConfig.cs b/Core/Resgrid.Model/CheckInTimerConfig.cs
new file mode 100644
index 00000000..846e5c8e
--- /dev/null
+++ b/Core/Resgrid.Model/CheckInTimerConfig.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ public class CheckInTimerConfig : IEntity
+ {
+ public string CheckInTimerConfigId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ public int TimerTargetType { get; set; }
+
+ public int? UnitTypeId { get; set; }
+
+ public int DurationMinutes { get; set; }
+
+ public int WarningThresholdMinutes { get; set; }
+
+ public bool IsEnabled { get; set; }
+
+ public string CreatedByUserId { get; set; }
+
+ public DateTime CreatedOn { get; set; }
+
+ public DateTime? UpdatedOn { get; set; }
+
+ public string ActiveForStates { get; set; }
+
+ [NotMapped]
+ public string TableName => "CheckInTimerConfigs";
+
+ [NotMapped]
+ public string IdName => "CheckInTimerConfigId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CheckInTimerConfigId; }
+ set { CheckInTimerConfigId = (string)value; }
+ }
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" };
+ }
+}
diff --git a/Core/Resgrid.Model/CheckInTimerOverride.cs b/Core/Resgrid.Model/CheckInTimerOverride.cs
new file mode 100644
index 00000000..7d6dbc8d
--- /dev/null
+++ b/Core/Resgrid.Model/CheckInTimerOverride.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ public class CheckInTimerOverride : IEntity
+ {
+ public string CheckInTimerOverrideId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ public int? CallTypeId { get; set; }
+
+ public int? CallPriority { get; set; }
+
+ public int TimerTargetType { get; set; }
+
+ public int? UnitTypeId { get; set; }
+
+ public int DurationMinutes { get; set; }
+
+ public int WarningThresholdMinutes { get; set; }
+
+ public bool IsEnabled { get; set; }
+
+ public string CreatedByUserId { get; set; }
+
+ public DateTime CreatedOn { get; set; }
+
+ public DateTime? UpdatedOn { get; set; }
+
+ public string ActiveForStates { get; set; }
+
+ [NotMapped]
+ public string TableName => "CheckInTimerOverrides";
+
+ [NotMapped]
+ public string IdName => "CheckInTimerOverrideId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CheckInTimerOverrideId; }
+ set { CheckInTimerOverrideId = (string)value; }
+ }
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" };
+ }
+}
diff --git a/Core/Resgrid.Model/CheckInTimerStatus.cs b/Core/Resgrid.Model/CheckInTimerStatus.cs
new file mode 100644
index 00000000..7f2f7cf5
--- /dev/null
+++ b/Core/Resgrid.Model/CheckInTimerStatus.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Resgrid.Model
+{
+ public class CheckInTimerStatus
+ {
+ public int TargetType { get; set; }
+
+ public string TargetEntityId { get; set; }
+
+ public string TargetName { get; set; }
+
+ public int? UnitId { get; set; }
+
+ public DateTime? LastCheckIn { get; set; }
+
+ public int DurationMinutes { get; set; }
+
+ public int WarningThresholdMinutes { get; set; }
+
+ public double ElapsedMinutes { get; set; }
+
+ public string Status { get; set; }
+ }
+}
diff --git a/Core/Resgrid.Model/CheckInTimerTargetType.cs b/Core/Resgrid.Model/CheckInTimerTargetType.cs
new file mode 100644
index 00000000..e3826879
--- /dev/null
+++ b/Core/Resgrid.Model/CheckInTimerTargetType.cs
@@ -0,0 +1,13 @@
+namespace Resgrid.Model
+{
+ public enum CheckInTimerTargetType
+ {
+ Personnel = 0,
+ UnitType = 1,
+ IC = 2,
+ PAR = 3,
+ HazmatExposure = 4,
+ SectorRotation = 5,
+ Rehab = 6
+ }
+}
diff --git a/Core/Resgrid.Model/DepartmentSettingTypes.cs b/Core/Resgrid.Model/DepartmentSettingTypes.cs
index 7634f1ec..99c05316 100644
--- a/Core/Resgrid.Model/DepartmentSettingTypes.cs
+++ b/Core/Resgrid.Model/DepartmentSettingTypes.cs
@@ -39,5 +39,6 @@ public enum DepartmentSettingTypes
PersonnelOnUnitSetUnitStatus = 35,
Require2FAForAdmins = 36,
PaddleCustomerId = 37,
+ CheckInTimersAutoEnableForNewCalls = 38,
}
}
diff --git a/Core/Resgrid.Model/PermissionTypes.cs b/Core/Resgrid.Model/PermissionTypes.cs
index 37aa46a9..254f3afe 100644
--- a/Core/Resgrid.Model/PermissionTypes.cs
+++ b/Core/Resgrid.Model/PermissionTypes.cs
@@ -28,7 +28,8 @@ public enum PermissionTypes
ManageWorkflowCredentials = 23,
ViewWorkflowRuns = 24,
ViewUdfFields = 25,
- ManageRoutes = 26
+ ManageRoutes = 26,
+ DeleteLog = 27
}
}
diff --git a/Core/Resgrid.Model/Repositories/ICalendarItemCheckInRepository.cs b/Core/Resgrid.Model/Repositories/ICalendarItemCheckInRepository.cs
new file mode 100644
index 00000000..ab232d90
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICalendarItemCheckInRepository.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICalendarItemCheckInRepository : IRepository
+ {
+ Task GetCheckInByCalendarItemAndUserAsync(int calendarItemId, string userId);
+ Task> GetCheckInsByCalendarItemIdAsync(int calendarItemId);
+ Task> GetCheckInsByDepartmentAndDateRangeAsync(int departmentId, DateTime start, DateTime end);
+ Task> GetCheckInsByUserAndDateRangeAsync(string userId, int departmentId, DateTime start, DateTime end);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICheckInRecordRepository.cs b/Core/Resgrid.Model/Repositories/ICheckInRecordRepository.cs
new file mode 100644
index 00000000..a1ef2e4f
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICheckInRecordRepository.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICheckInRecordRepository : IRepository
+ {
+ Task> GetByCallIdAsync(int callId);
+ Task GetLastCheckInForUserOnCallAsync(int callId, string userId);
+ Task GetLastCheckInForUnitOnCallAsync(int callId, int unitId);
+ Task> GetByDepartmentIdAndDateRangeAsync(int departmentId, DateTime start, DateTime end);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICheckInTimerConfigRepository.cs b/Core/Resgrid.Model/Repositories/ICheckInTimerConfigRepository.cs
new file mode 100644
index 00000000..9a306519
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICheckInTimerConfigRepository.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICheckInTimerConfigRepository : IRepository
+ {
+ Task> GetByDepartmentIdAsync(int departmentId);
+ Task GetByDepartmentAndTargetAsync(int departmentId, int timerTargetType, int? unitTypeId);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICheckInTimerOverrideRepository.cs b/Core/Resgrid.Model/Repositories/ICheckInTimerOverrideRepository.cs
new file mode 100644
index 00000000..43da7877
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICheckInTimerOverrideRepository.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICheckInTimerOverrideRepository : IRepository
+ {
+ Task> GetByDepartmentIdAsync(int departmentId);
+ Task> GetMatchingOverridesAsync(int departmentId, int? callTypeId, int? callPriority);
+ }
+}
diff --git a/Core/Resgrid.Model/ResolvedCheckInTimer.cs b/Core/Resgrid.Model/ResolvedCheckInTimer.cs
new file mode 100644
index 00000000..13e9abdb
--- /dev/null
+++ b/Core/Resgrid.Model/ResolvedCheckInTimer.cs
@@ -0,0 +1,21 @@
+namespace Resgrid.Model
+{
+ public class ResolvedCheckInTimer
+ {
+ public int TargetType { get; set; }
+
+ public int? UnitTypeId { get; set; }
+
+ public string TargetEntityId { get; set; }
+
+ public string TargetName { get; set; }
+
+ public int DurationMinutes { get; set; }
+
+ public int WarningThresholdMinutes { get; set; }
+
+ public bool IsFromOverride { get; set; }
+
+ public string ActiveForStates { get; set; }
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IAuthorizationService.cs b/Core/Resgrid.Model/Services/IAuthorizationService.cs
index 33883133..bc5b5499 100644
--- a/Core/Resgrid.Model/Services/IAuthorizationService.cs
+++ b/Core/Resgrid.Model/Services/IAuthorizationService.cs
@@ -342,5 +342,15 @@ public interface IAuthorizationService
/// The department identifier of the resource being modified.
/// true if the user is the managing member or a department admin; otherwise false.
Task CanUserModifyDepartmentAsync(string userId, int departmentId);
+
+ Task CanUserCheckInToCalendarEventAsync(string userId, int calendarItemId);
+
+ Task CanUserAdminCheckInCalendarEventAsync(string userId, int calendarItemId, string targetUserId);
+
+ Task CanUserEditCalendarCheckInAsync(string userId, string checkInId);
+
+ Task CanUserDeleteCalendarCheckInAsync(string userId, string checkInId);
+
+ Task CanUserViewCalendarCheckInsAsync(string userId, int calendarItemId);
}
}
diff --git a/Core/Resgrid.Model/Services/ICalendarService.cs b/Core/Resgrid.Model/Services/ICalendarService.cs
index 8a5f597e..14037915 100644
--- a/Core/Resgrid.Model/Services/ICalendarService.cs
+++ b/Core/Resgrid.Model/Services/ICalendarService.cs
@@ -75,6 +75,8 @@ Task MarkAsNotifiedAsync(int calendarItemId,
Task NotifyNewCalendarItemAsync(CalendarItem calendarItem);
+ Task NotifyUsersAboutCalendarItemAsync(CalendarItem calendarItem, List userIds);
+
// ── External calendar sync ─────────────────────────────────────────────────
///
@@ -104,5 +106,31 @@ Task RegenerateCalendarSyncAsync(int departmentId, string userId,
/// returns null if the token is invalid, expired, or has been regenerated.
///
Task<(int DepartmentId, string UserId)?> ValidateCalendarFeedTokenAsync(string encryptedToken);
+
+ // ── Calendar Check-In Attendance ───────────────────────────────────────────
+
+ Task CheckInToEventAsync(int calendarItemId, string userId, string note,
+ string adminUserId = null, string latitude = null, string longitude = null,
+ CancellationToken cancellationToken = default(CancellationToken));
+
+ Task CheckOutFromEventAsync(int calendarItemId, string userId,
+ string note = null, string adminUserId = null, string latitude = null, string longitude = null,
+ CancellationToken cancellationToken = default(CancellationToken));
+
+ Task UpdateCheckInTimesAsync(string checkInId, DateTime checkInTime,
+ DateTime? checkOutTime, string checkInNote, string checkOutNote,
+ CancellationToken cancellationToken = default(CancellationToken));
+
+ Task GetCheckInByCalendarItemAndUserAsync(int calendarItemId, string userId);
+
+ Task GetCheckInByIdAsync(string checkInId);
+
+ Task> GetCheckInsByCalendarItemAsync(int calendarItemId);
+
+ Task DeleteCheckInAsync(string checkInId, CancellationToken cancellationToken = default(CancellationToken));
+
+ Task> GetCheckInsByDepartmentDateRangeAsync(int departmentId, DateTime start, DateTime end);
+
+ Task> GetCheckInsByUserDateRangeAsync(string userId, int departmentId, DateTime start, DateTime end);
}
}
diff --git a/Core/Resgrid.Model/Services/ICheckInTimerService.cs b/Core/Resgrid.Model/Services/ICheckInTimerService.cs
new file mode 100644
index 00000000..af58eb92
--- /dev/null
+++ b/Core/Resgrid.Model/Services/ICheckInTimerService.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Services
+{
+ public interface ICheckInTimerService
+ {
+ // Configuration CRUD
+ Task> GetTimerConfigsForDepartmentAsync(int departmentId);
+ Task SaveTimerConfigAsync(CheckInTimerConfig config, CancellationToken cancellationToken = default);
+ Task DeleteTimerConfigAsync(string configId, int departmentId, CancellationToken cancellationToken = default);
+
+ // Override CRUD
+ Task> GetTimerOverridesForDepartmentAsync(int departmentId);
+ Task SaveTimerOverrideAsync(CheckInTimerOverride ovr, CancellationToken cancellationToken = default);
+ Task DeleteTimerOverrideAsync(string overrideId, int departmentId, CancellationToken cancellationToken = default);
+
+ // Timer Resolution
+ Task> ResolveAllTimersForCallAsync(Call call);
+
+ // Check-in Operations
+ Task PerformCheckInAsync(CheckInRecord record, CancellationToken cancellationToken = default);
+ Task> GetCheckInsForCallAsync(int callId);
+ Task GetLastCheckInAsync(int callId, string userId, int? unitId);
+
+ // Timer Status Computation
+ Task> GetActiveTimerStatusesForCallAsync(Call call);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs b/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs
index 7c99446e..62d5d4c0 100644
--- a/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs
+++ b/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs
@@ -285,5 +285,7 @@ public interface IDepartmentSettingsService
Task GetDepartmentIdForPaddleCustomerIdAsync(string paddleCustomerId, bool bypassCache = false);
Task GetPaddleCustomerIdForDepartmentAsync(int departmentId);
+
+ Task GetCheckInTimersAutoEnableForNewCallsAsync(int departmentId);
}
}
diff --git a/Core/Resgrid.Services/AuditService.cs b/Core/Resgrid.Services/AuditService.cs
index f4901e3e..43d1bcb4 100644
--- a/Core/Resgrid.Services/AuditService.cs
+++ b/Core/Resgrid.Services/AuditService.cs
@@ -150,6 +150,17 @@ public string GetAuditLogTypeString(AuditLogTypes logType)
return "UDF Field Removed";
case AuditLogTypes.UdfFieldValueSaved:
return "UDF Field Values Saved";
+ // Calendar Check-In Attendance
+ case AuditLogTypes.CalendarCheckInPerformed:
+ return "Calendar Event Check-In";
+ case AuditLogTypes.CalendarCheckOutPerformed:
+ return "Calendar Event Check-Out";
+ case AuditLogTypes.CalendarCheckInUpdated:
+ return "Calendar Check-In Times Updated";
+ case AuditLogTypes.CalendarCheckInDeleted:
+ return "Calendar Check-In Deleted";
+ case AuditLogTypes.CalendarAdminCheckInPerformed:
+ return "Admin Calendar Check-In";
}
return $"Unknown ({logType})";
diff --git a/Core/Resgrid.Services/AuthorizationService.cs b/Core/Resgrid.Services/AuthorizationService.cs
index a7fcf11d..d821abbd 100644
--- a/Core/Resgrid.Services/AuthorizationService.cs
+++ b/Core/Resgrid.Services/AuthorizationService.cs
@@ -1526,5 +1526,178 @@ public async Task CanUserModifyDepartmentAsync(string userId, int departme
return department.IsUserAnAdmin(userId);
}
+ public async Task CanUserCheckInToCalendarEventAsync(string userId, int calendarItemId)
+ {
+ var department = await _departmentsService.GetDepartmentByUserIdAsync(userId);
+ var item = await _calendarService.GetCalendarItemByIdAsync(calendarItemId);
+
+ if (department == null || item == null)
+ return false;
+
+ if (item.DepartmentId != department.DepartmentId)
+ return false;
+
+ // Check-in disabled for this event
+ if (item.CheckInType == (int)CalendarItemCheckInTypes.Disabled)
+ return false;
+
+ // Self check-in mode: any department member can check themselves in
+ if (item.CheckInType == (int)CalendarItemCheckInTypes.SelfCheckIn)
+ return true;
+
+ // Admin-only mode: only creator, dept admin, or group admin can perform check-ins
+ if (item.CheckInType == (int)CalendarItemCheckInTypes.AdminOnly)
+ {
+ if (department.IsUserAnAdmin(userId))
+ return true;
+
+ if (!string.IsNullOrWhiteSpace(item.CreatorUserId) && item.CreatorUserId == userId)
+ return true;
+
+ var group = await _departmentGroupsService.GetGroupForUserAsync(userId, department.DepartmentId);
+ if (group != null && group.IsUserGroupAdmin(userId))
+ return true;
+
+ return false;
+ }
+
+ return false;
+ }
+
+ public async Task CanUserAdminCheckInCalendarEventAsync(string userId, int calendarItemId, string targetUserId)
+ {
+ var department = await _departmentsService.GetDepartmentByUserIdAsync(userId);
+ var item = await _calendarService.GetCalendarItemByIdAsync(calendarItemId);
+
+ if (department == null || item == null)
+ return false;
+
+ if (item.DepartmentId != department.DepartmentId)
+ return false;
+
+ // Check-in disabled for this event
+ if (item.CheckInType == (int)CalendarItemCheckInTypes.Disabled)
+ return false;
+
+ // Department admins can check in users in their department
+ if (department.IsUserAnAdmin(userId))
+ {
+ if (department.IsUserInDepartment(targetUserId))
+ return true;
+
+ return false;
+ }
+
+ // Calendar item creator can check in users in their department
+ if (!string.IsNullOrWhiteSpace(item.CreatorUserId) && item.CreatorUserId == userId)
+ {
+ if (department.IsUserInDepartment(targetUserId))
+ return true;
+
+ return false;
+ }
+
+ // Group admins can check in users in their group or child groups
+ var adminGroup = await _departmentGroupsService.GetGroupForUserAsync(userId, department.DepartmentId);
+ if (adminGroup != null && adminGroup.IsUserGroupAdmin(userId))
+ {
+ // Check if the target user is in the admin's group
+ if (adminGroup.IsUserInGroup(targetUserId))
+ return true;
+
+ // Check child groups
+ var childGroups = await _departmentGroupsService.GetAllChildDepartmentGroupsAsync(adminGroup.DepartmentGroupId);
+ if (childGroups != null)
+ {
+ foreach (var childGroup in childGroups)
+ {
+ if (childGroup.IsUserInGroup(targetUserId))
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public async Task CanUserEditCalendarCheckInAsync(string userId, string checkInId)
+ {
+ var department = await _departmentsService.GetDepartmentByUserIdAsync(userId);
+ var checkIn = await _calendarService.GetCheckInByIdAsync(checkInId);
+
+ if (department == null || checkIn == null)
+ return false;
+
+ if (checkIn.DepartmentId != department.DepartmentId)
+ return false;
+
+ if (checkIn.UserId == userId)
+ return true;
+
+ if (department.IsUserAnAdmin(userId))
+ return true;
+
+ // Calendar item creator can edit check-ins
+ var item = await _calendarService.GetCalendarItemByIdAsync(checkIn.CalendarItemId);
+ if (item != null && !string.IsNullOrWhiteSpace(item.CreatorUserId) && item.CreatorUserId == userId)
+ return true;
+
+ // Group admins can edit check-ins for their group members
+ var adminGroup = await _departmentGroupsService.GetGroupForUserAsync(userId, department.DepartmentId);
+ if (adminGroup != null && adminGroup.IsUserGroupAdmin(userId))
+ {
+ if (adminGroup.IsUserInGroup(checkIn.UserId))
+ return true;
+
+ var childGroups = await _departmentGroupsService.GetAllChildDepartmentGroupsAsync(adminGroup.DepartmentGroupId);
+ if (childGroups != null)
+ {
+ foreach (var childGroup in childGroups)
+ {
+ if (childGroup.IsUserInGroup(checkIn.UserId))
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public async Task CanUserDeleteCalendarCheckInAsync(string userId, string checkInId)
+ {
+ var department = await _departmentsService.GetDepartmentByUserIdAsync(userId);
+ var checkIn = await _calendarService.GetCheckInByIdAsync(checkInId);
+
+ if (department == null || checkIn == null)
+ return false;
+
+ if (checkIn.DepartmentId != department.DepartmentId)
+ return false;
+
+ if (department.IsUserAnAdmin(userId))
+ return true;
+
+ // Calendar item creator can delete check-ins
+ var item = await _calendarService.GetCalendarItemByIdAsync(checkIn.CalendarItemId);
+ if (item != null && !string.IsNullOrWhiteSpace(item.CreatorUserId) && item.CreatorUserId == userId)
+ return true;
+
+ return false;
+ }
+
+ public async Task CanUserViewCalendarCheckInsAsync(string userId, int calendarItemId)
+ {
+ var department = await _departmentsService.GetDepartmentByUserIdAsync(userId);
+ var item = await _calendarService.GetCalendarItemByIdAsync(calendarItemId);
+
+ if (department == null || item == null)
+ return false;
+
+ if (item.DepartmentId != department.DepartmentId)
+ return false;
+
+ return true;
+ }
+
}
}
diff --git a/Core/Resgrid.Services/CalendarService.cs b/Core/Resgrid.Services/CalendarService.cs
index 726699ff..46b0c326 100644
--- a/Core/Resgrid.Services/CalendarService.cs
+++ b/Core/Resgrid.Services/CalendarService.cs
@@ -24,11 +24,12 @@ public class CalendarService : ICalendarService
private readonly IDepartmentGroupsService _departmentGroupsService;
private readonly IDepartmentSettingsService _departmentSettingsService;
private readonly IEncryptionService _encryptionService;
+ private readonly ICalendarItemCheckInRepository _calendarItemCheckInRepository;
public CalendarService(ICalendarItemsRepository calendarItemRepository, ICalendarItemTypeRepository calendarItemTypeRepository,
ICalendarItemAttendeeRepository calendarItemAttendeeRepository, IDepartmentsService departmentsService, ICommunicationService communicationService,
IUserProfileService userProfileService, IDepartmentGroupsService departmentGroupsService, IDepartmentSettingsService departmentSettingsService,
- IEncryptionService encryptionService)
+ IEncryptionService encryptionService, ICalendarItemCheckInRepository calendarItemCheckInRepository)
{
_calendarItemRepository = calendarItemRepository;
_calendarItemTypeRepository = calendarItemTypeRepository;
@@ -39,6 +40,7 @@ public CalendarService(ICalendarItemsRepository calendarItemRepository, ICalenda
_departmentGroupsService = departmentGroupsService;
_departmentSettingsService = departmentSettingsService;
_encryptionService = encryptionService;
+ _calendarItemCheckInRepository = calendarItemCheckInRepository;
}
public async Task> GetAllCalendarItemsForDepartmentAsync(int departmentId)
@@ -541,6 +543,36 @@ public async Task NotifyNewCalendarItemAsync(CalendarItem calendarItem)
return false;
}
+ public async Task NotifyUsersAboutCalendarItemAsync(CalendarItem calendarItem, List userIds)
+ {
+ if (calendarItem == null || userIds == null || !userIds.Any())
+ return false;
+
+ var profiles = await _userProfileService.GetAllProfilesForDepartmentAsync(calendarItem.DepartmentId);
+ var departmentNumber = await _departmentSettingsService.GetTextToCallNumberForDepartmentAsync(calendarItem.DepartmentId);
+ var department = await _departmentsService.GetDepartmentByIdAsync(calendarItem.DepartmentId, false);
+
+ var adjustedDateTime = calendarItem.Start.TimeConverter(department);
+ var title = $"New: {calendarItem.Title}";
+
+ var message = String.IsNullOrWhiteSpace(calendarItem.Location)
+ ? $"on {adjustedDateTime.ToShortDateString()} - {adjustedDateTime.ToShortTimeString()}"
+ : $"on {adjustedDateTime.ToShortDateString()} - {adjustedDateTime.ToShortTimeString()} at {calendarItem.Location}";
+
+ if (ConfigHelper.CanTransmit(department.DepartmentId))
+ {
+ foreach (var userId in userIds)
+ {
+ if (profiles.ContainsKey(userId))
+ await _communicationService.SendCalendarAsync(userId, calendarItem.DepartmentId, message, departmentNumber, title, profiles[userId], department);
+ else
+ await _communicationService.SendCalendarAsync(userId, calendarItem.DepartmentId, message, departmentNumber, title, null, department);
+ }
+ }
+
+ return true;
+ }
+
public async Task> GetCalendarItemsToNotifyAsync(DateTime timestamp)
{
List itemsToNotify = new List();
@@ -715,5 +747,118 @@ private string BuildEncryptedToken(int departmentId, string userId, string syncG
.Replace('/', '_')
.TrimEnd('=');
}
+
+ #region Calendar Check-In Attendance
+
+ public async Task CheckInToEventAsync(int calendarItemId, string userId, string note,
+ string adminUserId = null, string latitude = null, string longitude = null,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var existing = await _calendarItemCheckInRepository.GetCheckInByCalendarItemAndUserAsync(calendarItemId, userId);
+ if (existing != null)
+ return existing;
+
+ var calendarItem = await _calendarItemRepository.GetByIdAsync(calendarItemId);
+ if (calendarItem == null)
+ return null;
+
+ var checkIn = new CalendarItemCheckIn
+ {
+ CalendarItemCheckInId = Guid.NewGuid().ToString(),
+ DepartmentId = calendarItem.DepartmentId,
+ CalendarItemId = calendarItemId,
+ UserId = userId,
+ CheckInTime = DateTime.UtcNow,
+ CheckInByUserId = adminUserId,
+ IsManualOverride = false,
+ CheckInNote = note,
+ CheckInLatitude = latitude,
+ CheckInLongitude = longitude,
+ Timestamp = DateTime.UtcNow
+ };
+
+ var saved = await _calendarItemCheckInRepository.SaveOrUpdateAsync(checkIn, cancellationToken);
+ return saved;
+ }
+
+ public async Task CheckOutFromEventAsync(int calendarItemId, string userId,
+ string note = null, string adminUserId = null, string latitude = null, string longitude = null,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var existing = await _calendarItemCheckInRepository.GetCheckInByCalendarItemAndUserAsync(calendarItemId, userId);
+ if (existing == null)
+ return null;
+
+ if (existing.CheckOutTime.HasValue)
+ return existing;
+
+ existing.CheckOutTime = DateTime.UtcNow;
+ existing.CheckOutByUserId = adminUserId;
+ existing.CheckOutNote = note;
+ existing.CheckOutLatitude = latitude;
+ existing.CheckOutLongitude = longitude;
+ var saved = await _calendarItemCheckInRepository.SaveOrUpdateAsync(existing, cancellationToken);
+ return saved;
+ }
+
+ public async Task UpdateCheckInTimesAsync(string checkInId, DateTime checkInTime,
+ DateTime? checkOutTime, string checkInNote, string checkOutNote,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var existing = await _calendarItemCheckInRepository.GetByIdAsync(checkInId);
+ if (existing == null)
+ return null;
+
+ if (checkOutTime.HasValue && checkOutTime.Value < checkInTime)
+ throw new ArgumentException("Check-out time cannot be earlier than check-in time.");
+
+ existing.CheckInTime = checkInTime;
+ existing.CheckOutTime = checkOutTime;
+ existing.IsManualOverride = true;
+ existing.CheckInNote = checkInNote;
+ existing.CheckOutNote = checkOutNote;
+
+ var saved = await _calendarItemCheckInRepository.SaveOrUpdateAsync(existing, cancellationToken);
+ return saved;
+ }
+
+ public async Task GetCheckInByCalendarItemAndUserAsync(int calendarItemId, string userId)
+ {
+ return await _calendarItemCheckInRepository.GetCheckInByCalendarItemAndUserAsync(calendarItemId, userId);
+ }
+
+ public async Task GetCheckInByIdAsync(string checkInId)
+ {
+ return await _calendarItemCheckInRepository.GetByIdAsync(checkInId);
+ }
+
+ public async Task> GetCheckInsByCalendarItemAsync(int calendarItemId)
+ {
+ var items = await _calendarItemCheckInRepository.GetCheckInsByCalendarItemIdAsync(calendarItemId);
+ return items?.ToList() ?? new List();
+ }
+
+ public async Task DeleteCheckInAsync(string checkInId, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var checkIn = await _calendarItemCheckInRepository.GetByIdAsync(checkInId);
+ if (checkIn == null)
+ return false;
+
+ return await _calendarItemCheckInRepository.DeleteAsync(checkIn, cancellationToken);
+ }
+
+ public async Task> GetCheckInsByDepartmentDateRangeAsync(int departmentId, DateTime start, DateTime end)
+ {
+ var items = await _calendarItemCheckInRepository.GetCheckInsByDepartmentAndDateRangeAsync(departmentId, start, end);
+ return items?.ToList() ?? new List();
+ }
+
+ public async Task> GetCheckInsByUserDateRangeAsync(string userId, int departmentId, DateTime start, DateTime end)
+ {
+ var items = await _calendarItemCheckInRepository.GetCheckInsByUserAndDateRangeAsync(userId, departmentId, start, end);
+ return items?.ToList() ?? new List();
+ }
+
+ #endregion Calendar Check-In Attendance
}
}
diff --git a/Core/Resgrid.Services/CheckInTimerService.cs b/Core/Resgrid.Services/CheckInTimerService.cs
new file mode 100644
index 00000000..c4d4fed8
--- /dev/null
+++ b/Core/Resgrid.Services/CheckInTimerService.cs
@@ -0,0 +1,384 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Services;
+
+namespace Resgrid.Services
+{
+ public class CheckInTimerService : ICheckInTimerService
+ {
+ private readonly ICheckInTimerConfigRepository _configRepository;
+ private readonly ICheckInTimerOverrideRepository _overrideRepository;
+ private readonly ICheckInRecordRepository _recordRepository;
+ private readonly IActionLogsService _actionLogsService;
+ private readonly IUnitsService _unitsService;
+ private readonly ICallsService _callsService;
+
+ public CheckInTimerService(
+ ICheckInTimerConfigRepository configRepository,
+ ICheckInTimerOverrideRepository overrideRepository,
+ ICheckInRecordRepository recordRepository,
+ IActionLogsService actionLogsService,
+ IUnitsService unitsService,
+ ICallsService callsService)
+ {
+ _configRepository = configRepository;
+ _overrideRepository = overrideRepository;
+ _recordRepository = recordRepository;
+ _actionLogsService = actionLogsService;
+ _unitsService = unitsService;
+ _callsService = callsService;
+ }
+
+ #region Configuration CRUD
+
+ public async Task> GetTimerConfigsForDepartmentAsync(int departmentId)
+ {
+ var configs = await _configRepository.GetByDepartmentIdAsync(departmentId);
+ return configs?.ToList() ?? new List();
+ }
+
+ public async Task SaveTimerConfigAsync(CheckInTimerConfig config, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(config.CheckInTimerConfigId))
+ {
+ config.CreatedOn = DateTime.UtcNow;
+ }
+ else
+ {
+ var existing = await _configRepository.GetByIdAsync(config.CheckInTimerConfigId);
+ if (existing != null && existing.DepartmentId != config.DepartmentId)
+ throw new UnauthorizedAccessException("Cannot modify a timer config belonging to another department.");
+
+ config.UpdatedOn = DateTime.UtcNow;
+ }
+
+ return await _configRepository.SaveOrUpdateAsync(config, cancellationToken);
+ }
+
+ public async Task DeleteTimerConfigAsync(string configId, int departmentId, CancellationToken cancellationToken = default)
+ {
+ var config = await _configRepository.GetByIdAsync(configId);
+ if (config == null)
+ return false;
+
+ if (config.DepartmentId != departmentId)
+ throw new UnauthorizedAccessException("Cannot delete a timer config belonging to another department.");
+
+ return await _configRepository.DeleteAsync(config, cancellationToken);
+ }
+
+ #endregion Configuration CRUD
+
+ #region Override CRUD
+
+ public async Task> GetTimerOverridesForDepartmentAsync(int departmentId)
+ {
+ var overrides = await _overrideRepository.GetByDepartmentIdAsync(departmentId);
+ return overrides?.ToList() ?? new List();
+ }
+
+ public async Task SaveTimerOverrideAsync(CheckInTimerOverride ovr, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(ovr.CheckInTimerOverrideId))
+ {
+ ovr.CreatedOn = DateTime.UtcNow;
+ }
+ else
+ {
+ var existing = await _overrideRepository.GetByIdAsync(ovr.CheckInTimerOverrideId);
+ if (existing != null && existing.DepartmentId != ovr.DepartmentId)
+ throw new UnauthorizedAccessException("Cannot modify a timer override belonging to another department.");
+
+ ovr.UpdatedOn = DateTime.UtcNow;
+ }
+
+ return await _overrideRepository.SaveOrUpdateAsync(ovr, cancellationToken);
+ }
+
+ public async Task DeleteTimerOverrideAsync(string overrideId, int departmentId, CancellationToken cancellationToken = default)
+ {
+ var ovr = await _overrideRepository.GetByIdAsync(overrideId);
+ if (ovr == null)
+ return false;
+
+ if (ovr.DepartmentId != departmentId)
+ throw new UnauthorizedAccessException("Cannot delete a timer override belonging to another department.");
+
+ return await _overrideRepository.DeleteAsync(ovr, cancellationToken);
+ }
+
+ #endregion Override CRUD
+
+ #region Timer Resolution
+
+ public async Task> ResolveAllTimersForCallAsync(Call call)
+ {
+ if (call == null || !call.CheckInTimersEnabled)
+ return new List();
+
+ var defaults = await _configRepository.GetByDepartmentIdAsync(call.DepartmentId);
+ var defaultList = defaults?.Where(c => c.IsEnabled).ToList() ?? new List();
+
+ // Parse call type as int for override matching
+ int? callTypeId = null;
+ if (!string.IsNullOrWhiteSpace(call.Type) && int.TryParse(call.Type, out int parsedType))
+ callTypeId = parsedType;
+
+ var overrides = await _overrideRepository.GetMatchingOverridesAsync(call.DepartmentId, callTypeId, call.Priority);
+ var overrideList = overrides?.ToList() ?? new List();
+
+ var resolved = new Dictionary();
+
+ // First, populate from defaults
+ foreach (var def in defaultList)
+ {
+ var targetId = def.UnitTypeId?.ToString();
+ var key = $"{def.TimerTargetType}_{def.UnitTypeId}_{targetId}";
+ resolved[key] = new ResolvedCheckInTimer
+ {
+ TargetType = def.TimerTargetType,
+ UnitTypeId = def.UnitTypeId,
+ TargetEntityId = targetId,
+ DurationMinutes = def.DurationMinutes,
+ WarningThresholdMinutes = def.WarningThresholdMinutes,
+ IsFromOverride = false,
+ ActiveForStates = def.ActiveForStates
+ };
+ }
+
+ // Then, apply overrides with scoring: type+priority=3, type-only=2, priority-only=1
+ var scoredOverrides = overrideList
+ .Select(o => new
+ {
+ Override = o,
+ Score = (o.CallTypeId.HasValue && o.CallPriority.HasValue) ? 3
+ : o.CallTypeId.HasValue ? 2
+ : o.CallPriority.HasValue ? 1
+ : 0
+ })
+ .OrderByDescending(x => x.Score)
+ .ToList();
+
+ foreach (var scored in scoredOverrides)
+ {
+ var o = scored.Override;
+ if (!o.IsEnabled)
+ continue;
+
+ var targetId = o.UnitTypeId?.ToString();
+ var key = $"{o.TimerTargetType}_{o.UnitTypeId}_{targetId}";
+
+ // Only apply if this is the best scoring override for this key
+ if (!resolved.ContainsKey(key) || !resolved[key].IsFromOverride)
+ {
+ resolved[key] = new ResolvedCheckInTimer
+ {
+ TargetType = o.TimerTargetType,
+ UnitTypeId = o.UnitTypeId,
+ TargetEntityId = targetId,
+ DurationMinutes = o.DurationMinutes,
+ WarningThresholdMinutes = o.WarningThresholdMinutes,
+ IsFromOverride = true,
+ ActiveForStates = o.ActiveForStates
+ };
+ }
+ }
+
+ return resolved.Values.ToList();
+ }
+
+ #endregion Timer Resolution
+
+ #region Check-in Operations
+
+ public async Task PerformCheckInAsync(CheckInRecord record, CancellationToken cancellationToken = default)
+ {
+ record.Timestamp = DateTime.UtcNow;
+ return await _recordRepository.SaveOrUpdateAsync(record, cancellationToken);
+ }
+
+ public async Task> GetCheckInsForCallAsync(int callId)
+ {
+ var records = await _recordRepository.GetByCallIdAsync(callId);
+ return records?.ToList() ?? new List();
+ }
+
+ public async Task GetLastCheckInAsync(int callId, string userId, int? unitId)
+ {
+ if (unitId.HasValue)
+ return await _recordRepository.GetLastCheckInForUnitOnCallAsync(callId, unitId.Value);
+
+ return await _recordRepository.GetLastCheckInForUserOnCallAsync(callId, userId);
+ }
+
+ #endregion Check-in Operations
+
+ #region Timer Status Computation
+
+ public async Task> GetActiveTimerStatusesForCallAsync(Call call)
+ {
+ if (call == null || !call.CheckInTimersEnabled || call.State != (int)CallStates.Active)
+ return new List();
+
+ var resolvedTimers = await ResolveAllTimersForCallAsync(call);
+ if (!resolvedTimers.Any())
+ return new List();
+
+ // Filter timers by ActiveForStates against current dispatched entity states
+ resolvedTimers = await FilterTimersByActiveStatesAsync(resolvedTimers, call);
+ if (!resolvedTimers.Any())
+ return new List();
+
+ var checkIns = await _recordRepository.GetByCallIdAsync(call.CallId);
+ var checkInList = checkIns?.ToList() ?? new List();
+
+ var statuses = new List();
+ var now = DateTime.UtcNow;
+
+ foreach (var timer in resolvedTimers)
+ {
+ // Find the latest check-in matching this timer's type and concrete target
+ var matchingCheckIns = checkInList
+ .Where(c => c.CheckInType == timer.TargetType);
+
+ if (timer.UnitTypeId.HasValue)
+ matchingCheckIns = matchingCheckIns.Where(c => c.UnitId == timer.UnitTypeId);
+
+ var latestCheckIn = matchingCheckIns
+ .OrderByDescending(c => c.Timestamp)
+ .FirstOrDefault();
+
+ var baseTime = latestCheckIn?.Timestamp ?? call.LoggedOn;
+ var elapsed = (now - baseTime).TotalMinutes;
+
+ string status;
+ if (elapsed < timer.DurationMinutes)
+ status = "Green";
+ else if (elapsed < timer.DurationMinutes + timer.WarningThresholdMinutes)
+ status = "Warning";
+ else
+ status = "Critical";
+
+ statuses.Add(new CheckInTimerStatus
+ {
+ TargetType = timer.TargetType,
+ TargetEntityId = timer.TargetEntityId,
+ TargetName = timer.TargetName ?? ((CheckInTimerTargetType)timer.TargetType).ToString(),
+ UnitId = latestCheckIn?.UnitId,
+ LastCheckIn = latestCheckIn?.Timestamp,
+ DurationMinutes = timer.DurationMinutes,
+ WarningThresholdMinutes = timer.WarningThresholdMinutes,
+ ElapsedMinutes = Math.Round(elapsed, 1),
+ Status = status
+ });
+ }
+
+ return statuses;
+ }
+
+ #endregion Timer Status Computation
+
+ #region State Filtering
+
+ private async Task> FilterTimersByActiveStatesAsync(List timers, Call call)
+ {
+ var timersWithStateFilter = timers.Where(t => !string.IsNullOrWhiteSpace(t.ActiveForStates)).ToList();
+ if (!timersWithStateFilter.Any())
+ return timers; // No filtering needed
+
+ // Populate call dispatches if not already loaded
+ if (call.Dispatches == null || call.UnitDispatches == null)
+ call = await _callsService.PopulateCallData(call, true, false, false, false, true, false, false, false, false);
+
+ // Build a set of current personnel action type IDs
+ var personnelStates = new Dictionary();
+ if (call.Dispatches != null)
+ {
+ foreach (var dispatch in call.Dispatches)
+ {
+ var lastAction = await _actionLogsService.GetLastActionLogForUserAsync(dispatch.UserId);
+ personnelStates[dispatch.UserId] = lastAction?.ActionTypeId ?? (int)ActionTypes.StandingBy;
+ }
+ }
+
+ // Build a set of current unit state IDs (keyed by UnitId)
+ var unitStates = new Dictionary();
+ if (call.UnitDispatches != null)
+ {
+ foreach (var unitDispatch in call.UnitDispatches)
+ {
+ var lastState = await _unitsService.GetLastUnitStateByUnitIdAsync(unitDispatch.UnitId);
+ unitStates[unitDispatch.UnitId] = lastState?.State ?? (int)UnitStateTypes.Available;
+ }
+ }
+
+ var result = new List();
+ foreach (var timer in timers)
+ {
+ if (string.IsNullOrWhiteSpace(timer.ActiveForStates))
+ {
+ result.Add(timer); // No filter = always active
+ continue;
+ }
+
+ var allowedStates = ParseActiveForStates(timer.ActiveForStates);
+
+ bool anyEntityMatches = false;
+
+ if (timer.TargetType == (int)CheckInTimerTargetType.UnitType)
+ {
+ // Check unit states
+ foreach (var kvp in unitStates)
+ {
+ // If timer is for a specific UnitType, we'd need to check the unit's type
+ // For now, check all dispatched units
+ if (allowedStates.Contains(kvp.Value))
+ {
+ anyEntityMatches = true;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Personnel-based timers (Personnel, IC, PAR, Hazmat, SectorRotation, Rehab)
+ foreach (var kvp in personnelStates)
+ {
+ if (allowedStates.Contains(kvp.Value))
+ {
+ anyEntityMatches = true;
+ break;
+ }
+ }
+ }
+
+ if (anyEntityMatches)
+ result.Add(timer);
+ }
+
+ return result;
+ }
+
+ private static HashSet ParseActiveForStates(string activeForStates)
+ {
+ var states = new HashSet();
+ if (string.IsNullOrWhiteSpace(activeForStates))
+ return states;
+
+ foreach (var part in activeForStates.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (int.TryParse(part.Trim(), out int stateId))
+ states.Add(stateId);
+ }
+
+ return states;
+ }
+
+ #endregion State Filtering
+ }
+}
diff --git a/Core/Resgrid.Services/CustomStateService.cs b/Core/Resgrid.Services/CustomStateService.cs
index 52f9bd44..11f6d9e7 100644
--- a/Core/Resgrid.Services/CustomStateService.cs
+++ b/Core/Resgrid.Services/CustomStateService.cs
@@ -230,7 +230,14 @@ public async Task GetCustomDetailByIdAsync(int detailId)
if (existingDetail != null)
{
+ existingDetail.ButtonText = detail.ButtonText;
+ existingDetail.ButtonColor = detail.ButtonColor;
+ existingDetail.TextColor = detail.TextColor;
+ existingDetail.GpsRequired = detail.GpsRequired;
+ existingDetail.NoteType = detail.NoteType;
+ existingDetail.DetailType = detail.DetailType;
existingDetail.Order = detail.Order;
+ existingDetail.BaseType = detail.BaseType;
}
}
}
diff --git a/Core/Resgrid.Services/DepartmentSettingsService.cs b/Core/Resgrid.Services/DepartmentSettingsService.cs
index 257be104..9aac66f4 100644
--- a/Core/Resgrid.Services/DepartmentSettingsService.cs
+++ b/Core/Resgrid.Services/DepartmentSettingsService.cs
@@ -787,5 +787,15 @@ async Task getSetting()
return null;
}
+
+ public async Task GetCheckInTimersAutoEnableForNewCallsAsync(int departmentId)
+ {
+ var s = await GetSettingByDepartmentIdType(departmentId, DepartmentSettingTypes.CheckInTimersAutoEnableForNewCalls);
+
+ if (s != null && bool.TryParse(s.Setting, out bool result))
+ return result;
+
+ return false;
+ }
}
}
diff --git a/Core/Resgrid.Services/ServicesModule.cs b/Core/Resgrid.Services/ServicesModule.cs
index 793d0405..daad563b 100644
--- a/Core/Resgrid.Services/ServicesModule.cs
+++ b/Core/Resgrid.Services/ServicesModule.cs
@@ -86,6 +86,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().InstancePerLifetimeScope();
// UDF Services
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
index 81c730f3..0415b44f 100644
--- a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
+++ b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
@@ -171,7 +171,6 @@ public static void AddLogClaims(ClaimsIdentity identity, bool isAdmin, List permissions, bool isGroupAdmin, List roles)
+ {
+ if (permissions != null && permissions.Any(x => x.PermissionType == (int)PermissionTypes.DeleteLog))
+ {
+ var permission = permissions.First(x => x.PermissionType == (int)PermissionTypes.DeleteLog);
+
+ if (permission.Action == (int)PermissionActions.DepartmentAdminsOnly && isAdmin)
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAndGroupAdmins && (isAdmin || isGroupAdmin))
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && isAdmin)
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin)
+ {
+ if (!String.IsNullOrWhiteSpace(permission.Data))
+ {
+ var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse);
+ var role = from r in roles
+ where roleIds.Contains(r.PersonnelRoleId)
+ select r;
+
+ if (role.Any())
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ }
+ }
+ else if (permission.Action == (int)PermissionActions.Everyone)
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ }
+ else
+ {
+ // Default: everyone can delete logs
identity.AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
}
diff --git a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
index 03844026..77d92928 100644
--- a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
+++ b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
@@ -175,6 +175,7 @@ public override async Task CreateAsync(TUser user)
ClaimsLogic.AddCallClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddActionClaims(id);
ClaimsLogic.AddLogClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
+ ClaimsLogic.AddDeleteLogClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddStaffingClaims(id);
ClaimsLogic.AddPersonnelClaims(id, departmentAdmin, permissions, isGroupAdmin,roles);
ClaimsLogic.AddUnitClaims(id, departmentAdmin);
diff --git a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
index 98a3a795..ab14bc4f 100644
--- a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
+++ b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
@@ -98,6 +98,7 @@ public async Task BuildTokenAsync(string userId, int departmentId)
ClaimsLogic.AddCallClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddActionClaims(id);
ClaimsLogic.AddLogClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
+ ClaimsLogic.AddDeleteLogClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddStaffingClaims(id);
ClaimsLogic.AddPersonnelClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddUnitClaims(id, departmentAdmin);
diff --git a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
index 2e57df03..f36fb6ee 100644
--- a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
+++ b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
@@ -178,7 +178,6 @@ public void AddLogClaims(bool isAdmin, List permissions, bool isGrou
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
- AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
else if (permission.Action == (int)PermissionActions.DepartmentAdminsOnly && !isAdmin)
{
@@ -189,7 +188,6 @@ public void AddLogClaims(bool isAdmin, List permissions, bool isGrou
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
- AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
else if (permission.Action == (int)PermissionActions.DepartmentAndGroupAdmins && !isAdmin && !isGroupAdmin)
{
@@ -200,7 +198,6 @@ public void AddLogClaims(bool isAdmin, List permissions, bool isGrou
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
- AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin)
{
@@ -214,7 +211,6 @@ where roleIds.Contains(r.PersonnelRoleId)
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
- AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
else
{
@@ -227,7 +223,6 @@ where roleIds.Contains(r.PersonnelRoleId)
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
- AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
}
@@ -236,6 +231,50 @@ where roleIds.Contains(r.PersonnelRoleId)
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.View));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Update));
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Create));
+ }
+ }
+
+ public void AddDeleteLogClaims(bool isAdmin, List permissions, bool isGroupAdmin, List roles)
+ {
+ if (permissions != null && permissions.Any(x => x.PermissionType == (int)PermissionTypes.DeleteLog))
+ {
+ var permission = permissions.First(x => x.PermissionType == (int)PermissionTypes.DeleteLog);
+
+ if (permission.Action == (int)PermissionActions.DepartmentAdminsOnly && isAdmin)
+ {
+ AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAndGroupAdmins && (isAdmin || isGroupAdmin))
+ {
+ AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && isAdmin)
+ {
+ AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ else if (permission.Action == (int)PermissionActions.DepartmentAdminsAndSelectRoles && !isAdmin)
+ {
+ if (!String.IsNullOrWhiteSpace(permission.Data))
+ {
+ var roleIds = permission.Data.Split(char.Parse(",")).Select(int.Parse);
+ var role = from r in roles
+ where roleIds.Contains(r.PersonnelRoleId)
+ select r;
+
+ if (role.Any())
+ {
+ AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ }
+ }
+ else if (permission.Action == (int)PermissionActions.Everyone)
+ {
+ AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
+ }
+ }
+ else
+ {
+ // Default: everyone can delete logs
AddClaim(new Claim(ResgridClaimTypes.Resources.Log, ResgridClaimTypes.Actions.Delete));
}
}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0056_AddingCheckInTimers.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0056_AddingCheckInTimers.cs
new file mode 100644
index 00000000..f0588b24
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0056_AddingCheckInTimers.cs
@@ -0,0 +1,117 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(56)]
+ public class M0056_AddingCheckInTimers : Migration
+ {
+ public override void Up()
+ {
+ // ── CheckInTimerConfigs ─────────────────────────────────
+ Create.Table("CheckInTimerConfigs")
+ .WithColumn("CheckInTimerConfigId").AsString(128).NotNullable().PrimaryKey()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("TimerTargetType").AsInt32().NotNullable()
+ .WithColumn("UnitTypeId").AsInt32().Nullable()
+ .WithColumn("DurationMinutes").AsInt32().NotNullable()
+ .WithColumn("WarningThresholdMinutes").AsInt32().NotNullable()
+ .WithColumn("IsEnabled").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("CreatedByUserId").AsString(128).NotNullable()
+ .WithColumn("CreatedOn").AsDateTime2().NotNullable()
+ .WithColumn("UpdatedOn").AsDateTime2().Nullable();
+
+ Create.ForeignKey("FK_CheckInTimerConfigs_Departments")
+ .FromTable("CheckInTimerConfigs").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.Index("IX_CheckInTimerConfigs_DepartmentId")
+ .OnTable("CheckInTimerConfigs")
+ .OnColumn("DepartmentId");
+
+ Create.UniqueConstraint("UQ_CheckInTimerConfigs_Dept_Target_Unit")
+ .OnTable("CheckInTimerConfigs")
+ .Columns("DepartmentId", "TimerTargetType", "UnitTypeId");
+
+ // ── CheckInTimerOverrides ──────────────────────────────
+ Create.Table("CheckInTimerOverrides")
+ .WithColumn("CheckInTimerOverrideId").AsString(128).NotNullable().PrimaryKey()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("CallTypeId").AsInt32().Nullable()
+ .WithColumn("CallPriority").AsInt32().Nullable()
+ .WithColumn("TimerTargetType").AsInt32().NotNullable()
+ .WithColumn("UnitTypeId").AsInt32().Nullable()
+ .WithColumn("DurationMinutes").AsInt32().NotNullable()
+ .WithColumn("WarningThresholdMinutes").AsInt32().NotNullable()
+ .WithColumn("IsEnabled").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("CreatedByUserId").AsString(128).NotNullable()
+ .WithColumn("CreatedOn").AsDateTime2().NotNullable()
+ .WithColumn("UpdatedOn").AsDateTime2().Nullable();
+
+ Create.ForeignKey("FK_CheckInTimerOverrides_Departments")
+ .FromTable("CheckInTimerOverrides").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.Index("IX_CheckInTimerOverrides_DepartmentId")
+ .OnTable("CheckInTimerOverrides")
+ .OnColumn("DepartmentId");
+
+ Create.UniqueConstraint("UQ_CheckInTimerOverrides_Dept_Call_Target_Unit")
+ .OnTable("CheckInTimerOverrides")
+ .Columns("DepartmentId", "CallTypeId", "CallPriority", "TimerTargetType", "UnitTypeId");
+
+ // ── CheckInRecords ─────────────────────────────────────
+ Create.Table("CheckInRecords")
+ .WithColumn("CheckInRecordId").AsString(128).NotNullable().PrimaryKey()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("CallId").AsInt32().NotNullable()
+ .WithColumn("CheckInType").AsInt32().NotNullable()
+ .WithColumn("UserId").AsString(128).NotNullable()
+ .WithColumn("UnitId").AsInt32().Nullable()
+ .WithColumn("Latitude").AsString(50).Nullable()
+ .WithColumn("Longitude").AsString(50).Nullable()
+ .WithColumn("Timestamp").AsDateTime2().NotNullable()
+ .WithColumn("Note").AsString(1000).Nullable();
+
+ Create.ForeignKey("FK_CheckInRecords_Departments")
+ .FromTable("CheckInRecords").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.ForeignKey("FK_CheckInRecords_Calls")
+ .FromTable("CheckInRecords").ForeignColumn("CallId")
+ .ToTable("Calls").PrimaryColumn("CallId");
+
+ Create.Index("IX_CheckInRecords_CallId")
+ .OnTable("CheckInRecords")
+ .OnColumn("CallId");
+
+ Create.Index("IX_CheckInRecords_DepartmentId_Timestamp")
+ .OnTable("CheckInRecords")
+ .OnColumn("DepartmentId").Ascending()
+ .OnColumn("Timestamp").Descending();
+
+ // ── Alter Calls ────────────────────────────────────────
+ Alter.Table("Calls")
+ .AddColumn("CheckInTimersEnabled").AsBoolean().NotNullable().WithDefaultValue(false);
+
+ // ── Alter CallQuickTemplates ────────────────────────────
+ Alter.Table("CallQuickTemplates")
+ .AddColumn("CheckInTimersEnabled").AsBoolean().Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("CheckInTimersEnabled").FromTable("CallQuickTemplates");
+ Delete.Column("CheckInTimersEnabled").FromTable("Calls");
+
+ Delete.ForeignKey("FK_CheckInRecords_Calls").OnTable("CheckInRecords");
+ Delete.ForeignKey("FK_CheckInRecords_Departments").OnTable("CheckInRecords");
+ Delete.Table("CheckInRecords");
+
+ Delete.ForeignKey("FK_CheckInTimerOverrides_Departments").OnTable("CheckInTimerOverrides");
+ Delete.Table("CheckInTimerOverrides");
+
+ Delete.ForeignKey("FK_CheckInTimerConfigs_Departments").OnTable("CheckInTimerConfigs");
+ Delete.Table("CheckInTimerConfigs");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0057_AddingCalendarItemCheckIns.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0057_AddingCalendarItemCheckIns.cs
new file mode 100644
index 00000000..a7173798
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0057_AddingCalendarItemCheckIns.cs
@@ -0,0 +1,63 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(57)]
+ public class M0057_AddingCalendarItemCheckIns : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("CalendarItemCheckIns")
+ .WithColumn("CalendarItemCheckInId").AsString(128).NotNullable().PrimaryKey()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("CalendarItemId").AsInt32().NotNullable()
+ .WithColumn("UserId").AsString(128).NotNullable()
+ .WithColumn("CheckInTime").AsDateTime2().NotNullable()
+ .WithColumn("CheckOutTime").AsDateTime2().Nullable()
+ .WithColumn("CheckInByUserId").AsString(128).Nullable()
+ .WithColumn("CheckOutByUserId").AsString(128).Nullable()
+ .WithColumn("IsManualOverride").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("CheckInNote").AsString(1000).Nullable()
+ .WithColumn("CheckOutNote").AsString(1000).Nullable()
+ .WithColumn("CheckInLatitude").AsString(50).Nullable()
+ .WithColumn("CheckInLongitude").AsString(50).Nullable()
+ .WithColumn("CheckOutLatitude").AsString(50).Nullable()
+ .WithColumn("CheckOutLongitude").AsString(50).Nullable()
+ .WithColumn("Timestamp").AsDateTime2().NotNullable();
+
+ Create.ForeignKey("FK_CalendarItemCheckIns_Departments")
+ .FromTable("CalendarItemCheckIns").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.ForeignKey("FK_CalendarItemCheckIns_CalendarItems")
+ .FromTable("CalendarItemCheckIns").ForeignColumn("CalendarItemId")
+ .ToTable("CalendarItems").PrimaryColumn("CalendarItemId");
+
+ Create.Index("IX_CalendarItemCheckIns_CalendarItemId")
+ .OnTable("CalendarItemCheckIns")
+ .OnColumn("CalendarItemId");
+
+ Create.Index("IX_CalendarItemCheckIns_DepartmentId_UserId")
+ .OnTable("CalendarItemCheckIns")
+ .OnColumn("DepartmentId").Ascending()
+ .OnColumn("UserId").Ascending();
+
+ Create.UniqueConstraint("UQ_CalendarItemCheckIns_CalItem_User")
+ .OnTable("CalendarItemCheckIns")
+ .Columns("CalendarItemId", "UserId");
+
+ // Add CheckInType to CalendarItems table
+ Alter.Table("CalendarItems")
+ .AddColumn("CheckInType").AsInt32().NotNullable().WithDefaultValue(0);
+ }
+
+ public override void Down()
+ {
+ Delete.Column("CheckInType").FromTable("CalendarItems");
+
+ Delete.ForeignKey("FK_CalendarItemCheckIns_CalendarItems").OnTable("CalendarItemCheckIns");
+ Delete.ForeignKey("FK_CalendarItemCheckIns_Departments").OnTable("CalendarItemCheckIns");
+ Delete.Table("CalendarItemCheckIns");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0058_FixCheckInTimerNullableUniqueConstraints.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0058_FixCheckInTimerNullableUniqueConstraints.cs
new file mode 100644
index 00000000..0a88f5be
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0058_FixCheckInTimerNullableUniqueConstraints.cs
@@ -0,0 +1,83 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(58)]
+ public class M0058_FixCheckInTimerNullableUniqueConstraints : Migration
+ {
+ public override void Up()
+ {
+ // Drop the existing unique constraints that prevent multiple NULLs
+ Delete.UniqueConstraint("UQ_CheckInTimerConfigs_Dept_Target_Unit")
+ .FromTable("CheckInTimerConfigs");
+
+ Delete.UniqueConstraint("UQ_CheckInTimerOverrides_Dept_Call_Target_Unit")
+ .FromTable("CheckInTimerOverrides");
+
+ // Replace with filtered unique indexes that only enforce uniqueness
+ // when nullable columns are NOT NULL, allowing multiple NULL rows.
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerConfigs_Dept_Target_Unit
+ ON CheckInTimerConfigs (DepartmentId, TimerTargetType, UnitTypeId)
+ WHERE UnitTypeId IS NOT NULL;
+ ");
+
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerOverrides_Dept_Call_Target_Unit
+ ON CheckInTimerOverrides (DepartmentId, CallTypeId, CallPriority, TimerTargetType, UnitTypeId)
+ WHERE CallTypeId IS NOT NULL AND CallPriority IS NOT NULL AND UnitTypeId IS NOT NULL;
+ ");
+
+ // Add compound indexes for "latest check-in" lookups by user and unit
+ Create.Index("IX_CheckInRecords_CallId_UserId_Timestamp")
+ .OnTable("CheckInRecords")
+ .OnColumn("CallId").Ascending()
+ .OnColumn("UserId").Ascending()
+ .OnColumn("Timestamp").Descending();
+
+ Create.Index("IX_CheckInRecords_CallId_UnitId_Timestamp")
+ .OnTable("CheckInRecords")
+ .OnColumn("CallId").Ascending()
+ .OnColumn("UnitId").Ascending()
+ .OnColumn("Timestamp").Descending();
+
+ // Add composite indexes for CalendarItemCheckIns query patterns
+ Create.Index("IX_CalendarItemCheckIns_CalendarItemId_CheckInTime")
+ .OnTable("CalendarItemCheckIns")
+ .OnColumn("CalendarItemId").Ascending()
+ .OnColumn("CheckInTime").Descending();
+
+ Create.Index("IX_CalendarItemCheckIns_DepartmentId_CheckInTime")
+ .OnTable("CalendarItemCheckIns")
+ .OnColumn("DepartmentId").Ascending()
+ .OnColumn("CheckInTime").Descending();
+
+ Create.Index("IX_CalendarItemCheckIns_DepartmentId_UserId_CheckInTime")
+ .OnTable("CalendarItemCheckIns")
+ .OnColumn("DepartmentId").Ascending()
+ .OnColumn("UserId").Ascending()
+ .OnColumn("CheckInTime").Descending();
+ }
+
+ public override void Down()
+ {
+ Delete.Index("IX_CalendarItemCheckIns_DepartmentId_UserId_CheckInTime").OnTable("CalendarItemCheckIns");
+ Delete.Index("IX_CalendarItemCheckIns_DepartmentId_CheckInTime").OnTable("CalendarItemCheckIns");
+ Delete.Index("IX_CalendarItemCheckIns_CalendarItemId_CheckInTime").OnTable("CalendarItemCheckIns");
+
+ Delete.Index("IX_CheckInRecords_CallId_UnitId_Timestamp").OnTable("CheckInRecords");
+ Delete.Index("IX_CheckInRecords_CallId_UserId_Timestamp").OnTable("CheckInRecords");
+
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerConfigs_Dept_Target_Unit ON CheckInTimerConfigs;");
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerOverrides_Dept_Call_Target_Unit ON CheckInTimerOverrides;");
+
+ Create.UniqueConstraint("UQ_CheckInTimerConfigs_Dept_Target_Unit")
+ .OnTable("CheckInTimerConfigs")
+ .Columns("DepartmentId", "TimerTargetType", "UnitTypeId");
+
+ Create.UniqueConstraint("UQ_CheckInTimerOverrides_Dept_Call_Target_Unit")
+ .OnTable("CheckInTimerOverrides")
+ .Columns("DepartmentId", "CallTypeId", "CallPriority", "TimerTargetType", "UnitTypeId");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0059_AddActiveForStatesToCheckInTimers.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0059_AddActiveForStatesToCheckInTimers.cs
new file mode 100644
index 00000000..0ad95193
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0059_AddActiveForStatesToCheckInTimers.cs
@@ -0,0 +1,23 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(59)]
+ public class M0059_AddActiveForStatesToCheckInTimers : Migration
+ {
+ public override void Up()
+ {
+ Alter.Table("CheckInTimerConfigs")
+ .AddColumn("ActiveForStates").AsString(200).Nullable();
+
+ Alter.Table("CheckInTimerOverrides")
+ .AddColumn("ActiveForStates").AsString(200).Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("ActiveForStates").FromTable("CheckInTimerConfigs");
+ Delete.Column("ActiveForStates").FromTable("CheckInTimerOverrides");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0060_FixCheckInTimerNullUniqueness.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0060_FixCheckInTimerNullUniqueness.cs
new file mode 100644
index 00000000..edba80f5
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0060_FixCheckInTimerNullUniqueness.cs
@@ -0,0 +1,88 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(60)]
+ public class M0060_FixCheckInTimerNullUniqueness : Migration
+ {
+ public override void Up()
+ {
+ // Drop the filtered unique indexes from M0058 that allow multiple NULL rows
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerConfigs_Dept_Target_Unit ON CheckInTimerConfigs;");
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerOverrides_Dept_Call_Target_Unit ON CheckInTimerOverrides;");
+
+ // Remove duplicate rows that may have accumulated while the filtered indexes
+ // allowed multiple NULL entries (keep the most recently modified row per group)
+ Execute.Sql(@"
+ WITH cte AS (
+ SELECT CheckInTimerConfigId,
+ ROW_NUMBER() OVER (
+ PARTITION BY DepartmentId, TimerTargetType, ISNULL(UnitTypeId, -1)
+ ORDER BY ISNULL(UpdatedOn, CreatedOn) DESC, CheckInTimerConfigId DESC
+ ) AS rn
+ FROM CheckInTimerConfigs
+ )
+ DELETE FROM cte WHERE rn > 1;
+ ");
+
+ Execute.Sql(@"
+ WITH cte AS (
+ SELECT CheckInTimerOverrideId,
+ ROW_NUMBER() OVER (
+ PARTITION BY DepartmentId, ISNULL(CallTypeId, -1), ISNULL(CallPriority, -1), TimerTargetType, ISNULL(UnitTypeId, -1)
+ ORDER BY ISNULL(UpdatedOn, CreatedOn) DESC, CheckInTimerOverrideId DESC
+ ) AS rn
+ FROM CheckInTimerOverrides
+ )
+ DELETE FROM cte WHERE rn > 1;
+ ");
+
+ // Add persisted computed columns that coalesce NULLs to sentinel values
+ // so SQL Server treats NULL as a distinct value for uniqueness (matching PostgreSQL NULLS NOT DISTINCT)
+ Execute.Sql(@"
+ ALTER TABLE CheckInTimerConfigs
+ ADD UnitTypeId_Unique AS ISNULL(UnitTypeId, -1) PERSISTED;
+ ");
+
+ Execute.Sql(@"
+ ALTER TABLE CheckInTimerOverrides
+ ADD CallTypeId_Unique AS ISNULL(CallTypeId, -1) PERSISTED,
+ CallPriority_Unique AS ISNULL(CallPriority, -1) PERSISTED,
+ UnitTypeId_Unique AS ISNULL(UnitTypeId, -1) PERSISTED;
+ ");
+
+ // Create unique indexes on the computed columns
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerConfigs_Dept_Target_Unit
+ ON CheckInTimerConfigs (DepartmentId, TimerTargetType, UnitTypeId_Unique);
+ ");
+
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerOverrides_Dept_Call_Target_Unit
+ ON CheckInTimerOverrides (DepartmentId, CallTypeId_Unique, CallPriority_Unique, TimerTargetType, UnitTypeId_Unique);
+ ");
+ }
+
+ public override void Down()
+ {
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerConfigs_Dept_Target_Unit ON CheckInTimerConfigs;");
+ Execute.Sql("DROP INDEX IF EXISTS UQ_CheckInTimerOverrides_Dept_Call_Target_Unit ON CheckInTimerOverrides;");
+
+ Execute.Sql("ALTER TABLE CheckInTimerOverrides DROP COLUMN CallTypeId_Unique, CallPriority_Unique, UnitTypeId_Unique;");
+ Execute.Sql("ALTER TABLE CheckInTimerConfigs DROP COLUMN UnitTypeId_Unique;");
+
+ // Restore the filtered indexes from M0058
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerConfigs_Dept_Target_Unit
+ ON CheckInTimerConfigs (DepartmentId, TimerTargetType, UnitTypeId)
+ WHERE UnitTypeId IS NOT NULL;
+ ");
+
+ Execute.Sql(@"
+ CREATE UNIQUE NONCLUSTERED INDEX UQ_CheckInTimerOverrides_Dept_Call_Target_Unit
+ ON CheckInTimerOverrides (DepartmentId, CallTypeId, CallPriority, TimerTargetType, UnitTypeId)
+ WHERE CallTypeId IS NOT NULL AND CallPriority IS NOT NULL AND UnitTypeId IS NOT NULL;
+ ");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0056_AddingCheckInTimersPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0056_AddingCheckInTimersPg.cs
new file mode 100644
index 00000000..75f844c7
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0056_AddingCheckInTimersPg.cs
@@ -0,0 +1,117 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(56)]
+ public class M0056_AddingCheckInTimersPg : Migration
+ {
+ public override void Up()
+ {
+ // ── checkIntimerconfigs ─────────────────────────────────
+ Create.Table("checkintimerconfigs")
+ .WithColumn("checkintimerconfigid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("timertargettype").AsInt32().NotNullable()
+ .WithColumn("unittypeid").AsInt32().Nullable()
+ .WithColumn("durationminutes").AsInt32().NotNullable()
+ .WithColumn("warningthresholdminutes").AsInt32().NotNullable()
+ .WithColumn("isenabled").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("createdbyuserid").AsCustom("citext").NotNullable()
+ .WithColumn("createdon").AsDateTime().NotNullable()
+ .WithColumn("updatedon").AsDateTime().Nullable();
+
+ Create.ForeignKey("fk_checkintimerconfigs_departments")
+ .FromTable("checkintimerconfigs").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.Index("ix_checkintimerconfigs_departmentid")
+ .OnTable("checkintimerconfigs")
+ .OnColumn("departmentid");
+
+ Create.UniqueConstraint("uq_checkintimerconfigs_dept_target_unit")
+ .OnTable("checkintimerconfigs")
+ .Columns("departmentid", "timertargettype", "unittypeid");
+
+ // ── checkintimeroverrides ──────────────────────────────
+ Create.Table("checkintimeroverrides")
+ .WithColumn("checkintimeroverrideid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("calltypeid").AsInt32().Nullable()
+ .WithColumn("callpriority").AsInt32().Nullable()
+ .WithColumn("timertargettype").AsInt32().NotNullable()
+ .WithColumn("unittypeid").AsInt32().Nullable()
+ .WithColumn("durationminutes").AsInt32().NotNullable()
+ .WithColumn("warningthresholdminutes").AsInt32().NotNullable()
+ .WithColumn("isenabled").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("createdbyuserid").AsCustom("citext").NotNullable()
+ .WithColumn("createdon").AsDateTime().NotNullable()
+ .WithColumn("updatedon").AsDateTime().Nullable();
+
+ Create.ForeignKey("fk_checkintimeroverrides_departments")
+ .FromTable("checkintimeroverrides").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.Index("ix_checkintimeroverrides_departmentid")
+ .OnTable("checkintimeroverrides")
+ .OnColumn("departmentid");
+
+ Create.UniqueConstraint("uq_checkintimeroverrides_dept_call_target_unit")
+ .OnTable("checkintimeroverrides")
+ .Columns("departmentid", "calltypeid", "callpriority", "timertargettype", "unittypeid");
+
+ // ── checkinrecords ─────────────────────────────────────
+ Create.Table("checkinrecords")
+ .WithColumn("checkinrecordid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("callid").AsInt32().NotNullable()
+ .WithColumn("checkintype").AsInt32().NotNullable()
+ .WithColumn("userid").AsCustom("citext").NotNullable()
+ .WithColumn("unitid").AsInt32().Nullable()
+ .WithColumn("latitude").AsCustom("citext").Nullable()
+ .WithColumn("longitude").AsCustom("citext").Nullable()
+ .WithColumn("timestamp").AsDateTime().NotNullable()
+ .WithColumn("note").AsCustom("citext").Nullable();
+
+ Create.ForeignKey("fk_checkinrecords_departments")
+ .FromTable("checkinrecords").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.ForeignKey("fk_checkinrecords_calls")
+ .FromTable("checkinrecords").ForeignColumn("callid")
+ .ToTable("calls").PrimaryColumn("callid");
+
+ Create.Index("ix_checkinrecords_callid")
+ .OnTable("checkinrecords")
+ .OnColumn("callid");
+
+ Create.Index("ix_checkinrecords_departmentid_timestamp")
+ .OnTable("checkinrecords")
+ .OnColumn("departmentid").Ascending()
+ .OnColumn("timestamp").Descending();
+
+ // ── Alter calls ────────────────────────────────────────
+ Alter.Table("calls")
+ .AddColumn("checkintimersenabled").AsBoolean().NotNullable().WithDefaultValue(false);
+
+ // ── Alter callquicktemplates ────────────────────────────
+ Alter.Table("callquicktemplates")
+ .AddColumn("checkintimersenabled").AsBoolean().Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("checkintimersenabled").FromTable("callquicktemplates");
+ Delete.Column("checkintimersenabled").FromTable("calls");
+
+ Delete.ForeignKey("fk_checkinrecords_calls").OnTable("checkinrecords");
+ Delete.ForeignKey("fk_checkinrecords_departments").OnTable("checkinrecords");
+ Delete.Table("checkinrecords");
+
+ Delete.ForeignKey("fk_checkintimeroverrides_departments").OnTable("checkintimeroverrides");
+ Delete.Table("checkintimeroverrides");
+
+ Delete.ForeignKey("fk_checkintimerconfigs_departments").OnTable("checkintimerconfigs");
+ Delete.Table("checkintimerconfigs");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0057_AddingCalendarItemCheckInsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0057_AddingCalendarItemCheckInsPg.cs
new file mode 100644
index 00000000..7189ceab
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0057_AddingCalendarItemCheckInsPg.cs
@@ -0,0 +1,63 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(57)]
+ public class M0057_AddingCalendarItemCheckInsPg : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("calendaritemcheckins")
+ .WithColumn("calendaritemcheckinid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("calendaritemid").AsInt32().NotNullable()
+ .WithColumn("userid").AsCustom("citext").NotNullable()
+ .WithColumn("checkintime").AsDateTime().NotNullable()
+ .WithColumn("checkouttime").AsDateTime().Nullable()
+ .WithColumn("checkinbyuserid").AsCustom("citext").Nullable()
+ .WithColumn("checkoutbyuserid").AsCustom("citext").Nullable()
+ .WithColumn("ismanualoverride").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("checkinnote").AsCustom("citext").Nullable()
+ .WithColumn("checkoutnote").AsCustom("citext").Nullable()
+ .WithColumn("checkinlatitude").AsCustom("citext").Nullable()
+ .WithColumn("checkinlongitude").AsCustom("citext").Nullable()
+ .WithColumn("checkoutlatitude").AsCustom("citext").Nullable()
+ .WithColumn("checkoutlongitude").AsCustom("citext").Nullable()
+ .WithColumn("timestamp").AsDateTime().NotNullable();
+
+ Create.ForeignKey("fk_calendaritemcheckins_departments")
+ .FromTable("calendaritemcheckins").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.ForeignKey("fk_calendaritemcheckins_calendaritems")
+ .FromTable("calendaritemcheckins").ForeignColumn("calendaritemid")
+ .ToTable("calendaritems").PrimaryColumn("calendaritemid");
+
+ Create.Index("ix_calendaritemcheckins_calendaritemid")
+ .OnTable("calendaritemcheckins")
+ .OnColumn("calendaritemid");
+
+ Create.Index("ix_calendaritemcheckins_departmentid_userid")
+ .OnTable("calendaritemcheckins")
+ .OnColumn("departmentid").Ascending()
+ .OnColumn("userid").Ascending();
+
+ Create.UniqueConstraint("uq_calendaritemcheckins_calitem_user")
+ .OnTable("calendaritemcheckins")
+ .Columns("calendaritemid", "userid");
+
+ // Add checkintype to calendaritems table
+ Alter.Table("calendaritems")
+ .AddColumn("checkintype").AsInt32().NotNullable().WithDefaultValue(0);
+ }
+
+ public override void Down()
+ {
+ Delete.Column("checkintype").FromTable("calendaritems");
+
+ Delete.ForeignKey("fk_calendaritemcheckins_calendaritems").OnTable("calendaritemcheckins");
+ Delete.ForeignKey("fk_calendaritemcheckins_departments").OnTable("calendaritemcheckins");
+ Delete.Table("calendaritemcheckins");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0058_FixCheckInTimerNullableUniqueConstraintsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0058_FixCheckInTimerNullableUniqueConstraintsPg.cs
new file mode 100644
index 00000000..7e1f6367
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0058_FixCheckInTimerNullableUniqueConstraintsPg.cs
@@ -0,0 +1,84 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(58)]
+ public class M0058_FixCheckInTimerNullableUniqueConstraintsPg : Migration
+ {
+ public override void Up()
+ {
+ // Drop the existing unique constraints that allow duplicate NULL rows
+ Delete.UniqueConstraint("uq_checkintimerconfigs_dept_target_unit")
+ .FromTable("checkintimerconfigs");
+
+ Delete.UniqueConstraint("uq_checkintimeroverrides_dept_call_target_unit")
+ .FromTable("checkintimeroverrides");
+
+ // Replace with NULLS NOT DISTINCT unique indexes (PostgreSQL 15+)
+ // so that NULL values are treated as equal for uniqueness checks,
+ // preventing duplicate "Any/None" rules.
+ Execute.Sql(@"
+ CREATE UNIQUE INDEX uq_checkintimerconfigs_dept_target_unit
+ ON checkintimerconfigs (departmentid, timertargettype, unittypeid)
+ NULLS NOT DISTINCT;
+ ");
+
+ Execute.Sql(@"
+ CREATE UNIQUE INDEX uq_checkintimeroverrides_dept_call_target_unit
+ ON checkintimeroverrides (departmentid, calltypeid, callpriority, timertargettype, unittypeid)
+ NULLS NOT DISTINCT;
+ ");
+
+ // Add compound indexes for "latest check-in" lookups by user and unit
+ Create.Index("ix_checkinrecords_callid_userid_timestamp")
+ .OnTable("checkinrecords")
+ .OnColumn("callid").Ascending()
+ .OnColumn("userid").Ascending()
+ .OnColumn("timestamp").Descending();
+
+ Create.Index("ix_checkinrecords_callid_unitid_timestamp")
+ .OnTable("checkinrecords")
+ .OnColumn("callid").Ascending()
+ .OnColumn("unitid").Ascending()
+ .OnColumn("timestamp").Descending();
+
+ // Add composite indexes for CalendarItemCheckIns query patterns
+ Create.Index("ix_calendaritemcheckins_calendaritemid_checkintime")
+ .OnTable("calendaritemcheckins")
+ .OnColumn("calendaritemid").Ascending()
+ .OnColumn("checkintime").Descending();
+
+ Create.Index("ix_calendaritemcheckins_departmentid_checkintime")
+ .OnTable("calendaritemcheckins")
+ .OnColumn("departmentid").Ascending()
+ .OnColumn("checkintime").Descending();
+
+ Create.Index("ix_calendaritemcheckins_departmentid_userid_checkintime")
+ .OnTable("calendaritemcheckins")
+ .OnColumn("departmentid").Ascending()
+ .OnColumn("userid").Ascending()
+ .OnColumn("checkintime").Descending();
+ }
+
+ public override void Down()
+ {
+ Delete.Index("ix_calendaritemcheckins_departmentid_userid_checkintime").OnTable("calendaritemcheckins");
+ Delete.Index("ix_calendaritemcheckins_departmentid_checkintime").OnTable("calendaritemcheckins");
+ Delete.Index("ix_calendaritemcheckins_calendaritemid_checkintime").OnTable("calendaritemcheckins");
+
+ Delete.Index("ix_checkinrecords_callid_unitid_timestamp").OnTable("checkinrecords");
+ Delete.Index("ix_checkinrecords_callid_userid_timestamp").OnTable("checkinrecords");
+
+ Execute.Sql("DROP INDEX IF EXISTS uq_checkintimerconfigs_dept_target_unit;");
+ Execute.Sql("DROP INDEX IF EXISTS uq_checkintimeroverrides_dept_call_target_unit;");
+
+ Create.UniqueConstraint("uq_checkintimerconfigs_dept_target_unit")
+ .OnTable("checkintimerconfigs")
+ .Columns("departmentid", "timertargettype", "unittypeid");
+
+ Create.UniqueConstraint("uq_checkintimeroverrides_dept_call_target_unit")
+ .OnTable("checkintimeroverrides")
+ .Columns("departmentid", "calltypeid", "callpriority", "timertargettype", "unittypeid");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0059_AddActiveForStatesToCheckInTimersPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0059_AddActiveForStatesToCheckInTimersPg.cs
new file mode 100644
index 00000000..fc637821
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0059_AddActiveForStatesToCheckInTimersPg.cs
@@ -0,0 +1,23 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(59)]
+ public class M0059_AddActiveForStatesToCheckInTimersPg : Migration
+ {
+ public override void Up()
+ {
+ Alter.Table("checkintimerconfigs")
+ .AddColumn("activeforstates").AsCustom("citext").Nullable();
+
+ Alter.Table("checkintimeroverrides")
+ .AddColumn("activeforstates").AsCustom("citext").Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("activeforstates").FromTable("checkintimerconfigs");
+ Delete.Column("activeforstates").FromTable("checkintimeroverrides");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0060_FixCheckInTimerNullUniquenessPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0060_FixCheckInTimerNullUniquenessPg.cs
new file mode 100644
index 00000000..97679347
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0060_FixCheckInTimerNullUniquenessPg.cs
@@ -0,0 +1,63 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(60)]
+ public class M0060_FixCheckInTimerNullUniquenessPg : Migration
+ {
+ public override void Up()
+ {
+ // Drop existing unique indexes from M0058
+ Execute.Sql("DROP INDEX IF EXISTS uq_checkintimerconfigs_dept_target_unit;");
+ Execute.Sql("DROP INDEX IF EXISTS uq_checkintimeroverrides_dept_call_target_unit;");
+
+ // Remove duplicate rows that may exist from before the NULLS NOT DISTINCT
+ // constraint was applied (keep the most recently modified row per group)
+ Execute.Sql(@"
+ DELETE FROM checkintimerconfigs
+ WHERE checkintimerconfigid IN (
+ SELECT checkintimerconfigid FROM (
+ SELECT checkintimerconfigid,
+ ROW_NUMBER() OVER (
+ PARTITION BY departmentid, timertargettype, COALESCE(unittypeid, -1)
+ ORDER BY COALESCE(updatedon, createdon) DESC, checkintimerconfigid DESC
+ ) AS rn
+ FROM checkintimerconfigs
+ ) sub WHERE rn > 1
+ );
+ ");
+
+ Execute.Sql(@"
+ DELETE FROM checkintimeroverrides
+ WHERE checkintimeroverrideid IN (
+ SELECT checkintimeroverrideid FROM (
+ SELECT checkintimeroverrideid,
+ ROW_NUMBER() OVER (
+ PARTITION BY departmentid, COALESCE(calltypeid, -1), COALESCE(callpriority, -1), timertargettype, COALESCE(unittypeid, -1)
+ ORDER BY COALESCE(updatedon, createdon) DESC, checkintimeroverrideid DESC
+ ) AS rn
+ FROM checkintimeroverrides
+ ) sub WHERE rn > 1
+ );
+ ");
+
+ // Recreate NULLS NOT DISTINCT unique indexes
+ Execute.Sql(@"
+ CREATE UNIQUE INDEX uq_checkintimerconfigs_dept_target_unit
+ ON checkintimerconfigs (departmentid, timertargettype, unittypeid)
+ NULLS NOT DISTINCT;
+ ");
+
+ Execute.Sql(@"
+ CREATE UNIQUE INDEX uq_checkintimeroverrides_dept_call_target_unit
+ ON checkintimeroverrides (departmentid, calltypeid, callpriority, timertargettype, unittypeid)
+ NULLS NOT DISTINCT;
+ ");
+ }
+
+ public override void Down()
+ {
+ // No-op: indexes already exist from M0058 and are just being rebuilt here
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CalendarItemCheckInRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CalendarItemCheckInRepository.cs
new file mode 100644
index 00000000..3d586115
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CalendarItemCheckInRepository.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Queries.Calendar;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CalendarItemCheckInRepository : RepositoryBase, ICalendarItemCheckInRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CalendarItemCheckInRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration,
+ IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task GetCheckInByCalendarItemAndUserAsync(int calendarItemId, string userId)
+ {
+ try
+ {
+ var selectFunction = new Func>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CalendarItemId", calendarItemId);
+ dynamicParameters.Add("UserId", userId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryFirstOrDefaultAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetCheckInsByCalendarItemIdAsync(int calendarItemId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CalendarItemId", calendarItemId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetCheckInsByDepartmentAndDateRangeAsync(int departmentId, DateTime start, DateTime end)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+ dynamicParameters.Add("StartDate", start);
+ dynamicParameters.Add("EndDate", end);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetCheckInsByUserAndDateRangeAsync(string userId, int departmentId, DateTime start, DateTime end)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("UserId", userId);
+ dynamicParameters.Add("DepartmentId", departmentId);
+ dynamicParameters.Add("StartDate", start);
+ dynamicParameters.Add("EndDate", end);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CheckInRecordRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CheckInRecordRepository.cs
new file mode 100644
index 00000000..b49b1a48
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CheckInRecordRepository.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Framework;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+using Resgrid.Repositories.DataRepository.Queries.CheckIns;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CheckInRecordRepository : RepositoryBase, ICheckInRecordRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CheckInRecordRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration,
+ IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetByCallIdAsync(int callId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CallId", callId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task GetLastCheckInForUserOnCallAsync(int callId, string userId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CallId", callId);
+ dynamicParameters.Add("UserId", userId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task GetLastCheckInForUnitOnCallAsync(int callId, int unitId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CallId", callId);
+ dynamicParameters.Add("UnitId", unitId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetByDepartmentIdAndDateRangeAsync(int departmentId, DateTime start, DateTime end)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+ dynamicParameters.Add("StartDate", start);
+ dynamicParameters.Add("EndDate", end);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerConfigRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerConfigRepository.cs
new file mode 100644
index 00000000..8ba38255
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerConfigRepository.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Framework;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+using Resgrid.Repositories.DataRepository.Queries.CheckIns;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CheckInTimerConfigRepository : RepositoryBase, ICheckInTimerConfigRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CheckInTimerConfigRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration,
+ IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetByDepartmentIdAsync(int departmentId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task GetByDepartmentAndTargetAsync(int departmentId, int timerTargetType, int? unitTypeId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+ dynamicParameters.Add("TimerTargetType", timerTargetType);
+ dynamicParameters.Add("UnitTypeId", unitTypeId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return (await selectFunction(conn)).FirstOrDefault();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerOverrideRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerOverrideRepository.cs
new file mode 100644
index 00000000..bb79599e
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CheckInTimerOverrideRepository.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Dapper;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Framework;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+using Resgrid.Repositories.DataRepository.Queries.CheckIns;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CheckInTimerOverrideRepository : RepositoryBase, ICheckInTimerOverrideRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CheckInTimerOverrideRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration,
+ IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetByDepartmentIdAsync(int departmentId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetMatchingOverridesAsync(int departmentId, int? callTypeId, int? callPriority)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+ dynamicParameters.Add("CallTypeId", callTypeId);
+ dynamicParameters.Add("CallPriority", callPriority);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
index 49e071c1..e086d2ee 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
@@ -509,6 +509,28 @@ protected SqlConfiguration() { }
public string SelectUnacknowledgedRouteDeviationsByDepartmentQuery { get; set; }
#endregion Routes
+ #region CheckIns
+ public string CheckInTimerConfigsTableName { get; set; }
+ public string CheckInTimerOverridesTableName { get; set; }
+ public string CheckInRecordsTableName { get; set; }
+ public string SelectCheckInTimerConfigsByDepartmentIdQuery { get; set; }
+ public string SelectCheckInTimerConfigByDepartmentAndTargetQuery { get; set; }
+ public string SelectCheckInTimerOverridesByDepartmentIdQuery { get; set; }
+ public string SelectMatchingCheckInTimerOverridesQuery { get; set; }
+ public string SelectCheckInRecordsByCallIdQuery { get; set; }
+ public string SelectLastCheckInForUserOnCallQuery { get; set; }
+ public string SelectLastCheckInForUnitOnCallQuery { get; set; }
+ public string SelectCheckInRecordsByDepartmentIdAndDateRangeQuery { get; set; }
+ #endregion CheckIns
+
+ #region CalendarItemCheckIns
+ public string CalendarItemCheckInsTableName { get; set; }
+ public string SelectCalendarItemCheckInByItemAndUserQuery { get; set; }
+ public string SelectCalendarItemCheckInsByItemIdQuery { get; set; }
+ public string SelectCalendarItemCheckInsByDeptDateRangeQuery { get; set; }
+ public string SelectCalendarItemCheckInsByUserDateRangeQuery { get; set; }
+ #endregion CalendarItemCheckIns
+
// Identity
#region Table Names
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
index daeb4232..220ee03f 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
@@ -185,6 +185,14 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ // CheckIn Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
+ // Calendar Check-In Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
// SSO Repositories
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
index 333148f5..48a8e786 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
@@ -184,6 +184,14 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ // CheckIn Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
+ // Calendar Check-In Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
// SSO Repositories
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
index 18119d65..d92d974c 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
@@ -184,6 +184,14 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ // CheckIn Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
+ // Calendar Check-In Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
// SSO Repositories
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
index d0df4789..90b38dbb 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
@@ -184,6 +184,14 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ // CheckIn Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
+ // Calendar Check-In Repositories
+ builder.RegisterType().As().InstancePerLifetimeScope();
+
// SSO Repositories
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInByItemAndUserQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInByItemAndUserQuery.cs
new file mode 100644
index 00000000..031066a4
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInByItemAndUserQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.Calendar
+{
+ public class SelectCalendarItemCheckInByItemAndUserQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCalendarItemCheckInByItemAndUserQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCalendarItemCheckInByItemAndUserQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CalendarItemCheckInsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%CALITEMID%", "%USERID%" },
+ new string[] { "CalendarItemId", "UserId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByDeptDateRangeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByDeptDateRangeQuery.cs
new file mode 100644
index 00000000..4f6206a2
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByDeptDateRangeQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.Calendar
+{
+ public class SelectCalendarItemCheckInsByDeptDateRangeQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCalendarItemCheckInsByDeptDateRangeQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCalendarItemCheckInsByDeptDateRangeQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CalendarItemCheckInsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DID%", "%STARTDATE%", "%ENDDATE%" },
+ new string[] { "DepartmentId", "StartDate", "EndDate" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByItemIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByItemIdQuery.cs
new file mode 100644
index 00000000..d59ea7a8
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByItemIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.Calendar
+{
+ public class SelectCalendarItemCheckInsByItemIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCalendarItemCheckInsByItemIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCalendarItemCheckInsByItemIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CalendarItemCheckInsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%CALITEMID%" },
+ new string[] { "CalendarItemId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByUserDateRangeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByUserDateRangeQuery.cs
new file mode 100644
index 00000000..2b81c88b
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calendar/SelectCalendarItemCheckInsByUserDateRangeQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.Calendar
+{
+ public class SelectCalendarItemCheckInsByUserDateRangeQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCalendarItemCheckInsByUserDateRangeQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCalendarItemCheckInsByUserDateRangeQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CalendarItemCheckInsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%USERID%", "%DID%", "%STARTDATE%", "%ENDDATE%" },
+ new string[] { "UserId", "DepartmentId", "StartDate", "EndDate" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByCallIdQuery.cs
new file mode 100644
index 00000000..c394c088
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByCallIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CheckIns
+{
+ public class SelectCheckInRecordsByCallIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCheckInRecordsByCallIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCheckInRecordsByCallIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CheckInRecordsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%CALLID%" },
+ new string[] { "CallId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByDepartmentIdAndDateRangeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByDepartmentIdAndDateRangeQuery.cs
new file mode 100644
index 00000000..97007fdc
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInRecordsByDepartmentIdAndDateRangeQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CheckIns
+{
+ public class SelectCheckInRecordsByDepartmentIdAndDateRangeQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCheckInRecordsByDepartmentIdAndDateRangeQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCheckInRecordsByDepartmentIdAndDateRangeQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CheckInRecordsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DID%", "%STARTDATE%", "%ENDDATE%" },
+ new string[] { "DepartmentId", "StartDate", "EndDate" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigByDepartmentAndTargetQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigByDepartmentAndTargetQuery.cs
new file mode 100644
index 00000000..09ec1768
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigByDepartmentAndTargetQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CheckIns
+{
+ public class SelectCheckInTimerConfigByDepartmentAndTargetQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCheckInTimerConfigByDepartmentAndTargetQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCheckInTimerConfigByDepartmentAndTargetQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CheckInTimerConfigsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DID%", "%TTT%", "%UTID%" },
+ new string[] { "DepartmentId", "TimerTargetType", "UnitTypeId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigsByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigsByDepartmentIdQuery.cs
new file mode 100644
index 00000000..5ff92f43
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerConfigsByDepartmentIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CheckIns
+{
+ public class SelectCheckInTimerConfigsByDepartmentIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCheckInTimerConfigsByDepartmentIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCheckInTimerConfigsByDepartmentIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CheckInTimerConfigsTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DID%" },
+ new string[] { "DepartmentId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ return GetQuery();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerOverridesByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerOverridesByDepartmentIdQuery.cs
new file mode 100644
index 00000000..3edd54c1
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CheckIns/SelectCheckInTimerOverridesByDepartmentIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CheckIns
+{
+ public class SelectCheckInTimerOverridesByDepartmentIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCheckInTimerOverridesByDepartmentIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCheckInTimerOverridesByDepartmentIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CheckInTimerOverridesTableName,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DID%" },
+ new string[] { "DepartmentId" });
+
+ return query;
+ }
+
+ public string GetQuery