Implement basic features
This commit is contained in:
		
							parent
							
								
									68cc867d1d
								
							
						
					
					
						commit
						abc57d84cd
					
				
							
								
								
									
										124
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										124
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
use chrono::TimeZone;
 | 
					use chrono::{NaiveDateTime, TimeDelta, TimeZone, Datelike};
 | 
				
			||||||
use diesel::prelude::*;
 | 
					use diesel::{dsl::sql, prelude::*, sql_types};
 | 
				
			||||||
use dotenvy::dotenv;
 | 
					use dotenvy::dotenv;
 | 
				
			||||||
use self::models::*;
 | 
					use self::models::*;
 | 
				
			||||||
use std::net::SocketAddr;
 | 
					use std::net::SocketAddr;
 | 
				
			||||||
| 
						 | 
					@ -41,20 +41,20 @@ async fn main() {
 | 
				
			||||||
    // build our application with a route
 | 
					    // build our application with a route
 | 
				
			||||||
    let app = Router::new()
 | 
					    let app = Router::new()
 | 
				
			||||||
        // `GET /` goes to `root`
 | 
					        // `GET /` goes to `root`
 | 
				
			||||||
        .route("/", get(root))
 | 
					        .route("/", get(|state| get_metrics(state, None)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .route("/history", post(add_period))
 | 
					        .route("/api/metrics", get(|state| get_metrics(state, Some(true))))
 | 
				
			||||||
        .route("/history", get(get_history))
 | 
					        .route("/api/history", post(add_period))
 | 
				
			||||||
        .route("/history/:id", put(update_period))
 | 
					        .route("/api/history", get(get_history))
 | 
				
			||||||
        .route("/history/:id", delete(delete_period))
 | 
					        .route("/api/history/:id", put(update_period))
 | 
				
			||||||
        .route("/history/:id", get(get_period))
 | 
					        .route("/api/history/:id", delete(delete_period))
 | 
				
			||||||
 | 
					        .route("/api/history/:id", get(get_period))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .route("/tracking", get(get_tracking))
 | 
					        .route("/api/tracking", get(get_tracking))
 | 
				
			||||||
        .route("/tracking", post(start_tracking))
 | 
					        .route("/api/tracking", post(start_tracking))
 | 
				
			||||||
        .route("/tracking", delete(stop_tracking))
 | 
					        .route("/api/tracking", delete(stop_tracking))
 | 
				
			||||||
        .with_state(pool);
 | 
					        .with_state(pool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // run our app with hyper, listening globally on port 3000
 | 
					 | 
				
			||||||
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
 | 
					    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
 | 
				
			||||||
    tracing::debug!("listening on {addr}");
 | 
					    tracing::debug!("listening on {addr}");
 | 
				
			||||||
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
 | 
					    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
 | 
				
			||||||
| 
						 | 
					@ -66,6 +66,7 @@ struct HistoryQuery {
 | 
				
			||||||
    count: Option<i64>,
 | 
					    count: Option<i64>,
 | 
				
			||||||
    since: Option<chrono::NaiveDateTime>,
 | 
					    since: Option<chrono::NaiveDateTime>,
 | 
				
			||||||
    until: Option<chrono::NaiveDateTime>,
 | 
					    until: Option<chrono::NaiveDateTime>,
 | 
				
			||||||
 | 
					    project: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_history(
 | 
					async fn get_history(
 | 
				
			||||||
| 
						 | 
					@ -73,16 +74,21 @@ async fn get_history(
 | 
				
			||||||
    query: Query<HistoryQuery>,
 | 
					    query: Query<HistoryQuery>,
 | 
				
			||||||
) -> Result<Json<Vec<WorkPeriod>>, (StatusCode, Json<Error>)> {
 | 
					) -> Result<Json<Vec<WorkPeriod>>, (StatusCode, Json<Error>)> {
 | 
				
			||||||
    let count = query.count.unwrap_or(10);
 | 
					    let count = query.count.unwrap_or(10);
 | 
				
			||||||
    let start = query.since.unwrap_or_else(|| chrono::Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().naive_utc());
 | 
					 | 
				
			||||||
    let end = query.until.unwrap_or_else(|| chrono::Utc::now().naive_utc());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let conn = pool.get().await.map_err(internal_error)?;
 | 
					    let conn = pool.get().await.map_err(internal_error)?;
 | 
				
			||||||
    let res = conn.interact(move |conn|
 | 
					    let res = conn.interact(move |conn|
 | 
				
			||||||
        work_periods::table.select(WorkPeriod::as_select())
 | 
					        {
 | 
				
			||||||
        .filter(work_periods::start_time.between(start, end))
 | 
					            let mut selection = work_periods::table.select(WorkPeriod::as_select()).into_boxed().order(work_periods::start_time.desc());
 | 
				
			||||||
        .order(work_periods::start_time.desc())
 | 
					            if query.since.is_some() || query.until.is_some() {
 | 
				
			||||||
        .limit(count)
 | 
					                let start = query.since.unwrap_or_else(|| chrono::Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().naive_local());
 | 
				
			||||||
        .load(conn)
 | 
					                let end = query.until.unwrap_or_else(|| chrono::offset::Local::now().naive_local());
 | 
				
			||||||
 | 
					                selection = selection.filter(work_periods::start_time.between(start, end));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if query.project.is_some() {
 | 
				
			||||||
 | 
					                selection = selection.filter(work_periods::project.eq(query.project.as_ref().unwrap()));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            selection.limit(count).load(conn)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    ).await
 | 
					    ).await
 | 
				
			||||||
        .map_err(internal_error)?
 | 
					        .map_err(internal_error)?
 | 
				
			||||||
        .map_err(internal_error)?;
 | 
					        .map_err(internal_error)?;
 | 
				
			||||||
| 
						 | 
					@ -101,9 +107,79 @@ async fn get_tracking(
 | 
				
			||||||
    Ok(Json(res))
 | 
					    Ok(Json(res))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct Status {
 | 
				
			||||||
 | 
					    today: String,
 | 
				
			||||||
 | 
					    week: String,
 | 
				
			||||||
 | 
					    month: String,
 | 
				
			||||||
 | 
					    year: String,
 | 
				
			||||||
 | 
					    active: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_duration(periods: Vec<(NaiveDateTime, NaiveDateTime)>) -> i64 {
 | 
				
			||||||
 | 
					    periods.iter()
 | 
				
			||||||
 | 
					        .map(|(start, end)| end.signed_duration_since(*start))
 | 
				
			||||||
 | 
					        .sum::<TimeDelta>().num_seconds()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_since(pool: &deadpool_diesel::sqlite::Pool, start: Option<NaiveDateTime>) -> Result<i64, (StatusCode, Json<Error>)> {
 | 
				
			||||||
 | 
					    if let Some(start) = start {
 | 
				
			||||||
 | 
					        let conn = pool.get().await.map_err(internal_error)?;
 | 
				
			||||||
 | 
					        Ok(get_duration(conn.interact(move |conn| work_periods::table.filter(work_periods::start_time.ge(start)).select((
 | 
				
			||||||
 | 
					                work_periods::start_time,
 | 
				
			||||||
 | 
					                sql::<sql_types::Timestamp>("COALESCE(end_time, datetime('now', 'localtime'))")
 | 
				
			||||||
 | 
					        )).load::<(NaiveDateTime, NaiveDateTime)>(conn)).await.map_err(internal_error)?.map_err(internal_error)?))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(0)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn format_duration(total_seconds: i64) -> String {
 | 
				
			||||||
 | 
					    let days = total_seconds / (24 * 3600);
 | 
				
			||||||
 | 
					    let hours = (total_seconds % (24 * 3600)) / 3600;
 | 
				
			||||||
 | 
					    let minutes = (total_seconds % 3600) / 60;
 | 
				
			||||||
 | 
					    let seconds = total_seconds % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut parts = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if days > 0 { parts.push(format!("{}d", days)); }
 | 
				
			||||||
 | 
					    if hours > 0 { parts.push(format!("{}h", hours)); }
 | 
				
			||||||
 | 
					    if minutes > 0 { parts.push(format!("{}m", minutes)); }
 | 
				
			||||||
 | 
					    if seconds > 0 || parts.is_empty() { parts.push(format!("{}s", seconds)); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parts.join(" ")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// basic handler that responds with a static string
 | 
					// basic handler that responds with a static string
 | 
				
			||||||
async fn root() -> &'static str {
 | 
					async fn get_metrics(State(pool): State<deadpool_diesel::sqlite::Pool>, seconds: Option<bool>) -> Result<Json<Status>, (StatusCode, Json<Error>)> {
 | 
				
			||||||
    "Hello, World!"
 | 
					    let conn = pool.get().await.map_err(internal_error)?;
 | 
				
			||||||
 | 
					    let current_time = chrono::offset::Local::now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let active_sessions = conn.interact(|conn| work_periods::table.filter(work_periods::end_time.is_null()).select((
 | 
				
			||||||
 | 
					                work_periods::start_time,
 | 
				
			||||||
 | 
					                sql::<sql_types::Timestamp>("COALESCE(end_time, datetime('now'))")
 | 
				
			||||||
 | 
					    )).load::<(NaiveDateTime, NaiveDateTime)>(conn)).await.map_err(internal_error)?.map_err(internal_error)?;
 | 
				
			||||||
 | 
					    let mut today = current_time.date_naive();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let started_yesterday = active_sessions.iter().any(|(start, _)| start.date() != today);
 | 
				
			||||||
 | 
					    let active_duration = get_duration(active_sessions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if active_duration > 0 && started_yesterday {
 | 
				
			||||||
 | 
					        today = today - chrono::Duration::days(1); // If we are currently tracking, we haven't started today yet
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let today_start = today.and_hms_opt(0, 0, 0);
 | 
				
			||||||
 | 
					    let week_start = (today - chrono::Duration::days(current_time.weekday().num_days_from_monday() as i64)).and_hms_opt(0, 0, 0);
 | 
				
			||||||
 | 
					    let month_start = today.with_day(1).unwrap().and_hms_opt(0, 0, 0);
 | 
				
			||||||
 | 
					    let year_start = today.with_day(1).unwrap().with_month(1).unwrap().and_hms_opt(0, 0, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let transform = if let Some(true) = seconds { |x| format!("{}", x) } else { format_duration };
 | 
				
			||||||
 | 
					    Ok(Json(Status {
 | 
				
			||||||
 | 
					        today: transform(get_since(&pool, today_start).await?),
 | 
				
			||||||
 | 
					        week: transform(get_since(&pool, week_start).await?),
 | 
				
			||||||
 | 
					        month: transform(get_since(&pool, month_start).await?),
 | 
				
			||||||
 | 
					        year: transform(get_since(&pool, year_start).await?),
 | 
				
			||||||
 | 
					        active: transform(active_duration),
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn stop_tracking(State(pool): State<deadpool_diesel::sqlite::Pool>, payload: Option<Json<WorkPeriod>>) -> Result<StatusCode, (StatusCode, Json<Error>)> {
 | 
					async fn stop_tracking(State(pool): State<deadpool_diesel::sqlite::Pool>, payload: Option<Json<WorkPeriod>>) -> Result<StatusCode, (StatusCode, Json<Error>)> {
 | 
				
			||||||
| 
						 | 
					@ -111,12 +187,12 @@ async fn stop_tracking(State(pool): State<deadpool_diesel::sqlite::Pool>, payloa
 | 
				
			||||||
        match payload {
 | 
					        match payload {
 | 
				
			||||||
            Some(Json(payload)) => {
 | 
					            Some(Json(payload)) => {
 | 
				
			||||||
                diesel::update(work_periods::table.filter(work_periods::end_time.is_null()).find(payload.id))
 | 
					                diesel::update(work_periods::table.filter(work_periods::end_time.is_null()).find(payload.id))
 | 
				
			||||||
                    .set(work_periods::end_time.eq(Some(chrono::Utc::now().naive_utc())))
 | 
					                    .set(work_periods::end_time.eq(Some(chrono::offset::Local::now().naive_local())))
 | 
				
			||||||
                    .execute(conn)
 | 
					                    .execute(conn)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            None => {
 | 
					            None => {
 | 
				
			||||||
                diesel::update(work_periods::table.filter(work_periods::end_time.is_null()))
 | 
					                diesel::update(work_periods::table.filter(work_periods::end_time.is_null()))
 | 
				
			||||||
                    .set(work_periods::end_time.eq(Some(chrono::Utc::now().naive_utc())))
 | 
					                    .set(work_periods::end_time.eq(Some(chrono::offset::Local::now().naive_local())))
 | 
				
			||||||
                    .execute(conn)
 | 
					                    .execute(conn)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -133,7 +209,7 @@ async fn start_tracking(State(pool): State<deadpool_diesel::sqlite::Pool>, Json(
 | 
				
			||||||
        })));
 | 
					        })));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let mut payload = payload.clone();
 | 
					    let mut payload = payload.clone();
 | 
				
			||||||
    payload.start_time = Some(chrono::Utc::now().naive_utc());
 | 
					    payload.start_time = Some(chrono::offset::Local::now().naive_local());
 | 
				
			||||||
    let conn = pool.get().await.map_err(internal_error)?;
 | 
					    let conn = pool.get().await.map_err(internal_error)?;
 | 
				
			||||||
    // insert your application logic here
 | 
					    // insert your application logic here
 | 
				
			||||||
    let res = conn
 | 
					    let res = conn
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue