feat: redo ui

This commit is contained in:
kjuulh 2025-03-25 20:53:07 +01:00
parent 57512a70ac
commit 1c68f67c3f
2 changed files with 56 additions and 100 deletions

View File

@ -1,6 +1,4 @@
use std::collections::BTreeMap; use chrono::Timelike;
use chrono::NaiveDate;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@ -60,92 +58,54 @@ async fn main() -> anyhow::Result<()> {
match cli.command.expect("to have a command available") { match cli.command.expect("to have a command available") {
Commands::List { limit, project } => { Commands::List { limit, project } => {
let days = timetable.group_by_day(); let days = &timetable
let days = days.iter().rev().take(limit).collect::<Vec<(_, _)>>(); .days
for (day, pairs) in days.iter() {
let hours = pairs
.iter() .iter()
.fold( .filter(|d| {
(chrono::Duration::default(), None), if let Some(project) = &project {
|(total, last_in), ev| match ev.r#type { Some(project) == d.project.as_ref()
InOut::In => (total, Some(ev)),
InOut::Out => {
if let Some(in_time) = last_in {
if in_time.project == project {
(total + (ev.timestamp - in_time.timestamp), None)
} else { } else {
(total, None) true
} }
} else { })
(total, None) .collect::<Vec<_>>();
} let days = days.iter().rev().take(limit).collect::<Vec<_>>();
}
InOut::Break => (total, last_in),
},
)
.0;
let break_time =
pairs
.iter()
.fold(chrono::TimeDelta::zero(), |acc, e| match e.r#type {
InOut::Break => acc + chrono::Duration::minutes(30),
_ => acc,
});
for day in days {
println!( println!(
"{}: {}h{}m{} mins\n {}", "day: {}{}\n {}:{}{}",
day, day.clock_in.format("%Y/%m/%d"),
hours.num_hours(), if let Some(project) = &day.project {
hours.num_minutes() % 60, format!(" project: {}", project)
if break_time.num_minutes() > 0 {
format!(", break: {}", break_time.num_minutes())
} else { } else {
"".into() "".into()
}, },
pairs day.clock_in.hour(),
.iter() day.clock_in.minute(),
.map(|d| format!( if let Some(clockout) = &day.clock_out {
"{} - {}{}", format!(" - {}:{}", clockout.hour(), clockout.minute())
d.timestamp.with_timezone(&chrono::Local).format("%H:%M"),
match d.r#type {
InOut::In => "clocked in ",
InOut::Out => "clocked out",
InOut::Break => "break",
},
if let Some(project) = &d.project {
format!(" - project: {}", project)
} else { } else {
"".into() " - unclosed".into()
} }
)) )
.collect::<Vec<String>>()
.join("\n ")
);
} }
} }
Commands::Break { project } => {
timetable.days.push(Day {
timestamp: now,
r#type: InOut::Break,
project,
});
}
Commands::In { project } => { Commands::In { project } => {
timetable.days.push(Day { timetable.days.push(Day {
timestamp: now, clock_in: now,
r#type: InOut::In, clock_out: None,
project, breaks: Vec::default(),
});
}
Commands::Out { project } => {
timetable.days.push(Day {
timestamp: now,
r#type: InOut::Out,
project, project,
}); });
} }
Commands::Out { project } => match timetable.get_day(project, now) {
Some(day) => day.clock_out = Some(now),
None => todo!(),
},
Commands::Break { project } => match timetable.get_day(project, now) {
Some(day) => day.breaks.push(Break {}),
None => todo!(),
},
} }
if let Some(parent) = dir.parent() { if let Some(parent) = dir.parent() {
@ -160,19 +120,16 @@ async fn main() -> anyhow::Result<()> {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
struct Day { struct Day {
timestamp: chrono::DateTime<chrono::Utc>, clock_in: chrono::DateTime<chrono::Utc>,
#[serde(rename = "type")] clock_out: Option<chrono::DateTime<chrono::Utc>>,
r#type: InOut,
breaks: Vec<Break>,
project: Option<String>, project: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Default, Clone, Debug, Serialize, Deserialize)]
enum InOut { struct Break {}
In,
Out,
Break,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[derive(Default, Clone, Debug, Serialize, Deserialize)]
struct TimeTable { struct TimeTable {
@ -180,21 +137,19 @@ struct TimeTable {
} }
impl TimeTable { impl TimeTable {
/// Groups entries by calendar day in ascending order by timestamp pub fn get_day<'a>(
pub fn group_by_day(&self) -> BTreeMap<NaiveDate, Vec<&Day>> { &'a mut self,
let mut grouped: BTreeMap<NaiveDate, Vec<&Day>> = BTreeMap::new(); project: Option<String>,
now: chrono::DateTime<chrono::Utc>,
// First pass: group entries by date ) -> Option<&'a mut Day> {
for day in &self.days { let item = self.days.iter_mut().find(|d| {
let date = day.timestamp.date_naive(); if d.project == project {
grouped.entry(date).or_default().push(day); return false;
} }
// Second pass: sort each day's entries by timestamp d.clock_in.format("%Y-%m-%d").to_string() == now.format("%Y-%m-%d").to_string()
for entries in grouped.values_mut() { });
entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
}
grouped item
} }
} }

1
timetable.json Normal file
View File

@ -0,0 +1 @@
{"days":[{"timestamp":"2025-03-25T08:00:00.055002Z","type":"In","project":null},{"timestamp":"2025-03-25T16:17:27.767509Z","type":"Out","project":null},{"timestamp":"2025-03-25T19:14:40.167673Z","type":"In","project":"spare"},{"timestamp":"2025-03-25T19:15:12.757436Z","type":"Out","project":"spare"}]}