Skip to main content

kasl/db/
workdays.rs

1//! Daily work session tracking and time management operations.
2//!
3//! Provides functionality for managing daily work sessions, including automatic
4//! start/end time tracking, time adjustments, and period-based querying.
5//!
6//! ## Features
7//!
8//! - **Session Tracking**: Automatic recording of daily work start and end times
9//! - **Time Adjustments**: Manual correction of work session boundaries
10//! - **Period Queries**: Efficient retrieval of workdays by date ranges
11//! - **Duplicate Prevention**: Ensures only one workday record per date
12//! - **Timezone Handling**: Consistent local timezone management for all operations
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! # fn main() -> anyhow::Result<()> {
18//! use kasl::db::workdays::Workdays;
19//! use chrono::Local;
20//!
21//! let mut workdays = Workdays::new()?;
22//! let today = Local::now().date_naive();
23//!
24//! workdays.insert_start(today)?;
25//! workdays.insert_end(today)?;
26//! # Ok(())
27//! # }
28//! ```
29
30use crate::{db::db::Db, libs::messages::Message, msg_error_anyhow};
31use anyhow::Result;
32use chrono::{NaiveDate, NaiveDateTime};
33use rusqlite::{Connection, OptionalExtension};
34
35/// SQL schema for the workdays table.
36///
37/// Defines the structure for storing daily work sessions with date uniqueness
38/// constraints and proper temporal data types. The schema ensures data integrity
39/// and supports efficient date-based queries for reporting and analysis.
40const SCHEMA_WORKDAYS: &str = "CREATE TABLE IF NOT EXISTS workdays (
41    id INTEGER PRIMARY KEY,
42    date DATE NOT NULL UNIQUE,
43    start TIMESTAMP NOT NULL,
44    end TIMESTAMP
45);";
46
47/// Insert a new workday start record with current timestamp.
48///
49/// Creates a workday record for the specified date using the current time
50/// as the start timestamp. The end time is left NULL to indicate an active
51/// work session. Uses local timezone for consistent time handling.
52const INSERT_START: &str = "INSERT INTO workdays (date, start) VALUES (?1, datetime(CURRENT_TIMESTAMP, 'localtime'))";
53
54/// Update an existing workday with current end timestamp.
55///
56/// Completes a work session by setting the end timestamp to the current time.
57/// This marks the completion of the workday and enables duration calculations
58/// for productivity analysis and reporting.
59const UPDATE_END: &str = "UPDATE workdays SET end = datetime(CURRENT_TIMESTAMP, 'localtime') WHERE date = ?1";
60
61/// Retrieve a specific workday by date.
62///
63/// Fetches complete workday information including ID, date, start time, and
64/// end time (if available) for a specific calendar date. Used for detailed
65/// workday analysis and time adjustment operations.
66const SELECT_BY_DATE: &str = "SELECT id, date, start, end FROM workdays WHERE date = ?1";
67
68/// Retrieve all workdays within a calendar month.
69///
70/// Fetches workdays for the month containing the specified date using SQL
71/// date functions. Useful for monthly reporting, productivity analysis, and
72/// work pattern identification across longer time periods.
73const SELECT_BY_MONTH: &str = "SELECT id, date, start, end FROM workdays WHERE strftime('%Y-%m', date) = strftime('%Y-%m', ?1)";
74
75/// Update the start time of an existing workday.
76///
77/// Allows manual adjustment of work session start times for correction of
78/// automatic tracking errors or manual time entry scenarios. Maintains
79/// data integrity while providing flexibility for time management.
80const UPDATE_START: &str = "UPDATE workdays SET start = ?1 WHERE date = ?2";
81
82/// Update the end time of an existing workday with specific timestamp.
83///
84/// Sets a specific end timestamp for a workday, enabling manual time
85/// adjustments and corrections to automatic time tracking records.
86const UPDATE_END_TIME: &str = "UPDATE workdays SET end = ?1 WHERE date = ?2";
87
88/// Remove the end time from a workday, marking it as ongoing.
89///
90/// Clears the end timestamp to indicate an active or incomplete work session.
91/// Useful for correcting mistakenly ended sessions or resuming work tracking.
92const UNSET_END_TIME: &str = "UPDATE workdays SET end = NULL WHERE date = ?1";
93
94/// Represents a complete workday record with temporal boundaries.
95///
96/// A workday encapsulates a single day's work session with start and end times,
97/// providing the fundamental unit for time tracking and productivity analysis.
98/// Each workday corresponds to one calendar date and contains the temporal
99/// boundaries of work activity for that day.
100///
101/// ## Time Representation
102///
103/// All timestamps use `NaiveDateTime` to represent local timezone times
104/// consistently across the application. This approach avoids timezone
105/// complexity while maintaining accuracy for user-centric time tracking.
106///
107/// ## Completion States
108///
109/// - **Active Session**: `end` is `None`, indicating ongoing work
110/// - **Completed Session**: `end` is `Some(timestamp)`, work session finished
111/// - **Historical Record**: Both start and end times available for analysis
112#[derive(Debug, Clone)]
113pub struct Workday {
114    /// Database-assigned unique identifier.
115    ///
116    /// Used for internal database operations and referential integrity.
117    /// Automatically assigned when the workday is created.
118    pub id: i32,
119
120    /// Calendar date for this work session.
121    ///
122    /// Each date can have only one workday record, enforced by database
123    /// constraints. Represents the calendar day when work was performed,
124    /// independent of the actual start/end times which may span midnight.
125    pub date: NaiveDate,
126
127    /// Timestamp when work session began.
128    ///
129    /// Records the exact moment work started for this date, using local
130    /// timezone for consistency. This timestamp is always required and
131    /// serves as the foundation for work duration calculations.
132    pub start: NaiveDateTime,
133
134    /// Timestamp when work session ended, if completed.
135    ///
136    /// `None` indicates an active/ongoing work session that hasn't been
137    /// completed yet. `Some(timestamp)` indicates a finished work session
138    /// with the exact completion time for duration calculations.
139    pub end: Option<NaiveDateTime>,
140}
141
142/// Database manager for workday operations and time tracking functionality.
143///
144/// The `Workdays` struct provides a comprehensive interface for managing daily
145/// work sessions, including creation, modification, and querying of workday
146/// records. It handles database connections, ensures data integrity, and
147/// provides efficient access patterns for time tracking operations.
148///
149/// ## Design Principles
150///
151/// - **One Session Per Day**: Each calendar date has at most one workday record
152/// - **Local Timezone**: All timestamps use local timezone for user clarity
153/// - **Automatic Tracking**: Supports both automatic and manual time entry
154/// - **Data Integrity**: Enforces constraints and validation for reliable tracking
155///
156/// ## Connection Management
157///
158/// Each instance maintains its own database connection and ensures the
159/// workdays table schema is properly initialized during construction.
160pub struct Workdays {
161    /// Direct database connection for workday operations.
162    ///
163    /// Provides transactional access to the workdays table with
164    /// optimized performance for time tracking queries and updates.
165    pub conn: Connection,
166}
167
168impl Workdays {
169    /// Creates a new Workdays manager and initializes the database schema.
170    ///
171    /// This constructor establishes a database connection, ensures the workdays
172    /// table exists with proper constraints, and prepares the manager for
173    /// time tracking operations. Schema creation is idempotent and safe for
174    /// repeated initialization.
175    ///
176    /// # Returns
177    ///
178    /// Returns a new `Workdays` instance ready for workday management
179    /// operations, or an error if database initialization fails.
180    ///
181    /// # Example
182    ///
183    /// ```rust,no_run
184    /// # fn main() -> anyhow::Result<()> {
185    /// use kasl::db::workdays::Workdays;
186    ///
187    /// let mut workdays = Workdays::new()?;
188    /// // Ready for workday tracking
189    /// # Ok(())
190    /// # }
191    /// ```
192    ///
193    /// # Database Integration
194    ///
195    /// The workdays table schema is created if it doesn't exist, ensuring
196    /// the manager can operate regardless of database initialization order.
197    /// This provides robustness in different deployment scenarios.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if:
202    /// - Database connection cannot be established
203    /// - Schema creation fails due to permissions or corruption
204    /// - Table initialization encounters constraint violations
205    pub fn new() -> Result<Self> {
206        let db = Db::new()?;
207
208        // Initialize the workdays table schema
209        db.conn.execute(SCHEMA_WORKDAYS, [])?;
210
211        Ok(Workdays { conn: db.conn })
212    }
213
214    /// Records the start of a work session for the specified date.
215    ///
216    /// This method creates a new workday record with the current timestamp
217    /// as the start time, or does nothing if a workday already exists for
218    /// the given date. This prevents duplicate workday records while allowing
219    /// safe repeated calls to start tracking.
220    ///
221    /// ## Duplicate Handling
222    ///
223    /// The method checks for existing workday records before insertion to
224    /// prevent database constraint violations. If a workday already exists
225    /// for the specified date, the operation succeeds without modification.
226    ///
227    /// ## Timezone Consistency
228    ///
229    /// Uses local timezone for timestamp recording to ensure consistency
230    /// with user expectations and interface displays. All workday times
231    /// are recorded in the system's local timezone.
232    ///
233    /// # Arguments
234    ///
235    /// * `date` - Calendar date for which to start work session tracking
236    ///
237    /// # Returns
238    ///
239    /// Returns `Ok(())` if the start time is recorded or already exists,
240    /// or an error if the database operation fails.
241    ///
242    /// # Example
243    ///
244    /// ```rust,no_run
245    /// # use kasl::db::workdays::Workdays;
246    /// use chrono::Local;
247    ///
248    /// # fn main() -> anyhow::Result<()> {
249    /// let mut workdays = Workdays::new()?;
250    /// let today = Local::now().date_naive();
251    /// workdays.insert_start(today)?; // Start tracking work for today
252    /// # Ok(())
253    /// # }
254    /// ```
255    ///
256    /// # Idempotency
257    ///
258    /// This operation is idempotent - calling it multiple times with the
259    /// same date has the same effect as calling it once. This makes it
260    /// safe for use in automatic tracking systems.
261    pub fn insert_start(&mut self, date: NaiveDate) -> Result<()> {
262        let date_str = date.format("%Y-%m-%d").to_string();
263
264        // Check if workday already exists to prevent duplicates
265        if self.fetch(date)?.is_none() {
266            self.conn.execute(INSERT_START, [&date_str])?;
267        }
268
269        Ok(())
270    }
271
272    /// Records the end of a work session for the specified date.
273    ///
274    /// This method updates an existing workday record by setting the end
275    /// timestamp to the current time. It marks the completion of a work
276    /// session and enables duration calculations for the workday.
277    ///
278    /// ## Prerequisites
279    ///
280    /// The workday must already exist (created via `insert_start`) for this
281    /// operation to succeed. The method updates the existing record rather
282    /// than creating a new one, maintaining data integrity.
283    ///
284    /// ## Completion Semantics
285    ///
286    /// Once a workday has an end time, it represents a completed work session
287    /// for that date. The end time can be modified later using time adjustment
288    /// methods if corrections are needed.
289    ///
290    /// # Arguments
291    ///
292    /// * `date` - Calendar date for which to end work session tracking
293    ///
294    /// # Returns
295    ///
296    /// Returns `Ok(())` if the end time is recorded successfully, or an error
297    /// if no workday exists for the date or the database operation fails.
298    ///
299    /// # Example
300    ///
301    /// ```rust,no_run
302    /// # use kasl::db::workdays::Workdays;
303    /// use chrono::Local;
304    ///
305    /// # fn main() -> anyhow::Result<()> {
306    /// let mut workdays = Workdays::new()?;
307    /// let today = Local::now().date_naive();
308    ///
309    /// // Start and end a work session
310    /// workdays.insert_start(today)?;
311    /// // ... work happens ...
312    /// workdays.insert_end(today)?; // Mark work as completed
313    /// # Ok(())
314    /// # }
315    /// ```
316    ///
317    /// # Error Conditions
318    ///
319    /// - No workday record exists for the specified date
320    /// - Database connection or constraint failures
321    /// - Concurrent modification conflicts
322    pub fn insert_end(&mut self, date: NaiveDate) -> Result<()> {
323        let date_str = date.format("%Y-%m-%d").to_string();
324        self.conn.execute(UPDATE_END, [&date_str])?;
325        Ok(())
326    }
327
328    /// Retrieves a complete workday record for the specified date.
329    ///
330    /// This method fetches detailed workday information including all temporal
331    /// data and metadata for a specific calendar date. It provides the primary
332    /// mechanism for accessing workday details for analysis and reporting.
333    ///
334    /// ## Data Parsing
335    ///
336    /// The method handles automatic conversion from database string formats
337    /// to appropriate Rust types (`NaiveDate`, `NaiveDateTime`), providing
338    /// type safety and convenience for downstream operations.
339    ///
340    /// ## Return Semantics
341    ///
342    /// - `Some(Workday)`: Complete workday record found for the date
343    /// - `None`: No workday exists for the specified date
344    /// - `Error`: Database access or parsing failures
345    ///
346    /// # Arguments
347    ///
348    /// * `date` - Calendar date for which to retrieve workday information
349    ///
350    /// # Returns
351    ///
352    /// Returns `Some(Workday)` if a record exists, `None` if no workday is
353    /// found for the date, or an error if the database query fails.
354    ///
355    /// # Example
356    ///
357    /// ```rust,no_run
358    /// # use kasl::db::workdays::Workdays;
359    /// use chrono::Local;
360    ///
361    /// # fn main() -> anyhow::Result<()> {
362    /// let mut workdays = Workdays::new()?;
363    /// let today = Local::now().date_naive();
364    ///
365    /// if let Some(workday) = workdays.fetch(today)? {
366    ///     println!("Work started at: {}", workday.start);
367    ///     if let Some(end_time) = workday.end {
368    ///         println!("Work ended at: {}", end_time);
369    ///     } else {
370    ///         println!("Work session is still active");
371    ///     }
372    /// } else {
373    ///     println!("No work session recorded for today");
374    /// }
375    /// # Ok(())
376    /// # }
377    /// ```
378    ///
379    /// # Data Integrity
380    ///
381    /// The method assumes well-formed timestamp data in the database and
382    /// will return errors if timestamp parsing fails due to data corruption
383    /// or unexpected format changes.
384    pub fn fetch(&mut self, date: NaiveDate) -> Result<Option<Workday>> {
385        let date_str = date.format("%Y-%m-%d").to_string();
386
387        let workday = self
388            .conn
389            .query_row(SELECT_BY_DATE, [&date_str], |row| {
390                Ok(Workday {
391                    id: row.get(0)?,
392                    date: NaiveDate::parse_from_str(&row.get::<_, String>(1)?, "%Y-%m-%d").unwrap(),
393                    start: NaiveDateTime::parse_from_str(&row.get::<_, String>(2)?, "%Y-%m-%d %H:%M:%S").unwrap(),
394                    end: row
395                        .get::<_, Option<String>>(3)?
396                        .map(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").unwrap()),
397                })
398            })
399            .optional()?;
400
401        Ok(workday)
402    }
403
404    /// Retrieves all workdays within the calendar month containing the specified date.
405    ///
406    /// This method fetches workday records for an entire month using SQL date
407    /// functions to determine month boundaries. It provides efficient bulk
408    /// access to workday data for monthly reporting, productivity analysis,
409    /// and trend identification.
410    ///
411    /// ## Month Calculation
412    ///
413    /// The method uses SQL `strftime` functions to extract year-month components
414    /// and match them against the target date's month. This approach handles
415    /// month boundaries correctly across different year transitions.
416    ///
417    /// ## Result Processing
418    ///
419    /// All workdays are loaded into memory and returned as a vector, providing
420    /// convenient access for analysis operations. The results maintain
421    /// chronological order for consistent processing.
422    ///
423    /// # Arguments
424    ///
425    /// * `date` - Any date within the target month for workday retrieval
426    ///
427    /// # Returns
428    ///
429    /// Returns a vector of all workdays in the same month as the specified date,
430    /// or an error if the database query or parsing fails.
431    ///
432    /// # Example
433    ///
434    /// ```rust,no_run
435    /// # use kasl::db::workdays::Workdays;
436    /// use chrono::Local;
437    ///
438    /// # fn main() -> anyhow::Result<()> {
439    /// let mut workdays = Workdays::new()?;
440    /// let current_month = Local::now().date_naive();
441    ///
442    /// let monthly_workdays = workdays.fetch_month(current_month)?;
443    /// println!("Found {} workdays this month", monthly_workdays.len());
444    ///
445    /// for workday in monthly_workdays {
446    ///     if let Some(end_time) = workday.end {
447    ///         let duration = end_time - workday.start;
448    ///         println!("Date: {}, Duration: {:?}", workday.date, duration);
449    ///     }
450    /// }
451    /// # Ok(())
452    /// # }
453    /// ```
454    ///
455    /// # Performance Considerations
456    ///
457    /// - Loads all monthly workdays into memory simultaneously
458    /// - Efficient for typical monthly workday counts (20-30 records)
459    /// - May need optimization for very large historical datasets
460    /// - Consider pagination for bulk historical data processing
461    ///
462    /// # Use Cases
463    ///
464    /// - Monthly productivity reports
465    /// - Work pattern analysis and trend identification
466    /// - Timesheet generation and validation
467    /// - Historical work data export and backup
468    pub fn fetch_month(&mut self, date: NaiveDate) -> Result<Vec<Workday>> {
469        let date_str = date.format("%Y-%m-%d").to_string();
470
471        // Prepare statement for monthly workday query
472        let mut stmt = self.conn.prepare(SELECT_BY_MONTH)?;
473
474        // Execute query and process results
475        let workday_iter = stmt.query_map([&date_str], |row| {
476            Ok(Workday {
477                id: row.get(0)?,
478                date: NaiveDate::parse_from_str(&row.get::<_, String>(1)?, "%Y-%m-%d").unwrap(),
479                start: NaiveDateTime::parse_from_str(&row.get::<_, String>(2)?, "%Y-%m-%d %H:%M:%S").unwrap(),
480                end: row
481                    .get::<_, Option<String>>(3)?
482                    .map(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").unwrap()),
483            })
484        })?;
485
486        // Collect all workday results
487        let mut workdays = Vec::new();
488        for workday in workday_iter {
489            workdays.push(workday?);
490        }
491
492        Ok(workdays)
493    }
494
495    /// Updates the start time of an existing workday to a specific timestamp.
496    ///
497    /// This method provides manual adjustment capabilities for correcting
498    /// automatic time tracking errors or implementing manual time entry
499    /// scenarios. It modifies the start timestamp while preserving all
500    /// other workday properties and relationships.
501    ///
502    /// ## Time Adjustment Use Cases
503    ///
504    /// - Correcting automatic tracking start times that were recorded incorrectly
505    /// - Manual time entry for workdays that weren't automatically tracked
506    /// - Adjusting for delayed system startup or application launch
507    /// - Retroactive time corrections based on external time records
508    ///
509    /// ## Data Validation
510    ///
511    /// The method validates that the workday exists before attempting the update
512    /// and returns an error if no matching record is found. This ensures
513    /// referential integrity and provides clear error feedback.
514    ///
515    /// # Arguments
516    ///
517    /// * `date` - Calendar date of the workday to modify
518    /// * `new_start` - New start timestamp to apply to the workday
519    ///
520    /// # Returns
521    ///
522    /// Returns `Ok(())` if the update succeeds, or an error if the workday
523    /// doesn't exist or the database operation fails.
524    ///
525    /// # Example
526    ///
527    /// ```rust,no_run
528    /// # use kasl::db::workdays::Workdays;
529    /// use chrono::{Local, NaiveDateTime};
530    ///
531    /// # fn main() -> anyhow::Result<()> {
532    /// let mut workdays = Workdays::new()?;
533    /// let today = Local::now().date_naive();
534    ///
535    /// // Correct start time to 9:00 AM
536    /// let corrected_start = NaiveDateTime::parse_from_str(
537    ///     &format!("{} 09:00:00", today.format("%Y-%m-%d")),
538    ///     "%Y-%m-%d %H:%M:%S"
539    /// )?;
540    ///
541    /// workdays.update_start(today, corrected_start)?;
542    /// # Ok(())
543    /// # }
544    /// ```
545    ///
546    /// # Error Conditions
547    ///
548    /// - No workday exists for the specified date
549    /// - Database constraint violations or connection failures
550    /// - Invalid timestamp format or timezone issues
551    pub fn update_start(&mut self, date: NaiveDate, new_start: NaiveDateTime) -> Result<()> {
552        let date_str = date.format("%Y-%m-%d").to_string();
553        let start_str = new_start.format("%Y-%m-%d %H:%M:%S").to_string();
554
555        let affected = self.conn.execute(UPDATE_START, [&start_str, &date_str])?;
556
557        if affected == 0 {
558            return Err(msg_error_anyhow!(Message::WorkdayUpdateFailed));
559        }
560
561        Ok(())
562    }
563
564    /// Updates the end time of an existing workday or clears it to mark as ongoing.
565    ///
566    /// This method provides flexible end time management, supporting both
567    /// specific timestamp updates and clearing end times to mark workdays
568    /// as ongoing or incomplete. It enables comprehensive time adjustment
569    /// and correction capabilities.
570    ///
571    /// ## Operation Modes
572    ///
573    /// - **Set End Time**: `Some(timestamp)` sets a specific completion time
574    /// - **Clear End Time**: `None` removes the end time, marking as ongoing
575    /// - **Correction**: Modify existing end times for accuracy improvements
576    ///
577    /// ## State Transitions
578    ///
579    /// The method supports all valid workday state transitions:
580    /// - Completed → Ongoing (clear end time)
581    /// - Ongoing → Completed (set end time)
582    /// - Completed → Completed (adjust end time)
583    ///
584    /// # Arguments
585    ///
586    /// * `date` - Calendar date of the workday to modify
587    /// * `new_end` - New end timestamp (`Some`) or clear end time (`None`)
588    ///
589    /// # Returns
590    ///
591    /// Returns `Ok(())` if the update succeeds, or an error if the workday
592    /// doesn't exist or the database operation fails.
593    ///
594    /// # Example
595    ///
596    /// ```rust,no_run
597    /// # use kasl::db::workdays::Workdays;
598    /// use chrono::{Local, NaiveDateTime};
599    ///
600    /// # fn main() -> anyhow::Result<()> {
601    /// let mut workdays = Workdays::new()?;
602    /// let today = Local::now().date_naive();
603    ///
604    /// // Set specific end time
605    /// let end_time = NaiveDateTime::parse_from_str(
606    ///     &format!("{} 17:30:00", today.format("%Y-%m-%d")),
607    ///     "%Y-%m-%d %H:%M:%S"
608    /// )?;
609    /// workdays.update_end(today, Some(end_time))?;
610    ///
611    /// // Clear end time (mark as ongoing)
612    /// workdays.update_end(today, None)?;
613    /// # Ok(())
614    /// # }
615    /// ```
616    ///
617    /// # Data Consistency
618    ///
619    /// The method ensures database consistency by validating workday existence
620    /// and providing clear error feedback for failed operations. This maintains
621    /// data integrity across time adjustment operations.
622    pub fn update_end(&mut self, date: NaiveDate, new_end: Option<NaiveDateTime>) -> Result<()> {
623        let date_str = date.format("%Y-%m-%d").to_string();
624        let end_str = new_end.map(|e| e.format("%Y-%m-%d %H:%M:%S").to_string());
625
626        let affected = match end_str {
627            Some(end) => self.conn.execute(UPDATE_END_TIME, [&end, &date_str])?,
628            None => self.conn.execute(UNSET_END_TIME, [&date_str])?,
629        };
630
631        // Validate that a workday record was actually updated
632        if affected == 0 {
633            return Err(msg_error_anyhow!(Message::WorkdayUpdateFailed));
634        }
635
636        Ok(())
637    }
638}