rtimelogger 0.8.5

A simple cross-platform CLI tool to track working hours, lunch breaks, and calculate surplus time
Documentation
use crate::db::pool::DbPool;
use crate::errors::{AppError, AppResult};
use crate::models::event::Event;
use crate::models::event_type::EventType;
use crate::models::location::Location;

use chrono::{NaiveDate, NaiveTime};
use rusqlite::{Connection, Result, Row, params};

pub fn load_events_by_date(pool: &mut DbPool, date: &NaiveDate) -> AppResult<Vec<Event>> {
    let mut stmt = pool.conn.prepare(
        "SELECT * FROM events
         WHERE date = ?1
         ORDER BY time ASC",
    )?;

    let date_str = date.format("%Y-%m-%d").to_string();
    let rows = stmt.query_map([date_str], map_row)?;

    let mut out = Vec::new();
    for r in rows {
        out.push(r?);
    }
    Ok(out)
}

pub fn map_row(row: &Row) -> Result<Event> {
    let date_str: String = row.get("date")?;
    let time_str: String = row.get("time")?;

    let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").map_err(|_| {
        rusqlite::Error::FromSqlConversionFailure(
            0,
            rusqlite::types::Type::Text,
            Box::new(AppError::InvalidDate(date_str.clone())),
        )
    })?;

    let time = NaiveTime::parse_from_str(&time_str, "%H:%M").map_err(|_| {
        rusqlite::Error::FromSqlConversionFailure(
            0,
            rusqlite::types::Type::Text,
            Box::new(AppError::InvalidTime(time_str.clone())),
        )
    })?;

    let kind_str: String = row.get("kind")?;
    let kind = EventType::from_db_str(&kind_str).ok_or_else(|| {
        rusqlite::Error::FromSqlConversionFailure(
            0,
            rusqlite::types::Type::Text,
            Box::new(AppError::InvalidEventType(format!(
                "Invalid kind: {}",
                kind_str
            ))),
        )
    })?;

    let loc_str: String = row.get("position")?;
    let location = Location::from_db_str(&loc_str).ok_or_else(|| {
        rusqlite::Error::FromSqlConversionFailure(
            0,
            rusqlite::types::Type::Text,
            Box::new(AppError::InvalidPosition(format!(
                "Invalid location: {}",
                loc_str
            ))),
        )
    })?;

    Ok(Event {
        id: row.get("id")?,
        date,
        time,
        kind,
        location,
        lunch: row.get("lunch_break")?,
        work_gap: row.get::<_, i32>("work_gap")? == 1,
        pair: row.get("pair")?,
        source: row.get("source")?,
        meta: row.get("meta")?,
        created_at: row.get("created_at")?,
    })
}

pub fn insert_event(conn: &Connection, ev: &Event) -> AppResult<()> {
    conn.execute(
        "INSERT INTO events (date, time, kind, position, lunch_break, work_gap, pair, source, meta, created_at)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
        params![
            ev.date.format("%Y-%m-%d").to_string(),
            ev.time.format("%H:%M").to_string(),
            ev.kind.to_db_str(),
            ev.location.to_db_str(),
            ev.lunch.unwrap_or(0),
            if ev.work_gap { 1 } else { 0 },
            ev.pair,
            ev.source,
            ev.meta,
            ev.created_at,
        ],
    )?;
    Ok(())
}

pub fn update_event(conn: &Connection, ev: &Event) -> AppResult<()> {
    conn.execute(
        "UPDATE events
         SET date = ?1, time = ?2, kind = ?3,
             position = ?4, lunch_break = ?5,
             work_gap = ?6, pair = ?7,
             source = ?8, meta = ?9, created_at = ?10
         WHERE id = ?11",
        params![
            ev.date.to_string(),
            ev.time.format("%H:%M").to_string(),
            ev.kind.to_db_str(),
            ev.location.to_db_str(),
            ev.lunch.unwrap_or(0),
            if ev.work_gap { 1 } else { 0 },
            ev.pair,
            ev.source,
            ev.meta,
            ev.created_at,
            ev.id,
        ],
    )?;
    Ok(())
}

pub fn delete_event(pool: &mut DbPool, id: i32) -> Result<()> {
    pool.conn.execute("DELETE FROM events WHERE id = ?", [id])?;
    Ok(())
}

/// Carica la "pair logica" N-esima per una certa data (ricostruita in memoria).
pub fn load_pair_by_index(
    conn: &Connection,
    date: &NaiveDate,
    pair_index: usize, // 1-based dal CLI
) -> AppResult<(Option<Event>, Option<Event>)> {
    let mut stmt = conn.prepare("SELECT * FROM events WHERE date = ?1 ORDER BY time ASC")?;
    let rows = stmt.query_map([date.to_string()], map_row)?;

    let mut events: Vec<Event> = Vec::new();
    for r in rows {
        events.push(r?);
    }

    if events.is_empty() {
        return Err(AppError::InvalidPair(pair_index));
    }

    let mut pairs: Vec<(Option<Event>, Option<Event>)> = Vec::new();

    for ev in events.into_iter() {
        match ev.kind {
            EventType::In => pairs.push((Some(ev), None)),
            EventType::Out => {
                if let Some(last) = pairs.last_mut()
                    && last.1.is_none()
                {
                    last.1 = Some(ev);
                    continue;
                }
                pairs.push((None, Some(ev)));
            }
        }
    }

    if pair_index == 0 {
        return Err(AppError::InvalidPair(0));
    }

    let idx = pair_index - 1;
    if idx >= pairs.len() {
        return Err(AppError::InvalidPair(pair_index));
    }

    Ok(pairs[idx].clone())
}