FEA: added billing module (WIP)
This commit is contained in:
parent
27295c704c
commit
2a6bdead8b
|
@ -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';
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
alter table billing_states
|
||||
drop column "order";
|
|
@ -0,0 +1,3 @@
|
|||
-- Your SQL goes here
|
||||
alter table billing_states
|
||||
add "order" int;
|
|
@ -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}}
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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 = {};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}());
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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">
|
||||
|
|
|
@ -1 +1 @@
|
|||
v0.2-80-gf8eca4a
|
||||
v0.2-84-g27295c7
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod states;
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -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"))
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod read;
|
|
@ -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)),
|
||||
}
|
||||
}
|
|
@ -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>")]
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
}))
|
||||
}
|
|
@ -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,
|
||||
}))
|
||||
}
|
|
@ -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,
|
||||
}))
|
||||
}
|
|
@ -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,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub mod event;
|
||||
pub mod close_event;
|
||||
pub mod edit_times;
|
||||
pub mod approve;
|
|
@ -55,6 +55,7 @@ table! {
|
|||
state_id -> Text,
|
||||
description -> Nullable<Text>,
|
||||
final_approve -> Bool,
|
||||
order -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue