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