mdvault_core/domain/
services.rs1use std::fs;
7use std::path::Path;
8
9use chrono::Local;
10
11use crate::config::types::ResolvedConfig;
12
13pub struct DailyLogService;
15
16impl DailyLogService {
17 pub fn log_creation(
29 config: &ResolvedConfig,
30 note_type: &str,
31 title: &str,
32 note_id: &str,
33 output_path: &Path,
34 ) -> Result<(), String> {
35 let today = Local::now().format("%Y-%m-%d").to_string();
36 let time = Local::now().format("%H:%M").to_string();
37
38 let daily_path = config.vault_root.join(format!("Journal/Daily/{}.md", today));
40
41 if let Some(parent) = daily_path.parent() {
43 fs::create_dir_all(parent)
44 .map_err(|e| format!("Could not create daily directory: {e}"))?;
45 }
46
47 let mut content = match fs::read_to_string(&daily_path) {
49 Ok(c) => c,
50 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
51 let content = format!(
53 "---\ntype: daily\ndate: {}\n---\n\n# {}\n\n## Log\n",
54 today, today
55 );
56 fs::write(&daily_path, &content)
57 .map_err(|e| format!("Could not create daily note: {e}"))?;
58 content
59 }
60 Err(e) => return Err(format!("Could not read daily note: {e}")),
61 };
62
63 let rel_path =
65 output_path.strip_prefix(&config.vault_root).unwrap_or(output_path);
66 let link = rel_path.file_stem().and_then(|s| s.to_str()).unwrap_or("note");
67
68 let id_display =
70 if note_id.is_empty() { String::new() } else { format!(" {}", note_id) };
71
72 let log_entry = format!(
73 "- **{}**: Created {}{}: [[{}|{}]]\n",
74 time, note_type, id_display, link, title
75 );
76
77 if let Some(log_pos) = content.find("## Log") {
79 let after_log = &content[log_pos + 6..]; let insert_pos = if let Some(next_section) = after_log.find("\n## ") {
82 log_pos + 6 + next_section
83 } else {
84 content.len()
85 };
86
87 content.insert_str(insert_pos, &format!("\n{}", log_entry));
89 } else {
90 content.push_str(&format!("\n## Log\n{}", log_entry));
92 }
93
94 fs::write(&daily_path, &content)
96 .map_err(|e| format!("Could not write daily note: {e}"))?;
97
98 Ok(())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use std::path::PathBuf;
106 use tempfile::tempdir;
107
108 fn make_test_config(vault_root: PathBuf) -> ResolvedConfig {
109 ResolvedConfig {
110 active_profile: "test".into(),
111 vault_root: vault_root.clone(),
112 templates_dir: vault_root.join(".mdvault/templates"),
113 captures_dir: vault_root.join(".mdvault/captures"),
114 macros_dir: vault_root.join(".mdvault/macros"),
115 typedefs_dir: vault_root.join(".mdvault/typedefs"),
116 excluded_folders: vec![],
117 security: Default::default(),
118 logging: Default::default(),
119 activity: Default::default(),
120 }
121 }
122
123 #[test]
124 fn test_log_creation_creates_daily_note() {
125 let tmp = tempdir().unwrap();
126 let config = make_test_config(tmp.path().to_path_buf());
127 let output_path = tmp.path().join("Projects/TST/Tasks/TST-001.md");
128
129 fs::create_dir_all(output_path.parent().unwrap()).unwrap();
131 fs::write(&output_path, "test").unwrap();
132
133 let result = DailyLogService::log_creation(
134 &config,
135 "task",
136 "Test Task",
137 "TST-001",
138 &output_path,
139 );
140
141 assert!(result.is_ok());
142
143 let today = Local::now().format("%Y-%m-%d").to_string();
145 let daily_path = tmp.path().join(format!("Journal/Daily/{}.md", today));
146 assert!(daily_path.exists());
147
148 let content = fs::read_to_string(&daily_path).unwrap();
149 assert!(content.contains("type: daily"));
150 assert!(content.contains("## Log"));
151 assert!(content.contains("Created task TST-001"));
152 assert!(content.contains("[[TST-001|Test Task]]"));
153 }
154
155 #[test]
156 fn test_log_creation_appends_to_existing() {
157 let tmp = tempdir().unwrap();
158 let config = make_test_config(tmp.path().to_path_buf());
159
160 let today = Local::now().format("%Y-%m-%d").to_string();
162 let daily_path = tmp.path().join(format!("Journal/Daily/{}.md", today));
163 fs::create_dir_all(daily_path.parent().unwrap()).unwrap();
164 fs::write(
165 &daily_path,
166 "---\ntype: daily\n---\n\n# Today\n\n## Log\n- Existing entry\n",
167 )
168 .unwrap();
169
170 let output_path = tmp.path().join("Projects/NEW/NEW-001.md");
171 fs::create_dir_all(output_path.parent().unwrap()).unwrap();
172 fs::write(&output_path, "test").unwrap();
173
174 let result = DailyLogService::log_creation(
175 &config,
176 "project",
177 "New Project",
178 "NEW",
179 &output_path,
180 );
181
182 assert!(result.is_ok());
183
184 let content = fs::read_to_string(&daily_path).unwrap();
185 assert!(content.contains("- Existing entry"));
186 assert!(content.contains("Created project NEW"));
187 }
188}