1use crate::model::{Label, TimeLog, User};
4use crate::{TimeDeltaExt, filters};
5use chrono::{Duration, Local};
6use std::collections::{HashMap, HashSet};
7
8#[must_use]
10fn get_total_time_by_user_in_last_n_days(
11 time_logs: &[TimeLog],
12 days: Duration,
13) -> HashMap<&User, Duration> {
14 let filtered = filters::filter_by_last_n_days(time_logs, days);
15 filters::total_time_spent_by_user(filtered).collect()
16}
17
18#[must_use]
20fn get_total_time_by_user_yesterday(time_logs: &[TimeLog]) -> HashMap<&User, Duration> {
21 let yesterday = Local::now().date_naive() - Duration::days(1);
22 let filtered = filters::filter_by_date(time_logs, yesterday, yesterday);
23 filters::total_time_spent_by_user(filtered).collect()
24}
25
26#[must_use]
28fn get_todays_time_logs(time_logs: &[TimeLog]) -> Vec<&TimeLog> {
29 let today = Local::now().date_naive();
30 filters::filter_by_date(time_logs, today, today).collect()
31}
32
33#[must_use]
35fn get_duration_or_zero<'a>(map: &HashMap<&'a User, Duration>, user: &'a User) -> Duration {
36 map.get(user).copied().unwrap_or(Duration::zero())
37}
38
39#[must_use]
42pub fn populate_table_timelogs_in_timeframes_by_user(
43 time_logs: &[TimeLog],
44) -> (Vec<Vec<String>>, &[&str]) {
45 let today_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::days(1));
46 let yesterday_timelogs = get_total_time_by_user_yesterday(time_logs);
47 let this_week_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::weeks(1));
48 let this_month_timelogs = get_total_time_by_user_in_last_n_days(time_logs, Duration::days(30));
49
50 let total_today = today_timelogs.values().copied().sum::<Duration>();
52 let total_yesterday = yesterday_timelogs.values().copied().sum::<Duration>();
53 let total_this_week = this_week_timelogs.values().copied().sum::<Duration>();
54 let total_this_month = this_month_timelogs.values().copied().sum::<Duration>();
55 let total_time = filters::total_time_spent(time_logs);
56
57 let mut table_data: Vec<_> = filters::total_time_spent_by_user(time_logs)
58 .map(|(user, total_time_of_user)| {
59 vec![
60 user.name.clone(),
61 get_duration_or_zero(&today_timelogs, user).to_hm_string(),
62 get_duration_or_zero(&yesterday_timelogs, user).to_hm_string(),
63 get_duration_or_zero(&this_week_timelogs, user).to_hm_string(),
64 get_duration_or_zero(&this_month_timelogs, user).to_hm_string(),
65 total_time_of_user.to_hm_string(),
66 ]
67 })
68 .collect();
69
70 table_data.push(vec![
71 "Total".to_string(),
72 total_today.to_hm_string(),
73 total_yesterday.to_hm_string(),
74 total_this_week.to_hm_string(),
75 total_this_month.to_hm_string(),
76 total_time.to_hm_string(),
77 ]);
78
79 let table_header = &[
80 "User",
81 "Today",
82 "Yesterday",
83 "Last seven days",
84 "Last 30 days",
85 "Total time spent",
86 ];
87
88 (table_data, table_header)
89}
90
91#[must_use]
94pub fn populate_table_timelogs_by_label<'a>(
95 time_logs: &[TimeLog],
96 label_filter: Option<&HashSet<String>>,
97 label_others: Option<&Label>,
98) -> (Vec<Vec<String>>, &'a [&'a str]) {
99 let time_by_label = filters::group_by_label(time_logs, label_filter, label_others);
100
101 let table_data: Vec<_> = time_by_label
102 .map(|(label, timelogs)| {
103 let total_time = filters::total_time_spent(timelogs);
104 let label_name = match label {
105 Some(label) => label.title.clone(),
106 None => "No label".to_string(),
107 };
108 vec![label_name, total_time.to_hm_string()]
109 })
110 .collect();
111
112 let table_header = &["Label", "Time Spent"];
113
114 (table_data, table_header)
115}
116
117#[must_use]
120pub fn populate_table_timelogs_by_milestone<'a>(
121 time_logs: &[TimeLog],
122) -> (Vec<Vec<String>>, &'a [&'a str]) {
123 let time_by_milestone = filters::group_by_milestone(time_logs);
124
125 let table_data: Vec<_> = time_by_milestone
126 .map(|(milestone, timelogs)| {
127 let total_time = filters::total_time_spent(timelogs);
128 let title = milestone.map_or_else(|| "No milestone".to_string(), |m| m.title.clone());
129 vec![title, total_time.to_hm_string()]
130 })
131 .collect();
132 let table_header = &["Milestone", "Time Spent"];
133 (table_data, table_header)
134}
135
136#[must_use]
137pub fn populate_table_todays_timelogs(time_logs: &[TimeLog]) -> (Vec<Vec<String>>, &[&str]) {
138 let todays_timelogs = get_todays_time_logs(time_logs);
139
140 let mut table_data: Vec<_> = todays_timelogs
141 .iter()
142 .map(|timelog| {
143 let date_time = timelog.spent_at.to_rfc2822();
144 let user = timelog.user.to_string();
145 let time_spent = timelog.time_spent.to_hm_string();
146 let summary = timelog.summary.clone().unwrap_or_default();
147 let trackable_item = timelog.trackable_item.common.title.clone();
148 vec![date_time, user, time_spent, summary, trackable_item]
149 })
150 .collect();
151
152 table_data.sort();
153
154 let table_header = &["Date", "User", "Time Spent", "Summary", "Trackable item"];
155 (table_data, table_header)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::model::{
162 Issue, Label, Labels, MergeRequest, Milestone, TimeLog, TrackableItem, TrackableItemFields,
163 TrackableItemKind, User,
164 };
165 use chrono::{Duration, Local, Timelike};
166
167 const NUMBER_OF_LOGS: usize = 5;
168
169 fn create_test_user(name: &str) -> User {
170 User {
171 username: name.to_string(),
172 name: name.to_string(),
173 }
174 }
175
176 fn get_timelogs() -> [TimeLog; NUMBER_OF_LOGS] {
177 let now = Local::now().with_second(0).unwrap();
178 let c4_issue = TrackableItem {
179 common: TrackableItemFields {
180 title: "Create C4 model".to_string(),
181 milestone: Some(Milestone {
182 title: "End of Elaboration".to_string(),
183 ..Default::default()
184 }),
185 labels: Labels {
186 labels: vec![Label {
187 title: "Documentation".into(),
188 }],
189 },
190 ..Default::default()
191 },
192 kind: TrackableItemKind::Issue(Issue::default()),
193 };
194
195 [
196 TimeLog {
197 spent_at: now - Duration::days(2),
198 time_spent: Duration::seconds(3600),
199 summary: None,
200 user: create_test_user("user1"),
201 trackable_item: TrackableItem {
202 common: TrackableItemFields {
203 title: "Meeting notes".to_string(),
204 ..Default::default()
205 },
206 kind: TrackableItemKind::Issue(Issue::default()),
207 },
208 },
209 TimeLog {
210 spent_at: now - Duration::days(1),
211 time_spent: Duration::seconds(3600),
212 summary: Some("test".to_string()),
213 user: create_test_user("user2"),
214 trackable_item: c4_issue.clone(),
215 },
216 TimeLog {
217 spent_at: now,
218 time_spent: Duration::seconds(1800),
219 summary: Some("test".to_string()),
220 user: create_test_user("user1"),
221 trackable_item: TrackableItem {
222 common: TrackableItemFields {
223 title: "Create CI/CD pipeline".to_string(),
224 milestone: None,
225 labels: Labels {
226 labels: vec![
227 Label {
228 title: "Documentation".into(),
229 },
230 Label {
231 title: "Development".into(),
232 },
233 ],
234 },
235 ..Default::default()
236 },
237 kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
238 },
239 },
240 TimeLog {
241 spent_at: now - Duration::hours(1),
242 time_spent: Duration::seconds(5400),
243 summary: Some("Fix a big bug".to_string()),
244 user: create_test_user("user3"),
245 trackable_item: TrackableItem {
246 common: TrackableItemFields {
247 title: "Coughing in my microphone causes segfault".to_string(),
248 labels: Labels {
249 labels: vec![
250 Label {
251 title: "Bug".into(),
252 },
253 Label {
254 title: "Development".into(),
255 },
256 ],
257 },
258 ..Default::default()
259 },
260 kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
261 },
262 },
263 TimeLog {
264 spent_at: now - Duration::minutes(15),
265 time_spent: Duration::seconds(7200),
266 summary: None,
267 user: create_test_user("user1"),
268 trackable_item: c4_issue.clone(),
269 },
270 ]
271 }
272
273 fn to_name_map(m: &HashMap<&User, Duration>) -> HashMap<String, Duration> {
275 m.iter()
276 .map(|(u, d)| (u.name.clone(), *d))
277 .collect::<HashMap<_, _>>()
278 }
279
280 #[test]
281 fn test_get_duration_or_zero_existing_user() {
282 const DURATION: Duration = Duration::seconds(1200);
283 let user = create_test_user("user1");
284 let mut map = HashMap::new();
285 map.insert(&user, DURATION);
286
287 let result = get_duration_or_zero(&map, &user);
288 assert_eq!(result, DURATION);
289 }
290
291 #[test]
292 fn test_get_duration_or_zero_missing_user() {
293 const DURATION: Duration = Duration::seconds(1200);
294 let user_in_map = create_test_user("user1");
295 let user_not_in_map = create_test_user("user4");
296 let mut map = HashMap::new();
297 map.insert(&user_in_map, DURATION);
298
299 let result = get_duration_or_zero(&map, &user_not_in_map);
300 assert_eq!(result, Duration::zero());
301 }
302
303 #[test]
304 fn test_total_time_by_user_yesterday() {
305 const NUMBER_OF_USERS: usize = 1;
306 const TIME_SPENT: Duration = Duration::seconds(3600);
307
308 let time_logs = get_timelogs();
309 let result = get_total_time_by_user_yesterday(&time_logs);
310 let name_map = to_name_map(&result);
311
312 assert_eq!(name_map.len(), NUMBER_OF_USERS);
313 assert_eq!(name_map.get("user2"), Some(&TIME_SPENT));
314 }
315
316 #[test]
317 fn test_total_time_by_user_in_last_n_days_empty() {
318 const N_DAYS: Duration = Duration::days(7);
319 let time_logs = vec![];
320 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
321 assert!(result.is_empty());
322 }
323
324 #[test]
325 fn test_total_time_by_user_in_last_n_days_one_day() {
326 const NUMBER_OF_USERS: usize = 2;
327 const N_DAYS: Duration = Duration::days(1);
328 const TIME_SPENT_USER_1: Duration = Duration::seconds(9000);
329 const TIME_SPENT_USER_3: Duration = Duration::seconds(5400);
330
331 let time_logs = get_timelogs();
332 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
333 let name_map = to_name_map(&result);
334
335 assert_eq!(name_map.len(), NUMBER_OF_USERS);
336 assert_eq!(name_map.get("user1"), Some(&TIME_SPENT_USER_1));
337 assert_eq!(name_map.get("user3"), Some(&TIME_SPENT_USER_3));
338 assert!(!name_map.contains_key("user2"));
339 }
340
341 #[test]
342 fn test_total_time_by_user_in_last_n_days_seven_days() {
343 const N_DAYS: Duration = Duration::days(7);
344 const TIME_SPENT_USER_1: Duration = Duration::seconds(12600);
345 const TIME_SPENT_USER_2: Duration = Duration::seconds(3600);
346 const TIME_SPENT_USER_3: Duration = Duration::seconds(5400);
347
348 let time_logs = get_timelogs();
349 let result = get_total_time_by_user_in_last_n_days(&time_logs, N_DAYS);
350 let name_map = to_name_map(&result);
351
352 assert_eq!(name_map.get("user1"), Some(&TIME_SPENT_USER_1));
353 assert_eq!(name_map.get("user2"), Some(&TIME_SPENT_USER_2));
354 assert_eq!(name_map.get("user3"), Some(&TIME_SPENT_USER_3));
355 }
356
357 #[test]
358 fn test_create_table_timelogs_in_timeframes_by_user_header_and_rows() {
359 const NUMBER_OF_STATS: usize = 5;
360 const NUMBER_OF_COLUMNS: usize = NUMBER_OF_STATS + 1;
361
362 let time_logs = get_timelogs();
363 let (table, _header) = populate_table_timelogs_in_timeframes_by_user(&time_logs);
364
365 let name_column: HashSet<String> = table.iter().map(|row| row[0].clone()).collect();
367 assert!(name_column.contains("user1"));
368 assert!(name_column.contains("user2"));
369 assert!(name_column.contains("user3"));
370
371 for row in &table {
373 assert_eq!(row.len(), NUMBER_OF_COLUMNS);
374 }
375 }
376
377 #[test]
378 fn test_create_table_timelogs_by_label() {
379 let time_logs = get_timelogs();
380 let (table, _header) = populate_table_timelogs_by_label(&time_logs, None, None);
381
382 let label_time_spent_map: HashMap<String, String> = table
383 .into_iter()
384 .map(|row| (row[0].clone(), row[1].clone()))
385 .collect();
386
387 let expected_map = std::collections::HashMap::from([
388 ("Documentation".to_string(), "03h 30m".to_string()),
389 ("Development".to_string(), "02h 00m".to_string()),
390 ("Bug".to_string(), "01h 30m".to_string()),
391 ("No label".to_string(), "01h 00m".to_string()),
392 ]);
393
394 assert_eq!(label_time_spent_map, expected_map);
395 }
396
397 #[test]
398 fn test_create_table_timelogs_by_label_with_others() {
399 let time_logs = get_timelogs();
400 let label_documentation = Label {
401 title: "Documentation".into(),
402 };
403 let label_others = Label {
404 title: "Others".into(),
405 };
406
407 let selected = HashSet::from([label_documentation.title.clone()]);
408 let (table, _) =
409 populate_table_timelogs_by_label(&time_logs, Some(&selected), Some(&label_others));
410
411 let label_time_spent_map: HashMap<String, String> = table
412 .into_iter()
413 .map(|row| (row[0].clone(), row[1].clone()))
414 .collect();
415
416 let expected_map = HashMap::from([
417 ("Documentation".to_string(), "03h 30m".to_string()),
418 ("Others".to_string(), "02h 30m".to_string()),
419 ]);
420 assert_eq!(label_time_spent_map, expected_map);
421 }
422
423 #[test]
424 fn test_populate_table_todays_timelogs() {
425 let time_logs = get_timelogs();
426 let (table, _) = populate_table_todays_timelogs(&time_logs);
427
428 assert!(table.is_sorted());
429
430 let now = Local::now().with_second(0).unwrap();
431 let log3 = now.to_rfc2822();
432 let log2 = (now - Duration::minutes(15)).to_rfc2822();
433 let log1 = (now - Duration::hours(1)).to_rfc2822();
434
435 let expected_table = [
436 [
437 &log1,
438 "user3",
439 "01h 30m",
440 "Fix a big bug",
441 "Coughing in my microphone causes segfault",
442 ],
443 [&log2, "user1", "02h 00m", "", "Create C4 model"],
444 [&log3, "user1", "00h 30m", "test", "Create CI/CD pipeline"],
445 ];
446 assert_eq!(table, expected_table);
447 }
448}