use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{Coordinates3D, SurvivorId, TriageStatus};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertId(Uuid);
impl AlertId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for AlertId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for AlertId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Priority {
Critical = 1,
High = 2,
Medium = 3,
Low = 4,
}
impl Priority {
pub fn from_triage(status: &TriageStatus) -> Self {
match status {
TriageStatus::Immediate => Priority::Critical,
TriageStatus::Delayed => Priority::High,
TriageStatus::Minor => Priority::Medium,
TriageStatus::Deceased => Priority::Low,
TriageStatus::Unknown => Priority::Medium,
}
}
pub fn value(&self) -> u8 {
*self as u8
}
pub fn color(&self) -> &'static str {
match self {
Priority::Critical => "red",
Priority::High => "orange",
Priority::Medium => "yellow",
Priority::Low => "blue",
}
}
pub fn audio_pattern(&self) -> &'static str {
match self {
Priority::Critical => "rapid_beep",
Priority::High => "double_beep",
Priority::Medium => "single_beep",
Priority::Low => "soft_tone",
}
}
}
impl std::fmt::Display for Priority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Priority::Critical => write!(f, "CRITICAL"),
Priority::High => write!(f, "HIGH"),
Priority::Medium => write!(f, "MEDIUM"),
Priority::Low => write!(f, "LOW"),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertPayload {
pub title: String,
pub message: String,
pub triage_status: TriageStatus,
pub location: Option<Coordinates3D>,
pub recommended_action: String,
pub deadline: Option<DateTime<Utc>>,
pub metadata: std::collections::HashMap<String, String>,
}
impl AlertPayload {
pub fn new(
title: impl Into<String>,
message: impl Into<String>,
triage_status: TriageStatus,
) -> Self {
Self {
title: title.into(),
message: message.into(),
triage_status,
location: None,
recommended_action: String::new(),
deadline: None,
metadata: std::collections::HashMap::new(),
}
}
pub fn with_location(mut self, location: Coordinates3D) -> Self {
self.location = Some(location);
self
}
pub fn with_action(mut self, action: impl Into<String>) -> Self {
self.recommended_action = action.into();
self
}
pub fn with_deadline(mut self, deadline: DateTime<Utc>) -> Self {
self.deadline = Some(deadline);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlertStatus {
Pending,
Acknowledged,
InProgress,
Resolved,
Cancelled,
Expired,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertResolution {
pub resolution_type: ResolutionType,
pub notes: String,
pub resolved_by: Option<String>,
pub resolved_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResolutionType {
Rescued,
FalsePositive,
Deceased,
Superseded,
TimedOut,
Other,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Alert {
id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
payload: AlertPayload,
status: AlertStatus,
created_at: DateTime<Utc>,
acknowledged_at: Option<DateTime<Utc>>,
acknowledged_by: Option<String>,
resolution: Option<AlertResolution>,
escalation_count: u32,
}
impl Alert {
pub fn new(survivor_id: SurvivorId, priority: Priority, payload: AlertPayload) -> Self {
Self {
id: AlertId::new(),
survivor_id,
priority,
payload,
status: AlertStatus::Pending,
created_at: Utc::now(),
acknowledged_at: None,
acknowledged_by: None,
resolution: None,
escalation_count: 0,
}
}
pub fn id(&self) -> &AlertId {
&self.id
}
pub fn survivor_id(&self) -> &SurvivorId {
&self.survivor_id
}
pub fn priority(&self) -> Priority {
self.priority
}
pub fn payload(&self) -> &AlertPayload {
&self.payload
}
pub fn status(&self) -> &AlertStatus {
&self.status
}
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
pub fn acknowledged_at(&self) -> Option<&DateTime<Utc>> {
self.acknowledged_at.as_ref()
}
pub fn acknowledged_by(&self) -> Option<&str> {
self.acknowledged_by.as_deref()
}
pub fn resolution(&self) -> Option<&AlertResolution> {
self.resolution.as_ref()
}
pub fn escalation_count(&self) -> u32 {
self.escalation_count
}
pub fn acknowledge(&mut self, by: impl Into<String>) {
self.status = AlertStatus::Acknowledged;
self.acknowledged_at = Some(Utc::now());
self.acknowledged_by = Some(by.into());
}
pub fn start_work(&mut self) {
if self.status == AlertStatus::Acknowledged {
self.status = AlertStatus::InProgress;
}
}
pub fn resolve(&mut self, resolution: AlertResolution) {
self.status = AlertStatus::Resolved;
self.resolution = Some(resolution);
}
pub fn cancel(&mut self, reason: &str) {
self.status = AlertStatus::Cancelled;
self.resolution = Some(AlertResolution {
resolution_type: ResolutionType::Other,
notes: reason.to_string(),
resolved_by: None,
resolved_at: Utc::now(),
});
}
pub fn escalate(&mut self) {
self.escalation_count += 1;
if self.priority != Priority::Critical {
self.priority = match self.priority {
Priority::Low => Priority::Medium,
Priority::Medium => Priority::High,
Priority::High => Priority::Critical,
Priority::Critical => Priority::Critical,
};
}
}
pub fn is_pending(&self) -> bool {
self.status == AlertStatus::Pending
}
pub fn is_active(&self) -> bool {
matches!(
self.status,
AlertStatus::Pending | AlertStatus::Acknowledged | AlertStatus::InProgress
)
}
pub fn age(&self) -> chrono::Duration {
Utc::now() - self.created_at
}
pub fn time_since_ack(&self) -> Option<chrono::Duration> {
self.acknowledged_at.map(|t| Utc::now() - t)
}
pub fn needs_escalation(&self, max_pending_seconds: i64) -> bool {
if !self.is_pending() {
return false;
}
self.age().num_seconds() > max_pending_seconds
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_payload() -> AlertPayload {
AlertPayload::new(
"Survivor Detected",
"Vital signs detected in Zone A",
TriageStatus::Immediate,
)
}
#[test]
fn test_alert_creation() {
let survivor_id = SurvivorId::new();
let alert = Alert::new(
survivor_id.clone(),
Priority::Critical,
create_test_payload(),
);
assert_eq!(alert.survivor_id(), &survivor_id);
assert_eq!(alert.priority(), Priority::Critical);
assert!(alert.is_pending());
assert!(alert.is_active());
}
#[test]
fn test_alert_lifecycle() {
let mut alert = Alert::new(SurvivorId::new(), Priority::High, create_test_payload());
assert!(alert.is_pending());
alert.acknowledge("Team Alpha");
assert_eq!(alert.status(), &AlertStatus::Acknowledged);
assert_eq!(alert.acknowledged_by(), Some("Team Alpha"));
alert.start_work();
assert_eq!(alert.status(), &AlertStatus::InProgress);
alert.resolve(AlertResolution {
resolution_type: ResolutionType::Rescued,
notes: "Survivor extracted successfully".to_string(),
resolved_by: Some("Team Alpha".to_string()),
resolved_at: Utc::now(),
});
assert_eq!(alert.status(), &AlertStatus::Resolved);
assert!(!alert.is_active());
}
#[test]
fn test_alert_escalation() {
let mut alert = Alert::new(SurvivorId::new(), Priority::Low, create_test_payload());
alert.escalate();
assert_eq!(alert.priority(), Priority::Medium);
assert_eq!(alert.escalation_count(), 1);
alert.escalate();
assert_eq!(alert.priority(), Priority::High);
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
}
#[test]
fn test_priority_from_triage() {
assert_eq!(
Priority::from_triage(&TriageStatus::Immediate),
Priority::Critical
);
assert_eq!(
Priority::from_triage(&TriageStatus::Delayed),
Priority::High
);
assert_eq!(
Priority::from_triage(&TriageStatus::Minor),
Priority::Medium
);
}
}