kasl/db/breaks.rs
1//! Manual break periods management for productivity optimization.
2//!
3//! Handles user-defined break periods that complement automatic pause detection for improved productivity metrics.
4//!
5//! ## Features
6//!
7//! - **Manual Break Creation**: Add intentional break periods to improve productivity
8//! - **Smart Placement**: Avoid conflicts with existing pauses and maintain minimum work intervals
9//! - **Daily Management**: Retrieve and manage breaks for specific dates
10//! - **Productivity Integration**: Seamlessly integrate with productivity calculations
11//!
12//! ## Usage
13//!
14//! ```rust
15//! use kasl::db::breaks::{Breaks, Break};
16//! use chrono::{NaiveDate, Duration};
17//!
18//! let breaks_db = Breaks::new()?;
19//! let date = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
20//!
21//! let break_record = Break {
22//! id: None,
23//! date,
24//! start: date.and_hms_opt(12, 0, 0).unwrap(),
25//! end: date.and_hms_opt(12, 30, 0).unwrap(),
26//! duration: Duration::minutes(30),
27//! reason: Some("Lunch break".to_string()),
28//! created_at: None,
29//! };
30//!
31//! breaks_db.insert(&break_record)?;
32//! ```
33
34use crate::db::db::Db;
35use anyhow::Result;
36use chrono::{Duration, NaiveDate, NaiveDateTime};
37use rusqlite::params;
38
39/// Represents a manually added break period.
40///
41/// Manual breaks are user-defined periods that complement automatic pause detection
42/// to provide more accurate productivity calculations. Unlike automatic pauses,
43/// breaks are intentionally added to represent planned rest periods.
44#[derive(Debug, Clone)]
45pub struct Break {
46 /// Unique identifier for the break record
47 pub id: Option<i64>,
48
49 /// Date this break belongs to
50 pub date: NaiveDate,
51
52 /// Start time of the break
53 pub start: NaiveDateTime,
54
55 /// End time of the break
56 pub end: NaiveDateTime,
57
58 /// Duration of the break in minutes
59 pub duration: Duration,
60
61 /// Optional reason for the break
62 pub reason: Option<String>,
63
64 /// When this break record was created
65 pub created_at: Option<NaiveDateTime>,
66}
67
68/// Database operations for manual break management.
69///
70/// Provides CRUD operations for break records with proper error handling
71/// and integration with the existing database infrastructure.
72pub struct Breaks {
73 db: Db,
74}
75
76impl Breaks {
77 /// Create a new Breaks database manager.
78 ///
79 /// Initializes the breaks manager with database connection and ensures
80 /// the breaks table exists through the migration system.
81 ///
82 /// # Returns
83 ///
84 /// Returns a configured Breaks instance ready for database operations.
85 ///
86 /// # Examples
87 ///
88 /// ```rust
89 /// let breaks_db = Breaks::new()?;
90 /// ```
91 pub fn new() -> Result<Self> {
92 let db = Db::new()?;
93 Ok(Self { db })
94 }
95
96 /// Insert a new break record into the database.
97 ///
98 /// Creates a new break record with the provided information. The break
99 /// duration is calculated and stored automatically based on start and end times.
100 ///
101 /// # Arguments
102 ///
103 /// * `break_record` - The break information to insert
104 ///
105 /// # Returns
106 ///
107 /// Returns `Ok(())` on successful insertion, or an error if the database
108 /// operation fails or validation errors occur.
109 ///
110 /// # Examples
111 ///
112 /// ```rust
113 /// let break_record = Break {
114 /// id: None,
115 /// date: date,
116 /// start: start_time,
117 /// end: end_time,
118 /// duration: Duration::minutes(30),
119 /// reason: Some("Lunch break".to_string()),
120 /// created_at: None,
121 /// };
122 ///
123 /// breaks_db.insert(&break_record)?;
124 /// ```
125 pub fn insert(&self, break_record: &Break) -> Result<i64> {
126 let conn = &self.db.conn;
127
128 let _result = conn.execute(
129 "INSERT INTO breaks (date, start_time, end_time, duration, reason, created_at)
130 VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'))",
131 params![
132 break_record.date,
133 break_record.start,
134 break_record.end,
135 break_record.duration.num_minutes(),
136 break_record.reason,
137 ],
138 )?;
139
140 Ok(conn.last_insert_rowid())
141 }
142
143 /// Retrieve all breaks for a specific date.
144 ///
145 /// Returns all manual break records for the given date, ordered by start time.
146 /// This is typically used for daily productivity calculations and reporting.
147 ///
148 /// # Arguments
149 ///
150 /// * `date` - The date to retrieve breaks for
151 ///
152 /// # Returns
153 ///
154 /// Returns a vector of Break records for the specified date, or an error
155 /// if the database query fails.
156 ///
157 /// # Examples
158 ///
159 /// ```rust
160 /// let today = chrono::Local::now().date_naive();
161 /// let breaks = breaks_db.get_daily_breaks(today)?;
162 /// ```
163 pub fn get_daily_breaks(&self, date: NaiveDate) -> Result<Vec<Break>> {
164 let conn = &self.db.conn;
165
166 let mut stmt = conn.prepare(
167 "SELECT id, date, start_time, end_time, duration, reason, created_at
168 FROM breaks
169 WHERE date = ?1
170 ORDER BY start_time"
171 )?;
172
173 let break_iter = stmt.query_map(params![date], |row| {
174 Ok(Break {
175 id: Some(row.get(0)?),
176 date: row.get(1)?,
177 start: row.get(2)?,
178 end: row.get(3)?,
179 duration: Duration::minutes(row.get::<_, i64>(4)?),
180 reason: row.get(5)?,
181 created_at: row.get(6)?,
182 })
183 })?;
184
185 let mut breaks = Vec::new();
186 for break_result in break_iter {
187 breaks.push(break_result?);
188 }
189
190 Ok(breaks)
191 }
192
193 /// Delete a break record by ID.
194 ///
195 /// Removes a break record from the database. This is typically used
196 /// for correcting mistakes or adjusting productivity calculations.
197 ///
198 /// # Arguments
199 ///
200 /// * `id` - The ID of the break record to delete
201 ///
202 /// # Returns
203 ///
204 /// Returns `Ok(())` on successful deletion, or an error if the record
205 /// doesn't exist or the database operation fails.
206 ///
207 /// # Examples
208 ///
209 /// ```rust
210 /// breaks_db.delete(break_id)?;
211 /// ```
212 pub fn delete(&self, id: i64) -> Result<()> {
213 let conn = &self.db.conn;
214
215 let affected_rows = conn.execute("DELETE FROM breaks WHERE id = ?1", params![id])?;
216
217 if affected_rows == 0 {
218 return Err(anyhow::anyhow!("Break record with ID {} not found", id));
219 }
220
221 Ok(())
222 }
223
224 /// Get a specific break by ID.
225 ///
226 /// Retrieves a single break record by its unique identifier.
227 ///
228 /// # Arguments
229 ///
230 /// * `id` - The ID of the break record to retrieve
231 ///
232 /// # Returns
233 ///
234 /// Returns `Some(Break)` if found, `None` if not found, or an error
235 /// if the database query fails.
236 pub fn get_by_id(&self, id: i64) -> Result<Option<Break>> {
237 let conn = &self.db.conn;
238
239 let mut stmt = conn.prepare(
240 "SELECT id, date, start_time, end_time, duration, reason, created_at
241 FROM breaks
242 WHERE id = ?1"
243 )?;
244
245 let mut break_iter = stmt.query_map(params![id], |row| {
246 Ok(Break {
247 id: Some(row.get(0)?),
248 date: row.get(1)?,
249 start: row.get(2)?,
250 end: row.get(3)?,
251 duration: Duration::minutes(row.get::<_, i64>(4)?),
252 reason: row.get(5)?,
253 created_at: row.get(6)?,
254 })
255 })?;
256
257 match break_iter.next() {
258 Some(break_result) => Ok(Some(break_result?)),
259 None => Ok(None),
260 }
261 }
262}