FEA: added billing module (WIP)

This commit is contained in:
Keanu D?lle 2022-02-24 02:28:53 +01:00
parent 27295c704c
commit 2a6bdead8b
39 changed files with 1328 additions and 41 deletions

View File

@ -0,0 +1,24 @@
-- This file should undo anything in `up.sql`
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.personnel.edit';
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.personnel.view';
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.approve';
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.view';
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.start_end_times.edit';
DELETE
FROM roles_permissions
WHERE permission_id = 'modules.event_billing.start_end_times.view';

View File

@ -0,0 +1,18 @@
-- Your SQL goes here
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.approve', DEFAULT);
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.personnel.edit', DEFAULT);
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.personnel.view', DEFAULT);
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.start_end_times.edit', DEFAULT);
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.start_end_times.view', DEFAULT);
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
VALUES ('admin', 'modules.event_billing.view', DEFAULT);

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
alter table billing_states
drop column "order";

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
alter table billing_states
add "order" int;

View File

@ -0,0 +1,14 @@
<a class="hide goto_edit_times" href="/portal/eb/edit_times?event_id={{event.entity_id}}">
<button class="btn btn-primary">Abrechnung fortsetzen</button>
</a>
<a class="hide goto_personnel_billing" href="/portal/eb/personnel_billing?event_id={{event.entity_id}}">
<button class="btn btn-primary">Abrechnung fortsetzen</button>
</a>
<a class="hide goto_close_event" href="/portal/eb/close_event?event_id={{event.entity_id}}">
<button class="btn btn-primary" style="margin-right: 5px;">Abrechnung starten</button>
</a>
{{#each states}}
<a class="hide goto_approve_{{state_id}}" href="/portal/eb/approve/{{state_id}}?event_id={{../event.entity.id}}">
<button class="btn btn-success">{{description}}</button>
</a>
{{/each}}

View File

@ -0,0 +1,66 @@
<div class="instance col-lg-4" data-instance-id="{{instance_id}}" data-template-id="{{template_id}}"
style="padding: 2px;">
<div class="card">
<div class="card-header">
{{name}}
<button class="btn btn-success eb_eu_instance_save_btn" style="float: right;" type="button">Speichern
</button>
</div>
<div class="card-body">
<div class="row">
<p class="col-3">geplant:</p>
<p class="col-9">
{{planned_time}}
</p>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">realer Beginn: </label>
<div class="col-sm-9">
<input class="form-control eb_eu_instance_real_start_time qsf" type="datetime-local"
value="{{real_start_time}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">reales Ende: </label>
<div class="col-sm-9">
<input class="form-control eb_eu_instance_real_end_time qsf" type="datetime-local"
value="{{real_end_time}}">
</div>
</div>
<h5>Personal:</h5>
<div class="eu_cast_instance_personal">
{{#each positions}}
<div class="card" style="margin-bottom: 5px;">
<div class="card-body">
<div class="form-group row eb_eu_instance_personal_position"
data-instance-id="{{../instance_id}}"
data-member-id="{{taken_by}}" data-member-name="{{member_name}}"
data-position-id="{{position_id}}">
<label class="col-3 col-form-label">{{name}}</label>
<div class="input-group col-9">
{{> search base=this.base type="member"}}
</div>
</div>
<div class="form-group row">
<label class="col-form-label col-form-label-sm col-3">Von: </label>
<div class="input-group col-9">
<input class="form-control form-control-sm eb_eu_instance_position_real_start_time qsf"
type="datetime-local"
value="{{real_start_time}}">
</div>
</div>
<div class="form-group row">
<label class="col-form-label col-form-label-sm col-3">Bis: </label>
<div class="input-group col-9">
<input class="form-control form-control-sm eb_eu_instance_position_real_end_time qsf"
type="datetime-local"
value="{{real_end_time}}">
</div>
</div>
</div>
</div>
{{/each}}
</div>
</div>
</div>
</div>

View File

@ -85,6 +85,9 @@
<textarea rows="4" class="form-control" id="other_intern">{{other_intern}}</textarea>
</div>
</div>
<div class="form-group row">
<input class="form-control hide" id="state" type="hidden" value="{{state}}"></input>
</div>
</div>
</div>
<div class="row">

View File

@ -0,0 +1,8 @@
<ul class="progressbar">
<li class="progress_event_closed">Einsatz geschlossen</li>
<li class="progress_times_confirmed">Einsatzzeiten bestätigt</li>
<li class="progress_personnel_billing">Personalabrechnung</li>
{{#each this}}
<li class="progress_approve_{{state_id}}">{{description}}</li>
{{/each}}
</ul>

View File

@ -277,4 +277,84 @@ button.modified {
/* Overwriting card-header min-height to unify header height if some cards have buttons inside */
.card-header {
min-height: 56px !important;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.3rem;
}
.container {
width: 600px;
margin: 100px auto;
}
.progressbar {
counter-reset: step;
}
.progressbar li {
list-style-type: none;
float: left;
font-size: 12px;
position: relative;
text-align: center;
text-transform: uppercase;
color: #7d7d7d;
}
.progressbar li:before {
width: 30px;
height: 30px;
content: counter(step);
counter-increment: step;
line-height: 30px;
border: 2px solid #7d7d7d;
display: block;
text-align: center;
margin: 0 auto 10px auto;
border-radius: 50%;
background-color: white;
}
.progressbar li:after {
width: 100%;
height: 2px;
content: '';
position: absolute;
background-color: #7d7d7d;
top: 15px;
left: -50%;
z-index: -1;
}
.progressbar li:first-child:after {
content: none;
}
.progressbar li.active {
color: green;
}
.progressbar li.active:before {
border-color: #55b776;
}
.progressbar li.active + li:after {
background-color: #55b776;
}
.nav-progressbar {
height: 100px;
}
.hide {
display: none;
}

View File

@ -114,6 +114,7 @@ EventEditModule = (function () {
event.name = $("#name").val();
event.start = $("#start").val();
event.end = $("#end").val();
event.state = parseInt($("#state").val());
if ($("#site").val().length > 0) {
event.site = $("#site").val();
}
@ -140,6 +141,7 @@ EventEditModule = (function () {
if ($("#contact_on_site_phone").val().length > 0) {
event.contact_on_site_phone = $("#contact_on_site_phone").val();
}
console.log(event);
$.ajax({
url: '/api/events/' + event_id,
type: 'PUT',
@ -300,7 +302,7 @@ EventEditModule = (function () {
}
};
let deactivate_modified = function () {
//TODO
};
let save = async function () {
let data = {};

View File

@ -0,0 +1,305 @@
$(document).ready(async function () {
let searchParams = new URLSearchParams(window.location.search);
event_id = searchParams.get('event_id');
await EventBilling.setup(event_id);
});
EventBilling = (function () {
let templates = {};
let event = {};
let billing_states = {};
let event_min_billing_state = {};
let setup = async function (event_id) {
await load_templates();
await load_event(event_id);
await setup_pagination();
let pathname = window.location.pathname;
if (pathname === "/portal/eb/event") {
await event_view.show_action_buttons();
} else if (pathname === "/portal/eb/close_event") {
await close_event_view.setup();
} else if (pathname === "/portal/eb/edit_times") {
await edit_times_view.setup();
}
};
let setup_pagination = async function () {
await Promise.all([load_billing_states(), load_min_billing_states_for_event(event.entity_id)]).then(function (res) {
billing_states = res[0];
event_min_billing_state = res[1];
});
$(".nav-progressbar").html(templates.progressbar(billing_states));
$(".action_btn").html(templates.eb_action_buttons({event: event, states: billing_states}));
$(".progressbar > li").css("width", (100 / (3 + billing_states.length)) + "%");
//Mark active
if (event.state >= 4) {
$(".progress_event_closed").addClass("active");
}
if (event.state >= 6) {
$(".progress_times_confirmed").addClass("active");
}
if (event.state >= 8) {
$(".progress_personnel_billing").addClass("active");
for (let state of billing_states) {
$(".progress_approve_" + state.state_id).addClass("active");
if (state.state_id === event_min_billing_state) {
break;
}
}
}
};
let load_billing_states = async function () {
const res = $.ajax({
url: '/api/billing_states',
type: 'GET',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
return res;
}
};
let load_min_billing_states_for_event = async function (event_id) {
const res = $.ajax({
url: '/api/events/' + event_id + '/billing_states/min',
type: 'GET',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
return res;
}
};
let load_templates = async function () {
const progressbar = $.get("/templates/progressbar.hbs");
const eb_action_buttons = $.get("/templates/eb_action_buttons.hbs");
const search = $.get("/templates/search.hbs");
await Promise.all([progressbar, eb_action_buttons, search]).then(function (res) {
templates.progressbar = Handlebars.compile(res[0]);
templates.eb_action_buttons = Handlebars.compile(res[1]);
Handlebars.registerPartial('search', Handlebars.compile(res[2]));
});
};
let load_event = async function (event_id) {
await $.ajax({
url: '/api/events/' + event_id,
type: 'GET',
contentType: 'application/json',
success: function (data) {
if (is_ok(data)) {
event = data;
}
},
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
};
let event_view = (function () {
let show_action_buttons = async function () {
//Show action button
if (event.state < 4) {
$(".goto_close_event").show();
} else if (event.state === 4) {
$(".goto_edit_times").show();
} else if (event.state === 6) {
$(".goto_personnel_billing").show();
} else if (event.state === 8) {
if (!event_min_billing_state.error) {
//Show next approval step (billing_state) button
for (let i = 0; i < billing_states.length; i++) {
if (billing_states[i].state_id === event_min_billing_state) {
if (billing_states[i + 1]) {
$(".goto_approve_" + billing_states[i + 1].state_id).show();
}
}
}
}
}
};
return {
show_action_buttons
}
}());
let close_event_view = (function () {
let setup = function () {
$(".close_event_btn").off("click").on("click", async function () {
try {
await close_event();
window.location = "/portal/eb/edit_times?event_id=" + event.entity_id;
} catch (e) {
//TODO: proper error reporting
}
});
};
let close_event = async function () {
const res = $.ajax({
url: '/api/events/' + event.entity_id + '/close',
type: 'PUT',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
return res;
} else {
throw res;
}
}
return {
setup
}
}());
let edit_times_view = (function () {
let eu_instances = {};
let load_local_templates = async function () {
const eu_instance_card = $.get("/templates/eb_eu_instance_card.hbs");
await Promise.all([eu_instance_card]).then(function (res) {
templates.eu_instance_card = Handlebars.compile(res[0]);
});
};
let setup = async function () {
await load_local_templates();
eu_instances = await load_eu_instances(event.entity_id);
await load_eu_instance_positions();
$(".eu_instances_container").empty();
for (let instance of eu_instances) {
if (instance.planned_start_time && instance.planned_end_time) {
let date = new Date(instance.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(instance.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) {
instance.planned_time = start_date + " " + start_time + end_time;
} else {
instance.planned_time = start_date + " " + start_time + end_date + " " + end_time;
}
}
for (let position of instance.positions) {
position.base = "search_" + instance.instance_id + "_" + position.position_id;
if (position.taken_by) {
position.taken_by_member = await get_member(position.taken_by);
}
}
$(".eu_instances_container").append(templates.eu_instance_card(instance));
for (let position of instance.positions) {
let val_name = null;
if (position.taken_by) {
val_name = position.taken_by_member.firstname + " " + position.taken_by_member.lastname;
}
var member_search = new MiniSearchbar(position.base, null, position.taken_by, val_name, null);
member_search.setup();
}
}
console.log(eu_instances);
$(".eb_eu_instance_save_btn").off("click").on("click", save);
$(".approve_times_btn").off("click").on("click", approve_times);
$(".temp3").on("change", function () {
console.log("Test");
$(this).closest(".instance").find(".temp1").val($(this).find("option:selected").data("temp1"));
$(this).closest(".instance").find(".temp2").val($(this).find("option:selected").data("temp2"));
});
};
let load_eu_instances = async function () {
const res = $.ajax({
url: '/api/events/' + event.entity_id + '/instances',
type: 'GET',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
return res;
} else {
throw res;
}
};
let load_eu_instance_positions = async function () {
let queue = [];
for (let instance of eu_instances) {
queue.push(load_positions_for_instance_new(instance.instance_id));
}
await Promise.all(queue).then(function (res) {
for (let i = 0; i < eu_instances.length; i++) {
eu_instances[i].positions = res[i];
}
});
};
let save = async function () {
console.log("Saving instance.");
let parent = $(this).closest(".instance");
console.log(parent);
let instance_patch = {};
instance_patch.instance_id = parent.data("instance-id") || undefined;
instance_patch.real_start_time = parent.find(".eb_eu_instance_real_start_time").val() || undefined;
instance_patch.real_end_time = parent.find(".eb_eu_instance_real_end_time").val() || undefined;
await save_eu_instance(instance_patch);
};
let save_eu_instance = async function (data) {
const res = $.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.");
},
});
if (is_ok(res)) {
return res;
} else {
throw res;
}
};
let approve_times = async function () {
const res = $.ajax({
url: '/api/events/' + event.entity_id + '/approve_times',
type: 'PUT',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
window.location.href = "/portal/eb/approve?event_id=" + event.entity_id;
} else {
throw res;
}
};
return {
setup
}
}());
return {
setup
}
}());

View File

@ -151,9 +151,25 @@ let load_positions_for_instance = async function(instance_id){
return res;
}
};
let load_vehicle_positions_for_instance = async function(instance_id){
let load_positions_for_instance_new = async function (instance_id) {
const res = $.ajax({
url: '/api/events/instances/' + instance_id + '/positions',
type: 'GET',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
}
});
if (is_ok(res)) {
return res;
} else {
throw res;
}
};
let load_vehicle_positions_for_instance = async function (instance_id) {
const res = await $.ajax({
url: '/api/events/instances/'+instance_id+'/vehicle_positions',
url: '/api/events/instances/' + instance_id + '/vehicle_positions',
type: 'GET',
contentType: 'application/json',
timeout: 3000,

View File

@ -58,7 +58,9 @@ function MiniSearchbar(pbase, pcallback, value_id, value_name, delete_callback){
}
$("#"+ms.base+"_remove").off("click").on("click", function(){
ms.remove(ms);
ms.delete_callback(this);
if (ms.delete_callback) {
ms.delete_callback(this);
}
});
$('body').click(function(evt){
if(evt.target.class === ms.base+"-search-result-overlay")

View File

@ -0,0 +1,26 @@
{{> header }}
<div class="container-fluid">
<div class="row">
<div class="wrapper">
{{> sidebar }}
<div id="content">
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
<div class="nav-progressbar"></div>
<div class="col">
<div class="row">
<div class="card w-100">
<div class="card-body align-items-center w-100">
<h3 style="text-align: center">Abrechnung bestätigen & freigeben</h3>
<p class="text-danger" style="text-align: center"><b>Achtung:</b> Hiermit wird die
Richtigkeit der Abrechnung verbindlich bestätigt.</p>
<span class="d-flex justify-content-center"><button
class="btn btn-danger close_event_btn" role="button">Freigeben</button></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{> footer }}

View File

@ -0,0 +1,28 @@
{{> header }}
<div class="container-fluid">
<div class="row">
<div class="wrapper">
{{> sidebar }}
<div id="content">
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
<div class="nav-progressbar"></div>
<div class="col">
<div class="row">
<div class="card">
<div class="card-body align-items-center">
<h3 style="text-align: center">Einsatz schließen</h3>
<p class="text-danger"><b>Achtung:</b> Sobald der Einsatz geschlossen wurde können sich
keine Helfer mehr eintragen und der Einsatz kann über die Einsatzverwaltung nicht
mehr verändert werden. Änderungen sind nur noch über das Abrechnungsmodul möglich!
</p>
<span class="d-flex justify-content-center"><button
class="btn btn-danger close_event_btn" role="button">Einsatz schließen</button></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{> footer }}

View File

@ -0,0 +1,34 @@
{{> header }}
<div class="container-fluid">
<div class="row">
<div class="wrapper">
{{> sidebar }}
<div id="content">
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
<div class="nav-progressbar"></div>
<div class="col">
<div class="row">
<div class="card w-100">
<div class="card-body eu_instances_container row">
</div>
</div>
<div class="card w-100">
<div class="card-body align-items-center">
<h3 style="text-align: center">Einsatzzeiten bestätigen</h3>
<p class="text-info" style="text-align: center;">Bei allen Einsatzeinheiten ohne realer
Beginn oder reales Ende wird die geplante Zeit verwendet.</p>
<p class="text-danger" style="text-align: center;"><b>Achtung:</b> Hiermit bestätigst du
persönlich, dass die eingetragenen Einsatzzeiten korrekt sind.</p>
<span class="d-flex justify-content-center"><button
class="btn btn-danger approve_times_btn"
role="button">Einsatzzeiten bestätigen</button></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{> footer }}

View File

@ -0,0 +1,63 @@
{{> header }}
<div class="container-fluid">
<div class="row">
<div class="wrapper">
{{> sidebar }}
<div id="content">
<h1 style="text-align: center">Einsatzabrechnung {{event.name}}</h1>
<div class="nav-progressbar"></div>
<div class="col">
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
Einsatz
</div>
<div class="card-body">
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="event_name">Name</label>
<div class="col-sm-9">
<input class="form-control qsf" disabled id="event_name" readonly
type="text" value="{{event.name}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="event_start">Anfang</label>
<div class="col-sm-9">
<input class="form-control qsf" disabled id="event_start" readonly
type="datetime-local" value="{{event.start}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="event_end">Ende</label>
<div class="col-sm-9">
<input class="form-control qsf" disabled id="event_end" readonly
type="datetime-local" value="{{event.end}}">
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">
Aktionen
</div>
<div class="card-body">
<div class="d-flex justify-content-center w-100 action_btn">
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
</div>
</div>
</div>
</div>
</div>
</div>
{{> footer }}

View File

@ -57,15 +57,24 @@
<ul class="nav nav-tabs edit_event_cast_navtabs" id="edit_event_cast_tab" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" href="#edit_event_cast_core_data_tabpanel" id="core-tab" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#edit_event_cast_core_data_tabpanel" aria-controls="core-tab" aria-selected="true">Basisdaten</a>
<a class="nav-link active" href="#edit_event_cast_core_data_tabpanel" id="core-tab"
data-toggle="tab" data-bs-toggle="tab"
data-bs-target="#edit_event_cast_core_data_tabpanel" aria-controls="core-tab"
aria-selected="true">Basisdaten</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#edit_event_cast_cast_tabpanel" id="cast-tab" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#edit_event_cast_cast_tabpanel" aria-controls="cast-tab" aria-selected="true">Besetzung</a>
<a class="nav-link" href="#edit_event_cast_cast_tabpanel" id="cast-tab" data-toggle="tab"
data-bs-toggle="tab" data-bs-target="#edit_event_cast_cast_tabpanel"
aria-controls="cast-tab" aria-selected="true">Besetzung</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#" id="edit_event_delete_link">Löschen</a>
</li>
</ul><br>
<li class="nav-item" role="presentation">
<a class="nav-link" href="/portal/eb/event?event_id={{event_id}}" id="billing_link">Einsatzabrechnung</a>
</li>
</ul>
<br>
<div class="tab-content" id="edit_event_cast_tab_content">
<div class="tab-pane fade show active edit_event_core_data" role="tabpanel" aria-labelledby="core-tab" id="edit_event_cast_core_data_tabpanel"></div>
<div class="tab-pane fade" role="tabpanel" aria-labelledby="cast-tab" id="edit_event_cast_cast_tabpanel">

View File

@ -1 +1 @@
v0.2-80-gf8eca4a
v0.2-84-g27295c7

View File

@ -0,0 +1 @@
pub mod states;

View File

@ -0,0 +1,55 @@
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::State;
use crate::database::controller::connector::establish_connection;
use crate::Settings;
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct BillingState {
state_id: String,
description: Option<String>,
final_approve: bool,
}
pub fn get_billing_states(settings: &State<Settings>) -> Result<Vec<BillingState>, diesel::result::Error> {
use crate::schema::billing_states::dsl::*;
let connection = establish_connection(settings);
match billing_states.order(order.asc()).select((state_id, description, final_approve)).get_results(&connection) {
Ok(bs) => Ok(bs),
Err(e) => {
error!("Couldn't get billing states: {}", e);
Err(e)
}
}
}
pub fn get_billing_states_for_event(settings: &State<Settings>, event: uuid::Uuid) -> Result<Vec<String>, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match eu_instances.filter(event_id.eq(event)).filter(billing_state_id.is_not_null()).select(billing_state_id).get_results(&connection) {
Ok(states) => Ok(states.iter().map(|state: &Option<String>| state.clone().unwrap()).collect()),
Err(e) => {
error!("Couldn't get billing states from instance for event: {}", e);
Err(e)
}
}
}
pub fn get_min_billing_states_for_event(settings: &State<Settings>, event: uuid::Uuid) -> Result<Option<String>, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
use crate::schema::billing_states::dsl::*;
let connection = establish_connection(settings);
match eu_instances.inner_join(crate::schema::billing_states::table).filter(event_id.eq(event)).select(billing_state_id).order(order.asc()).first(&connection) {
Ok(state) => Ok(state),
Err(e) => {
error!("Couldn't get min billing states from instance for event: {}", e);
Err(e)
}
}
}

View File

@ -1,9 +1,15 @@
use crate::helper::settings::Settings;
use diesel::{Connection, PgConnection};
use rocket::State;
use crate::helper::settings::Settings;
pub fn establish_connection(settings: &State<Settings>) -> PgConnection {
//TODO: return Error
let db_url = settings.database.connection_string.clone();
PgConnection::establish(&db_url).expect(&format!("Error connecting to database."))
match PgConnection::establish(&db_url) {
Ok(con) => con,
Err(e) => {
panic!("Couldn't connect to database: {}. Database offline?", e);
//TODO: Return Error
}
}
}

View File

@ -15,6 +15,7 @@ use crate::schema::eu_positions_templates;
pub mod templates;
pub mod instances;
//TODO: migrate to multiple files to improve readability
//TODO: document this whole file...
pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {
use crate::schema::events::dsl::*;
@ -30,12 +31,46 @@ pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diese
}
}
pub fn change_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error>{
/// Sets state for event from 0 or 2 to 4 and therefore locks the event.
/// Used in API endpoint for billing module
/// Returns an empty Ok result or an [diesel::result::Error].
pub fn close_event(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
use crate::schema::events::dsl::*;
let connection = establish_connection(settings);
match diesel::update(events.filter(entity_id.eq(data.entity_id))).set(data).get_result(&connection){
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(any(vec!(0, 2))))).set(state.eq(4)).execute(&connection) {
Ok(_) => Ok(()),
Err(e) => {
error!("Couldn't close event: {}", e);
Err(e)
}
}
}
/// Sets state for event from 4 to 6 and therefore locks the event.
/// Used in API endpoint for billing module
/// Returns an empty Ok result or an [diesel::result::Error].
pub fn approve_event_times(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<(), diesel::result::Error> {
use crate::schema::events::dsl::*;
let connection = establish_connection(settings);
match diesel::update(events.filter(entity_id.eq(event_id)).filter(state.eq(4))).set(state.eq(6)).execute(&connection) {
Ok(_) => Ok(()),
Err(e) => {
error!("Couldn't close event: {}", e);
Err(e)
}
}
}
pub fn change_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {
use crate::schema::events::dsl::*;
let connection = establish_connection(settings);
match diesel::update(events.filter(entity_id.eq(data.entity_id))).set(data).get_result(&connection) {
Ok(org) => Ok(org),
Err(e) => {
error!("Couldn't update event: {}", e);

View File

@ -24,4 +24,5 @@ pub mod entities;
pub mod organisers;
pub mod events;
pub mod event_requests;
pub mod permissions;
pub mod permissions;
pub mod billing;

View File

@ -1,5 +1,5 @@
use crate::helper::sitebuilder::model::alerts::{Alert, AlertClass};
use crate::helper::sitebuilder::model::sidebar::{EventManagement, MemberManagement, ResourceManagement, Sidebar, Summary, Communicator, Settings};
use crate::helper::sitebuilder::model::sidebar::{Communicator, EventBilling, EventManagement, MemberManagement, ResourceManagement, Settings, Sidebar, Summary};
use crate::modules::member_management::model::member::Member;
impl Alert {
@ -29,6 +29,10 @@ impl Sidebar {
visible: member.has_permission(crate::permissions::modules::event_management::VIEW.to_string()),
active: false,
},
event_billing: EventBilling {
visible: member.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()),
active: false,
},
communicator: Communicator {
visible: member.has_permission(crate::permissions::modules::communicator::VIEW.to_string()),
active: false,

View File

@ -6,6 +6,7 @@ pub struct Sidebar {
pub resource_management: ResourceManagement,
pub member_management: MemberManagement,
pub event_management: EventManagement,
pub event_billing: EventBilling,
pub communicator: Communicator,
pub settings: Settings,
}
@ -34,6 +35,12 @@ pub struct EventManagement {
pub active: bool,
}
#[derive(Serialize)]
pub struct EventBilling {
pub visible: bool,
pub active: bool,
}
#[derive(Serialize)]
pub struct Communicator {
pub visible: bool,

View File

@ -114,9 +114,9 @@ fn rocket() -> _ {
.manage(cookie_storage)
.manage(mail_queue)
.manage(mail_templates)
.mount(
"/",
routes![
.mount( //TODO: find a way to get rip of this crap
"/",
routes![
modules::dashboard::view::dashboard,
modules::welcome::view::welcome_get::welcome_get,
modules::welcome::view::welcome_post::welcome_post,
@ -192,6 +192,10 @@ fn rocket() -> _ {
modules::api::events::create::create_event,
modules::api::events::update::update_event,
modules::api::events::delete::delete_event,
modules::api::events::update::close_event,
modules::api::events::update::approve_times,
modules::api::events::read::read_billing_states_for_event,
modules::api::events::read::read_min_billing_states_for_event,
modules::event_management::event_unit_positions::event_unit_positions,
modules::event_management::event_unit_templates::event_unit_templates,
modules::api::members::licenses::read::read_license_categories,
@ -250,6 +254,11 @@ fn rocket() -> _ {
modules::api::permissions::roles::update::api_remove_member_from_role,
modules::api::permissions::roles::update::api_update_role,
modules::print::event_note::print_event_note,
modules::event_billing::event::event_view,
modules::event_billing::close_event::close_event,
modules::event_billing::edit_times::edit_times,
modules::event_billing::approve::approve,
modules::api::billing_states::read::read_billing_states,
],
)
.mount("/css", FileServer::from("resources/css"))

View File

@ -0,0 +1 @@
pub mod read;

View File

@ -0,0 +1,23 @@
use rocket::serde::json::Json;
use rocket::State;
use crate::database::controller::billing::states::{BillingState, get_billing_states};
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use crate::Settings;
#[get("/api/billing_states", format = "json")]
pub fn read_billing_states(settings: &State<Settings>, cookie: SessionCookie) -> Result<Json<Vec<BillingState>>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
if !caller.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()) {
return Err(Json(ApiError::new(403, "Keine Berechtigung Abrechnungsstatusse abzurufen!".to_string()).to_wrapper()));
}
match get_billing_states(settings) {
Ok(states) => Ok(Json(states)),
Err(e) => Err(translate_diesel(e)),
}
}

View File

@ -1,17 +1,18 @@
use chrono::NaiveDateTime;
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
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_option_uuid};
use crate::database::model::events::Event;
use rocket::State;
use crate::database::controller::entities::generate_entity;
use crate::database::controller::events::add_event;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct CreateEventData{
pub struct CreateEventData {
pub(crate) name: String,
pub(crate) start: String,
pub(crate) end: String,
@ -24,6 +25,7 @@ pub struct CreateEventData{
pub(crate) related_group: Option<String>,
pub(crate) other: Option<String>,
pub(crate) other_intern: Option<String>,
pub(crate) state: Option<i16>,
}
#[post("/api/events", format = "json", data = "<create_event_data>")]

View File

@ -2,6 +2,7 @@ use chrono::{Duration, Local, NaiveDateTime};
use rocket::serde::json::Json;
use rocket::State;
use crate::database::controller::billing::states::{get_billing_states_for_event, get_min_billing_states_for_event};
use crate::database::controller::events::{get_event, get_event_count, get_events, get_events_for_member_in_future, get_instance_positions};
use crate::database::controller::events::instances::instances::get_instances;
use crate::database::model::events::Event;
@ -176,11 +177,43 @@ pub fn check_event_cast_status(settings: &State<Settings>, cookie: SessionCookie
}
let full = open_positions.len() == 0;
let open_positions = if full{None}else{Some(open_positions)};
let open_positions = if full { None } else { Some(open_positions) };
Ok(Json(EventCastStatus{
Ok(Json(EventCastStatus {
event_id: event_id,
full,
open_positions
open_positions,
}))
}
/// Returns list of all non null billing_states for specific event
#[get("/api/events/<event_id>/billing_states", format = "json", rank = 3)]
pub fn read_billing_states_for_event(settings: &State<Settings>, cookie: SessionCookie, event_id: String) -> Result<Json<Vec<String>>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
if !caller.has_permission(crate::permissions::modules::event_management::VIEW.to_string()) {
return Err(Json(ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper()))
}
match get_billing_states_for_event(settings, parse_uuid_string(event_id)?) {
Ok(states) => Ok(Json(states)),
Err(e) => Err(translate_diesel(e)),
}
}
/// Returns minimal billing_state for specific event
/// Ignores event units with null as billing_state_id
/// Used to determine if a specific billing state is fulfilled for all instances on one event
#[get("/api/events/<event_id>/billing_states/min", format = "json", rank = 3)]
pub fn read_min_billing_states_for_event(settings: &State<Settings>, cookie: SessionCookie, event_id: String) -> Result<Json<Option<String>>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
if !caller.has_permission(crate::permissions::modules::event_management::VIEW.to_string()) {
return Err(Json(ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper()))
}
match get_min_billing_states_for_event(settings, parse_uuid_string(event_id)?) {
Ok(state) => Ok(Json(state)),
Err(e) => Err(translate_diesel(e)),
}
}

View File

@ -1,14 +1,16 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use rocket::serde::json::Json;
use crate::modules::api::events::create::CreateEventData;
use crate::database::model::events::Event;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid, parse_uuid_string};
use chrono::NaiveDateTime;
use diesel::result::Error;
use rocket::serde::json::Json;
use rocket::State;
use crate::database::controller::events::{change_event, get_event};
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::database::controller::events::change_event;
use crate::modules::api::events::create::CreateEventData;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
#[put("/api/events/<entity_id>", format = "json", data = "<update_event_data>")]
pub fn update_event(
@ -18,6 +20,7 @@ pub fn update_event(
update_event_data: Json<CreateEventData>,
) -> Result<Json<Event>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
//TODO: Check if caller is member_responsible -> if so skip this check
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsatz zu bearbeiten!".to_string()).to_wrapper(),
@ -42,7 +45,14 @@ pub fn update_event(
}
};
let input: Event = Event{
let state = match ecd.state {
Some(state) => state,
None => 0,
};
//TODO: Remove this parse_option_uuid crap. Just use rocket + serde deserialisation by replacing string to proper type in CreateEventData
//TODO: related_request
let input: Event = Event {
entity_id: parse_uuid_string(entity_id)?,
name: ecd.name,
start,
@ -57,11 +67,89 @@ pub fn update_event(
other: ecd.other,
other_intern: ecd.other_intern,
related_request: None,
state: 0
state,
};
match change_event(settings, input){
match change_event(settings, input) {
Ok(event) => Ok(Json(event)),
Err(e) => Err(translate_diesel(e))
}
}
#[put("/api/events/<entity_id>/close", format = "json")]
pub fn close_event(
settings: &State<Settings>,
cookie: SessionCookie,
entity_id: String,
) -> Result<(), Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
let event_id = parse_uuid_string(entity_id)?;
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("{} tried to close event with invalid event_id {}: {}", caller.entity_id, event_id, e);
return Err(Json(ApiError::new(404, "No event with this entity_id found.".to_string()).to_wrapper()));
}
};
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
warn!("No permissions to close event for member {}", caller.entity_id);
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
}
}
None => {
warn!("No permissions to close event for member {}", caller.entity_id);
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
}
}
}
match crate::database::controller::events::close_event(settings, event_id) {
Ok(_) => Ok(()),
Err(e) => Err(translate_diesel(e))
}
}
#[put("/api/events/<entity_id>/approve_times", format = "json")]
pub fn approve_times(
settings: &State<Settings>,
cookie: SessionCookie,
entity_id: String,
) -> Result<(), Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
let event_id = parse_uuid_string(entity_id)?;
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("{} tried to close event with invalid event_id {}: {}", caller.entity_id, event_id, e);
return Err(Json(ApiError::new(404, "No event with this entity_id found.".to_string()).to_wrapper()));
}
};
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
warn!("No permissions to close event for member {}", caller.entity_id);
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
}
}
None => {
warn!("No permissions to close event for member {}", caller.entity_id);
return Err(Json(ApiError::new(403, "No permissions to edit event.".to_string()).to_wrapper()));
}
}
}
match crate::database::controller::events::approve_event_times(settings, event_id) {
Ok(_) => Ok(()),
Err(e) => Err(translate_diesel(e))
}
}

View File

@ -10,4 +10,5 @@ pub mod appointments;
pub mod info;
pub mod event_organisers;
pub mod events;
pub mod permissions;
pub mod permissions;
pub mod billing_states;

View File

@ -0,0 +1,78 @@
use rocket::http::Status;
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::controller::events::get_event;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::modules::event_billing::event::EventBilling;
use crate::Settings;
/// UI for viewing, editing & approving event unit times.
/// required permissions:
/// * modules.event_billing.start_end_times.view OR be member_responsible for event to view real start/end times
/// * modules.event_billing.start_end_times.edit OR be member_responsible for event to edit real start/end times
#[get("/portal/eb/approve?<event_id>")]
pub fn approve(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
let caller = match cookie.member {
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
Some(caller) => caller,
None => return Err(Status::Unauthorized),
};
let event_id: uuid::Uuid = match event_id.parse() {
Ok(event_id) => event_id,
Err(e) => {
warn!("Unparsable event_id in close_event: {}", e);
return Err(Status::BadRequest);
}
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("Couldn't load event: {}", e);
return Err(Status::NotFound);
}
};
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::VIEW.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Status::Forbidden);
}
}
None => {
return Err(Status::Forbidden);
}
}
}
let header = Header {
html_language: "de".to_string(),
site_title: "Einsatzabrechnung".to_string(),
stylesheets: vec![Stylesheet {
path: "/css/errms.css".to_string(),
}],
};
let footer = Footer {
scripts: vec![Script {
path: "/js/event_billing.js".to_string(),
}, Script {
path: "/js/mini_searchbar.js".to_string(),
}],
};
let mut sidebar = Sidebar::new(caller.clone());
sidebar.event_billing.active = true;
Ok(Template::render("module_eb_approve", EventBilling {
header,
footer,
sidebar,
caller: caller.entity_id,
event,
}))
}

View File

@ -0,0 +1,75 @@
use rocket::http::Status;
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::controller::events::get_event;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::modules::event_billing::event::EventBilling;
use crate::Settings;
/// Close Event UI
/// required permissions:
/// * modules.event_management.events.edit OR be member_responsible for event
#[get("/portal/eb/close_event?<event_id>")]
pub fn close_event(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
let caller = match cookie.member {
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
Some(caller) => caller,
None => return Err(Status::Unauthorized),
};
let event_id: uuid::Uuid = match event_id.parse() {
Ok(event_id) => event_id,
Err(e) => {
warn!("Unparsable event_id in close_event: {}", e);
return Err(Status::BadRequest);
}
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("Couldn't load event: {}", e);
return Err(Status::NotFound);
}
};
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Status::Forbidden);
}
}
None => {
return Err(Status::Forbidden);
}
}
}
let header = Header {
html_language: "de".to_string(),
site_title: "Einsatzabrechnung".to_string(),
stylesheets: vec![Stylesheet {
path: "/css/errms.css".to_string(),
}],
};
let footer = Footer {
scripts: vec![Script {
path: "/js/event_billing.js".to_string(),
}],
};
let mut sidebar = Sidebar::new(caller.clone());
sidebar.event_billing.active = true;
Ok(Template::render("module_eb_close_event", EventBilling {
header,
footer,
sidebar,
caller: caller.entity_id,
event,
}))
}

View File

@ -0,0 +1,78 @@
use rocket::http::Status;
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::controller::events::get_event;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::modules::event_billing::event::EventBilling;
use crate::Settings;
/// UI for viewing, editing & approving event unit times.
/// required permissions:
/// * modules.event_billing.start_end_times.view OR be member_responsible for event to view real start/end times
/// * modules.event_billing.start_end_times.edit OR be member_responsible for event to edit real start/end times
#[get("/portal/eb/edit_times?<event_id>")]
pub fn edit_times(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
let caller = match cookie.member {
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
Some(caller) => caller,
None => return Err(Status::Unauthorized),
};
let event_id: uuid::Uuid = match event_id.parse() {
Ok(event_id) => event_id,
Err(e) => {
warn!("Unparsable event_id in close_event: {}", e);
return Err(Status::BadRequest);
}
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("Couldn't load event: {}", e);
return Err(Status::NotFound);
}
};
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::VIEW.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Status::Forbidden);
}
}
None => {
return Err(Status::Forbidden);
}
}
}
let header = Header {
html_language: "de".to_string(),
site_title: "Einsatzabrechnung".to_string(),
stylesheets: vec![Stylesheet {
path: "/css/errms.css".to_string(),
}],
};
let footer = Footer {
scripts: vec![Script {
path: "/js/event_billing.js".to_string(),
}, Script {
path: "/js/mini_searchbar.js".to_string(),
}],
};
let mut sidebar = Sidebar::new(caller.clone());
sidebar.event_billing.active = true;
Ok(Template::render("module_eb_edit_times", EventBilling {
header,
footer,
sidebar,
caller: caller.entity_id,
event,
}))
}

