rtimelogger 0.8.8

A simple cross-platform CLI tool to track working hours, lunch breaks, and calculate surplus time
Documentation
use super::{event_type::EventType, location::Location};
use crate::db::pool::DbPool;
use chrono::{Local, NaiveDate, NaiveTime};
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
pub struct Event {
    pub id: i32,
    pub date: NaiveDate,    // ⇔ events.date (TEXT "YYYY-MM-DD")
    pub time: NaiveTime,    // ⇔ events.time (TEXT "HH:MM")
    pub kind: EventType,    // ⇔ events.kind  ('in' | 'out')
    pub location: Location, // ⇔ events.position ('O','R','H','C','M')
    pub lunch: Option<i32>, // ⇔ events.lunch_break (INT, default 0)
    pub work_gap: bool,     // ⇔ events.meta/work_gap logica futura

    pub pair: i32,             // ⇔ events.pair (INT NOT NULL DEFAULT 0)
    pub source: String,        // ⇔ events.source (TEXT, default 'cli')
    pub meta: Option<String>,  // ⇔ events.meta (TEXT, default '')
    pub notes: Option<String>, // ⇔ events.notes (TEXT, optional workday notes)
    pub created_at: String,    // ⇔ events.created_at (TEXT, ISO8601)
}

#[derive(Debug, Clone, Default)]
pub struct EventExtras {
    pub lunch: Option<i32>,
    pub work_gap: bool,
    pub meta: Option<String>,
    pub source: Option<String>,
    pub notes: Option<String>,
    pub pair: Option<i32>,
    pub created_at: Option<String>,
}

impl Event {
    /// Costruttore "di alto livello" per eventi creati dalla CLI.
    /// - Imposta `pair = 0` (sarà ricalcolato da recalc_all_pairs)
    /// - Imposta `created_at = now() in ISO8601`
    pub fn new(
        id: i32,
        date: NaiveDate,
        time: NaiveTime,
        kind: EventType,
        location: Location,
        extras: EventExtras,
    ) -> Self {
        Self {
            id,
            date,
            time,
            kind,
            location,
            lunch: extras.lunch,
            work_gap: extras.work_gap,
            pair: extras.pair.unwrap_or(0),
            source: extras.source.unwrap_or_else(|| "cli".to_string()),
            meta: extras.meta,
            notes: extras.notes,
            created_at: extras
                .created_at
                .unwrap_or_else(|| Local::now().to_rfc3339()),
        }
    }

    pub fn date_str(&self) -> String {
        self.date.format("%Y-%m-%d").to_string()
    }
    pub fn time_str(&self) -> String {
        self.time.format("%H:%M").to_string()
    }

    pub fn timestamp(&self) -> chrono::DateTime<Local> {
        let dt = self.date.and_time(self.time);
        // convert naive to Local
        dt.and_local_timezone(Local).unwrap()
    }

    pub fn get_date_time(&self) -> String {
        self.date
            .and_time(self.time)
            .format("%Y-%m-%d %H:%M")
            .to_string()
    }

    pub fn has_events_for_dates(pool: &mut DbPool, dates: &[NaiveDate]) -> rusqlite::Result<bool> {
        if dates.is_empty() {
            return Ok(false);
        }

        // Converti le date in stringhe "YYYY-MM-DD"
        let date_strings: Vec<String> = dates
            .iter()
            .map(|d| d.format("%Y-%m-%d").to_string())
            .collect();

        // Crea una lista di placeholder: ?, ?, ?, ...
        let placeholders = vec!["?"; date_strings.len()].join(",");

        // Query con IN (...)
        let sql = format!(
            "SELECT 1 FROM events WHERE date IN ({}) LIMIT 1",
            placeholders
        );

        // Converti in una lista di &dyn ToSql per rusqlite
        let params: Vec<&dyn rusqlite::ToSql> = date_strings
            .iter()
            .map(|s| s as &dyn rusqlite::ToSql)
            .collect();

        let exists = {
            let conn = &mut pool.conn;
            let mut stmt = conn.prepare(&sql)?;
            stmt.exists(rusqlite::params_from_iter(params))?
        };

        Ok(exists)
    }

    #[cfg(test)]
    pub fn test_with_meta(meta: Option<&str>) -> Self {
        Self {
            id: 0,
            date: Default::default(),
            time: Default::default(),
            kind: EventType::In,
            location: Location::Office,
            lunch: None,
            work_gap: false,
            pair: 0,
            source: "".to_string(),
            meta: meta.map(|s| s.to_string()),
            notes: None,
            // Inizializza qui TUTTI gli altri campi con valori “dummy” validi.
            // Esempi tipici:
            // id: 0,
            // date: chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
            // kind: EventKind::In,
            // location: Location::Office,
            // lunch: None,
            // source: "test".into(),
            // pair: 0,
            // work_gap: false,
            // ...
            created_at: "".to_string(),
        }
    }
}