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}