stint_core/models/
entry.rs1use time::OffsetDateTime;
4
5use super::types::{EntryId, ProjectId, SessionId};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum EntrySource {
10 Manual,
12 Hook,
14 Added,
16}
17
18impl EntrySource {
19 pub fn as_str(&self) -> &'static str {
21 match self {
22 Self::Manual => "manual",
23 Self::Hook => "hook",
24 Self::Added => "added",
25 }
26 }
27
28 pub fn from_str_value(s: &str) -> Option<Self> {
30 match s {
31 "manual" => Some(Self::Manual),
32 "hook" => Some(Self::Hook),
33 "added" => Some(Self::Added),
34 _ => None,
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct TimeEntry {
42 pub id: EntryId,
44 pub project_id: ProjectId,
46 pub session_id: Option<SessionId>,
48 pub start: OffsetDateTime,
50 pub end: Option<OffsetDateTime>,
52 pub duration_secs: Option<i64>,
54 pub source: EntrySource,
56 pub notes: Option<String>,
58 pub tags: Vec<String>,
60 pub created_at: OffsetDateTime,
62 pub updated_at: OffsetDateTime,
64}
65
66impl TimeEntry {
67 pub fn is_running(&self) -> bool {
69 self.end.is_none()
70 }
71
72 pub fn computed_duration_secs(&self) -> Option<i64> {
77 if let Some(d) = self.duration_secs {
78 return Some(d);
79 }
80 self.end.map(|end| (end - self.start).whole_seconds())
81 }
82}
83
84#[derive(Debug, Default)]
86pub struct EntryFilter {
87 pub project_id: Option<ProjectId>,
89 pub from: Option<OffsetDateTime>,
91 pub to: Option<OffsetDateTime>,
93 pub tags: Vec<String>,
95 pub source: Option<EntrySource>,
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use time::macros::datetime;
103
104 fn make_entry(
105 start: OffsetDateTime,
106 end: Option<OffsetDateTime>,
107 duration_secs: Option<i64>,
108 ) -> TimeEntry {
109 TimeEntry {
110 id: EntryId::new(),
111 project_id: ProjectId::new(),
112 session_id: None,
113 start,
114 end,
115 duration_secs,
116 source: EntrySource::Manual,
117 notes: None,
118 tags: vec![],
119 created_at: start,
120 updated_at: start,
121 }
122 }
123
124 #[test]
125 fn running_entry_has_no_end() {
126 let entry = make_entry(datetime!(2026-01-01 9:00 UTC), None, None);
127 assert!(entry.is_running());
128 }
129
130 #[test]
131 fn stopped_entry_is_not_running() {
132 let entry = make_entry(
133 datetime!(2026-01-01 9:00 UTC),
134 Some(datetime!(2026-01-01 10:30 UTC)),
135 None,
136 );
137 assert!(!entry.is_running());
138 }
139
140 #[test]
141 fn computed_duration_from_timestamps() {
142 let entry = make_entry(
143 datetime!(2026-01-01 9:00 UTC),
144 Some(datetime!(2026-01-01 10:30 UTC)),
145 None,
146 );
147 assert_eq!(entry.computed_duration_secs(), Some(5400)); }
149
150 #[test]
151 fn explicit_duration_takes_precedence() {
152 let entry = make_entry(
153 datetime!(2026-01-01 0:00 UTC),
154 None,
155 Some(9000), );
157 assert_eq!(entry.computed_duration_secs(), Some(9000));
158 }
159
160 #[test]
161 fn running_entry_without_duration_returns_none() {
162 let entry = make_entry(datetime!(2026-01-01 9:00 UTC), None, None);
163 assert_eq!(entry.computed_duration_secs(), None);
164 }
165}