Skip to main content

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}