Skip to main content

mdvault_core/domain/behaviors/
meeting.rs

1//! Meeting note type behavior.
2//!
3//! Meetings have:
4//! - ID generated from date and counter (MTG-2025-01-15-001)
5//! - Date prompt (defaults to today)
6//! - Attendees prompt
7//! - Logging to daily note
8//! - Output path: Meetings/{id}.md
9
10use std::path::PathBuf;
11use std::sync::Arc;
12
13use chrono::Local;
14
15use crate::types::TypeDefinition;
16
17use super::super::context::{CreationContext, FieldPrompt, PromptContext, PromptType};
18use super::super::traits::{
19    DomainError, DomainResult, NoteBehavior, NoteIdentity, NoteLifecycle, NotePrompts,
20};
21
22/// Behavior implementation for meeting notes.
23pub struct MeetingBehavior {
24    typedef: Option<Arc<TypeDefinition>>,
25}
26
27impl MeetingBehavior {
28    /// Create a new MeetingBehavior, optionally wrapping a Lua typedef override.
29    pub fn new(typedef: Option<Arc<TypeDefinition>>) -> Self {
30        Self { typedef }
31    }
32}
33
34impl NoteIdentity for MeetingBehavior {
35    fn generate_id(&self, ctx: &CreationContext) -> DomainResult<Option<String>> {
36        // ID generation is handled in before_create
37        // Return existing ID if already set
38        if let Some(ref id) = ctx.core_metadata.meeting_id {
39            return Ok(Some(id.clone()));
40        }
41        Ok(None)
42    }
43
44    fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
45        // Check Lua typedef for output template first
46        if let Some(ref td) = self.typedef
47            && let Some(ref output) = td.output
48        {
49            return super::render_output_template(output, ctx);
50        }
51
52        // Default path: Meetings/{meeting-id}.md
53        let meeting_id =
54            ctx.core_metadata.meeting_id.as_ref().ok_or_else(|| {
55                DomainError::PathResolution("meeting-id not set".into())
56            })?;
57
58        Ok(ctx.config.vault_root.join(format!("Meetings/{}.md", meeting_id)))
59    }
60
61    fn core_fields(&self) -> Vec<&'static str> {
62        vec!["type", "title", "meeting-id", "date", "attendees"]
63    }
64}
65
66impl NoteLifecycle for MeetingBehavior {
67    fn before_create(&self, ctx: &mut CreationContext) -> DomainResult<()> {
68        // Get or default date to today
69        let date = ctx
70            .get_var("date")
71            .map(|s| s.to_string())
72            .unwrap_or_else(|| Local::now().format("%Y-%m-%d").to_string());
73
74        // Generate meeting ID: MTG-YYYY-MM-DD-NNN
75        let meeting_id = generate_meeting_id(&ctx.config.vault_root, &date)?;
76
77        // Set core metadata
78        ctx.core_metadata.meeting_id = Some(meeting_id.clone());
79        ctx.core_metadata.date = Some(date.clone());
80        ctx.set_var("meeting-id", &meeting_id);
81        ctx.set_var("date", &date);
82
83        Ok(())
84    }
85
86    fn after_create(&self, ctx: &CreationContext, _content: &str) -> DomainResult<()> {
87        // Log to daily note
88        if let Some(ref output_path) = ctx.output_path {
89            let meeting_id = ctx.core_metadata.meeting_id.as_deref().unwrap_or("");
90            if let Err(e) = super::super::services::DailyLogService::log_creation(
91                ctx.config,
92                "meeting",
93                &ctx.title,
94                meeting_id,
95                output_path,
96            ) {
97                // Log warning but don't fail the creation
98                tracing::warn!("Failed to log to daily note: {}", e);
99            }
100        }
101
102        Ok(())
103    }
104}
105
106impl NotePrompts for MeetingBehavior {
107    fn type_prompts(&self, ctx: &PromptContext) -> Vec<FieldPrompt> {
108        let mut prompts = vec![];
109
110        // Date prompt (if not provided)
111        if !ctx.provided_vars.contains_key("date") && !ctx.batch_mode {
112            prompts.push(FieldPrompt {
113                field_name: "date".into(),
114                prompt_text: "Meeting date".into(),
115                prompt_type: PromptType::Text,
116                required: false,
117                default_value: Some(Local::now().format("%Y-%m-%d").to_string()),
118            });
119        }
120
121        // Attendees prompt (if not provided)
122        if !ctx.provided_vars.contains_key("attendees") && !ctx.batch_mode {
123            prompts.push(FieldPrompt {
124                field_name: "attendees".into(),
125                prompt_text: "Who's attending?".into(),
126                prompt_type: PromptType::Text,
127                required: false,
128                default_value: None,
129            });
130        }
131
132        prompts
133    }
134}
135
136impl NoteBehavior for MeetingBehavior {
137    fn type_name(&self) -> &'static str {
138        "meeting"
139    }
140}
141
142// --- Helper functions ---
143
144use std::fs;
145
146/// Generate a meeting ID by scanning the Meetings directory for the given date.
147fn generate_meeting_id(vault_root: &std::path::Path, date: &str) -> DomainResult<String> {
148    let meetings_dir = vault_root.join("Meetings");
149    let prefix = format!("MTG-{}-", date);
150
151    let mut max_num = 0u32;
152
153    if meetings_dir.exists() {
154        for entry in fs::read_dir(&meetings_dir).map_err(DomainError::Io)? {
155            let entry = entry.map_err(DomainError::Io)?;
156            let name = entry.file_name();
157            let name_str = name.to_string_lossy();
158
159            // Parse MTG-YYYY-MM-DD-XXX.md pattern
160            if let Some(stem) = name_str.strip_suffix(".md")
161                && stem.starts_with(&prefix)
162                && let Some(num_str) = stem.strip_prefix(&prefix)
163                && let Ok(num) = num_str.parse::<u32>()
164            {
165                max_num = max_num.max(num);
166            }
167        }
168    }
169
170    Ok(format!("{}{:03}", prefix, max_num + 1))
171}