mirror of
https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator.git
synced 2026-04-12 16:55:04 +02:00
Add TimeSheetGenerator schema compatible page
This commit is contained in:
parent
722c05f983
commit
8f9d304761
6 changed files with 265 additions and 2 deletions
|
|
@ -26,6 +26,13 @@ pub async fn get() -> Markup {
|
|||
form #form {
|
||||
h1 { "Arbeitszeitdokumentationsgenerator" }
|
||||
|
||||
p {
|
||||
"Want to use "
|
||||
a href="https://github.com/kit-sdq/TimeSheetGenerator" { "TimeSheetGenerator" }
|
||||
"-compatible JSON instead? "
|
||||
a href="/tsg" { "Go here!" }
|
||||
}
|
||||
|
||||
div #header {
|
||||
label #l-month for="i-month" { "Monat / Jahr:" }
|
||||
input #i-month name="month" type="month" placeholder=(month) value=(month) {}
|
||||
|
|
|
|||
31
src/endpoints/tsg.css
Normal file
31
src/endpoints/tsg.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
:root {
|
||||
font-family: Arial, FreeSans, sans-serif;
|
||||
}
|
||||
form {
|
||||
max-width: 210mm; /* DIN-A 4 */
|
||||
margin: 0 auto;
|
||||
padding: 0 5mm 5mm;
|
||||
border: 2px solid black;
|
||||
border-radius: 0 8mm 0 8mm;
|
||||
}
|
||||
h1 {
|
||||
color: #009682;
|
||||
text-align: center;
|
||||
}
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 14lh;
|
||||
margin: 1mm 0;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
margin: 4mm auto 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
#info.success {
|
||||
color: #070;
|
||||
}
|
||||
#info.error {
|
||||
color: #900;
|
||||
}
|
||||
67
src/endpoints/tsg.js
Normal file
67
src/endpoints/tsg.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const form = document.getElementById("form");
|
||||
const submit = document.getElementById("submit");
|
||||
const info = document.getElementById("info");
|
||||
|
||||
function showStatus(msg) {
|
||||
info.classList.remove("success");
|
||||
info.classList.remove("error");
|
||||
info.textContent = msg;
|
||||
info.scrollIntoView();
|
||||
}
|
||||
|
||||
function showSuccess(msg) {
|
||||
info.classList.add("success");
|
||||
info.classList.remove("error");
|
||||
info.textContent = msg;
|
||||
info.scrollIntoView();
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
info.classList.add("error");
|
||||
info.classList.remove("success");
|
||||
info.textContent = msg;
|
||||
info.scrollIntoView();
|
||||
}
|
||||
|
||||
submit.addEventListener("click", async () => {
|
||||
const data = new FormData(form);
|
||||
const dataJson = JSON.stringify({
|
||||
global: JSON.parse(data.get("global")),
|
||||
month: JSON.parse(data.get("month")),
|
||||
});
|
||||
|
||||
try {
|
||||
showStatus("Generiere...");
|
||||
|
||||
const response = await fetch("/tsg", {
|
||||
method: "post",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: dataJson,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
const reason = await response.text();
|
||||
showError(`Generieren fehlgeschlagen:\n${reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let blob = await response.blob();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("loadend", () => {
|
||||
let element = document.createElement("a");
|
||||
element.setAttribute("href", reader.result);
|
||||
element.setAttribute("download", "Arbeitszeitdokumentation.pdf");
|
||||
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
|
||||
showSuccess("Generieren erfolgreich!");
|
||||
});
|
||||
reader.readAsDataURL(blob);
|
||||
} catch (e) {
|
||||
showError(`Generieren fehlgeschlagen:\n${e}`);
|
||||
}
|
||||
});
|
||||
156
src/endpoints/tsg.rs
Normal file
156
src/endpoints/tsg.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use axum::{
|
||||
http::{header, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
endpoints::page,
|
||||
render::{self, Entry, Note, Timesheet, WorkingArea},
|
||||
};
|
||||
|
||||
pub async fn get() -> Markup {
|
||||
page(
|
||||
html! {
|
||||
style { (PreEscaped(include_str!("tsg.css"))) }
|
||||
script type="module" { (PreEscaped(include_str!("tsg.js"))) }
|
||||
},
|
||||
html! {
|
||||
form #form {
|
||||
h1 { "Arbeitszeitdokumentationsgenerator" }
|
||||
|
||||
p {
|
||||
"Want to use a fancy-looking form instead? "
|
||||
a href="/" { "Go here!" }
|
||||
}
|
||||
|
||||
textarea name="global" placeholder="Global.json" {}
|
||||
textarea name="month" placeholder="Month.json" {}
|
||||
|
||||
button #submit type="button" { "Arbeitszeitdokumentation generieren" }
|
||||
|
||||
pre #info {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn default_vacation() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GlobalJson {
|
||||
name: String,
|
||||
staff_id: i64,
|
||||
department: String,
|
||||
working_time: String,
|
||||
wage: f64,
|
||||
working_area: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EntryJson {
|
||||
action: String,
|
||||
day: u32,
|
||||
start: String,
|
||||
end: String,
|
||||
pause: Option<String>,
|
||||
#[serde(default = "default_vacation")]
|
||||
vacation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MonthJson {
|
||||
year: u32,
|
||||
month: u32,
|
||||
pred_transfer: Option<String>,
|
||||
entries: Vec<EntryJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PostJson {
|
||||
global: GlobalJson,
|
||||
month: MonthJson,
|
||||
}
|
||||
|
||||
fn error_response<S: ToString>(msg: S) -> Response {
|
||||
(StatusCode::BAD_REQUEST, msg.to_string()).into_response()
|
||||
}
|
||||
|
||||
fn parse_span(span_str: &str) -> Option<u32> {
|
||||
let mut parts = span_str.split(':');
|
||||
|
||||
let hours = parts.next()?.parse::<u32>().ok()?;
|
||||
let minutes = parts.next()?.parse::<u32>().ok()?;
|
||||
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if minutes != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(hours)
|
||||
}
|
||||
|
||||
pub async fn post(json: Json<PostJson>) -> Response {
|
||||
let json = json.0;
|
||||
|
||||
// Parse working area
|
||||
let working_area = match &json.global.working_area as &str {
|
||||
"gf" => WorkingArea::Großforschung,
|
||||
"ub" => WorkingArea::Unibereich,
|
||||
_ => {
|
||||
return error_response(format!(
|
||||
"invalid working area: {:?}",
|
||||
json.global.working_area
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Parse working time
|
||||
let Some(monthly_hours) = parse_span(&json.global.working_time) else {
|
||||
return error_response(format!(
|
||||
"invalid working_time: {:?}",
|
||||
json.global.working_time
|
||||
));
|
||||
};
|
||||
|
||||
let entries = json
|
||||
.month
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|e| Entry {
|
||||
task: e.action,
|
||||
day: e.day,
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
rest: e.pause,
|
||||
note: if e.vacation { Some(Note::Urlaub) } else { None },
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let timesheet = Timesheet {
|
||||
name: json.global.name,
|
||||
staff_id: json.global.staff_id.to_string(),
|
||||
department: json.global.department,
|
||||
working_area,
|
||||
monthly_hours,
|
||||
hourly_wage: json.global.wage.to_string(),
|
||||
validate: true,
|
||||
carry_prev_month: json.month.pred_transfer,
|
||||
year: json.month.year,
|
||||
month: json.month.month,
|
||||
entries,
|
||||
};
|
||||
|
||||
match render::render(timesheet) {
|
||||
Ok(pdf) => ([(header::CONTENT_TYPE, "application/pdf")], pdf).into_response(),
|
||||
Err(errors) => error_response(errors.join("\n")),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue