Skip to main content

kasl/db/
templates.rs

1//! Reusable task templates: named blueprints tasks are created from.
2//!
3//! ```rust,no_run
4//! # fn main() -> anyhow::Result<()> {
5//! use kasl::db::templates::{Templates, TaskTemplate};
6//!
7//! let mut templates = Templates::new()?;
8//! let template = TaskTemplate::new(
9//!     "daily-standup".to_string(),
10//!     "Prepare for daily standup".to_string(),
11//!     "Review yesterday's work and plan today".to_string(),
12//!     50
13//! );
14//! templates.create(&template)?;
15//! # Ok(())
16//! # }
17//! ```
18
19use crate::db::db::Db;
20use crate::libs::messages::Message;
21use crate::msg_error_anyhow;
22use anyhow::Result;
23use rusqlite::{Connection, params};
24use serde::{Deserialize, Serialize};
25
26const SCHEMA_TEMPLATES: &str = "CREATE TABLE IF NOT EXISTS task_templates (
27    id INTEGER PRIMARY KEY,
28    name TEXT NOT NULL UNIQUE,
29    task_name TEXT NOT NULL,
30    comment TEXT,
31    completeness INTEGER DEFAULT 100,
32    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
33)";
34
35const INSERT_TEMPLATE: &str = "INSERT INTO task_templates (name, task_name, comment, completeness) VALUES (?1, ?2, ?3, ?4)";
36const UPDATE_TEMPLATE: &str = "UPDATE task_templates SET task_name = ?2, comment = ?3, completeness = ?4 WHERE name = ?1";
37const DELETE_TEMPLATE: &str = "DELETE FROM task_templates WHERE name = ?1";
38const SELECT_ALL_TEMPLATES: &str = "SELECT * FROM task_templates ORDER BY name";
39const SELECT_TEMPLATE_BY_NAME: &str = "SELECT * FROM task_templates WHERE name = ?1";
40const SEARCH_TEMPLATES: &str = "SELECT * FROM task_templates WHERE name LIKE ?1 OR task_name LIKE ?1 ORDER BY name";
41
42/// A task blueprint. `name` identifies the template ("code-review");
43/// `task_name` becomes the created task's title ("Review PR #123").
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TaskTemplate {
46    /// Database primary key; `None` until saved.
47    pub id: Option<i32>,
48
49    /// Unique template name users type to select it.
50    pub name: String,
51
52    /// Title given to tasks created from this template.
53    pub task_name: String,
54
55    /// Default notes for created tasks.
56    pub comment: String,
57
58    /// Default completeness for created tasks.
59    pub completeness: i32,
60
61    /// Set by the database on insert.
62    pub created_at: Option<String>,
63}
64
65impl TaskTemplate {
66    /// Builds an unsaved template; id and timestamp are assigned on `create`.
67    ///
68    /// ```rust
69    /// use kasl::db::templates::TaskTemplate;
70    ///
71    /// let template = TaskTemplate::new(
72    ///     "morning-routine".to_string(),
73    ///     "Complete morning routine".to_string(),
74    ///     "Check emails, review calendar, plan day".to_string(),
75    ///     25 // Start at 25% since some prep is already done
76    /// );
77    /// # let _ = template;
78    /// ```
79    pub fn new(name: String, task_name: String, comment: String, completeness: i32) -> Self {
80        Self {
81            id: None,
82            name,
83            task_name,
84            comment,
85            completeness,
86            created_at: None,
87        }
88    }
89}
90
91/// Template table access.
92pub struct Templates {
93    conn: Connection,
94}
95
96impl Templates {
97    /// Opens the database and ensures the templates table exists
98    /// (migration v2 creates it officially).
99    ///
100    /// ```rust,no_run
101    /// # fn main() -> anyhow::Result<()> {
102    /// use kasl::db::templates::Templates;
103    ///
104    /// let mut templates = Templates::new()?;
105    /// # Ok(())
106    /// # }
107    /// ```
108    pub fn new() -> Result<Self> {
109        let db = Db::new()?;
110        db.conn.execute(SCHEMA_TEMPLATES, [])?;
111        Ok(Templates { conn: db.conn })
112    }
113
114    /// Inserts the template; the unique name constraint rejects duplicates.
115    ///
116    /// ```rust,no_run
117    /// # fn main() -> anyhow::Result<()> {
118    /// use kasl::db::templates::{Templates, TaskTemplate};
119    ///
120    /// let mut templates = Templates::new()?;
121    /// let template = TaskTemplate::new(
122    ///     "weekly-review".to_string(),
123    ///     "Weekly review and planning".to_string(),
124    ///     "Review accomplishments and plan next week".to_string(),
125    ///     0
126    /// );
127    /// templates.create(&template)?;
128    /// # Ok(())
129    /// # }
130    /// ```
131    pub fn create(&mut self, template: &TaskTemplate) -> Result<()> {
132        let affected = self.conn.execute(
133            INSERT_TEMPLATE,
134            params![template.name, template.task_name, template.comment, template.completeness],
135        )?;
136
137        if affected == 0 {
138            return Err(msg_error_anyhow!(Message::TemplateCreateFailed));
139        }
140
141        Ok(())
142    }
143
144    /// Updates the template's content by name; errors if it does not exist.
145    pub fn update(&mut self, template: &TaskTemplate) -> Result<()> {
146        let affected = self.conn.execute(
147            UPDATE_TEMPLATE,
148            params![template.name, template.task_name, template.comment, template.completeness],
149        )?;
150
151        if affected == 0 {
152            return Err(msg_error_anyhow!(Message::TemplateNotFound(template.name.clone())));
153        }
154
155        Ok(())
156    }
157
158    /// Deletes the template by name; tasks created from it are unaffected.
159    ///
160    /// ```rust,no_run
161    /// # use kasl::db::templates::Templates;
162    /// # fn main() -> anyhow::Result<()> {
163    /// let mut templates = Templates::new()?;
164    /// templates.delete("obsolete-template")?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    pub fn delete(&mut self, name: &str) -> Result<()> {
169        let affected = self.conn.execute(DELETE_TEMPLATE, params![name])?;
170
171        if affected == 0 {
172            return Err(msg_error_anyhow!(Message::TemplateNotFound(name.to_string())));
173        }
174
175        Ok(())
176    }
177
178    /// Returns every template, sorted by name.
179    ///
180    /// ```rust,no_run
181    /// # use kasl::db::templates::Templates;
182    /// # fn main() -> anyhow::Result<()> {
183    /// let mut templates = Templates::new()?;
184    /// let all_templates = templates.get_all()?;
185    /// for template in all_templates {
186    ///     println!("Template: {} -> {}", template.name, template.task_name);
187    /// }
188    /// # Ok(())
189    /// # }
190    /// ```
191    pub fn get_all(&mut self) -> Result<Vec<TaskTemplate>> {
192        let mut stmt = self.conn.prepare(SELECT_ALL_TEMPLATES)?;
193        let template_iter = stmt.query_map([], |row| {
194            Ok(TaskTemplate {
195                id: row.get(0)?,
196                name: row.get(1)?,
197                task_name: row.get(2)?,
198                comment: row.get(3)?,
199                completeness: row.get(4)?,
200                created_at: row.get(5)?,
201            })
202        })?;
203
204        let mut templates = Vec::new();
205        for template in template_iter {
206            templates.push(template?);
207        }
208        Ok(templates)
209    }
210
211    /// Fetches one template by exact name; `search` is the fuzzy variant.
212    ///
213    /// ```rust,no_run
214    /// # use kasl::db::templates::Templates;
215    /// # fn main() -> anyhow::Result<()> {
216    /// let mut templates = Templates::new()?;
217    /// if let Some(template) = templates.get("daily-standup")? {
218    ///     println!("Found template: {}", template.task_name);
219    /// } else {
220    ///     println!("Template 'daily-standup' not found");
221    /// }
222    /// # Ok(())
223    /// # }
224    /// ```
225    pub fn get(&mut self, name: &str) -> Result<Option<TaskTemplate>> {
226        let mut stmt = self.conn.prepare(SELECT_TEMPLATE_BY_NAME)?;
227        let mut template_iter = stmt.query_map(params![name], |row| {
228            Ok(TaskTemplate {
229                id: row.get(0)?,
230                name: row.get(1)?,
231                task_name: row.get(2)?,
232                comment: row.get(3)?,
233                completeness: row.get(4)?,
234                created_at: row.get(5)?,
235            })
236        })?;
237
238        match template_iter.next() {
239            Some(Ok(template)) => Ok(Some(template)),
240            Some(Err(e)) => Err(e.into()),
241            None => Ok(None),
242        }
243    }
244
245    /// Substring search over template names and task names, sorted by name.
246    ///
247    /// ```rust,no_run
248    /// # use kasl::db::templates::Templates;
249    /// # fn main() -> anyhow::Result<()> {
250    /// let mut templates = Templates::new()?;
251    ///
252    /// let review_templates = templates.search("review")?;
253    /// let standup_templates = templates.search("standup")?;
254    ///
255    /// for template in review_templates {
256    ///     println!("Found: {} -> {}", template.name, template.task_name);
257    /// }
258    /// # let _ = standup_templates;
259    /// # Ok(())
260    /// # }
261    /// ```
262    pub fn search(&mut self, query: &str) -> Result<Vec<TaskTemplate>> {
263        let search_pattern = format!("%{}%", query);
264        let mut stmt = self.conn.prepare(SEARCH_TEMPLATES)?;
265
266        let template_iter = stmt.query_map(params![search_pattern], |row| {
267            Ok(TaskTemplate {
268                id: row.get(0)?,
269                name: row.get(1)?,
270                task_name: row.get(2)?,
271                comment: row.get(3)?,
272                completeness: row.get(4)?,
273                created_at: row.get(5)?,
274            })
275        })?;
276
277        let mut templates = Vec::new();
278        for template in template_iter {
279            templates.push(template?);
280        }
281
282        Ok(templates)
283    }
284
285    /// True when a template with this exact name exists.
286    ///
287    /// ```rust,no_run
288    /// # use kasl::db::templates::{Templates, TaskTemplate};
289    /// # fn main() -> anyhow::Result<()> {
290    /// let mut templates = Templates::new()?;
291    /// if templates.exists("daily-standup")? {
292    ///     println!("Template already exists");
293    /// } else {
294    ///     let template = TaskTemplate::new(
295    ///         "daily-standup".to_string(),
296    ///         "Prepare for daily standup".to_string(),
297    ///         "Review progress and plan".to_string(),
298    ///         0
299    ///     );
300    ///     templates.create(&template)?;
301    /// }
302    /// # Ok(())
303    /// # }
304    /// ```
305    pub fn exists(&mut self, name: &str) -> Result<bool> {
306        Ok(self.get(name)?.is_some())
307    }
308}