use crate::db::jira_inbox::{JiraInboxItem, LeftReason};
use crate::libs::output::moment;
use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
use serde::Serialize;
pub fn percent(value: f64) -> f64 {
(value * 10.0).round() / 10.0
}
pub fn seconds(duration: &Duration) -> i64 {
duration.num_seconds()
}
#[derive(Debug, Clone, Serialize)]
pub struct Task {
pub id: Option<i32>,
pub chain_id: Option<i32>,
pub created_at: Option<String>,
pub name: String,
pub comment: String,
pub completeness: i32,
pub tags: Vec<String>,
pub jira_key: Option<String>,
}
impl From<&crate::libs::task::Task> for Task {
fn from(task: &crate::libs::task::Task) -> Self {
Task {
id: task.id,
chain_id: task.task_id,
created_at: task.timestamp.as_ref().map(|at| at.replacen(' ', "T", 1)),
name: task.name.clone(),
comment: task.comment.clone(),
completeness: task.completeness.unwrap_or(100),
tags: task.tags.iter().map(|tag| tag.name.clone()).collect(),
jira_key: task.jira_key.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Tasks {
pub tasks: Vec<Task>,
}
impl Tasks {
pub fn of(tasks: &[crate::libs::task::Task]) -> Self {
Tasks {
tasks: tasks.iter().map(Task::from).collect(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Interval {
#[serde(serialize_with = "moment::local")]
pub start: NaiveDateTime,
#[serde(serialize_with = "moment::local")]
pub end: NaiveDateTime,
pub seconds: i64,
}
impl From<&crate::libs::report::WorkInterval> for Interval {
fn from(interval: &crate::libs::report::WorkInterval) -> Self {
Interval {
start: interval.start,
end: interval.end,
seconds: seconds(&interval.duration),
}
}
}
#[derive(Debug, Serialize)]
pub struct Workday {
#[serde(serialize_with = "moment::local")]
pub start: NaiveDateTime,
#[serde(serialize_with = "moment::local_opt")]
pub end: Option<NaiveDateTime>,
}
#[derive(Debug, Default, Serialize)]
pub struct Dropped {
pub count: usize,
pub seconds: i64,
}
#[derive(Debug, Default, Serialize)]
pub struct InboxCounts {
pub total: i64,
pub fresh: i64,
pub taken: i64,
}
impl From<crate::db::jira_inbox::InboxCounts> for InboxCounts {
fn from(counts: crate::db::jira_inbox::InboxCounts) -> Self {
InboxCounts {
total: counts.total,
fresh: counts.fresh,
taken: counts.taken,
}
}
}
#[derive(Debug, Serialize)]
pub struct Day {
pub date: NaiveDate,
pub workday: Option<Workday>,
pub intervals: Vec<Interval>,
pub work_seconds: i64,
pub productivity: Option<f64>,
pub below_threshold: bool,
pub dropped: Dropped,
pub tasks: Vec<Task>,
pub inbox: InboxCounts,
}
#[derive(Debug, Serialize)]
pub struct MonthDay {
pub date: NaiveDate,
pub work_seconds: i64,
pub productivity: f64,
pub rest_day: bool,
}
#[derive(Debug, Serialize)]
pub struct Month {
pub month: String,
pub days: Vec<MonthDay>,
pub total_seconds: i64,
pub average_seconds: i64,
pub productivity: Option<f64>,
}
#[derive(Debug, Serialize)]
pub struct Pause {
pub id: i32,
#[serde(serialize_with = "moment::local")]
pub start: NaiveDateTime,
#[serde(serialize_with = "moment::local_opt")]
pub end: Option<NaiveDateTime>,
pub seconds: Option<i64>,
pub kept: bool,
}
impl From<&crate::libs::pause::Pause> for Pause {
fn from(pause: &crate::libs::pause::Pause) -> Self {
Pause {
id: pause.id,
start: pause.start,
end: pause.end,
seconds: pause.duration.as_ref().map(seconds),
kept: pause.protected,
}
}
}
#[derive(Debug, Serialize)]
pub struct Pauses {
pub date: NaiveDate,
pub min_minutes: u64,
pub pauses: Vec<Pause>,
pub total_seconds: i64,
}
#[derive(Debug, Serialize)]
pub struct Tag {
pub id: Option<i32>,
pub name: String,
pub color: Option<String>,
}
impl From<&crate::db::tags::Tag> for Tag {
fn from(tag: &crate::db::tags::Tag) -> Self {
Tag {
id: tag.id,
name: tag.name.clone(),
color: tag.color.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Tags {
pub tags: Vec<Tag>,
}
#[derive(Debug, Serialize)]
pub struct TagTasks {
pub tag: Tag,
pub tasks: Vec<Task>,
}
#[derive(Debug, Serialize)]
pub struct Template {
pub name: String,
pub task_name: String,
pub comment: String,
pub completeness: i32,
}
impl From<&crate::db::templates::TaskTemplate> for Template {
fn from(template: &crate::db::templates::TaskTemplate) -> Self {
Template {
name: template.name.clone(),
task_name: template.task_name.clone(),
comment: template.comment.clone(),
completeness: template.completeness,
}
}
}
#[derive(Debug, Serialize)]
pub struct TemplateShown {
pub template: Template,
}
#[derive(Debug, Serialize)]
pub struct Templates {
pub templates: Vec<Template>,
}
impl Templates {
pub fn of(templates: &[crate::db::templates::TaskTemplate]) -> Self {
Templates {
templates: templates.iter().map(Template::from).collect(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Issue {
pub key: String,
pub summary: String,
pub status: Option<String>,
pub status_id: Option<String>,
pub priority: Option<String>,
pub score: Option<f64>,
#[serde(rename = "type")]
pub issue_type: Option<String>,
pub project: Option<String>,
pub url: String,
pub pinned: bool,
pub dismissed: bool,
pub ruled_by: Option<String>,
pub badge: Option<String>,
#[serde(serialize_with = "moment::local")]
pub first_seen: NaiveDateTime,
#[serde(serialize_with = "moment::local")]
pub last_seen: NaiveDateTime,
#[serde(serialize_with = "moment::local_opt")]
pub changed_at: Option<NaiveDateTime>,
#[serde(serialize_with = "moment::local_opt")]
pub taken_at: Option<NaiveDateTime>,
#[serde(serialize_with = "moment::local_opt")]
pub snoozed_until: Option<NaiveDateTime>,
#[serde(serialize_with = "moment::local_opt")]
pub gone_at: Option<NaiveDateTime>,
pub left: Option<&'static str>,
pub resolution: Option<String>,
}
impl Issue {
pub fn at(item: &JiraInboxItem, now: NaiveDateTime) -> Self {
Issue {
key: item.issue_key.clone(),
summary: item.summary.clone(),
status: Some(item.status_name.clone()).filter(|name| !name.is_empty()),
status_id: item.status_id.clone(),
priority: item.priority.clone(),
score: item.sort_value,
issue_type: item.issue_type.clone(),
project: item.project.clone(),
url: item.url.clone(),
pinned: item.pinned,
dismissed: item.dismissed,
ruled_by: item.ruled_by.clone(),
badge: item.badge(now),
first_seen: item.first_seen,
last_seen: item.last_seen,
changed_at: item.changed_at,
taken_at: item.taken_at,
snoozed_until: item.snoozed_until,
gone_at: item.gone_at,
left: item.left_reason.map(|reason| match reason {
LeftReason::Resolved => "resolved",
LeftReason::Moved => "moved",
}),
resolution: item.resolution.clone(),
}
}
pub fn now(item: &JiraInboxItem) -> Self {
Self::at(item, Local::now().naive_local())
}
}
#[derive(Debug, Serialize)]
pub struct InboxList {
pub issues: Vec<Issue>,
pub total: usize,
pub filter: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Reason {
pub what: String,
pub value: String,
pub because: String,
}
impl From<&crate::libs::inbox_filter::Reason> for Reason {
fn from(reason: &crate::libs::inbox_filter::Reason) -> Self {
Reason {
what: reason.what.to_string(),
value: reason.value.clone(),
because: reason.because.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct IssueShown {
pub issue: Issue,
pub why: Option<Vec<Reason>>,
}
#[derive(Debug, Serialize)]
pub struct Rule {
pub number: usize,
pub name: Option<String>,
pub when: String,
pub then: String,
pub matches: String,
pub waiting: usize,
}
impl From<&crate::libs::inbox_rules::RuleStanding> for Rule {
fn from(standing: &crate::libs::inbox_rules::RuleStanding) -> Self {
Rule {
number: standing.number,
name: standing.name.clone(),
when: standing.when.clone(),
then: standing.then.clone(),
matches: standing.now.clone(),
waiting: standing.waiting,
}
}
}
#[derive(Debug, Serialize)]
pub struct RuleSkipped {
pub number: usize,
pub reason: String,
}
#[derive(Debug, Serialize)]
pub struct RuleActed {
pub key: String,
pub pinned: Option<String>,
pub dismissed: Option<String>,
pub urgent: Option<String>,
pub woke: bool,
}
impl From<&crate::libs::inbox_rules::Ruled> for RuleActed {
fn from(ruled: &crate::libs::inbox_rules::Ruled) -> Self {
RuleActed {
key: ruled.issue_key.clone(),
pinned: ruled.pinned.clone(),
dismissed: ruled.dismissed.clone(),
urgent: ruled.urgent.clone(),
woke: ruled.woke,
}
}
}
#[derive(Debug, Serialize)]
pub struct Rules {
pub rules: Vec<Rule>,
pub skipped: Vec<RuleSkipped>,
pub applied: Option<Vec<RuleActed>>,
}
#[derive(Debug, Serialize)]
pub struct OwedDay {
pub date: NaiveDate,
#[serde(serialize_with = "moment::local")]
pub queued_at: NaiveDateTime,
pub attempts: i32,
#[serde(serialize_with = "moment::local_opt")]
pub last_attempt_at: Option<NaiveDateTime>,
pub last_error: Option<String>,
}
impl From<&crate::db::server_outbox::OwedDay> for OwedDay {
fn from(day: &crate::db::server_outbox::OwedDay) -> Self {
OwedDay {
date: day.date,
queued_at: day.queued_at,
attempts: day.attempts,
last_attempt_at: day.last_attempt_at,
last_error: day.last_error.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Queue {
pub days: Vec<OwedDay>,
}
#[derive(Debug, Serialize)]
pub struct Pulse {
pub enabled: bool,
#[serde(serialize_with = "moment::utc_opt")]
pub sent_at: Option<chrono::DateTime<chrono::Utc>>,
pub state: Option<&'static str>,
#[serde(serialize_with = "moment::utc_opt")]
pub attempted_at: Option<chrono::DateTime<chrono::Utc>>,
pub error: Option<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ServerStatus {
pub connected: bool,
pub url: Option<String>,
pub token: Option<bool>,
pub reachable: Option<bool>,
pub server_version: Option<String>,
pub api_version: Option<String>,
pub user: Option<String>,
pub agent: Option<String>,
pub problem: Option<String>,
pub pulse: Option<Pulse>,
}
#[derive(Debug, Serialize)]
pub struct Stored {
pub what: String,
pub detail: String,
}
#[derive(Debug, Serialize)]
pub struct Manifest {
pub level: String,
pub summary: String,
pub stored: Vec<Stored>,
pub never_collected: Vec<String>,
pub visible_to: Vec<String>,
pub retention: String,
pub on_change: String,
#[serde(serialize_with = "moment::utc_opt")]
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl From<crate::api::kasl_server::PrivacyManifest> for Manifest {
fn from(manifest: crate::api::kasl_server::PrivacyManifest) -> Self {
Manifest {
level: manifest.level,
summary: manifest.summary,
stored: manifest
.stored
.into_iter()
.map(|stored| Stored {
what: stored.what,
detail: stored.detail,
})
.collect(),
never_collected: manifest.never_collected,
visible_to: manifest.visible_to,
retention: manifest.retention,
on_change: manifest.on_change,
updated_at: manifest.updated_at,
}
}
}
#[derive(Debug, Serialize)]
pub struct Autostart {
pub enabled: bool,
}
#[derive(Debug, Serialize)]
pub struct Field {
pub name: String,
pub value: String,
}
#[derive(Debug, Serialize)]
pub struct Payload {
pub url: String,
pub date: NaiveDate,
pub fields: Vec<Field>,
pub tasks: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moments_are_whole_local_seconds_with_a_t() {
let at = NaiveDate::from_ymd_opt(2026, 10, 15).unwrap().and_hms_nano_opt(9, 1, 2, 345_678_901).unwrap();
let json = serde_json::to_value(Workday { start: at, end: None }).unwrap();
assert_eq!(json, serde_json::json!({ "start": "2026-10-15T09:01:02", "end": null }));
}
#[test]
fn a_task_timestamp_takes_the_t_and_a_task_without_completeness_reads_done() {
let mut task = crate::libs::task::Task::new("Ship", "", None);
task.timestamp = Some("2026-10-15 09:01:02".to_string());
task.completeness = None;
let json = Task::from(&task);
assert_eq!(json.created_at.as_deref(), Some("2026-10-15T09:01:02"));
assert_eq!(json.completeness, 100);
}
#[test]
fn percentages_keep_one_decimal() {
assert_eq!(percent(87.345), 87.3);
assert_eq!(percent(100.0), 100.0);
}
}