Skip to main content

trailcache_core/models/
unit.rs

1//! Domain models for unit/troop information.
2//!
3//! These types represent unit data in a clean domain format,
4//! decoupled from the API response structures.
5
6use serde::{Deserialize, Serialize};
7
8/// Key 3 leadership positions for a unit.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
11#[cfg_attr(feature = "ts", ts(export))]
12pub struct Key3Leaders {
13    pub scoutmaster: Option<Leader>,
14    pub committee_chair: Option<Leader>,
15    pub charter_org_rep: Option<Leader>,
16}
17
18/// A leader with name information.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
21#[cfg_attr(feature = "ts", ts(export))]
22pub struct Leader {
23    pub first_name: String,
24    pub last_name: String,
25}
26
27impl Leader {
28    pub fn full_name(&self) -> String {
29        format!("{} {}", self.first_name, self.last_name)
30    }
31}
32
33/// Unit registration and contact information.
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
36#[cfg_attr(feature = "ts", ts(export))]
37pub struct UnitInfo {
38    pub name: Option<String>,
39    pub website: Option<String>,
40    pub registration_url: Option<String>,
41    pub district_name: Option<String>,
42    pub council_name: Option<String>,
43    pub charter_org_name: Option<String>,
44    pub charter_expiry: Option<String>,
45    pub meeting_location: Option<MeetingLocation>,
46    pub contacts: Vec<UnitContact>,
47    /// Pre-computed charter status display text, e.g. "Expires Mar 15, 2026".
48    #[serde(default)]
49    pub charter_status_display: Option<String>,
50    /// Pre-computed flag: true if charter is expired.
51    #[serde(default)]
52    pub charter_expired: Option<bool>,
53}
54
55impl UnitInfo {
56    /// Populate computed charter fields from `charter_expiry`.
57    pub fn with_computed_fields(mut self) -> Self {
58        if let Some(ref expiry) = self.charter_expiry {
59            if let Some((status, formatted)) = crate::utils::check_expiration(expiry) {
60                self.charter_status_display = Some(status.format_expiry(&formatted));
61                self.charter_expired = Some(matches!(status, crate::utils::ExpirationStatus::Expired));
62            }
63        }
64        self
65    }
66}
67
68/// Meeting location address.
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
71#[cfg_attr(feature = "ts", ts(export))]
72pub struct MeetingLocation {
73    pub address_line1: Option<String>,
74    pub address_line2: Option<String>,
75    pub city: Option<String>,
76    pub state: Option<String>,
77    pub zip: Option<String>,
78}
79
80impl MeetingLocation {
81    /// Format the address as a single line.
82    pub fn formatted(&self) -> Option<String> {
83        let mut parts = Vec::new();
84        if let Some(ref line1) = self.address_line1 {
85            if !line1.is_empty() {
86                parts.push(line1.clone());
87            }
88        }
89        if let Some(ref city) = self.city {
90            if !city.is_empty() {
91                let city_state = match &self.state {
92                    Some(state) if !state.is_empty() => format!("{}, {}", city, state),
93                    _ => city.clone(),
94                };
95                parts.push(city_state);
96            }
97        }
98        if parts.is_empty() {
99            None
100        } else {
101            Some(parts.join(", "))
102        }
103    }
104}
105
106/// A unit contact person.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
109#[cfg_attr(feature = "ts", ts(export))]
110pub struct UnitContact {
111    pub first_name: Option<String>,
112    pub last_name: Option<String>,
113    pub email: Option<String>,
114    pub phone: Option<String>,
115}
116
117impl UnitContact {
118    pub fn full_name(&self) -> String {
119        format!(
120            "{} {}",
121            self.first_name.as_deref().unwrap_or(""),
122            self.last_name.as_deref().unwrap_or("")
123        )
124        .trim()
125        .to_string()
126    }
127}
128
129/// Organization profile information.
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
132#[cfg_attr(feature = "ts", ts(export))]
133pub struct OrgProfile {
134    pub name: Option<String>,
135    pub full_name: Option<String>,
136    pub charter_org_name: Option<String>,
137    pub charter_exp_date: Option<String>,
138    pub charter_status: Option<String>,
139}
140
141/// A commissioner assigned to the unit.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
144#[cfg_attr(feature = "ts", ts(export))]
145pub struct Commissioner {
146    pub first_name: Option<String>,
147    pub last_name: Option<String>,
148    pub position: Option<String>,
149}
150
151impl Commissioner {
152    pub fn full_name(&self) -> String {
153        format!(
154            "{} {}",
155            self.first_name.as_deref().unwrap_or(""),
156            self.last_name.as_deref().unwrap_or("")
157        )
158        .trim()
159        .to_string()
160    }
161
162    #[allow(dead_code)] // Used in tests
163    pub fn position_display(&self) -> &str {
164        self.position.as_deref().unwrap_or("Unknown")
165    }
166}