use time::OffsetDateTime;
use super::types::{EntryId, ProjectId, SessionId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntrySource {
Manual,
Hook,
Added,
}
impl EntrySource {
pub fn as_str(&self) -> &'static str {
match self {
Self::Manual => "manual",
Self::Hook => "hook",
Self::Added => "added",
}
}
pub fn from_str_value(s: &str) -> Option<Self> {
match s {
"manual" => Some(Self::Manual),
"hook" => Some(Self::Hook),
"added" => Some(Self::Added),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TimeEntry {
pub id: EntryId,
pub project_id: ProjectId,
pub session_id: Option<SessionId>,
pub start: OffsetDateTime,
pub end: Option<OffsetDateTime>,
pub duration_secs: Option<i64>,
pub source: EntrySource,
pub notes: Option<String>,
pub tags: Vec<String>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
impl TimeEntry {
pub fn is_running(&self) -> bool {
self.end.is_none()
}
pub fn computed_duration_secs(&self) -> Option<i64> {
if let Some(d) = self.duration_secs {
return Some(d);
}
self.end.map(|end| (end - self.start).whole_seconds())
}
}
#[derive(Debug, Default)]
pub struct EntryFilter {
pub project_id: Option<ProjectId>,
pub from: Option<OffsetDateTime>,
pub to: Option<OffsetDateTime>,
pub tags: Vec<String>,
pub source: Option<EntrySource>,
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
fn make_entry(
start: OffsetDateTime,
end: Option<OffsetDateTime>,
duration_secs: Option<i64>,
) -> TimeEntry {
TimeEntry {
id: EntryId::new(),
project_id: ProjectId::new(),
session_id: None,
start,
end,
duration_secs,
source: EntrySource::Manual,
notes: None,
tags: vec![],
created_at: start,
updated_at: start,
}
}
#[test]
fn running_entry_has_no_end() {
let entry = make_entry(datetime!(2026-01-01 9:00 UTC), None, None);
assert!(entry.is_running());
}
#[test]
fn stopped_entry_is_not_running() {
let entry = make_entry(
datetime!(2026-01-01 9:00 UTC),
Some(datetime!(2026-01-01 10:30 UTC)),
None,
);
assert!(!entry.is_running());
}
#[test]
fn computed_duration_from_timestamps() {
let entry = make_entry(
datetime!(2026-01-01 9:00 UTC),
Some(datetime!(2026-01-01 10:30 UTC)),
None,
);
assert_eq!(entry.computed_duration_secs(), Some(5400)); }
#[test]
fn explicit_duration_takes_precedence() {
let entry = make_entry(
datetime!(2026-01-01 0:00 UTC),
None,
Some(9000), );
assert_eq!(entry.computed_duration_secs(), Some(9000));
}
#[test]
fn running_entry_without_duration_returns_none() {
let entry = make_entry(datetime!(2026-01-01 9:00 UTC), None, None);
assert_eq!(entry.computed_duration_secs(), None);
}
}