feat: make it somewhat pretty

This commit is contained in:
2025-03-25 22:07:06 +01:00
parent 30fe1bc609
commit d52eebbd24
3 changed files with 313 additions and 25 deletions

View File

@@ -17,3 +17,4 @@ uuid = { version = "1.7.0", features = ["v4"] }
dirs = "6.0.0"
serde_json = "1.0.140"
chrono = { version = "0.4.40", features = ["serde"] }
inquire = { version = "0.7.5", features = ["chrono"] }

View File

@@ -1,5 +1,7 @@
use chrono::Timelike;
use anyhow::Context;
use chrono::{Local, Timelike, Utc};
use clap::{Parser, Subcommand};
use inquire::validator::Validation;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
@@ -31,6 +33,7 @@ enum Commands {
#[arg(long = "project")]
project: Option<String>,
},
Resolve {},
}
#[tokio::main]
@@ -73,20 +76,27 @@ async fn main() -> anyhow::Result<()> {
for day in days {
println!(
"day: {}{}\n {}:{}{}",
day.clock_in.format("%Y/%m/%d"),
"{}{}{}\n {}{}\n",
day.clock_in.with_timezone(&Local {}).format("%Y-%m-%d"),
if let Some(project) = &day.project {
format!(" project: {}", project)
} else {
"".into()
},
day.clock_in.hour(),
day.clock_in.minute(),
if day.breaks.is_empty() {
"".into()
} else {
format!(
" breaks: {}min",
day.breaks.iter().fold(0, |acc, _| acc + 30)
)
},
day.clock_in.with_timezone(&Local {}).format("%H:%M"),
if let Some(clockout) = &day.clock_out {
format!(" - {}:{}", clockout.hour(), clockout.minute())
format!(" - {}", clockout.with_timezone(&Local {}).format("%H:%M"))
} else {
" - unclosed".into()
}
},
)
}
}
@@ -106,18 +116,95 @@ async fn main() -> anyhow::Result<()> {
Some(day) => day.breaks.push(Break {}),
None => todo!(),
},
Commands::Resolve {} => {
let to_resolve = timetable
.days
.iter_mut()
.filter(|d| d.clock_out.is_none())
.collect::<Vec<_>>();
if to_resolve.is_empty() {
println!("Nothing to resolve, good job... :)");
return Ok(());
}
for day in to_resolve {
let local = day.clock_in.with_timezone(&Local {});
let clock_in = local.time();
println!(
"Resolve day: {}{}\n clocked in: {}",
day.clock_in.format("%Y/%m/%d"),
if let Some(project) = &day.project {
format!("\n project: {}", project)
} else {
"".into()
},
day.clock_in.format("%H:%M")
);
let output = inquire::Text::new("When did you clock out (16 or 16:30)")
.with_validator(move |v: &str| match parse_string_to_time(v) {
Ok(time) => {
if time <= clock_in {
return Ok(Validation::Invalid(
inquire::validator::ErrorMessage::Custom(
"clock out has to be after clockin".into(),
),
));
}
Ok(Validation::Valid)
}
Err(e) => Ok(Validation::Invalid(
inquire::validator::ErrorMessage::Custom(e.to_string()),
)),
})
.prompt()?;
let time = parse_string_to_time(&output)?;
day.clock_out = Some(
local
.with_hour(time.hour())
.expect("to be able to set hour")
.with_minute(time.minute())
.expect("to be able to set minute")
.with_timezone(&Utc {}),
);
}
}
}
if let Some(parent) = dir.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = tokio::fs::File::create(dir).await?;
file.write_all(&serde_json::to_vec(&timetable)?).await?;
file.write_all(&serde_json::to_vec_pretty(&timetable)?)
.await?;
file.flush().await?;
Ok(())
}
fn parse_string_to_time(v: &str) -> anyhow::Result<chrono::NaiveTime> {
chrono::NaiveTime::parse_from_str(v, "%H:%M")
.or_else(|_| {
v.parse::<u32>()
.context("failed to parse to hour")
.and_then(|h| {
if (0..=23).contains(&h) {
Ok(h)
} else {
anyhow::bail!("hours have to be within 0 and 23")
}
})
.map(|h| chrono::NaiveTime::from_hms_opt(h, 0, 0))
.ok()
.flatten()
.context("failed to parse value")
})
.context("failed to parse int to hour")
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Day {
clock_in: chrono::DateTime<chrono::Utc>,
@@ -143,7 +230,7 @@ impl TimeTable {
now: chrono::DateTime<chrono::Utc>,
) -> Option<&mut Day> {
let item = self.days.iter_mut().find(|d| {
if d.project == project {
if d.project != project {
return false;
}