1use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Deadline {
11 pub id: TemporalId,
13
14 pub label: String,
16
17 pub due_at: DateTime<Utc>,
19
20 pub warn_at: Option<DateTime<Utc>>,
22
23 pub deadline_type: DeadlineType,
25
26 pub consequence: Consequence,
28
29 pub status: DeadlineStatus,
31
32 pub dependencies: Vec<TemporalId>,
34
35 pub created_at: DateTime<Utc>,
37
38 pub completed_at: Option<DateTime<Utc>>,
40
41 pub tags: Vec<String>,
43
44 pub context: Option<String>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum DeadlineType {
51 Hard,
53 Soft,
55 Target,
57 Recurring,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum Consequence {
64 None,
66 Minor {
68 description: String,
70 },
71 Moderate {
73 description: String,
75 },
76 Severe {
78 description: String,
80 },
81 Critical {
83 description: String,
85 },
86 Quantified {
88 description: String,
90 cost_estimate: f64,
92 cost_unit: String,
94 },
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub enum DeadlineStatus {
100 Pending,
102 Warning,
104 Overdue,
106 Completed,
108 CompletedLate,
110 Cancelled,
112}
113
114impl Deadline {
115 pub fn new(label: impl Into<String>, due_at: DateTime<Utc>) -> Self {
117 Self {
118 id: TemporalId::new(),
119 label: label.into(),
120 due_at,
121 warn_at: None,
122 deadline_type: DeadlineType::Soft,
123 consequence: Consequence::None,
124 status: DeadlineStatus::Pending,
125 dependencies: Vec::new(),
126 created_at: Utc::now(),
127 completed_at: None,
128 tags: Vec::new(),
129 context: None,
130 }
131 }
132
133 pub fn time_remaining(&self) -> Option<ChronoDuration> {
135 if self.status == DeadlineStatus::Completed
136 || self.status == DeadlineStatus::CompletedLate
137 || self.status == DeadlineStatus::Cancelled
138 {
139 return None;
140 }
141 let now = Utc::now();
142 if now < self.due_at {
143 Some(self.due_at - now)
144 } else {
145 None
146 }
147 }
148
149 pub fn overdue_by(&self) -> Option<ChronoDuration> {
151 let now = Utc::now();
152 if now > self.due_at
153 && self.status != DeadlineStatus::Completed
154 && self.status != DeadlineStatus::CompletedLate
155 && self.status != DeadlineStatus::Cancelled
156 {
157 Some(now - self.due_at)
158 } else {
159 None
160 }
161 }
162
163 pub fn update_status(&mut self) {
165 if self.status == DeadlineStatus::Completed
166 || self.status == DeadlineStatus::CompletedLate
167 || self.status == DeadlineStatus::Cancelled
168 {
169 return;
170 }
171
172 let now = Utc::now();
173
174 if now > self.due_at {
175 self.status = DeadlineStatus::Overdue;
176 } else if let Some(warn_at) = self.warn_at {
177 if now > warn_at {
178 self.status = DeadlineStatus::Warning;
179 }
180 }
181 }
182
183 pub fn complete(&mut self) {
185 let now = Utc::now();
186 self.completed_at = Some(now);
187
188 if now > self.due_at {
189 self.status = DeadlineStatus::CompletedLate;
190 } else {
191 self.status = DeadlineStatus::Completed;
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_new_deadline() {
202 let due = Utc::now() + ChronoDuration::hours(24);
203 let d = Deadline::new("Ship v1", due);
204 assert_eq!(d.status, DeadlineStatus::Pending);
205 assert!(d.time_remaining().is_some());
206 }
207
208 #[test]
209 fn test_complete_on_time() {
210 let due = Utc::now() + ChronoDuration::hours(24);
211 let mut d = Deadline::new("Test", due);
212 d.complete();
213 assert_eq!(d.status, DeadlineStatus::Completed);
214 assert!(d.completed_at.is_some());
215 }
216
217 #[test]
218 fn test_overdue_status() {
219 let due = Utc::now() - ChronoDuration::hours(1);
220 let mut d = Deadline::new("Late", due);
221 d.update_status();
222 assert_eq!(d.status, DeadlineStatus::Overdue);
223 }
224}