1use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Schedule {
11 pub id: TemporalId,
13
14 pub label: String,
16
17 pub start_at: DateTime<Utc>,
19
20 pub duration_secs: i64,
22
23 pub recurrence: Option<Recurrence>,
25
26 pub priority: Priority,
28
29 pub flexible: bool,
31
32 pub buffer_before_secs: Option<i64>,
34
35 pub buffer_after_secs: Option<i64>,
37
38 pub dependencies: Vec<TemporalId>,
40
41 pub blocked_by: Vec<TemporalId>,
43
44 pub status: ScheduleStatus,
46
47 pub created_at: DateTime<Utc>,
49
50 pub tags: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Recurrence {
57 pub pattern: RecurrencePattern,
59
60 pub ends: RecurrenceEnd,
62
63 pub exceptions: Vec<DateTime<Utc>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum RecurrencePattern {
70 Daily {
72 interval: u32,
74 },
75
76 Weekly {
78 interval: u32,
80 days: Vec<u32>,
82 },
83
84 Monthly {
86 interval: u32,
88 day_of_month: u32,
90 },
91
92 Cron {
94 expression: String,
96 },
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum RecurrenceEnd {
102 Never,
104 After {
106 count: u32,
108 },
109 On {
111 date: DateTime<Utc>,
113 },
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum Priority {
119 Critical,
121 High,
123 Medium,
125 Low,
127 Optional,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133pub enum ScheduleStatus {
134 Scheduled,
136 InProgress,
138 Completed,
140 Cancelled,
142 Rescheduled,
144}
145
146impl Schedule {
147 pub fn new(label: impl Into<String>, start_at: DateTime<Utc>, duration_secs: i64) -> Self {
149 Self {
150 id: TemporalId::new(),
151 label: label.into(),
152 start_at,
153 duration_secs,
154 recurrence: None,
155 priority: Priority::Medium,
156 flexible: true,
157 buffer_before_secs: None,
158 buffer_after_secs: None,
159 dependencies: Vec::new(),
160 blocked_by: Vec::new(),
161 status: ScheduleStatus::Scheduled,
162 created_at: Utc::now(),
163 tags: Vec::new(),
164 }
165 }
166
167 pub fn end_at(&self) -> DateTime<Utc> {
169 self.start_at + ChronoDuration::seconds(self.duration_secs)
170 }
171
172 pub fn conflicts_with(&self, other: &Schedule) -> bool {
174 let self_end = self.end_at();
175 let other_end = other.end_at();
176 self.start_at < other_end && self_end > other.start_at
177 }
178
179 pub fn next_occurrence(&self, after: DateTime<Utc>) -> Option<DateTime<Utc>> {
181 self.recurrence.as_ref().map(|r| match &r.pattern {
182 RecurrencePattern::Daily { interval } => {
183 let days_since = (after - self.start_at).num_days();
184 let next_interval = ((days_since / *interval as i64) + 1) * *interval as i64;
185 self.start_at + ChronoDuration::days(next_interval)
186 }
187 RecurrencePattern::Weekly { interval, .. } => {
188 let weeks_since = (after - self.start_at).num_weeks();
189 let next_interval = ((weeks_since / *interval as i64) + 1) * *interval as i64;
190 self.start_at + ChronoDuration::weeks(next_interval)
191 }
192 RecurrencePattern::Monthly { interval, .. } => {
193 let months_since = (after - self.start_at).num_days() / 30;
195 let next_interval = ((months_since / *interval as i64) + 1) * *interval as i64;
196 self.start_at + ChronoDuration::days(next_interval * 30)
197 }
198 RecurrencePattern::Cron { .. } => {
199 after + ChronoDuration::days(1)
201 }
202 })
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_end_at() {
212 let start = Utc::now();
213 let s = Schedule::new("Standup", start, 1800); assert_eq!(s.end_at(), start + ChronoDuration::seconds(1800));
215 }
216
217 #[test]
218 fn test_conflicts() {
219 let now = Utc::now();
220 let a = Schedule::new("A", now, 3600);
221 let b = Schedule::new("B", now + ChronoDuration::seconds(1800), 3600);
222 assert!(a.conflicts_with(&b));
223 }
224
225 #[test]
226 fn test_no_conflict() {
227 let now = Utc::now();
228 let a = Schedule::new("A", now, 3600);
229 let b = Schedule::new("B", now + ChronoDuration::seconds(7200), 3600);
230 assert!(!a.conflicts_with(&b));
231 }
232}