implement datetime parameter [refs #19]

This commit is contained in:
Thomas Weinhold 2026-01-07 22:58:03 +01:00
commit a49f81799c
10 changed files with 198 additions and 29 deletions

View file

@ -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;
}
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;
}

View file

@ -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();

View file

@ -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

View file

@ -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());

View file

@ -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 = "<div class=\"NdpcCollPresetView\">".to_string();
html += &format!("<div class=\"NdpcCollPresetViewTitle\">{}</div>", self.label);
let mut html = "<div class=\"PresetView\">".to_string();
html += &format!("<div class=\"PresetViewTitle\">{}</div>", self.label);
// general
html+= "<div>";
// self.get_parameter("")
html += &self.html_view_general();
html+= "</div>";
html += "</div>";
html
}
fn html_view_general(&self) -> String {
let mut html = "<div class=\"PresetViewGeneral\">".to_string();
fn format_param(param: &dyn NdpcParameter, icon: &str) -> String {
format!("<div title=\"{}\" class=\"{}\">{} {}</div>",
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 += "</div>";
html
}
}
impl NdpcCollection for CollPreset {

View file

@ -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 {

View file

@ -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![] }

View file

@ -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;

View file

@ -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<String>,
meta: NdpcParameterMetaData,
value: DateTime<Utc>,
}
impl ParamDateTimeLocal {
pub fn new(upid: &'static str, label: String, unit: Option<String>, default: DateTime<Utc>, description: String) -> Self { Self {
upid,
label,
description,
unit,
meta: NdpcParameterMetaData::new(),
value: default,
} }
pub fn value(&self) -> &DateTime<Utc> { &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!("<input type=\"datetime-local\" data-utc=\"{:?}\"/>", self.value)
}
}

View file

@ -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>;