1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
4pub enum Category {
5 #[default]
6 General,
7 Work,
8 Home,
9 Hobby,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Session {
14 pub start_min: u16,
15 #[serde(default)]
16 pub end_min: Option<u16>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum TaskState {
21 Planned,
22 Active,
23 Paused,
24 Done,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Task {
29 pub title: String,
30 pub estimate_min: u16,
31 pub actual_min: u16,
32 #[serde(default)]
34 pub actual_carry_sec: u16,
35 #[serde(default)]
38 pub started_at_min: Option<u16>,
39 #[serde(default)]
42 pub finished_at_min: Option<u16>,
43 #[serde(default)]
46 pub sessions: Vec<Session>,
47 pub state: TaskState,
48 #[serde(default)]
51 pub planned_ymd: u32,
52 #[serde(default)]
53 pub done_ymd: Option<u32>,
54 #[serde(default)]
56 pub category: Category,
57 #[serde(default)]
61 pub fixed_start_min: Option<u16>,
62}
63
64impl Task {
65 pub fn new(title: &str, estimate_min: u16) -> Self {
66 Self {
67 title: title.to_string(),
68 estimate_min,
69 actual_min: 0,
70 actual_carry_sec: 0,
71 started_at_min: None,
72 finished_at_min: None,
73 sessions: Vec::new(),
74 state: TaskState::Planned,
75 planned_ymd: crate::date::today_ymd(),
76 done_ymd: None,
77 category: Category::General,
78 fixed_start_min: None,
79 }
80 }
81}
82
83impl Task {
84 pub fn start_session(&mut self, now_min: u16) {
85 let need_new = self.sessions.last().map_or(true, |s| s.end_min.is_some());
87 if need_new {
88 self.sessions.push(Session { start_min: now_min, end_min: None });
89 }
90 }
91 pub fn end_session(&mut self, now_min: u16) {
92 if let Some(last) = self.sessions.last_mut() {
93 if last.end_min.is_none() {
94 last.end_min = Some(now_min);
95 }
96 }
97 }
98}
99
100#[derive(Debug, Default)]
101pub struct DayPlan {
102 pub tasks: Vec<Task>,
103 active: Option<usize>,
104}
105
106impl DayPlan {
107 pub fn new(tasks: Vec<Task>) -> Self {
108 let active = tasks.iter().position(|t| matches!(t.state, TaskState::Active));
109 Self { tasks, active }
110 }
111
112 pub fn active_index(&self) -> Option<usize> {
113 self.active
114 }
115
116 pub fn add_task(&mut self, task: Task) -> usize {
117 self.tasks.push(task);
118 self.tasks.len() - 1
119 }
120
121 pub fn start(&mut self, index: usize) {
123 if let Some(cur) = self.active {
124 if cur != index {
125 if let Some(t) = self.tasks.get_mut(cur) {
126 t.state = TaskState::Paused;
127 }
128 } else {
129 return;
131 }
132 }
133 if let Some(t) = self.tasks.get_mut(index) {
134 t.state = TaskState::Active;
135 self.active = Some(index);
136 }
137 }
138
139 pub fn pause_active(&mut self) {
140 if let Some(cur) = self.active.take() {
141 if let Some(t) = self.tasks.get_mut(cur) {
142 t.state = TaskState::Paused;
143 }
144 }
145 }
146
147 pub fn finish_at(&mut self, index: usize, today_ymd: u32) {
150 if index >= self.tasks.len() {
151 return;
152 }
153 if self.active == Some(index) {
154 self.active = None;
155 if let Some(t) = self.tasks.get_mut(index) {
156 t.state = TaskState::Done;
157 t.done_ymd = Some(today_ymd);
158 }
159 } else if let Some(t) = self.tasks.get_mut(index) {
160 t.state = TaskState::Done;
161 t.done_ymd = Some(today_ymd);
162 }
163 }
164
165 pub fn add_actual_to_active(&mut self, minutes: u16) {
166 if let Some(cur) = self.active {
167 if let Some(t) = self.tasks.get_mut(cur) {
168 t.actual_min = t.actual_min.saturating_add(minutes);
169 }
170 }
171 }
172
173 pub fn remaining_total_min(&self) -> u16 {
174 self.tasks
175 .iter()
176 .map(|t| match t.state {
177 TaskState::Done => 0,
178 _ => t.estimate_min.saturating_sub(t.actual_min),
179 })
180 .sum()
181 }
182
183 pub fn esd(&self, now_min: u16) -> u16 {
184 let est_sum: u16 = self
186 .tasks
187 .iter()
188 .map(|t| match t.state {
189 TaskState::Done => 0,
190 _ => t.estimate_min,
191 })
192 .sum();
193 let base = match self.latest_actual_finish_min() {
195 Some(last) => last.max(now_min),
196 None => now_min,
197 };
198 esd_from(base, &[est_sum])
199 }
200
201 fn latest_actual_finish_min(&self) -> Option<u16> {
203 let mut max_end: Option<u16> = None;
204 for t in &self.tasks {
205 if let Some(f) = t.finished_at_min {
206 max_end = Some(max_end.map_or(f, |m| m.max(f)));
207 }
208 if let Some(e) = t.sessions.iter().filter_map(|s| s.end_min).max() {
209 max_end = Some(max_end.map_or(e, |m| m.max(e)));
210 }
211 }
212 max_end
213 }
214
215 pub fn reorder_down(&mut self, index: usize) -> usize {
216 if index + 1 >= self.tasks.len() {
217 return index;
218 }
219 self.tasks.swap(index, index + 1);
220 if let Some(a) = self.active.as_mut() {
221 if *a == index {
222 *a = index + 1;
223 } else if *a == index + 1 {
224 *a = index;
225 }
226 }
227 index + 1
228 }
229
230 pub fn reorder_up(&mut self, index: usize) -> usize {
231 if index == 0 || index >= self.tasks.len() {
232 return index;
233 }
234 self.tasks.swap(index - 1, index);
235 if let Some(a) = self.active.as_mut() {
236 if *a == index {
237 *a = index - 1;
238 } else if *a == index - 1 {
239 *a = index;
240 }
241 }
242 index - 1
243 }
244
245 pub fn move_index(&mut self, from: usize, to_slot: usize) -> usize {
248 let len = self.tasks.len();
249 if len == 0 || from >= len {
250 return from;
251 }
252 let slot = to_slot.min(len);
254 let dest_final = if from < slot { slot - 1 } else { slot };
256 if dest_final == from {
257 return from;
258 }
259 let item = self.tasks.remove(from);
260 self.tasks.insert(dest_final, item);
261 if let Some(a) = self.active.as_mut() {
263 if *a == from {
264 *a = dest_final;
265 } else if from < dest_final {
266 if *a > from && *a <= dest_final {
268 *a = a.saturating_sub(1);
269 }
270 } else {
271 if *a >= dest_final && *a < from {
273 *a = a.saturating_add(1);
274 }
275 }
276 }
277 dest_final
278 }
279
280 pub fn adjust_estimate(&mut self, index: usize, delta_min: i16) {
281 if let Some(t) = self.tasks.get_mut(index) {
282 let cur = t.estimate_min as i16 + delta_min;
283 t.estimate_min = cur.max(0) as u16;
284 }
285 }
286
287 pub fn remove(&mut self, index: usize) -> Option<Task> {
288 if index >= self.tasks.len() {
289 return None;
290 }
291 if let Some(a) = self.active {
293 if a == index {
294 self.active = None;
295 } else if a > index {
296 self.active = Some(a - 1);
297 }
298 }
299 Some(self.tasks.remove(index))
300 }
301}
302
303pub fn esd_from(now_min: u16, remaining_mins: &[u16]) -> u16 {
305 let sum: u16 = remaining_mins.iter().copied().sum();
306 now_min.saturating_add(sum)
307}
308
309pub fn tc_log_line(task: &Task) -> String {
310 let state = match task.state {
311 TaskState::Planned => "Planned",
312 TaskState::Active => "Active",
313 TaskState::Paused => "Paused",
314 TaskState::Done => "Done",
315 };
316 format!(
317 "tc-log | {} | act:{}m | est:{}m | state:{}",
318 task.title, task.actual_min, task.estimate_min, state
319 )
320}