1use std::{
2 collections::{BTreeMap, HashMap},
3 sync::Arc,
4};
5
6use getset::{Getters, MutGetters, Setters};
7use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions};
8use tracing::debug;
9use typed_builder::TypedBuilder;
10
11use wildmatch::WildMatch;
12
13use crate::{
14 commands::{
15 hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions,
16 UpdateOptions,
17 },
18 domain::{
19 activity::{
20 Activity, ActivityGroup, ActivityGuid, ActivityItem, ActivityKind, ActivitySession,
21 },
22 category,
23 filter::{ActivityFilterKind, FilterOptions, FilteredActivities},
24 reflection::{SummaryActivityGroup, SummaryGroupByCategory},
25 status::ActivityStatusKind,
26 },
27 error::{ActivityStoreErrorKind, PaceOptResult, PaceResult},
28 storage::{
29 ActivityQuerying, ActivityReadOps, ActivityStateManagement, ActivityStorage,
30 ActivityWriteOps, StorageKind, SyncStorage,
31 },
32};
33
34#[derive(TypedBuilder, Getters, Setters, MutGetters)]
36#[getset(get = "pub", get_mut = "pub", set = "pub")]
37pub struct ActivityStore {
38 cache: ActivityStoreCache,
40
41 storage: Arc<StorageKind>,
43}
44
45#[derive(Debug, TypedBuilder, Getters, Setters, MutGetters, Clone, Eq, PartialEq, Default)]
46#[getset(get = "pub", get_mut = "pub", set = "pub")]
47pub struct ActivityStoreCache {
48 by_start_date: BTreeMap<PaceDate, Vec<ActivityItem>>,
49}
50
51type Category = String;
52
53type Subcategory = Option<String>;
54
55type Description = String;
56
57impl ActivityStore {
58 pub fn with_storage(storage: Arc<StorageKind>) -> PaceResult<Self> {
73 debug!("Creating activity store with storage: {}", storage);
74
75 let mut store = Self {
76 cache: ActivityStoreCache::default(),
77 storage,
78 };
79
80 store.setup_storage()?;
81
82 store.populate_caches()?;
83
84 Ok(store)
85 }
86
87 fn populate_caches(&mut self) -> PaceResult<()> {
99 self.cache.by_start_date = self
100 .storage
101 .group_activities_by_start_date()?
102 .ok_or(ActivityStoreErrorKind::PopulatingCache)?;
103
104 Ok(())
105 }
106
107 #[tracing::instrument(skip(self))]
108 pub fn summary_groups_by_category_for_time_range(
109 &self,
110 filter_opts: FilterOptions,
111 time_range_opts: TimeRangeOptions,
112 ) -> PaceOptResult<SummaryGroupByCategory> {
113 let Some(activity_guids) = self.list_activities_by_time_range(time_range_opts)? else {
114 debug!("No activities found for time range: {:?}", time_range_opts);
115
116 return Ok(None);
117 };
118
119 let mut summary_groups: SummaryGroupByCategory = BTreeMap::new();
121
122 let mut activity_sessions_lookup_by_category: HashMap<
123 (Category, Subcategory, Description),
124 Vec<ActivitySession>,
125 > = HashMap::new();
126
127 let _ = self.end_all_active_intermissions(EndOptions::default())?;
129 let _ = self.end_all_activities(EndOptions::default())?;
130
131 for activity_guid in activity_guids {
132 let activity_item = self.read_activity(activity_guid)?;
133
134 let activity_category = activity_item
135 .activity()
136 .category()
137 .as_deref()
138 .unwrap_or("Uncategorized")
139 .to_string();
140
141 if let Some(category) = filter_opts.category() {
143 let (filter_category, activity_category) = if *filter_opts.case_sensitive() {
144 (category.clone(), activity_category.clone())
145 } else {
146 (category.to_lowercase(), activity_category.to_lowercase())
147 };
148
149 if !WildMatch::new(&filter_category).matches(&activity_category) {
150 continue;
151 }
152 }
153
154 let mut activity_session = ActivitySession::new(activity_item.clone());
155
156 if let Some(intermissions) =
157 self.list_intermissions_for_activity_id(*activity_item.guid())?
158 {
159 activity_session.add_multiple_intermissions(intermissions);
160 };
161
162 let (category, subcategory) =
164 category::split_category_by_category_separator(&activity_category, None);
165
166 _ = activity_sessions_lookup_by_category
168 .entry((
169 category,
170 subcategory,
171 activity_item.activity().description().to_owned(),
172 ))
173 .and_modify(|e| e.push(activity_session.clone()))
174 .or_insert_with(|| vec![activity_session.clone()]);
175 }
176
177 debug!(
178 "Activity sessions lookup by category: {:#?}",
179 activity_sessions_lookup_by_category
180 );
181
182 for ((category, subcategory, description), activity_sessions) in
184 &activity_sessions_lookup_by_category
185 {
186 if activity_sessions.is_empty() {
187 continue;
189 }
190
191 let subcategory = subcategory.clone().unwrap_or_default();
194
195 let activity_group = ActivityGroup::with_multiple_sessions(
197 description.clone(),
198 activity_sessions.clone(),
199 );
200
201 _ = summary_groups
202 .entry((category.clone(), subcategory))
203 .and_modify(|e| e.add_activity_group(activity_group.clone()))
204 .or_insert_with(|| SummaryActivityGroup::with_activity_group(activity_group));
205 }
206
207 Ok(Some(summary_groups))
208 }
209}
210
211impl ActivityStorage for ActivityStore {
212 #[tracing::instrument(skip(self))]
213 fn setup_storage(&self) -> PaceResult<()> {
214 self.storage.setup_storage()
215 }
216}
217
218impl SyncStorage for ActivityStore {
219 #[tracing::instrument(skip(self))]
220 fn sync(&self) -> PaceResult<()> {
221 self.storage.sync()
222 }
223}
224
225impl ActivityReadOps for ActivityStore {
226 #[tracing::instrument(skip(self))]
227 fn read_activity(&self, activity_id: ActivityGuid) -> PaceResult<ActivityItem> {
228 self.storage.read_activity(activity_id)
229 }
230
231 #[tracing::instrument(skip(self))]
232 fn list_activities(&self, filter: ActivityFilterKind) -> PaceOptResult<FilteredActivities> {
233 self.storage.list_activities(filter)
234 }
235}
236
237impl ActivityWriteOps for ActivityStore {
238 #[tracing::instrument(skip(self))]
239 fn create_activity(&self, activity: Activity) -> PaceResult<ActivityItem> {
240 self.storage.create_activity(activity)
241 }
242
243 #[tracing::instrument(skip(self))]
244 fn update_activity(
245 &self,
246 activity_id: ActivityGuid,
247 updated_activity: Activity,
248 update_opts: UpdateOptions,
249 ) -> PaceResult<ActivityItem> {
250 self.storage
251 .update_activity(activity_id, updated_activity, update_opts)
252 }
253
254 #[tracing::instrument(skip(self))]
255 fn delete_activity(
256 &self,
257 activity_id: ActivityGuid,
258 delete_opts: DeleteOptions,
259 ) -> PaceResult<ActivityItem> {
260 self.storage.delete_activity(activity_id, delete_opts)
261 }
262}
263
264impl ActivityStateManagement for ActivityStore {
265 #[tracing::instrument(skip(self))]
266 fn begin_activity(&self, activity: Activity) -> PaceResult<ActivityItem> {
267 self.storage.begin_activity(activity)
268 }
269
270 #[tracing::instrument(skip(self))]
271 fn end_activity(
272 &self,
273 activity_id: ActivityGuid,
274 end_opts: EndOptions,
275 ) -> PaceResult<ActivityItem> {
276 self.storage.end_activity(activity_id, end_opts)
277 }
278
279 #[tracing::instrument(skip(self))]
280 fn end_all_activities(&self, end_opts: EndOptions) -> PaceOptResult<Vec<ActivityItem>> {
281 self.storage.end_all_activities(end_opts)
282 }
283
284 #[tracing::instrument(skip(self))]
285 fn end_last_unfinished_activity(&self, end_opts: EndOptions) -> PaceOptResult<ActivityItem> {
286 self.storage.end_last_unfinished_activity(end_opts)
287 }
288
289 #[tracing::instrument(skip(self))]
290 fn hold_most_recent_active_activity(
291 &self,
292 hold_opts: HoldOptions,
293 ) -> PaceOptResult<ActivityItem> {
294 self.storage.hold_most_recent_active_activity(hold_opts)
295 }
296
297 #[tracing::instrument(skip(self))]
298 fn end_all_active_intermissions(
299 &self,
300 end_opts: EndOptions,
301 ) -> PaceOptResult<Vec<ActivityGuid>> {
302 self.storage.end_all_active_intermissions(end_opts)
303 }
304
305 #[tracing::instrument(skip(self))]
306 fn resume_activity(
307 &self,
308 activity_id: ActivityGuid,
309 resume_opts: ResumeOptions,
310 ) -> PaceResult<ActivityItem> {
311 self.storage.resume_activity(activity_id, resume_opts)
312 }
313
314 #[tracing::instrument(skip(self))]
315 fn hold_activity(
316 &self,
317 activity_id: ActivityGuid,
318 hold_opts: HoldOptions,
319 ) -> PaceResult<ActivityItem> {
320 self.storage.hold_activity(activity_id, hold_opts)
321 }
322
323 #[tracing::instrument(skip(self))]
324 fn resume_most_recent_activity(
325 &self,
326 resume_opts: ResumeOptions,
327 ) -> PaceOptResult<ActivityItem> {
328 self.storage.resume_most_recent_activity(resume_opts)
329 }
330}
331
332impl ActivityQuerying for ActivityStore {
333 #[tracing::instrument(skip(self))]
334 fn list_activities_by_id(&self) -> PaceOptResult<BTreeMap<ActivityGuid, Activity>> {
335 self.storage.list_activities_by_id()
336 }
337
338 #[tracing::instrument(skip(self))]
339 fn group_activities_by_duration_range(
340 &self,
341 ) -> PaceOptResult<BTreeMap<PaceDurationRange, Vec<ActivityItem>>> {
342 self.storage.group_activities_by_duration_range()
343 }
344
345 #[tracing::instrument(skip(self))]
346 fn group_activities_by_start_date(
347 &self,
348 ) -> PaceOptResult<BTreeMap<PaceDate, Vec<ActivityItem>>> {
349 self.storage.group_activities_by_start_date()
350 }
351
352 #[tracing::instrument(skip(self))]
353 fn list_activities_with_intermissions(
354 &self,
355 ) -> PaceOptResult<BTreeMap<ActivityGuid, Vec<ActivityItem>>> {
356 self.storage.list_activities_with_intermissions()
357 }
358
359 #[tracing::instrument(skip(self))]
360 fn group_activities_by_keywords(
361 &self,
362 keyword_opts: KeywordOptions,
363 ) -> PaceOptResult<BTreeMap<String, Vec<ActivityItem>>> {
364 self.storage.group_activities_by_keywords(keyword_opts)
365 }
366
367 #[tracing::instrument(skip(self))]
368 fn group_activities_by_kind(&self) -> PaceOptResult<BTreeMap<ActivityKind, Vec<ActivityItem>>> {
369 self.storage.group_activities_by_kind()
370 }
371
372 #[tracing::instrument(skip(self))]
373 fn list_activities_by_time_range(
374 &self,
375 time_range_opts: TimeRangeOptions,
376 ) -> PaceOptResult<Vec<ActivityGuid>> {
377 self.storage.list_activities_by_time_range(time_range_opts)
378 }
379
380 #[tracing::instrument(skip(self))]
381 fn group_activities_by_status(
382 &self,
383 ) -> PaceOptResult<BTreeMap<ActivityStatusKind, Vec<ActivityItem>>> {
384 self.storage.group_activities_by_status()
385 }
386}