View File

@ -0,0 +1,81 @@
use rocket::http::Status;
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::controller::events::get_event;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::Settings;
#[derive(Serialize)]
pub struct EventBilling {
pub header: Header,
pub footer: Footer,
pub sidebar: Sidebar,
pub caller: uuid::Uuid,
pub event: Event,
}
#[get("/portal/eb/event?<event_id>")]
pub fn event_view(cookie: SessionCookie, settings: &State<Settings>, event_id: String) -> Result<Template, Status> {
let caller = match cookie.member {
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
Some(caller) => caller,
None => return Err(Status::Unauthorized),
};
let event_id: uuid::Uuid = match event_id.parse() {
Ok(event_id) => event_id,
Err(e) => {
warn!("Unparsable event_id in event_billing_event_view: {}", e);
return Err(Status::BadRequest);
}
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => {
warn!("Couldn't load event: {}", e);
return Err(Status::NotFound);
}
};
if !caller.has_permission(crate::permissions::modules::event_billing::VIEW.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Status::Forbidden);
}
}
None => {
return Err(Status::Forbidden);
}
}
}
let header = Header {
html_language: "de".to_string(),
site_title: "Einsatzabrechnung".to_string(),
stylesheets: vec![Stylesheet {
path: "/css/errms.css".to_string(),
}],
};
let footer = Footer {
scripts: vec![Script {
path: "/js/event_billing.js".to_string(),
}],
};
let mut sidebar = Sidebar::new(caller.clone());
sidebar.event_billing.active = true;
Ok(Template::render("module_eb_event_view", EventBilling {
header,
footer,
sidebar,
caller: caller.entity_id,
event,
}))
}

View File

@ -0,0 +1,4 @@
pub mod event;
pub mod close_event;
pub mod edit_times;
pub mod approve;

View File

@ -55,6 +55,7 @@ table! {
state_id -> Text,
description -> Nullable<Text>,
final_approve -> Bool,
order -> Nullable<Int4>,
}
}