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}