1use crate::db::jira_inbox::{JiraInboxItem, LeftReason};
20use crate::libs::output::moment;
21use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
22use serde::Serialize;
23
24pub fn percent(value: f64) -> f64 {
26 (value * 10.0).round() / 10.0
27}
28
29pub fn seconds(duration: &Duration) -> i64 {
31 duration.num_seconds()
32}
33
34#[derive(Debug, Clone, Serialize)]
36pub struct Task {
37 pub id: Option<i32>,
38 pub chain_id: Option<i32>,
41 pub created_at: Option<String>,
43 pub name: String,
44 pub comment: String,
45 pub completeness: i32,
47 pub tags: Vec<String>,
48 pub jira_key: Option<String>,
50}
51
52impl From<&crate::libs::task::Task> for Task {
53 fn from(task: &crate::libs::task::Task) -> Self {
54 Task {
55 id: task.id,
56 chain_id: task.task_id,
57 created_at: task.timestamp.as_ref().map(|at| at.replacen(' ', "T", 1)),
59 name: task.name.clone(),
60 comment: task.comment.clone(),
61 completeness: task.completeness.unwrap_or(100),
62 tags: task.tags.iter().map(|tag| tag.name.clone()).collect(),
63 jira_key: task.jira_key.clone(),
64 }
65 }
66}
67
68#[derive(Debug, Serialize)]
70pub struct Tasks {
71 pub tasks: Vec<Task>,
72}
73
74impl Tasks {
75 pub fn of(tasks: &[crate::libs::task::Task]) -> Self {
76 Tasks {
77 tasks: tasks.iter().map(Task::from).collect(),
78 }
79 }
80}
81
82#[derive(Debug, Serialize)]
84pub struct Interval {
85 #[serde(serialize_with = "moment::local")]
86 pub start: NaiveDateTime,
87 #[serde(serialize_with = "moment::local")]
88 pub end: NaiveDateTime,
89 pub seconds: i64,
90}
91
92impl From<&crate::libs::report::WorkInterval> for Interval {
93 fn from(interval: &crate::libs::report::WorkInterval) -> Self {
94 Interval {
95 start: interval.start,
96 end: interval.end,
97 seconds: seconds(&interval.duration),
98 }
99 }
100}
101
102#[derive(Debug, Serialize)]
104pub struct Workday {
105 #[serde(serialize_with = "moment::local")]
106 pub start: NaiveDateTime,
107 #[serde(serialize_with = "moment::local_opt")]
108 pub end: Option<NaiveDateTime>,
109}
110
111#[derive(Debug, Default, Serialize)]
113pub struct Dropped {
114 pub count: usize,
115 pub seconds: i64,
116}
117
118#[derive(Debug, Default, Serialize)]
120pub struct InboxCounts {
121 pub total: i64,
122 pub fresh: i64,
123 pub taken: i64,
124}
125
126impl From<crate::db::jira_inbox::InboxCounts> for InboxCounts {
127 fn from(counts: crate::db::jira_inbox::InboxCounts) -> Self {
128 InboxCounts {
129 total: counts.total,
130 fresh: counts.fresh,
131 taken: counts.taken,
132 }
133 }
134}
135
136#[derive(Debug, Serialize)]
138pub struct Day {
139 pub date: NaiveDate,
140 pub workday: Option<Workday>,
142 pub intervals: Vec<Interval>,
144 pub work_seconds: i64,
146 pub productivity: Option<f64>,
147 pub below_threshold: bool,
149 pub dropped: Dropped,
151 pub tasks: Vec<Task>,
152 pub inbox: InboxCounts,
153}
154
155#[derive(Debug, Serialize)]
157pub struct MonthDay {
158 pub date: NaiveDate,
159 pub work_seconds: i64,
160 pub productivity: f64,
161 pub rest_day: bool,
163}
164
165#[derive(Debug, Serialize)]
167pub struct Month {
168 pub month: String,
170 pub days: Vec<MonthDay>,
171 pub total_seconds: i64,
172 pub average_seconds: i64,
173 pub productivity: Option<f64>,
175}
176
177#[derive(Debug, Serialize)]
179pub struct Pause {
180 pub id: i32,
181 #[serde(serialize_with = "moment::local")]
182 pub start: NaiveDateTime,
183 #[serde(serialize_with = "moment::local_opt")]
185 pub end: Option<NaiveDateTime>,
186 pub seconds: Option<i64>,
187 pub kept: bool,
189}
190
191impl From<&crate::libs::pause::Pause> for Pause {
192 fn from(pause: &crate::libs::pause::Pause) -> Self {
193 Pause {
194 id: pause.id,
195 start: pause.start,
196 end: pause.end,
197 seconds: pause.duration.as_ref().map(seconds),
198 kept: pause.protected,
199 }
200 }
201}
202
203#[derive(Debug, Serialize)]
205pub struct Pauses {
206 pub date: NaiveDate,
207 pub min_minutes: u64,
209 pub pauses: Vec<Pause>,
210 pub total_seconds: i64,
211}
212
213#[derive(Debug, Serialize)]
215pub struct Tag {
216 pub id: Option<i32>,
217 pub name: String,
218 pub color: Option<String>,
219}
220
221impl From<&crate::db::tags::Tag> for Tag {
222 fn from(tag: &crate::db::tags::Tag) -> Self {
223 Tag {
224 id: tag.id,
225 name: tag.name.clone(),
226 color: tag.color.clone(),
227 }
228 }
229}
230
231#[derive(Debug, Serialize)]
233pub struct Tags {
234 pub tags: Vec<Tag>,
235}
236
237#[derive(Debug, Serialize)]
239pub struct TagTasks {
240 pub tag: Tag,
241 pub tasks: Vec<Task>,
242}
243
244#[derive(Debug, Serialize)]
246pub struct Template {
247 pub name: String,
248 pub task_name: String,
249 pub comment: String,
250 pub completeness: i32,
251}
252
253impl From<&crate::db::templates::TaskTemplate> for Template {
254 fn from(template: &crate::db::templates::TaskTemplate) -> Self {
255 Template {
256 name: template.name.clone(),
257 task_name: template.task_name.clone(),
258 comment: template.comment.clone(),
259 completeness: template.completeness,
260 }
261 }
262}
263
264#[derive(Debug, Serialize)]
266pub struct TemplateShown {
267 pub template: Template,
268}
269
270#[derive(Debug, Serialize)]
272pub struct Templates {
273 pub templates: Vec<Template>,
274}
275
276impl Templates {
277 pub fn of(templates: &[crate::db::templates::TaskTemplate]) -> Self {
278 Templates {
279 templates: templates.iter().map(Template::from).collect(),
280 }
281 }
282}
283
284#[derive(Debug, Serialize)]
290pub struct Issue {
291 pub key: String,
292 pub summary: String,
293 pub status: Option<String>,
295 pub status_id: Option<String>,
296 pub priority: Option<String>,
297 pub score: Option<f64>,
299 #[serde(rename = "type")]
300 pub issue_type: Option<String>,
301 pub project: Option<String>,
302 pub url: String,
303 pub pinned: bool,
304 pub dismissed: bool,
305 pub ruled_by: Option<String>,
307 pub badge: Option<String>,
309 #[serde(serialize_with = "moment::local")]
310 pub first_seen: NaiveDateTime,
311 #[serde(serialize_with = "moment::local")]
312 pub last_seen: NaiveDateTime,
313 #[serde(serialize_with = "moment::local_opt")]
314 pub changed_at: Option<NaiveDateTime>,
315 #[serde(serialize_with = "moment::local_opt")]
316 pub taken_at: Option<NaiveDateTime>,
317 #[serde(serialize_with = "moment::local_opt")]
318 pub snoozed_until: Option<NaiveDateTime>,
319 #[serde(serialize_with = "moment::local_opt")]
320 pub gone_at: Option<NaiveDateTime>,
321 pub left: Option<&'static str>,
323 pub resolution: Option<String>,
325}
326
327impl Issue {
328 pub fn at(item: &JiraInboxItem, now: NaiveDateTime) -> Self {
330 Issue {
331 key: item.issue_key.clone(),
332 summary: item.summary.clone(),
333 status: Some(item.status_name.clone()).filter(|name| !name.is_empty()),
334 status_id: item.status_id.clone(),
335 priority: item.priority.clone(),
336 score: item.sort_value,
337 issue_type: item.issue_type.clone(),
338 project: item.project.clone(),
339 url: item.url.clone(),
340 pinned: item.pinned,
341 dismissed: item.dismissed,
342 ruled_by: item.ruled_by.clone(),
343 badge: item.badge(now),
344 first_seen: item.first_seen,
345 last_seen: item.last_seen,
346 changed_at: item.changed_at,
347 taken_at: item.taken_at,
348 snoozed_until: item.snoozed_until,
349 gone_at: item.gone_at,
350 left: item.left_reason.map(|reason| match reason {
351 LeftReason::Resolved => "resolved",
352 LeftReason::Moved => "moved",
353 }),
354 resolution: item.resolution.clone(),
355 }
356 }
357
358 pub fn now(item: &JiraInboxItem) -> Self {
360 Self::at(item, Local::now().naive_local())
361 }
362}
363
364#[derive(Debug, Serialize)]
366pub struct InboxList {
367 pub issues: Vec<Issue>,
369 pub total: usize,
372 pub filter: Vec<String>,
374}
375
376#[derive(Debug, Serialize)]
378pub struct Reason {
379 pub what: String,
380 pub value: String,
381 pub because: String,
382}
383
384impl From<&crate::libs::inbox_filter::Reason> for Reason {
385 fn from(reason: &crate::libs::inbox_filter::Reason) -> Self {
386 Reason {
387 what: reason.what.to_string(),
388 value: reason.value.clone(),
389 because: reason.because.clone(),
390 }
391 }
392}
393
394#[derive(Debug, Serialize)]
396pub struct IssueShown {
397 pub issue: Issue,
398 pub why: Option<Vec<Reason>>,
400}
401
402#[derive(Debug, Serialize)]
404pub struct Rule {
405 pub number: usize,
407 pub name: Option<String>,
408 pub when: String,
410 pub then: String,
412 pub matches: String,
414 pub waiting: usize,
416}
417
418impl From<&crate::libs::inbox_rules::RuleStanding> for Rule {
419 fn from(standing: &crate::libs::inbox_rules::RuleStanding) -> Self {
420 Rule {
421 number: standing.number,
422 name: standing.name.clone(),
423 when: standing.when.clone(),
424 then: standing.then.clone(),
425 matches: standing.now.clone(),
426 waiting: standing.waiting,
427 }
428 }
429}
430
431#[derive(Debug, Serialize)]
433pub struct RuleSkipped {
434 pub number: usize,
435 pub reason: String,
436}
437
438#[derive(Debug, Serialize)]
440pub struct RuleActed {
441 pub key: String,
442 pub pinned: Option<String>,
444 pub dismissed: Option<String>,
446 pub urgent: Option<String>,
448 pub woke: bool,
450}
451
452impl From<&crate::libs::inbox_rules::Ruled> for RuleActed {
453 fn from(ruled: &crate::libs::inbox_rules::Ruled) -> Self {
454 RuleActed {
455 key: ruled.issue_key.clone(),
456 pinned: ruled.pinned.clone(),
457 dismissed: ruled.dismissed.clone(),
458 urgent: ruled.urgent.clone(),
459 woke: ruled.woke,
460 }
461 }
462}
463
464#[derive(Debug, Serialize)]
466pub struct Rules {
467 pub rules: Vec<Rule>,
468 pub skipped: Vec<RuleSkipped>,
469 pub applied: Option<Vec<RuleActed>>,
471}
472
473#[derive(Debug, Serialize)]
475pub struct OwedDay {
476 pub date: NaiveDate,
477 #[serde(serialize_with = "moment::local")]
478 pub queued_at: NaiveDateTime,
479 pub attempts: i32,
480 #[serde(serialize_with = "moment::local_opt")]
481 pub last_attempt_at: Option<NaiveDateTime>,
482 pub last_error: Option<String>,
483}
484
485impl From<&crate::db::server_outbox::OwedDay> for OwedDay {
486 fn from(day: &crate::db::server_outbox::OwedDay) -> Self {
487 OwedDay {
488 date: day.date,
489 queued_at: day.queued_at,
490 attempts: day.attempts,
491 last_attempt_at: day.last_attempt_at,
492 last_error: day.last_error.clone(),
493 }
494 }
495}
496
497#[derive(Debug, Serialize)]
499pub struct Queue {
500 pub days: Vec<OwedDay>,
501}
502
503#[derive(Debug, Serialize)]
505pub struct Pulse {
506 pub enabled: bool,
507 #[serde(serialize_with = "moment::utc_opt")]
509 pub sent_at: Option<chrono::DateTime<chrono::Utc>>,
510 pub state: Option<&'static str>,
512 #[serde(serialize_with = "moment::utc_opt")]
514 pub attempted_at: Option<chrono::DateTime<chrono::Utc>>,
515 pub error: Option<String>,
517 pub warnings: Vec<String>,
519}
520
521#[derive(Debug, Serialize)]
526pub struct ServerStatus {
527 pub connected: bool,
528 pub url: Option<String>,
529 pub token: Option<bool>,
531 pub reachable: Option<bool>,
533 pub server_version: Option<String>,
534 pub api_version: Option<String>,
535 pub user: Option<String>,
537 pub agent: Option<String>,
538 pub problem: Option<String>,
540 pub pulse: Option<Pulse>,
542}
543
544#[derive(Debug, Serialize)]
546pub struct Stored {
547 pub what: String,
548 pub detail: String,
549}
550
551#[derive(Debug, Serialize)]
553pub struct Manifest {
554 pub level: String,
555 pub summary: String,
556 pub stored: Vec<Stored>,
557 pub never_collected: Vec<String>,
558 pub visible_to: Vec<String>,
559 pub retention: String,
560 pub on_change: String,
561 #[serde(serialize_with = "moment::utc_opt")]
562 pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
563}
564
565impl From<crate::api::kasl_server::PrivacyManifest> for Manifest {
566 fn from(manifest: crate::api::kasl_server::PrivacyManifest) -> Self {
567 Manifest {
568 level: manifest.level,
569 summary: manifest.summary,
570 stored: manifest
571 .stored
572 .into_iter()
573 .map(|stored| Stored {
574 what: stored.what,
575 detail: stored.detail,
576 })
577 .collect(),
578 never_collected: manifest.never_collected,
579 visible_to: manifest.visible_to,
580 retention: manifest.retention,
581 on_change: manifest.on_change,
582 updated_at: manifest.updated_at,
583 }
584 }
585}
586
587#[derive(Debug, Serialize)]
589pub struct Autostart {
590 pub enabled: bool,
591}
592
593#[derive(Debug, Serialize)]
595pub struct Field {
596 pub name: String,
597 pub value: String,
598}
599
600#[derive(Debug, Serialize)]
602pub struct Payload {
603 pub url: String,
604 pub date: NaiveDate,
605 pub fields: Vec<Field>,
607 pub tasks: serde_json::Value,
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn moments_are_whole_local_seconds_with_a_t() {
617 let at = NaiveDate::from_ymd_opt(2026, 10, 15).unwrap().and_hms_nano_opt(9, 1, 2, 345_678_901).unwrap();
618 let json = serde_json::to_value(Workday { start: at, end: None }).unwrap();
619 assert_eq!(json, serde_json::json!({ "start": "2026-10-15T09:01:02", "end": null }));
620 }
621
622 #[test]
623 fn a_task_timestamp_takes_the_t_and_a_task_without_completeness_reads_done() {
624 let mut task = crate::libs::task::Task::new("Ship", "", None);
625 task.timestamp = Some("2026-10-15 09:01:02".to_string());
626 task.completeness = None;
627 let json = Task::from(&task);
628 assert_eq!(json.created_at.as_deref(), Some("2026-10-15T09:01:02"));
629 assert_eq!(json.completeness, 100);
630 }
631
632 #[test]
633 fn percentages_keep_one_decimal() {
634 assert_eq!(percent(87.345), 87.3);
635 assert_eq!(percent(100.0), 100.0);
636 }
637}