FEA: show and edit planned datetimes for each event unit

This commit is contained in:
Keanu D?lle 2022-01-24 08:14:57 +01:00
parent f8eca4a944
commit 17341cafcf
14 changed files with 448 additions and 40 deletions

View File

@ -21,7 +21,24 @@
<input type="text" class="form-control" id="edit_event_cast_instance_name">
</div>
</div>
<button class="btn btn-success edit_event_cast_add_template" style="float: right">Hinzufügen</button><br><br>
<div class="form-group row">
<label class="col-sm-3 col-form-label"
for="edit_event_cast_instance_planned_start_time">Beginn: </label>
<div class="col-sm-9">
<input class="form-control" id="edit_event_cast_instance_planned_start_time"
type="datetime-local">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label"
for="edit_event_cast_instance_planned_end_time">Ende: </label>
<div class="col-sm-9">
<input class="form-control" id="edit_event_cast_instance_planned_end_time"
type="datetime-local">
</div>
</div>
<button class="btn btn-success edit_event_cast_add_template" style="float: right">Hinzufügen</button>
<br><br>
</div>
</div>

View File

@ -1,15 +1,25 @@
<div class="instance col-lg-4" style="padding: 2px;" data-instance-id="{{instance_id}}" data-template-id="{{template_id}}">
<div class="card">
<div class="card-header {{#if complete}}acs-green{{else}}acs-red{{/if}}">
{{name}}<button class="iconbutton remove_instance_button" style="float: right;"><svg color="red" width="20" height="20" fill="currentColor">
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
</svg></button>
<div class="card-header d-flex justify-content-between align-items-center {{#if complete}}acs-green{{else}}acs-red{{/if}}">
{{name}}
<div class="card-header-buttons">
<button class="iconbutton remove_instance_button" style="float: right;">
<svg color="red" fill="currentColor" height="20" width="20">
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
</svg>
</button>
<button class="btn btn-success btn-sm save-button" style="display: none;">Änderungen
Speichern
</button>
</div>
</div>
<div class="card-body">
<h5>Personal:</h5>
<div class="eu_cast_instance_personal">
{{#each positions}}
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-member-name="{{member_name}}" data-member-id="{{taken_by}}">
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{> search base=this.base type="member"}}
@ -21,7 +31,8 @@
<h5>Fahrzeuge:</h5>
<div class="eu_cast_instance_vehicles">
{{#each vehicle_positions}}
<div class="form-group row eu_cast_instance_vehicle_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-identifier="{{identifier}}" data-entity-id="{{taken_by}}">
<div class="form-group row eu_cast_instance_vehicle_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-identifier="{{identifier}}" data-entity-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{> search base=this.base type="vehicle"}}
@ -30,7 +41,27 @@
{{/each}}
</div>
{{/if}}
<hr>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Name: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_name qsf" type="text" value="{{name}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Beginn: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_planned_start_time qsf" type="datetime-local"
value="{{planned_start_time}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Ende: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_planned_end_time qsf" type="datetime-local"
value="{{planned_end_time}}">
</div>
</div>
</div>
</div>
</div>

View File

@ -4,16 +4,25 @@
{{name}}
</div>
<div class="card-body">
<div class="row">
<p class="col-4">Einsatzzeit:</p>
<p class="col-8">
{{planned_time}}
</p>
</div>
<h5>Personal:</h5>
<div class="eu_cast_instance_personal">
{{#each positions}}
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-member-name="{{member_name}}" data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{#if taken_by}}{{member_name}}{{else}}
<button class="btn btn-block btn-sm btn-secondary eu_cast_instance_self_register">Eintragen</button>
{{/if}}
</div>
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{#if taken_by}}{{member_name}}{{else}}
<button class="btn btn-block btn-sm btn-secondary eu_cast_instance_self_register">Eintragen
</button>
{{/if}}
</div>
</div>
{{/each}}
</div>

View File

@ -14,6 +14,7 @@ $(document).ready(function () {
EventEditModule = (function () {
let templates = {};
let event = {};
let instance_modified = false;
let load_templates = function () {
$.get("/templates/em_edit_event_core_data.hbs", function (res) {
@ -56,6 +57,9 @@ EventEditModule = (function () {
var template_search = new MiniSearchbar("edit_event_cast_template_search", null, null, null);
template_search.setup();
$("#edit_event_cast_instance_planned_start_time").val(event.start);
$("#edit_event_cast_instance_planned_end_time").val(event.end);
$(".edit_event_cast_add_template").on("click", add_template_instance);
load_instances();
}
@ -169,7 +173,8 @@ EventEditModule = (function () {
instance.template_id = template;
instance.name = instance_name;
instance.event_id = event_id;
instance.planned_start_time = $("#edit_event_cast_instance_planned_start_time").val() || undefined;
instance.planned_end_time = $("#edit_event_cast_instance_planned_end_time").val() || undefined;
$.ajax({
url: '/api/events/' + event_id + '/instances',
type: 'POST',
@ -215,7 +220,7 @@ EventEditModule = (function () {
if(this.vehicle_positions[i].taken_by){
let vehicle = await get_vehicle(this.vehicle_positions[i].taken_by);
this.vehicle_positions[i].identifier = vehicle.identifier;
}else{
} else {
vehicles_taken = false;
}
}
@ -223,15 +228,20 @@ EventEditModule = (function () {
$("#instances_container").append(templates.cast_instance(this));
$(".remove_instance_button").off("click").on("click", remove_instance);
$(".qsf").off("focusin focusout").on("focusin focusout", activate_modified).keyup(function (e) {
if (event.which === 13) {
activate_modified(e.target);
}
});
$(".eu_cast_instance_personal_position").each(function(){
$(".eu_cast_instance_personal_position").each(function () {
let pos = $(this).data("position-id");
let instance = $(this).data("instance-id");
let callback = async function(caller){
let callback = async function (caller) {
let ms = this;
let requirements_fulfilled = await check_position_requirements(pos, $(caller).data("entity-id"));
if(!requirements_fulfilled){
if (!requirements_fulfilled) {
$("#overwrite_position_requirements_modal").modal();
$(".overwrite_position_requirements_modal_submit").off("click").on("click", function(){
add_entity_to_position(instance, pos, $(caller).data("entity-id"));
@ -283,8 +293,45 @@ EventEditModule = (function () {
}
});
};
let activate_modified = function () {
if (!$(this).hasClass("modified")) { //Only execute on first modification
$(this).closest(".instance").find(".save-button").off("click").on("click", save).show(); //Show save button
$(this).addClass("modified");
}
};
let deactivate_modified = function () {
var delete_event = function(){
};
let save = async function () {
let data = {};
let instance = $(this).closest(".instance");
data.instance_id = instance.data("instance-id") || undefined;
data.name = instance.find(".eu_cast_instance_name").val() || undefined;
data.planned_start_time = instance.find(".eu_cast_instance_planned_start_time").val() || undefined;
data.planned_end_time = instance.find(".eu_cast_instance_planned_end_time").val() || undefined;
//TODO: error if no name
$.ajax({
type: "PATCH",
url: "/api/events/" + event_id + "/instances/" + data.instance_id,
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data),
error: function () {
alert("Es ist ein Fehler aufgetreten.");
},
success: async function (data) {
if (is_ok(data)) {
location.reload(); //TODO: soft reload
}
}
});
console.log("saving:" + data);
};
var delete_event = function () {
$.ajax({
url: '/api/events/' + event_id,
type: 'DELETE',

View File

@ -163,13 +163,29 @@ EventListModule = ( function() {
this.positions = await load_positions_for_instance(this.instance_id);
this.vehicle_positions = await load_vehicle_positions_for_instance(this.instance_id);
if (this.planned_start_time && this.planned_end_time) {
let date = new Date(this.planned_start_time);
let start_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
let start_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
date = new Date(this.planned_end_time);
let end_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
let end_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
if (start_date === end_date) {
this.planned_time = start_date + " " + start_time + end_time;
} else {
this.planned_time = start_date + " " + start_time + end_date + " " + end_time;
}
}
let positions_taken = true;
for(let i=0;i<this.positions.length;i++){
this.positions[i].base = "search_"+this.positions[i].instance_id+"_"+this.positions[i].position_id;
if(this.positions[i].taken_by){
for (let i = 0; i < this.positions.length; i++) {
this.positions[i].base = "search_" + this.positions[i].instance_id + "_" + this.positions[i].position_id;
if (this.positions[i].taken_by) {
let member = await get_member(this.positions[i].taken_by);
this.positions[i].member_name = member.firstname + " "+member.lastname;
}else{
this.positions[i].member_name = member.firstname + " " + member.lastname;
} else {
positions_taken = false;
}
}

View File

@ -1 +1 @@
v0.2-74-gfa2436e
v0.2-80-gf8eca4a

View File

@ -14,7 +14,6 @@ use crate::schema::eu_positions_templates;
pub mod templates;
pub mod instances;
//TODO: migrate to multiple files to improve readability
pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {

View File

@ -1,8 +1,11 @@
use std::convert::TryInto;
use chrono::Utc;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::State;
use crate::database::controller::connector::establish_connection;
use crate::modules::api::events::instances::update::PatchInstanceData;
use crate::schema::eu_instances;
use crate::Settings;
@ -34,4 +37,32 @@ pub fn get_instances(settings: &State<Settings>, event: uuid::Uuid) -> Result<Ve
Err(e)
}
}
}
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
#[table_name = "eu_instances"]
#[primary_key(entity_id)]
pub struct RawEventUnitInstanceChangeset {
pub(crate) instance_id: uuid::Uuid,
pub(crate) event_id: uuid::Uuid,
pub(crate) name: Option<String>,
pub(crate) planned_start_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) planned_end_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) real_start_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) real_end_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) billing_rate_id: Option<Option<uuid::Uuid>>,
pub(crate) billing_state_id: Option<Option<String>>,
}
pub fn update_instance(settings: &State<Settings>, patch: RawEventUnitInstanceChangeset) -> Result<RawEventUnitInstance, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match diesel::update(eu_instances.filter(event_id.eq(patch.event_id)).filter(instance_id.eq(patch.instance_id))).set(patch).get_result(&connection) {
Ok(update) => Ok(update),
Err(e) => {
error!("Couldn't patch event unit instance: {}", e);
Err(e)
}
}
}

View File

@ -15,4 +15,5 @@ pub mod mail_queue;
pub mod mail_templates;
pub mod permission;
pub mod order_enum;
pub mod time;
pub mod time;
pub mod serde_patch;

59
src/helper/serde_patch.rs Normal file
View File

@ -0,0 +1,59 @@
use std::convert::TryInto;
use serde::{Deserialize, Deserializer};
#[derive(Debug, PartialEq)]
pub enum Patch<T> {
Missing,
Null,
Value(T),
}
impl<T> Default for Patch<T> {
fn default() -> Self {
Patch::Missing
}
}
impl<T> From<Option<T>> for Patch<T> {
fn from(opt: Option<T>) -> Patch<T> {
match opt {
Some(v) => Patch::Value(v),
None => Patch::Null,
}
}
}
impl<T> Into<Option<Option<T>>> for Patch<T> {
fn into(self) -> Option<Option<T>> {
match self {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => Some(Some(v))
}
}
}
impl<T> TryInto<Option<T>> for Patch<T> {
type Error = ();
fn try_into(self) -> Result<Option<T>, Self::Error> {
match self {
Patch::Missing => Ok(None),
Patch::Null => Err(()),
Patch::Value(v) => Ok(Some(v))
}
}
}
impl<'de, T> Deserialize<'de> for Patch<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Option::deserialize(deserializer).map(Into::into)
}
}

View File

@ -212,6 +212,7 @@ fn rocket() -> _ {
modules::event_management::edit_event::edit_event,
modules::api::events::instances::create::create_instance,
modules::api::events::instances::read::read_instances,
modules::api::events::instances::update::patch_instance,
modules::api::events::event_units::position::create::create_event_unit_vehicle_position,
modules::api::events::event_units::templates::read::read_event_unit_template_vehicle_positions,
modules::api::events::event_units::position::delete::delete_event_unit_vehicle_positions,

View File

@ -19,7 +19,7 @@ use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
pub struct CreateInstanceData {
pub template_id: String,
pub name: String,
pub event_id: String,
pub event_id: uuid::Uuid,
pub planned_start_time: Option<String>,
pub planned_end_time: Option<String>,
}
@ -39,7 +39,7 @@ pub fn create_instance(
}
let cid = create_instance_data.into_inner();
if event_id != cid.event_id {
if parse_uuid_string(event_id.clone())? != cid.event_id {
return Err(Json(
ApiError::new(400, "Two different event_ids in body and parameter!".to_string()).to_wrapper(),
))

View File

@ -1,11 +1,20 @@
use crate::helper::settings::Settings;
use rocket::State;
use crate::helper::session_cookies::model::SessionCookie;
use std::convert::TryInto;
use chrono::{DateTime, ParseError, Utc};
use rocket::serde::json::Json;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::database::controller::events::change_position_instances;
use rocket::State;
use uuid::Uuid;
use crate::database::controller::events::{change_position_instances, get_event};
use crate::database::controller::events::instances::instances::{RawEventUnitInstanceChangeset, update_instance};
use crate::helper::serde_patch::Patch;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::time::datetime_str_to_utc;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::events::instances::read::EventUnitInstance;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use crate::modules::event_management::check_position_requirements::{check_position_requirements, RequirementParserError};
#[put("/api/events/instances/<instance_id>/positions/<position_id>/entities/<entity_id>", format = "json", rank = 1)]
@ -72,8 +81,179 @@ pub fn remove_entity_from_position(
));
}
match change_position_instances(settings, parse_uuid_string(instance_id)?, parse_uuid_string(position_id)?, None){
match change_position_instances(settings, parse_uuid_string(instance_id)?, parse_uuid_string(position_id)?, None) {
Ok(pos) => Ok(Json(pos)),
Err(e) => return Err(translate_diesel(e))
}
}
#[derive(Queryable, Deserialize)]
pub struct PatchInstanceData {
pub instance_id: uuid::Uuid,
#[serde(default)]
pub name: Patch<String>,
#[serde(default)]
pub planned_start_time: Patch<String>,
#[serde(default)]
pub planned_end_time: Patch<String>,
#[serde(default)]
pub real_start_time: Patch<String>,
#[serde(default)]
pub real_end_time: Patch<String>,
#[serde(default)]
pub billing_rate_id: Patch<uuid::Uuid>,
#[serde(default)]
pub billing_state_id: Patch<String>,
}
#[patch("/api/events/<event_id>/instances/<instance_id>", format = "json", data = "<patch_instance_data>")]
pub fn patch_instance(
settings: &State<Settings>,
cookie: SessionCookie,
patch_instance_data: Json<PatchInstanceData>,
event_id: String,
instance_id: String,
) -> Result<Json<EventUnitInstance>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member.clone())?;
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
));
}
let pid = patch_instance_data.into_inner();
let event_id = parse_uuid_string(event_id)?;
let instance_id = parse_uuid_string(instance_id)?;
if instance_id != pid.instance_id {
return Err(Json(ApiError::new(400, "instance_id in URI doesn't match instance_id in body data.".to_string()).to_wrapper()))
}
let name = match pid.name.try_into() {
Ok(name) => name,
Err(_) => return Err(Json(ApiError::new(400, "Cannot set name to null!".to_string()).to_wrapper()))
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => return Err(translate_diesel(e))
};
//Check if real_start_time or real_end_time is changed
if matches!(pid.real_start_time, Patch::Value(_)) || matches!(pid.real_start_time, Patch::Null) || matches!(pid.real_end_time, Patch::Value(_)) || matches!(pid.real_end_time, Patch::Null) {
//If so, check for additional permissions
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsatzzeiten zu verändern!".to_string()).to_wrapper(),
));
}
}
None => {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsatzzeiten zu verändern!".to_string()).to_wrapper(),
));
}
}
}
}
if matches!(pid.billing_rate_id, Patch::Value(_)) || matches!(pid.billing_rate_id, Patch::Null) || matches!(pid.billing_state_id, Patch::Value(_)) || matches!(pid.billing_state_id, Patch::Null) {
if !caller.has_permission(crate::permissions::modules::event_billing::personnel::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Abrechnung zu verändern!".to_string()).to_wrapper(),
));
}
}
None => {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Abrechnung zu verändern!".to_string()).to_wrapper(),
));
}
}
}
}
let planned_start_time = match pid.planned_start_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse planned start time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned start time!".to_string()).to_wrapper(),
))
}
}
}
};
let planned_end_time = match pid.planned_end_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse planned end time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned end time!".to_string()).to_wrapper(),
))
}
}
}
};
let real_start_time = match pid.real_start_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse real start time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse real start time!".to_string()).to_wrapper(),
))
}
}
}
};
let real_end_time = match pid.real_end_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse real end time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse real end time!".to_string()).to_wrapper(),
))
}
}
}
};
let data = RawEventUnitInstanceChangeset {
instance_id,
event_id,
name,
planned_start_time,
planned_end_time,
real_start_time,
real_end_time,
billing_rate_id: pid.billing_rate_id.into(),
billing_state_id: pid.billing_state_id.into(),
};
match update_instance(settings, data) {
Ok(res) => Ok(Json(EventUnitInstance::from_raw(res, settings, &cookie))),
Err(e) => return Err(translate_diesel(e))
}
}

View File

@ -142,11 +142,28 @@ pub mod modules {
pub const EDIT: &'static str = "modules.scheduler.appointments.edit";
}
}
pub mod settings{
pub mod settings {
pub const VIEW: &'static str = "modules.settings.view";
pub mod role_permissions{
pub mod role_permissions {
pub const VIEW: &'static str = "modules.settings.role_permissions.view";
pub const EDIT: &'static str = "modules.settings.role_permissions.edit";
}
}
pub mod event_billing {
pub const VIEW: &'static str = "modules.event_billing.view";
pub const APPROVE: &'static str = "modules.event_billing.approve";
pub mod personnel {
pub const VIEW: &'static str = "modules.event_billing.personnel.view";
pub const EDIT: &'static str = "modules.event_billing.personnel.edit";
}
pub mod start_end_times {
pub const VIEW: &'static str = "modules.event_billing.start_end_times.view";
pub const EDIT: &'static str = "modules.event_billing.start_end_times.edit";
}
}
}