use axum::{ http::{header, StatusCode}, response::{IntoResponse, Response}, }; use axum_extra::extract::Form; use maud::{html, Markup, PreEscaped}; use serde::Deserialize; use time::{macros::format_description, OffsetDateTime}; use crate::{ endpoints::page, render::{self, Entry, Note, Timesheet, WorkingArea}, }; pub async fn get() -> Markup { let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); let month = now.date().replace_day(1).unwrap().previous_day().unwrap(); let month = month.format(format_description!("[year]-[month]")).unwrap(); page( html! { style { (PreEscaped(include_str!("index.css"))) } script type="module" { (PreEscaped(include_str!("index.js"))) } }, html! { form #form { h1 { "Arbeitszeitdokumentationsgenerator" } div #header { label #l-month for="i-month" { "Monat / Jahr:" } input #i-month name="month" type="month" placeholder=(month) value=(month) {} label #l-name for="i-name" { "Name, Vorname des/r Beschäftigten:" } input #i-name name="name" type="text" placeholder="McStudentface, Student" {} label #l-staffid for="i-staffid" { "Personalnummer:" } input #i-staffid name="staff_id" type="number" placeholder="1337420" {} div #gfub { label #l-gf title="Großforschung" { "GF: " input #i-gf name="working_area" type="radio" value="GF" {} } label #l-ub for="i-ub" title="Unibereich" { "UB: " input #i-ub name="working_area" type="radio" value="UB" checked {} } } label #l-department for="i-department" title="Organisationseinheit" { "OE:" } input #i-department name="department" type="text" placeholder="Institut für Informatik" value="Institut für Informatik" {} label #l-monthlyhours for="i-monthlyhours" { "Vertraglich vereinbarte Arbeitszeit:" } div #mhhr { span { input #i-monthlyhours name="monthly_hours" type="number" value="40" {} " Std." } span { label #l-hourlywage for="i-hourlywage" { "Stundensatz: " } input #i-hourlywage name="hourly_wage" type="number" step="0.01" placeholder="14.09" {} " €" } } div #carry { span { label #l-carry for="i-carry" { "Übertrag vom Vormonat: " } input #i-carry .i-dur name="carry_prev_month" type="text" placeholder="00:00" {} } } } div #table { div #task { "Tätigkeit" br; "(Stichwort, Projekt)" } div { "Tag" } div { "Beginn" } div { "Ende" } div { "Pause" } div { "Arbeitszeit" } div { } div { "(hh:mm)" } div { "(hh:mm)" } div { "(hh:mm)" } div { } @for _ in 0..22 { div { input .i-task name="task" type="text" {} } div { input .i-day name="day" type="number" value="1" {} } div { input .i-dur name="start" type="text" placeholder="12:34" {} } div { input .i-dur name="end" type="text" placeholder="12:34" {} } div { input .i-dur name="rest" type="text" placeholder="00:00" {} } div { select name="note" value="" { option value="" { "Normal" } option value="U" { "Urlaub" } option value="K" { "Krankheit" } option value="F" { "Feiertag" } option value="S" { "Sonstiges" } } } } } button #submit type="button" { "Arbeitszeitdokumentation generieren" } pre #info {} } }, ) } #[derive(Debug, Deserialize)] pub struct PostForm { month: String, name: String, staff_id: String, working_area: String, department: String, monthly_hours: u32, hourly_wage: String, carry_prev_month: String, task: Vec, day: Vec, start: Vec, end: Vec, rest: Vec, note: Vec, } fn error_response(msg: S) -> Response { (StatusCode::BAD_REQUEST, msg.to_string()).into_response() } fn parse_month(month_str: &str) -> Option<(u32, u32)> { let mut parts = month_str.split('-'); let year = parts.next()?.parse::().ok()?; let month = parts.next()?.parse::().ok()?; if parts.next().is_some() { return None; } Some((year, month)) } pub async fn post(form: Form) -> Response { let form = form.0; // Parse working area let working_area = match &form.working_area as &str { "GF" => WorkingArea::Großforschung, "UB" => WorkingArea::Unibereich, _ => return error_response(format!("invalid working area: {:?}", form.working_area)), }; // Parse month let Some((year, month)) = parse_month(&form.month) else { return error_response(format!("invalid month: {:?}", form.month)); }; // Parse rests let rests = form .rest .into_iter() .map(|r| if r.is_empty() { None } else { Some(r) }) .collect::>(); // Parse notes let mut notes = vec![]; for note in form.note { let note = match ¬e as &str { "" => None, "U" => Some(Note::Urlaub), "K" => Some(Note::Krankheit), "F" => Some(Note::Feiertag), "S" => Some(Note::Sonstiges), _ => return error_response(format!("invalid note: {note:?}")), }; notes.push(note) } // Parse carry let carry_prev_month = if form.carry_prev_month.is_empty() { None } else { Some(form.carry_prev_month) }; let entries = (form.task.into_iter()) .zip(form.day.into_iter()) .zip(form.start.into_iter()) .zip(form.end.into_iter()) .zip(rests.into_iter()) .zip(notes.into_iter()) .filter_map(|(((((task, day), start), end), rest), note)| { if task.is_empty() || start.is_empty() || end.is_empty() { return None; }; Some(Entry { task, day, start, end, rest, note, }) }) .collect::>(); let timesheet = Timesheet { name: form.name, staff_id: form.staff_id, department: form.department, working_area, monthly_hours: form.monthly_hours, hourly_wage: form.hourly_wage, validate: true, carry_prev_month, year, month, entries, }; match render::render(timesheet) { Ok(pdf) => ([(header::CONTENT_TYPE, "application/pdf")], pdf).into_response(), Err(errors) => error_response(errors.join("\n")), } }