Skip to main content

oxios_markdown/
stats.rs

1//! Stats: today's completion report.
2//!
3//! Ported from files.md (`server/stats/stats.go`) by Artem Zakirullin.
4
5use crate::fs::{display_name, is_checklist_item, VirtualFs};
6use crate::types::{FileEntry, FsError, DIR_ARCHIVE};
7
8/// A completed item shown in today's report.
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct CompletedItem {
11    /// The display name (capitalized, no extension).
12    pub display_name: String,
13    /// Whether this is a checklist item.
14    pub is_checklist: bool,
15}
16
17/// Today's completion report.
18#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
19pub struct TodayReport {
20    /// Items completed today.
21    pub completed_items: Vec<CompletedItem>,
22    /// Total number of archived files (all time).
23    pub total_done: usize,
24}
25
26/// Get list of files done today (ctime > midnight UTC).
27pub fn done_today(fs: &VirtualFs) -> Result<Vec<FileEntry>, FsError> {
28    let all = fs.files_and_dirs(DIR_ARCHIVE)?;
29    let midnight = beginning_of_day_utc();
30    Ok(all
31        .into_iter()
32        .filter(|f| !f.is_dir && f.ctime > midnight)
33        .collect())
34}
35
36/// Get today's completion report: files done today + total archived count.
37pub fn today_report(fs: &VirtualFs) -> Result<TodayReport, FsError> {
38    let today_files = done_today(fs)?;
39    let all_archived = fs.files_and_dirs(DIR_ARCHIVE)?;
40    let total_done = all_archived.iter().filter(|f| !f.is_dir).count();
41
42    let completed_items: Vec<CompletedItem> = today_files
43        .iter()
44        .map(|f| {
45            let is_checklist = is_checklist_item(&f.name);
46            CompletedItem {
47                display_name: display_name(&f.name),
48                is_checklist,
49            }
50        })
51        .collect();
52
53    Ok(TodayReport {
54        completed_items,
55        total_done,
56    })
57}
58
59/// Format today's report as a string (matching Go output format).
60pub fn format_today_report(report: &TodayReport) -> String {
61    let mut lines: Vec<String> = Vec::new();
62    for item in &report.completed_items {
63        let emoji = if item.is_checklist { "ā˜‘ļø" } else { "āœ…" };
64        lines.push(format!("{} <b>{}</b>", emoji, item.display_name));
65    }
66    lines.push(format!("\nšŸ“Š {} tasks done in total", report.total_done));
67    lines.join("\n")
68}
69
70/// Returns the Unix timestamp (milliseconds) of midnight UTC today.
71fn beginning_of_day_utc() -> i64 {
72    use chrono::Utc;
73    let now = Utc::now();
74    let midnight = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
75    let dt = midnight.and_utc();
76    dt.timestamp_millis()
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use tempfile::TempDir;
83
84    fn setup() -> (VirtualFs, TempDir) {
85        let dir = TempDir::new().unwrap();
86        let fs = VirtualFs::new(dir.path().to_path_buf()).unwrap();
87        (fs, dir)
88    }
89
90    #[test]
91    fn test_done_today_empty() {
92        let (fs, _t) = setup();
93        let result = done_today(&fs).unwrap();
94        assert!(result.is_empty());
95    }
96
97    #[test]
98    fn test_today_report_empty() {
99        let (fs, _t) = setup();
100        let report = today_report(&fs).unwrap();
101        assert!(report.completed_items.is_empty());
102        assert_eq!(report.total_done, 0);
103    }
104
105    #[test]
106    fn test_today_report_with_files() {
107        let (fs, _t) = setup();
108        fs.write(DIR_ARCHIVE, "MyTask.md", "content").unwrap();
109        let report = today_report(&fs).unwrap();
110        assert_eq!(report.completed_items.len(), 1);
111        assert_eq!(report.total_done, 1);
112        assert_eq!(report.completed_items[0].display_name, "MyTask");
113        assert!(!report.completed_items[0].is_checklist);
114    }
115
116    #[test]
117    fn test_format_today_report() {
118        let report = TodayReport {
119            completed_items: vec![CompletedItem {
120                display_name: "Rust".into(),
121                is_checklist: false,
122            }],
123            total_done: 5,
124        };
125        let formatted = format_today_report(&report);
126        assert!(formatted.contains("āœ…"));
127        assert!(formatted.contains("<b>Rust</b>"));
128        assert!(formatted.contains("šŸ“Š 5 tasks done in total"));
129    }
130}