use crate::db::db::Db;
use crate::db::workdays::Workday;
use crate::libs::config::Config;
use crate::libs::pause::Pause;
use anyhow::Result;
use chrono::{Local, NaiveDate, NaiveDateTime, TimeDelta};
use parking_lot::Mutex;
use rusqlite::{Connection, params};
use std::sync::Arc;
const SCHEMA_PAUSES: &str = "CREATE TABLE IF NOT EXISTS pauses (
id INTEGER NOT NULL PRIMARY KEY,
start TIMESTAMP NOT NULL,
end TIMESTAMP,
duration INTEGER,
protected INTEGER NOT NULL DEFAULT 0,
reason TEXT
)";
const INSERT_PAUSE: &str = "INSERT INTO pauses (start) VALUES (datetime(CURRENT_TIMESTAMP, 'localtime'))";
const INSERT_PAUSE_WITH_TIME: &str = "INSERT INTO pauses (start) VALUES (?1)";
const UPDATE_PAUSE: &str = "UPDATE pauses SET end = (datetime(CURRENT_TIMESTAMP, 'localtime')), duration = ?1 WHERE id = ?2";
const SELECT_LAST_PAUSE: &str = "SELECT id, start FROM pauses WHERE end IS NULL ORDER BY id DESC LIMIT 1";
const SELECT_DAILY_PAUSES: &str =
"SELECT id, start, end, duration, protected FROM pauses WHERE date(start) = date(?1) AND end IS NOT NULL ORDER BY start ASC, id ASC";
const INSERT_MANUAL_PAUSE: &str = "INSERT INTO pauses (start, end, duration, protected, reason) VALUES (?1, ?2, ?3, ?4, ?5)";
const DELETE_PAUSE: &str = "DELETE FROM pauses WHERE id = ?";
pub struct Pauses {
pub conn: Arc<Mutex<Connection>>,
pub min_duration: Option<String>,
pub max_duration: Option<String>,
}
impl Pauses {
pub fn new() -> Result<Pauses> {
let db_conn = Db::new()?.conn;
db_conn.execute(SCHEMA_PAUSES, [])?;
Ok(Pauses {
conn: Arc::new(Mutex::new(db_conn)),
min_duration: None,
max_duration: None,
})
}
pub fn set_min_duration(&self, min_duration: u64) -> Self {
let min_duration_secs = (min_duration * 60) as i64; Self {
conn: self.conn.clone(),
min_duration: Some(min_duration_secs.to_string()),
max_duration: None,
}
}
pub fn set_max_duration(&self, max_duration: u64) -> Self {
let max_duration_secs = (max_duration * 60) as i64; Self {
conn: self.conn.clone(),
min_duration: None,
max_duration: Some(max_duration_secs.to_string()),
}
}
fn merge_consecutive_pauses(pauses: Vec<Pause>, max_gap_secs: i64) -> Vec<Pause> {
let mut merged: Vec<Pause> = Vec::with_capacity(pauses.len());
for pause in pauses {
if pause.protected {
merged.push(pause);
continue;
}
if let Some(last) = merged.last_mut()
&& !last.protected
&& let Some(last_end) = last.end
{
let gap = (pause.start - last_end).num_seconds();
if gap <= max_gap_secs {
let new_end = match pause.end {
Some(end) => Some(end.max(last_end)),
None => Some(last_end),
};
last.end = new_end;
if let Some(end) = last.end {
last.duration = Some(TimeDelta::seconds((end - last.start).num_seconds()));
}
continue;
}
}
merged.push(pause);
}
merged
}
fn duration_threshold_seconds(&self) -> Option<i64> {
self.min_duration.as_ref().or(self.max_duration.as_ref()).and_then(|d| d.parse::<i64>().ok())
}
pub fn insert_start(&self) -> rusqlite::Result<()> {
let conn_guard = self.conn.lock();
conn_guard.execute(INSERT_PAUSE, [])?;
Ok(())
}
pub fn insert_start_with_time(&self, start_time: NaiveDateTime) -> Result<()> {
let conn_guard = self.conn.lock();
let start_str = start_time.format("%Y-%m-%d %H:%M:%S").to_string();
conn_guard.execute(INSERT_PAUSE_WITH_TIME, [&start_str])?;
Ok(())
}
pub fn insert_end(&self) -> Result<()> {
let end = Local::now().naive_local();
let conn_guard = self.conn.lock();
let mut stmt = conn_guard.prepare(SELECT_LAST_PAUSE)?;
let pause_row = stmt.query_row([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)));
if let Ok((id, start_str)) = pause_row {
let start = NaiveDateTime::parse_from_str(&start_str, "%Y-%m-%d %H:%M:%S")?;
let duration = (end - start).num_seconds();
conn_guard.execute(UPDATE_PAUSE, [&duration.to_string(), &id.to_string()])?;
}
Ok(())
}
pub fn insert_manual(&self, start: NaiveDateTime, duration: TimeDelta, protected: bool, reason: Option<&str>) -> Result<i64> {
let end = start + duration;
let conn_guard = self.conn.lock();
conn_guard.execute(
INSERT_MANUAL_PAUSE,
params![
start.format("%Y-%m-%d %H:%M:%S").to_string(),
end.format("%Y-%m-%d %H:%M:%S").to_string(),
duration.num_seconds(),
protected as i64,
reason,
],
)?;
Ok(conn_guard.last_insert_rowid())
}
pub fn find_overlapping(&self, start: NaiveDateTime, end: NaiveDateTime) -> Result<Option<Pause>> {
let pauses = self.get_daily_pauses(start.date())?;
Ok(pauses.into_iter().find(|p| p.end.map(|p_end| p.start < end && start < p_end).unwrap_or(false)))
}
pub fn get_daily_pauses(&self, date: NaiveDate) -> Result<Vec<Pause>> {
let date_str = date.format("%Y-%m-%d").to_string();
let conn_guard = self.conn.lock();
let mut stmt = conn_guard.prepare(SELECT_DAILY_PAUSES)?;
let pause_iter = stmt.query_map([&date_str], |row| {
let start_str: String = row.get(1)?;
let end_str: Option<String> = row.get(2)?;
let duration: i64 = row.get(3).unwrap_or(0);
Ok(Pause {
id: row.get(0)?,
start: NaiveDateTime::parse_from_str(&start_str, "%Y-%m-%d %H:%M:%S").unwrap(),
end: end_str.map(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").unwrap()),
duration: Some(TimeDelta::seconds(duration)),
protected: row.get::<_, i64>(4).unwrap_or(0) != 0,
})
})?;
let mut pauses = Vec::new();
for pause_result in pause_iter {
pauses.push(pause_result?);
}
let max_gap_secs = Config::read().ok().and_then(|c| c.monitor).map(|m| m.pause_merge_gap as i64).unwrap_or(0);
let pauses = Self::merge_consecutive_pauses(pauses, max_gap_secs);
let pauses = match (&self.min_duration, &self.max_duration) {
(Some(_), _) => {
let min = self.duration_threshold_seconds().unwrap_or(0);
pauses
.into_iter()
.filter(|p| p.protected || p.duration.map(|d| d.num_seconds()).unwrap_or(0) >= min)
.collect()
}
(_, Some(_)) => {
let max = self.duration_threshold_seconds().unwrap_or(0);
pauses
.into_iter()
.filter(|p| !p.protected && p.duration.map(|d| d.num_seconds()).unwrap_or(0) < max)
.collect()
}
_ => pauses,
};
Ok(pauses)
}
pub fn get_workday_pauses(&self, workday: &Workday) -> Result<Vec<Pause>> {
let pauses = self.get_daily_pauses(workday.date)?;
Ok(filter_pauses_to_workday(pauses, workday))
}
pub fn delete(&self, id: i32) -> Result<()> {
let conn_guard = self.conn.lock();
conn_guard.execute(DELETE_PAUSE, params![id])?;
Ok(())
}
pub fn delete_many(&self, ids: &[i32]) -> Result<usize> {
if ids.is_empty() {
return Ok(0);
}
let conn_guard = self.conn.lock();
let mut deleted = 0;
for id in ids {
deleted += conn_guard.execute(DELETE_PAUSE, params![id])?;
}
Ok(deleted)
}
}
pub fn filter_pauses_to_workday(pauses: Vec<Pause>, workday: &Workday) -> Vec<Pause> {
let work_start = workday.start;
let work_end = workday.end;
pauses
.into_iter()
.filter_map(|mut pause| {
if let Some(end) = pause.end {
if end <= work_start {
return None;
}
} else if pause.start < work_start {
return None;
}
if let Some(work_end) = work_end
&& pause.start >= work_end
{
return None;
}
if pause.start < work_start {
pause.start = work_start;
}
if let (Some(end), Some(work_end)) = (pause.end, work_end)
&& end > work_end
{
pause.end = Some(work_end);
}
if let Some(end) = pause.end {
if end <= pause.start {
return None;
}
pause.duration = Some(end - pause.start);
}
Some(pause)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
fn workday(start: &str, end: Option<&str>) -> Workday {
Workday {
id: 1,
date: NaiveDate::from_ymd_opt(2026, 7, 29).unwrap(),
start: NaiveDateTime::parse_from_str(start, "%Y-%m-%d %H:%M:%S").unwrap(),
end: end.map(|e| NaiveDateTime::parse_from_str(e, "%Y-%m-%d %H:%M:%S").unwrap()),
}
}
fn pause(id: i32, start: &str, end: &str) -> Pause {
let start = NaiveDateTime::parse_from_str(start, "%Y-%m-%d %H:%M:%S").unwrap();
let end = NaiveDateTime::parse_from_str(end, "%Y-%m-%d %H:%M:%S").unwrap();
Pause {
id,
start,
end: Some(end),
duration: Some(end - start),
protected: false,
}
}
#[test]
fn drops_pauses_entirely_before_workday_start() {
let wd = workday("2026-07-29 10:04:30", None);
let pauses = vec![pause(1, "2026-07-29 10:02:23", "2026-07-29 10:04:26")];
let filtered = filter_pauses_to_workday(pauses, &wd);
assert!(filtered.is_empty());
}
#[test]
fn drops_pauses_entirely_after_workday_end() {
let wd = workday("2026-07-29 10:00:00", Some("2026-07-29 18:00:00"));
let pauses = vec![pause(1, "2026-07-29 18:30:00", "2026-07-29 18:45:00")];
let filtered = filter_pauses_to_workday(pauses, &wd);
assert!(filtered.is_empty());
}
#[test]
fn clips_pause_that_straddles_workday_start() {
let wd = workday("2026-07-29 10:00:00", Some("2026-07-29 18:00:00"));
let pauses = vec![pause(1, "2026-07-29 09:50:00", "2026-07-29 10:10:00")];
let filtered = filter_pauses_to_workday(pauses, &wd);
assert_eq!(filtered.len(), 1);
assert_eq!(
filtered[0].start,
NaiveDateTime::parse_from_str("2026-07-29 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap()
);
assert_eq!(
filtered[0].end.unwrap(),
NaiveDateTime::parse_from_str("2026-07-29 10:10:00", "%Y-%m-%d %H:%M:%S").unwrap()
);
assert_eq!(filtered[0].duration.unwrap().num_minutes(), 10);
}
#[test]
fn keeps_pause_fully_inside_workday() {
let wd = workday("2026-07-29 10:00:00", Some("2026-07-29 18:00:00"));
let pauses = vec![pause(1, "2026-07-29 12:00:00", "2026-07-29 12:20:00")];
let filtered = filter_pauses_to_workday(pauses, &wd);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].duration.unwrap().num_minutes(), 20);
}
}