diff --git a/src-api/src/main.rs b/src-api/src/main.rs index 5ee8b5c..db66482 100644 --- a/src-api/src/main.rs +++ b/src-api/src/main.rs @@ -92,9 +92,9 @@ async fn hours( state: web::Data, ) -> Result { let HoursRequest { id } = query.into_inner(); - let hours = routes::hours(id, &state.pg).await?; + let (learning, build) = routes::hours(id, &state.pg).await?; - Ok(HttpResponse::Ok().json(HoursResponse { hours })) + Ok(HttpResponse::Ok().json(HoursResponse { learning, build })) } #[get("/hours.csv")] diff --git a/src-api/src/model.rs b/src-api/src/model.rs index 232daef..c8f6c81 100644 --- a/src-api/src/model.rs +++ b/src-api/src/model.rs @@ -25,7 +25,8 @@ pub struct CSVRequest { #[derive(Serialize)] pub struct HoursResponse { - pub hours: f64, + pub learning: f64, + pub build: f64, } #[derive(Serialize)] diff --git a/src-api/src/routes/csv.rs b/src-api/src/routes/csv.rs index 1191dbc..b2084da 100644 --- a/src-api/src/routes/csv.rs +++ b/src-api/src/routes/csv.rs @@ -1,42 +1,169 @@ -use std::collections::HashMap; +use chrono::{Local, NaiveDateTime}; use crate::prelude::*; +use std::collections::HashMap; +struct CsvRow { + total: f64, + build: f64, + learning: f64, + dates: Vec, +} + +impl CsvRow { + fn new_learning(date: usize, total: f64) -> Self { + let mut dates = vec![0.0; date + 1]; + dates[date] = total; + + Self { + total, + build: 0.0, + learning: total, + dates, + } + } + + fn new_build(date: usize, total: f64) -> Self { + let mut dates = vec![0.0; date + 1]; + dates[date] = total; + + Self { + total, + build: total, + learning: 0.0, + dates, + } + } + + fn add_learning(&mut self, total: f64) { + self.learning += total; + self.total += total; + } + + fn add_build(&mut self, total: f64) { + self.build += total; + self.total += total; + } + + fn add_date(&mut self, index: usize, total: f64) { + if self.dates.len() <= index { + self.dates.resize(index + 1, 0.0); + } + + self.dates[index] += total; + } + + fn to_str(&self) -> String { + let mut row = format!( + "{:.2},{:.2},{:.2}", + self.total / 60.0, + self.build / 60.0, + self.learning / 60.0 + ); + + for date in &self.dates { + row.push_str(&format!(",{:.2}", date / 60.0)); + } + + row + } +} pub async fn csv(pg: &PgPool) -> Result { - let records = sqlx::query!( + sqlx::query!( + r#" + DELETE FROM records + WHERE sign_in - sign_out > INTERVAL '5 hours' + "# + ) + .execute(pg) + .await?; + + let learning_days = sqlx::query!( r#" - SELECT - student_id, - sign_in, - sign_out + SELECT student_id, sign_in, sign_out FROM records - WHERE in_progress = false - AND sign_out IS NOT NULL - AND sign_out < sign_in + INTERVAL '4 hours' + WHERE sign_out IS NOT NULL + AND in_progress = false + AND EXTRACT(MONTH FROM sign_in) <= 12 + AND EXTRACT(MONTH FROM sign_in) >= 11 "# ) .fetch_all(pg) .await?; - let mut hours = HashMap::new(); - let mut csv = String::from("id,hours\n"); + let build_hours = sqlx::query!( + r#" + SELECT student_id, sign_in, sign_out FROM records + WHERE sign_out IS NOT NULL + AND in_progress = false + AND EXTRACT(MONTH FROM sign_in) <= 5 + AND EXTRACT(MONTH FROM sign_in) >= 1 + "# + ) + .fetch_all(pg) + .await?; + + let mut idx = 0; + let mut header = String::from("student_id,total,build,learning"); + let mut dates = HashMap::new(); + let mut rows = HashMap::new(); + + let mut add_or_get_date = |date: &NaiveDateTime| { + let date = date + .and_local_timezone(Local) + .unwrap() + .format("%Y-%m-%d") + .to_string(); + + let entry = dates.get(&date); - for record in records { - let timein = record.sign_in; - let timeout = record.sign_out.unwrap(); - let duration = timeout.signed_duration_since(timein); - let mins = duration.num_minutes(); + if let Some(idx) = entry { + *idx + } else { + header.push_str(&format!(",{}", date)); + dates.insert(date, idx); + idx += 1; - hours - .entry(record.student_id) - .and_modify(|time| *time += mins) - .or_insert(mins); + idx - 1 + } + }; + + for learning_day in learning_days { + let student_id = learning_day.student_id; + let sign_in = learning_day.sign_in; + let sign_out = learning_day.sign_out.unwrap(); + let diff = sign_out.signed_duration_since(sign_in); + let mins = diff.num_minutes() as f64; + let idx = add_or_get_date(&sign_in); + + rows.entry(student_id) + .and_modify(|row: &mut CsvRow| { + row.add_learning(mins); + row.add_date(idx, mins); + }) + .or_insert(CsvRow::new_learning(idx, mins)); } - for (student_id, mins) in hours { - let hours = mins as f64 / 60.0; - csv.push_str(&format!("{},{}\n", student_id, hours)); + for build_day in build_hours { + let student_id = build_day.student_id; + let sign_in = build_day.sign_in; + let sign_out = build_day.sign_out.unwrap(); + let diff = sign_out.signed_duration_since(sign_in); + let mins = diff.num_minutes() as f64; + let idx = add_or_get_date(&sign_in); + + rows.entry(student_id) + .and_modify(|row: &mut CsvRow| { + row.add_build(mins); + row.add_date(idx, mins); + }) + .or_insert(CsvRow::new_build(idx, mins)); } - Ok(csv) + let csv = rows + .into_iter() + .map(|(id, data)| format!("{},{}\n", id, data.to_str())) + .collect::(); + + Ok(format!("{}\n{}", header, csv)) } diff --git a/src-api/src/routes/hours.rs b/src-api/src/routes/hours.rs index 44408e0..6c92006 100644 --- a/src-api/src/routes/hours.rs +++ b/src-api/src/routes/hours.rs @@ -1,28 +1,64 @@ use crate::prelude::*; -pub async fn hours(id: String, pg: &PgPool) -> Result { - let records = sqlx::query!( +pub async fn hours(id: String, pg: &PgPool) -> Result<(f64, f64), RouteError> { + sqlx::query!( + r#" + DELETE FROM records + WHERE sign_in - sign_out > INTERVAL '5 hours' + "# + ) + .execute(pg) + .await?; + + let learning_days = sqlx::query!( + r#" + SELECT sign_in, sign_out + FROM records + WHERE student_id = $1 + AND sign_out IS NOT NULL + AND in_progress = false + AND EXTRACT(MONTH FROM sign_in) <= 12 + AND EXTRACT(MONTH FROM sign_in) >= 11 + "#, + id + ) + .fetch_all(pg) + .await?; + + let build_hours = sqlx::query!( r#" SELECT sign_in, sign_out FROM records WHERE student_id = $1 AND sign_out IS NOT NULL AND in_progress = false + AND EXTRACT(MONTH FROM sign_in) <= 5 + AND EXTRACT(MONTH FROM sign_in) >= 1 "#, id ) .fetch_all(pg) .await?; - let mut minutes = 0; + let mut learning_mins = 0.0; + let mut build_mins = 0.0; + + for learning_day in learning_days { + let sign_in = learning_day.sign_in; + let sign_out = learning_day.sign_out.unwrap(); + let diff = sign_out.signed_duration_since(sign_in); + let mins = diff.num_minutes(); + + learning_mins += mins as f64; + } - for record in records { - let timein = record.sign_in; - let timeout = record.sign_out.unwrap(); - let duration = timeout.signed_duration_since(timein); - let new_minutes = duration.num_minutes(); + for build_day in build_hours { + let sign_in = build_day.sign_in; + let sign_out = build_day.sign_out.unwrap(); + let diff = sign_out.signed_duration_since(sign_in); + let mins = diff.num_minutes(); - minutes += new_minutes; + build_mins += mins as f64; } - Ok(minutes as f64 / 60.0) + Ok((learning_mins / 60.0, build_mins / 60.0)) } diff --git a/src/app/csv/page.tsx b/src/app/csv/page.tsx index ffe18ff..7ad4058 100644 --- a/src/app/csv/page.tsx +++ b/src/app/csv/page.tsx @@ -32,9 +32,22 @@ function Upload({ upload, error }: SubpageProps) { diff --git a/src/app/student/page.tsx b/src/app/student/page.tsx index 6afdf13..0064b6b 100644 --- a/src/app/student/page.tsx +++ b/src/app/student/page.tsx @@ -6,10 +6,11 @@ import { useTransitionOut } from '@lib/transitions'; import { useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { FetchError, GetError, tfetch } from '@lib/api'; +import { FetchError, GetError, HoursResponse, tfetch } from '@lib/api'; import { Label } from '@ui/label'; import { Spinner } from '@ui/spinner'; import sha256 from 'sha256'; +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@ui/table'; interface IdInputProps { error: string | React.JSX.Element, @@ -47,7 +48,7 @@ export default function Student() { const [error, setError] = useState(''); const [loading, setLoading] = useState(true); - const [hours, setHours] = useState(); + const [hours, setHours] = useState(); useEffect(() => { const id = params.get('id'); @@ -60,7 +61,7 @@ export default function Student() { setError(GetError(res.error!.ecode, res.error!.message)); return; } - setHours(res.result!.hours); + setHours(res.result!); }) .catch(FetchError(setError)) .finally(() => setLoading(false)); @@ -81,7 +82,31 @@ export default function Student() { opacity: +!loading, transition: 'all 0.5s ease' }}> - + + Student Hours + + + Hours Type + Hours Earned + Hours Remaining + Total Required + + + + + Learning Days + {hours?.learning} + {8 - (hours?.learning ?? 0)} + 8 + + + Build Season + {hours?.build} + {40 - (hours?.build ?? 0)} + 40 + + +
); diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx deleted file mode 100644 index 226ea2e..0000000 --- a/src/components/ui/pagination.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react'; -import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; - -import { cn } from '@/lib/utils'; -import { ButtonProps, buttonVariants } from '@/components/ui/button'; - -const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( -