diff --git a/rsc/css/league/main.css b/rsc/css/league/main.css index 7c4bbd5..2988329 100644 --- a/rsc/css/league/main.css +++ b/rsc/css/league/main.css @@ -1,8 +1,39 @@ -div.NdpcCollPresetView { - +div.PresetView { + border: thin solid var(--color-red); + width: max-content; } -div.NdpcCollPresetViewTitle { +div.PresetView svg { + height: 1em; +} + +div.PresetView div.PresetParamUnLocked { + color: var(--color-grey); + font-style: italic; +} + + + +div.PresetViewTitle { font-weight: bold; -} \ No newline at end of file + background-color: var(--color-red); + color: var(--color-white); + padding: 0.3em 0.5em; + text-align: center; +} + + + +div.PresetViewGeneral { + margin: 1em; +} + +div.PresetViewGeneral > div { + display: inline-block; + margin-left: 1em; +} + +div.PresetViewGeneral > div:first-child { + margin-left: 0; +} diff --git a/rsc/js/main.js b/rsc/js/main.js index 523732a..2c54bd4 100644 --- a/rsc/js/main.js +++ b/rsc/js/main.js @@ -31,8 +31,58 @@ document.addEventListener('DOMContentLoaded', function () { busy_spinner(true); }) } + + // convert utc to local time + document + .querySelectorAll('input[type="datetime-local"][data-utc]') + .forEach(input => { + const utc = input.dataset.utc; + if (!utc) return; + const date = new Date(utc); + if (isNaN(date)) return; + input.value = toDatetimeLocalValue(date); + }); }); +function toDatetimeLocalValue(date) { + const pad = n => String(n).padStart(2, "0"); + + return ( + date.getFullYear() + "-" + + pad(date.getMonth() + 1) + "-" + + pad(date.getDate()) + "T" + + pad(date.getHours()) + ":" + + pad(date.getMinutes()) + ); +} + +function datetimeLocalToUtcISOString(localValue) { + // localValue: "YYYY-MM-DDTHH:mm" + + const [datePart, timePart] = localValue.split("T"); + if (!datePart || !timePart) return null; + + const [year, month, day] = datePart.split("-").map(Number); + const [hour, minute] = timePart.split(":").map(Number); + + // Create Date in *local time* + const localDate = new Date( + year, + month - 1, // JS months are 0-based + day, + hour, + minute, + 0, + 0 + ); + + if (isNaN(localDate)) return null; + + // Convert to UTC + return localDate.toISOString(); +} + + function virtual_user_grade_callback(return_code, json_data, callback_data) { if (return_code == 200 || return_code == 204) { window.location.reload(); diff --git a/rsc/js/sslo_lib/ndpc.js b/rsc/js/sslo_lib/ndpc.js index 12a9089..0143fa3 100644 --- a/rsc/js/sslo_lib/ndpc.js +++ b/rsc/js/sslo_lib/ndpc.js @@ -43,8 +43,12 @@ function ndpcGetCollectionIoContainer(ndpc_collection) { // extract parameter value const param_input = child_param.getElementsByClassName("NdpcParameterInput")[0].children[1]; if (param_input.tagName === "INPUT" || param_input.tagName === "SELECT") { - // console.log("HERE: " + parameter_io.upid + "; " + param_input.value); - parameter_io.value = param_input.value; + if (param_input.type === "datetime-local") { + parameter_io.value = datetimeLocalToUtcISOString(param_input.value); + } else { + parameter_io.value = param_input.value; + } + // console.log("HERE: " + parameter_io.upid + "; " + param_input.value + "; " + parameter_io.value); } else { console.log("ERROR: Unexpected input type '" + param_input.tagName + "' for " + child_param.id); } @@ -190,7 +194,6 @@ function ndpcUpdate() { // iterate to find other parameter for (const other_parameter of collection.parentElement.parentElement.querySelectorAll(":scope > .NdpcContainerParameters > .NdpcParameter")) { - console.log("HERE: " + other_parameter) if (other_parameter.dataset.ndpcUpid === other_upid) { // check if value matches diff --git a/sslo_league/src/http/routes_html/presets.rs b/sslo_league/src/http/routes_html/presets.rs index 9110fe9..4177ffe 100644 --- a/sslo_league/src/http/routes_html/presets.rs +++ b/sslo_league/src/http/routes_html/presets.rs @@ -81,7 +81,7 @@ async fn generate_html(app_state: &AppState, // dialogue for preset editing if html.http_user_permissions.presets.edit().allowed() { - let title = format!("Edit Preset{}", match &preset{None=>"".to_string(), Some(p)=>p.name().await}); + let title = format!("Edit Preset: {}", match &preset{None=>"".to_string(), Some(p)=>p.name().await}); let mut dia = Dialog::new(&title, "PresetEditDialogue"); dia.add_button(DialogButtonStyle::Save, "Save Preset".to_string(), "savePreset".to_string()); dia.add_button(DialogButtonStyle::Cancel, "Cancel Editing".to_string(), "cancelPresetEdit".to_string()); diff --git a/sslo_league/src/ndpc/coll_preset.rs b/sslo_league/src/ndpc/coll_preset.rs index 88c6896..32909d8 100644 --- a/sslo_league/src/ndpc/coll_preset.rs +++ b/sslo_league/src/ndpc/coll_preset.rs @@ -25,7 +25,7 @@ mod param_weather_offset; use std::net::ToSocketAddrs; use sslo_lib::ndpc2::collection::{NdpcCollection, NdpcCollectionMetaData}; -use sslo_lib::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData}; +use sslo_lib::ndpc2::parameter::{NdpcAccess, NdpcParameter, NdpcParameterMetaData}; use crate::db2::DatabaseManager; @@ -33,12 +33,12 @@ use crate::db2::DatabaseManager; pub struct CollPreset { meta: NdpcCollectionMetaData, label: String, - coll_general: coll_general::CollPresetGeneral, - coll_rating: coll_rating::CollRating, - coll_session: coll_session::CollSession, - coll_server: coll_server::CollServer, - coll_classes: coll_car_classes::CollCarClasses, - coll_weathers: coll_weathers::CollWeathers, + pub coll_general: coll_general::CollPresetGeneral, + pub coll_rating: coll_rating::CollRating, + pub coll_session: coll_session::CollSession, + pub coll_server: coll_server::CollServer, + pub coll_classes: coll_car_classes::CollCarClasses, + pub coll_weathers: coll_weathers::CollWeathers, } impl CollPreset { @@ -58,17 +58,40 @@ impl CollPreset { } pub fn html_view(&self) -> String { - let mut html = "
".to_string(); - html += &format!("
{}
", self.label); + let mut html = "
".to_string(); + html += &format!("
{}
", self.label); // general html+= "
"; - // self.get_parameter("") + html += &self.html_view_general(); html+= "
"; html += "
"; html } + + fn html_view_general(&self) -> String { + let mut html = "
".to_string(); + + fn format_param(param: &dyn NdpcParameter, icon: &str) -> String { + format!("
{} {}
", + param.label(), + match param.acces() { + NdpcAccess::Locked => "PresetParamLocked", + NdpcAccess::Unlocked => "PresetParamUnLocked", + }, + icon, + param.value2str(), + ) + } + + html += &format_param(&self.coll_general.driving_aids, phosphor_svgs::icon::first_aid::DUOTONE); + html += &format_param(&self.coll_general.damage, phosphor_svgs::icon::heart_break::DUOTONE); + html += &format_param(&self.coll_general.penalties, phosphor_svgs::icon::sword::DUOTONE); + + html += "
"; + html + } } impl NdpcCollection for CollPreset { diff --git a/sslo_league/src/ndpc/coll_preset/coll_general.rs b/sslo_league/src/ndpc/coll_preset/coll_general.rs index bc953ca..b6fae6d 100644 --- a/sslo_league/src/ndpc/coll_preset/coll_general.rs +++ b/sslo_league/src/ndpc/coll_preset/coll_general.rs @@ -8,9 +8,9 @@ use crate::ndpc::coll_preset::param_sim::ParamSim; pub struct CollPresetGeneral { meta: NdpcCollectionMetaData, sim: ParamSim, - driving_aids: ParamDrivingAids, - damage: ParamDamage, - penalties: ParamPenalties, + pub driving_aids: ParamDrivingAids, + pub damage: ParamDamage, + pub penalties: ParamPenalties, } impl CollPresetGeneral { pub fn new() -> Self { diff --git a/sslo_league/src/ndpc/coll_preset/coll_session.rs b/sslo_league/src/ndpc/coll_preset/coll_session.rs index 3f18fed..d7042cb 100644 --- a/sslo_league/src/ndpc/coll_preset/coll_session.rs +++ b/sslo_league/src/ndpc/coll_preset/coll_session.rs @@ -1,4 +1,6 @@ +use utoipa::openapi::KnownFormat::DateTime; use sslo_lib::ndpc2::collection::{NdpcCollection, NdpcCollectionMetaData}; +use sslo_lib::ndpc2::param_date_time_local::ParamDateTimeLocal; use sslo_lib::ndpc2::param_i64::ParamI64; use sslo_lib::ndpc2::param_time::ParamTime; use sslo_lib::ndpc2::parameter::NdpcParameter; @@ -13,7 +15,8 @@ pub struct CollSession { qual_duration: ParamI64, race_distance: ParamRaceDistance, race_duration: ParamI64, - race_time: ParamTime, + race_date: ParamDateTimeLocal, + virtual_time: ParamTime, multiplicator: ParamI64, } impl CollSession { @@ -50,8 +53,14 @@ impl CollSession { "The duration of the race in minutes or laps".to_string(), ); - let race_time = ParamTime::new( - "race_time", "Virtual Race Time".to_string(), None, + let race_date = ParamDateTimeLocal::new( + "race_date", "Race Session Date".to_string(), None, + chrono::Utc::now(), + "When shall the race session start".to_string() + ); + + let virtual_time = ParamTime::new( + "virtual_time", "Virtual Time".to_string(), None, 14, 0, "The simulated, track-local daytime at when the race shall start".to_string() ); @@ -70,7 +79,8 @@ impl CollSession { qual_duration, race_distance: ParamRaceDistance::new(), race_duration, - race_time, + race_date, + virtual_time, multiplicator, } } @@ -82,10 +92,10 @@ impl NdpcCollection for CollSession { fn label(&self) -> &str { "Session" } fn child_parameters(&self) -> Vec<&dyn NdpcParameter> { - vec![&self.practice_opening, &self.practice_duration, &self.warmup_duration, &self.qual_duration, &self.race_distance, &self.race_duration, &self.race_time, &self.multiplicator] + vec![&self.practice_opening, &self.practice_duration, &self.warmup_duration, &self.qual_duration, &self.race_distance, &self.race_duration, &self.race_date, &self.virtual_time, &self.multiplicator] } fn child_parameters_mut(&mut self) -> Vec<&mut dyn NdpcParameter> { - vec![&mut self.practice_opening, &mut self.practice_duration, &mut self.warmup_duration, &mut self.qual_duration, &mut self.race_distance, &mut self.race_duration, &mut self.race_time, &mut self.multiplicator] + vec![&mut self.practice_opening, &mut self.practice_duration, &mut self.warmup_duration, &mut self.qual_duration, &mut self.race_distance, &mut self.race_duration, &mut self.race_date, &mut self.virtual_time, &mut self.multiplicator] } fn child_collections(&self) -> Vec<&dyn NdpcCollection> { vec![] } diff --git a/sslo_lib/src/ndpc2.rs b/sslo_lib/src/ndpc2.rs index 2f25aea..fac10fc 100644 --- a/sslo_lib/src/ndpc2.rs +++ b/sslo_lib/src/ndpc2.rs @@ -5,3 +5,4 @@ pub mod param_i64; pub mod param_enum; pub mod param_time; pub mod param_bool; +pub mod param_date_time_local; diff --git a/sslo_lib/src/ndpc2/param_date_time_local.rs b/sslo_lib/src/ndpc2/param_date_time_local.rs new file mode 100644 index 0000000..ebd48ca --- /dev/null +++ b/sslo_lib/src/ndpc2/param_date_time_local.rs @@ -0,0 +1,49 @@ +use chrono::{DateTime, ParseResult, Utc}; +use crate::error::SsloError; +use crate::ndpc2::parameter::{NdpcParameter, NdpcParameterMetaData}; + +pub struct ParamDateTimeLocal { + upid: &'static str, + label: String, + description: String, + unit: Option, + meta: NdpcParameterMetaData, + value: DateTime, +} + +impl ParamDateTimeLocal { + pub fn new(upid: &'static str, label: String, unit: Option, default: DateTime, description: String) -> Self { Self { + upid, + label, + description, + unit, + meta: NdpcParameterMetaData::new(), + value: default, + } } + + pub fn value(&self) -> &DateTime { &self.value } +} + +impl NdpcParameter for ParamDateTimeLocal { + fn upid(&self) -> &'static str { self.upid} + fn label(&self) -> &str { &self.label} + fn description(&self) -> &str { &self.description } + fn unit(&self) -> Option<&str> { match &self.unit {None=>None, Some(s)=>Some(s)} } + fn meta(&self) -> &NdpcParameterMetaData { &self.meta} + fn meta_mut(&mut self) -> &mut NdpcParameterMetaData { &mut self.meta } + + fn value_from_string(&mut self, value: &str) -> Result<(), SsloError> { + self.value = match DateTime::parse_from_rfc3339(value) { + Ok(dt) => dt.to_utc(), + Err(e) => {log::warn!("Invalid value='{}' for upid={}: {}", value, self.upid, e); + return Err(SsloError::NdpcValueMismatch(self.upid.to_string(), value.to_string()))} + }; + Ok(()) + } + + fn value2str(&self) -> String { format!("{:?}", self.value) } + + fn html_input(&self) -> String { + format!("", self.value) + } +} \ No newline at end of file diff --git a/sslo_lib/src/ndpc2/parameter.rs b/sslo_lib/src/ndpc2/parameter.rs index ba2536e..becb25d 100644 --- a/sslo_lib/src/ndpc2/parameter.rs +++ b/sslo_lib/src/ndpc2/parameter.rs @@ -4,14 +4,14 @@ use crate::error::SsloError; #[derive(Serialize, Deserialize, Clone, PartialEq)] #[derive(ToSchema)] -enum NdpcValueInheritance { +pub enum NdpcValueInheritance { Inherit, Overwrite, } #[derive(Serialize, Deserialize, Clone, PartialEq)] #[derive(ToSchema)] -enum NdpcAccess { +pub enum NdpcAccess { Locked, Unlocked, } @@ -64,6 +64,8 @@ pub trait NdpcParameter : Send { fn meta(&self) -> &NdpcParameterMetaData; fn meta_mut(&mut self) -> &mut NdpcParameterMetaData; + fn acces(&self) -> NdpcAccess { self.meta().access.clone() } + /// Shall set the parameter value /// If the passed value is invalid, the parameter shall remain unchanged fn value_from_string(&mut self, value: &str) -> Result<(), SsloError>;