Skip to content

Commit

Permalink
updateses
Browse files Browse the repository at this point in the history
  • Loading branch information
onlycs committed Nov 5, 2024
1 parent d887107 commit c506024
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 235 deletions.
4 changes: 2 additions & 2 deletions src-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ async fn hours(
state: web::Data<AppState>,
) -> Result<impl Responder, RouteError> {
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")]
Expand Down
3 changes: 2 additions & 1 deletion src-api/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pub struct CSVRequest {

#[derive(Serialize)]
pub struct HoursResponse {
pub hours: f64,
pub learning: f64,
pub build: f64,
}

#[derive(Serialize)]
Expand Down
175 changes: 151 additions & 24 deletions src-api/src/routes/csv.rs
Original file line number Diff line number Diff line change
@@ -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<f64>,
}

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<String, RouteError> {
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::<String>();

Ok(format!("{}\n{}", header, csv))
}
56 changes: 46 additions & 10 deletions src-api/src/routes/hours.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
use crate::prelude::*;

pub async fn hours(id: String, pg: &PgPool) -> Result<f64, RouteError> {
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))
}
19 changes: 16 additions & 3 deletions src/app/csv/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,22 @@ function Upload({ upload, error }: SubpageProps) {
</TooltipTrigger>
<TooltipContent>
<Label className="text-center text-md leading-5">
Upload a CSV that contains every student&apos;s name and id, with the field titles &quot;id,&quot; &quot;first,&quot; and &quot;last.&quot;<br />
You will download a file that contains the student&apos;s name, id, and hours. <br />
No student data ever leaves your computer.
Upload a CSV that contains every student&apos;s ids, <br />
and all other data other than hours (e.g. name, etc.) <br />
The CSV should have a header row. The leftmost column <br />
should be the student id. <br /><br />

Example input
<p className='font-mono'>
id, name, ... <br />
123456, John Doe, ... <br />
</p>

Example output
<p className='font-mono'>
id, name, ..., total, learning, build, ... (daily data) <br />
123456, John Doe, ..., 10, 5, 5, ... <br />
</p>
</Label>
</TooltipContent>
</Tooltip>
Expand Down
33 changes: 29 additions & 4 deletions src/app/student/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,7 +48,7 @@ export default function Student() {

const [error, setError] = useState<string | React.JSX.Element>('');
const [loading, setLoading] = useState(true);
const [hours, setHours] = useState<number | undefined>();
const [hours, setHours] = useState<HoursResponse | undefined>();

useEffect(() => {
const id = params.get('id');
Expand All @@ -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));
Expand All @@ -81,7 +82,31 @@ export default function Student() {
opacity: +!loading,
transition: 'all 0.5s ease'
}}>
<Label className='text-2xl'>Hours: {hours}</Label>
<Table className='text-lg rounded-md'>
<TableCaption>Student Hours</TableCaption>
<TableHeader>
<TableRow>
<TableHead className='w-48'>Hours Type</TableHead>
<TableHead className='w-44 text-center'>Hours Earned</TableHead>
<TableHead className='w-44 text-center'>Hours Remaining</TableHead>
<TableHead className='text-center'>Total Required</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='font-bold'>Learning Days</TableCell>
<TableCell className='text-center'>{hours?.learning}</TableCell>
<TableCell className='text-center'>{8 - (hours?.learning ?? 0)}</TableCell>
<TableCell className='text-center'>8</TableCell>
</TableRow>
<TableRow>
<TableCell className='font-bold'>Build Season</TableCell>
<TableCell className='text-center'>{hours?.build}</TableCell>
<TableCell className='text-center'>{40 - (hours?.build ?? 0)}</TableCell>
<TableCell className='text-center'>40</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</>
);
Expand Down
Loading

0 comments on commit c506024

Please sign in to comment.