trackWork 0.15.0

A terminal-based time tracking application for managing work sessions
use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime};

use crate::app::{at_work_span_of, App};
use crate::models::TimeEntry;

/// Per-day rollup shown as one column in the weekly summary.
pub struct DaySummary {
    pub date: NaiveDate,
    /// Effective at-work span (start, end, is_manual), if the day has any activity.
    pub span: Option<(NaiveDateTime, NaiveDateTime, bool)>,
    pub logged_min: i64,
    pub unlogged_min: i64,
    /// Span length minus off-work time (clamped to >= 0).
    pub workday_min: i64,
    /// Day has work entries but no off-work (lunch) entry.
    pub missing_lunch: bool,
    pub entries: Vec<TimeEntry>,
}

/// Monday of the ISO week containing `date`.
pub fn week_start_of(date: NaiveDate) -> NaiveDate {
    let days_from_monday = date.weekday().num_days_from_monday() as i64;
    date - Duration::days(days_from_monday)
}

/// Build the Mon–Sun summary for the week containing `anchor`.
/// Queries each day fresh from the DB so running timers stay live.
pub fn build_week(app: &App, anchor: NaiveDate) -> Vec<DaySummary> {
    let now = Local::now().naive_local();
    let monday = week_start_of(anchor);

    (0..7)
        .map(|i| {
            let date = monday + Duration::days(i);
            let entries = app.db.get_entries_for_date(date).unwrap_or_default();
            let (start_ov, end_ov) = app.db.get_day_overrides(date).unwrap_or((None, None));
            let span = at_work_span_of(&entries, start_ov, end_ov, now);

            let mut logged_min = 0i64;
            let mut unlogged_min = 0i64;
            let mut off_work_min = 0i64;
            let mut has_work = false;
            let mut has_off_work = false;

            for e in &entries {
                let minutes = e
                    .end_time
                    .unwrap_or(now)
                    .signed_duration_since(e.start_time)
                    .num_minutes()
                    .max(0);
                if e.off_work {
                    off_work_min += minutes;
                    has_off_work = true;
                } else {
                    has_work = true;
                    if e.logged {
                        logged_min += minutes;
                    } else {
                        unlogged_min += minutes;
                    }
                }
            }

            let span_min = span
                .map(|(s, en, _)| en.signed_duration_since(s).num_minutes().max(0))
                .unwrap_or(0);
            let workday_min = (span_min - off_work_min).max(0);

            DaySummary {
                date,
                span,
                logged_min,
                unlogged_min,
                workday_min,
                missing_lunch: has_work && !has_off_work,
                entries,
            }
        })
        .collect()
}