Compare commits

...

3 Commits

16 changed files with 444 additions and 47 deletions

View File

@ -36,4 +36,6 @@ default_pagination_limit = 20
#If set true members don't need billing related permissions if they are member_responsible for specific event
member_responsible_overwrites_permissions = true
send_personnel_billing_to_email = true
personnel_billing_email = "receiver@localhost"
personnel_billing_emails = "receiver@localhost,receiver2@localhost"
lump_sum_name = "Lump Sum"
money_for_time_name = "Money for Time"

View File

@ -37,4 +37,6 @@ default_pagination_limit = 20
#If set true members don't need billing related permissions if they are member_responsible for specific event
member_responsible_overwrites_permissions = true
send_personnel_billing_to_email = true
personnel_billing_email = "receiver@localhost"
personnel_billing_emails = "receiver@localhost,receiver2@localhost"
lump_sum_name = "Lump Sum"
money_for_time_name = "Money for Time"

View File

@ -0,0 +1,51 @@
<div class="card">
<a href="#collapse-{{entity_id}}" style="color: black !important" data-toggle="collapse"><div class="card-header {{#if event_status_green}}acs-green{{/if}}{{#if event_status_yellow}}acs-yellow{{/if}}{{#if event_status_red}}acs-red{{/if}}"><span class="font-weight-bold">{{timeframe}}: </span>{{name}} {{#each cast_status.open_positions}}<span class="badge badge-danger">{{position_name}}</span>{{/each}}</div></a>
<div id="collapse-{{entity_id}}" class="collapse eventlist_accordion_card" data-entity-id="{{entity_id}}">
<div class="card-body">
<ul class="nav nav-tabs event_list_navtabs" id="evenlist_tab-{{entity_id}}" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" href="#eventlist_core_tab-{{entity_id}}" id="core-tab-{{entity_id}}" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#eventlist_core_tab-{{entity_id}}" aria-controls="core-tab" aria-selected="true">Übersicht</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#eventlist_cast_tab-{{entity_id}}" id="cast-tab-{{entity_id}}" data-toggle="tab" data-bs-toggle="tab" data-bs-target="#eventlist_cast_tab-{{entity_id}}" aria-controls="cast-tab" aria-selected="false">Besetzung</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="/portal/em/event?id={{entity_id}}" id="edit-tab" aria-selected="false">Bearbeiten</a>
</li>
</ul><br>
<div class="tab-content">
<div class="tab-pane fade show active edit_event_core_data" role="tabpanel" aria-labelledby="core-tab" id="eventlist_core_tab-{{entity_id}}">
<div class="eventlist_accordion_card_overview row">
<div class="col-md-6">
<h5>Veranstaltung</h5>
<p><b>Name: </b>{{name}}</p>
<p><b>Art: </b>{{etype}}</p>
<p><b>Zeitraum: </b>{{timeframe}}</p>
<p><b>Veranstaltungsort: </b>{{site}}</p>
<p><b>Ansprechpartner vor Ort: </b>{{contact_on_site_name}} ({{contact_on_site_phone}})</p>
<h5>Intern</h5>
<p><b>Ansprechpartner: </b>{{member_responsible}}</p>
<p><b>Gruppe: </b>{{related_group}}</p>
<p><b>Anmerkung: </b>{{other}}</p>
</div>
<div class="col-md-6">
<h5>Veranstalter</h5>
<p><b>Firma/Organisation: </b>{{organiser.company}}</p>
<p><b>Name: </b>{{organiser.firstname}} {{organiser.lastname}}</p>
{{#if organiser.phone}}<p><b>Telefon: </b>{{organiser.phone}}</p>{{/if}}
{{#if organiser.email}}<p><b>Email: </b>{{organiser.email}}</p>{{/if}}
{{#if organiser.other}}<p><b>Sonstiges: </b>{{organiser.other}}</p>{{/if}}
<h5>Abrechnung</h5>
<p><b>Status:</b> {{state_name}}</p>
</div>
</div>
</div>
<div class="tab-pane fade" role="tabpanel" aria-labelledby="cast-tab" id="eventlist_cast_tab-{{entity_id}}">
<div id="eventlist_cast_instance_container-{{entity_id}}" class="row">
</div>
</div>
</div>
</div>
</div>
</div>

146
resources/js/eb_list.js Normal file
View File

@ -0,0 +1,146 @@
$(document).ready(async function () {
await EventBillingList.setup();
});
EventBillingList = (function () {
let templates = {};
let pending_requests = [];
let limit = 10;
let setup = async function () {
await load_templates();
await setup_pagination();
await load_events(0);
$(".event_billing_list_load").off("click").on("click", load_events);
};
let load_templates = function(){
$.get("/templates/eb_list_card.hbs", function( res) {
templates.eb_list_card = Handlebars.compile(res);
});
$.get("/templates/pagination.hbs", function( res) {
templates.pagination = Handlebars.compile(res);
});
};
let setup_pagination = function(){
pag = new Pagination("eb_list_pagination", templates.pagination, ".billingpag", limit, load_events);
};
let load_events = function(offset){
if(offset === undefined || !Number.isInteger(offset)){
offset = 0;
}
let args = "";
if($("#event_billing_list_start_datetime").val()){
args += "&start="+$("#event_billing_list_start_datetime").val();
}
if($("#event_billing_list_end_datetime").val()){
args += "&end="+$("#event_billing_list_end_datetime").val();
}
if($("#event_billing_list_groups_select").val()){
args += "&groups="+$("#event_billing_list_groups_select").val();
}
//Add filter for event status to args
let event_states = "";
if ($("#event_billing_list_status_unknown").prop("checked")){
event_states += "0,"
}
if ($("#event_billing_list_status_event_opened").prop("checked")){
event_states += "2,"
}
if ($("#event_billing_list_status_event_closed").prop("checked")){
event_states += "4,"
}
if ($("#event_billing_list_status_event_times_approved").prop("checked")){
event_states += "6,"
}
if ($("#event_billing_list_status_personnal_billing_done").prop("checked")){
event_states += "7,"
}
if ($("#event_billing_list_status_billing_approved").prop("checked")){
event_states += "8,"
}
if(event_states){
args += "&states="+event_states.slice(0, event_states.length - 1); //Remove last comma
}
//Load results
$.ajax({
type: "GET",
url: "/api/events/?limit="+limit+"&offset="+offset+args,
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
},
success: async function (data) {
if (is_ok(data)) {
$("#event_billing_list_accordion").html("").hide();
let event_loading_queue = [];
$(data.events).each(function () {
event_loading_queue.push(load_event(this))
});
async function load_event(event) {
if (event.member_responsible) {
let member = await get_member(event.member_responsible);
event.member_responsible = member.firstname + " " + member.lastname;
}
if (event.related_group) {
event.related_group = await get_related_group(event.related_group);
}
if (event.etype) {
event.etype = await get_event_type(event.etype);
}
if (event.organiser_id) {
event.organiser = await get_organiser(event.organiser_id);
}
let date = new Date(event.start);
event.timeframe = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear() + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
let date2 = new Date(event.end);
if (date.getDate() === date2.getDate()) {
event.timeframe += ('0' + date2.getHours()).slice(-2) + ':' + ('0' + date2.getMinutes()).slice(-2)
} else {
event.timeframe += event.timeframe = ('0' + date2.getDate()).slice(-2) + '.' + ('0' + (date2.getMonth() + 1)).slice(-2) + '.' + date2.getFullYear() + ' ' + ('0' + date2.getHours()).slice(-2) + ':' + ('0' + date2.getMinutes()).slice(-2);
}
event.cast_status = await load_event_cast_status(event.entity_id);
if (event.state === 2) {
event.state_name = "Einsatz geöffnet";
event.event_status_yellow = true;
} else if (event.state === 4) {
event.state_name = "Einsatz geschlossen";
event.event_status_red = true;
} else if (event.state === 6) {
event.state_name = "Einsatzzeiten bestätigt";
event.event_status_red = true;
} else if (event.state === 7) {
event.state_name = "Personalabrechnung abgeschlossen";
event.event_status_red = true;
} else if (event.state === 8) {
event.state_name = "Abrechnung abgeschlossen";
event.event_status_green = true;
}else{
event.state_name = "unbekannt";
}
return event;
}
await Promise.all(event_loading_queue).then((values) => {
for(val of values){
$("#event_billing_list_accordion").append(templates.eb_list_card(val))
}
$("#event_billing_list_accordion").show();
})
}
}
});
}
return{
setup
}
}());

View File

@ -280,20 +280,6 @@ EventListModule = ( function() {
alert("Du erfüllst nicht die nötigen Voraussetzungen für diese Position.");
}
}
let get_related_group = async function(entity_id){
const res = await $.ajax({
type: "GET",
url: "/api/groups/" + entity_id,
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
},
});
if (is_ok(res)) {
return res.name;
}
};
let check_edit_permission_callback = function(has_permission){
if(has_permission === true){
$(".eventlist_navtabs").each(function(){
@ -301,20 +287,7 @@ EventListModule = ( function() {
});
}
};
let load_event_cast_status = async function(event_id){
const res = await $.ajax({
type: "GET",
url: "/api/events/" + event_id+"/cast_status",
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
},
});
if (is_ok(res)) {
return res;
}
};
return{
load_events: load_events,
load_templates: load_templates,

View File

@ -383,4 +383,32 @@ let combine_start_end_time = function (start, end) {
} else {
return start_date + " " + start_time + end_date + " " + end_time;
}
}
};
let get_related_group = async function(entity_id){
const res = await $.ajax({
type: "GET",
url: "/api/groups/" + entity_id,
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
},
});
if (is_ok(res)) {
return res.name;
}
};
let load_event_cast_status = async function(event_id){
const res = await $.ajax({
type: "GET",
url: "/api/events/" + event_id+"/cast_status",
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Verbindung zum Server unterbrochen!");
},
});
if (is_ok(res)) {
return res;
}
};

View File

@ -0,0 +1,98 @@
{{> header }}
<div class="container-fluid">
<div class="row">
<div class="wrapper">
{{> sidebar }}
{{> searchbar}}
<hr>
<div class="col">
<div class="form-group row align-items-center">
<h1 class="col-sm-4">Übersicht Einsatzabrechnung</h1>
<label for="event_billing_list_start_datetime"
class="col-auto col-form-label font-weight-bold">Zeitraum:</label>
<div class="col-auto">
<input type="date" class="form-control" id="event_billing_list_start_datetime">
</div>
<label for="event_billing_list_end_datetime"
class="col-auto col-form-label font-weight-bold">bis:</label>
<div class="col-auto">
<input type="date" class="form-control" id="event_billing_list_end_datetime">
</div>
<div class="col-auto">
<select class="form-control" id="event_billing_list_groups_select">
<option value="">Alle Gruppen</option>
{{#each groups}}
<option value="{{group_id}}">{{name}}</option>
{{/each}}
</select>
</div>
</div>
<div class="form-group row align-items-center">
<b style="margin-left: 15px;">Nach Abrechnungsstatus filtern:</b>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_unknown">
<label class="form-check-label" for="event_billing_list_status_unknown">
Unbekannt
</label>
</div>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_event_opened">
<label class="form-check-label" for="event_billing_list_status_event_opened">
Geöffnet
</label>
</div>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_event_closed">
<label class="form-check-label" for="event_billing_list_status_event_closed">
Geschlossen
</label>
</div>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_event_times_approved">
<label class="form-check-label" for="event_billing_list_status_event_times_approved">
Einsatzzeiten bestätigt
</label>
</div>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_personnal_billing_done">
<label class="form-check-label" for="event_billing_list_status_personnal_billing_done">
Personalabrechnung abgeschlossen
</label>
</div>
<div class="form-check" style="margin-left: 15px;">
<input class="form-check-input" type="checkbox" id="event_billing_list_status_billing_approved">
<label class="form-check-label" for="event_billing_list_status_billing_approved">
Freigegeben
</label>
</div>
<button type="button" class="event_billing_list_load btn btn-primary" style="margin-left: 15px;">Laden
</button>
</div>
<div class="card">
<div class="card-header">Einsätze</div>
<div class="card-body">
<input type="hidden" id="caller_entity_id" value="{{caller}}">
<div id="event_billing_list_accordion">
</div>
<br>
<div class="row">
<label for="event_billing_list_num_of_res" class="col-2">Ergebnisse pro Seite:</label>
<select class="form-control col-1" id="event_billing_list_num_of_res">
<option data-num="10">10</option>
<option data-num="20">20</option>
<option data-num="50">50</option>
<option data-num="100">100</option>
</select>
</div>
<br>
<div class="row billingpag">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{> footer }}

View File

@ -94,6 +94,7 @@
<li><a href="/portal/em/eu_templates">Vorlagen</a></li>
<li><a href="/portal/em/eu_positions">Positionen</a></li>
<li><a href="/portal/em/requests">Anfragen</a></li>
<li><a href="/portal/eb/list">Abrechnung</a></li>
</ul>
{{/if}}
</li>
@ -171,6 +172,7 @@
<a class="list-group-item" href="/portal/em/eu_templates">Vorlagen</a>
<a class="list-group-item" href="/portal/em/eu_positions">Positionen</a>
<a class="list-group-item" href="/portal/em/requests">Anfragen</a>
<a class="list-group-item" href="/portal/eb/list">Abrechnung</a>
</ul>
{{/if}}
{{#if sidebar.settings.active}}

View File

@ -128,18 +128,19 @@ pub fn change_event(settings: &State<Settings>, data: Event) -> Result<Event, di
}
}
pub fn get_events(settings: &State<Settings>, startdate: NaiveDateTime, enddate: NaiveDateTime, limit: i64, offset: i64, groups: Option<Vec<uuid::Uuid>>) -> Result<Vec<Event>, diesel::result::Error>{
pub fn get_events(settings: &State<Settings>, startdate: NaiveDateTime, enddate: NaiveDateTime, limit: i64, offset: i64, groups: Option<Vec<uuid::Uuid>>, states: Option<Vec<i16>>) -> Result<Vec<Event>, diesel::result::Error>{
use crate::schema::events::dsl::*;
let connection = establish_connection(settings);
let mut query = events.order(start.asc()).filter(start.ge(startdate)).filter(end.le(enddate)).limit(limit).offset(offset).into_boxed();
match groups{
Some(groups) => {
query = query.filter(related_group.eq(any(groups)));
}
None => {}
if let Some(groups) = groups {
query = query.filter(related_group.eq(any(groups)));
};
if let Some(states) = states{
query = query.filter(state.eq(any(states)));
};
match query.get_results(&connection){

View File

@ -43,7 +43,9 @@ pub struct Api {
pub struct Billing {
pub member_responsible_overwrites_permissions: bool,
pub send_personnel_billing_to_email: bool,
pub personnel_billing_email: String,
pub personnel_billing_emails: String,
pub lump_sum_name: String,
pub money_for_time_name: String,
}
#[derive(Debug, Deserialize, Default, Clone)]

View File

@ -279,6 +279,7 @@ fn rocket() -> _ {
modules::api::personnel_billing::delete::delete_position_instance,
modules::api::personnel_billing::read::get_personnel_billing,
modules::api::events::update::approve,
modules::event_billing::event_billing_list::event_billing_list,
],
)
.mount("/css", FileServer::from("resources/css"))

View File

@ -18,7 +18,7 @@ pub struct EventList {
pub(crate) total_event_count: i64,
}
#[get("/api/events?<start>&<end>&<limit>&<offset>&<groups>", format = "json")]
#[get("/api/events?<start>&<end>&<limit>&<offset>&<groups>&<states>", format = "json")]
pub fn read_events(
settings: &State<Settings>,
cookie: SessionCookie,
@ -27,6 +27,7 @@ pub fn read_events(
limit: Option<i64>,
offset: Option<i64>,
groups: Option<String>,
states: Option<String>,
) -> Result<Json<EventList>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
@ -62,6 +63,23 @@ pub fn read_events(
};
let limit = limit.unwrap_or(settings.api.default_pagination_limit);
let offset = offset.unwrap_or(0);
let states = match states{
Some(states) => {
let mut res: Vec<i16> = vec![];
for state in states.split(","){
let parsed_state : i16 = match state.parse(){
Ok(state) => state,
Err(e) => {
error!("Couldn't parse transmitted event state: {}", e);
return Err(Json(ApiError::new(400, "Couldn't parse transmitted event states".to_string()).to_wrapper()))
}
};
res.push(parsed_state)
}
Some(res)
},
None => None
};
let groups = match groups{
Some(groups) => {
@ -74,7 +92,7 @@ pub fn read_events(
None => None,
};
let events = match get_events(settings, start, end, limit, offset, groups.clone()){
let events = match get_events(settings, start, end, limit, offset, groups.clone(), states){
Ok(events) => events,
Err(e) => return Err(translate_diesel(e)),
};

View File

@ -269,11 +269,22 @@ pub async fn approve(
debug!("Generated CSV: {}", csv);
if settings.billing.send_personnel_billing_to_email {
let attachement = Attachment::new(String::from("Einsatzabrechnung.txt")).body(csv, ContentType::parse("text/csv").unwrap());
let msg = Message::builder()
let mut msg = Message::builder()
.from(settings.mail.from.clone().parse().unwrap())
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
.to(settings.billing.personnel_billing_email.parse().unwrap())
.subject("Einsatzabrechnung")
.reply_to(settings.mail.reply_to.clone().parse().unwrap());
for receiver in settings.billing.personnel_billing_emails.split(","){
match receiver.parse(){
Ok(receiver) => {
msg = msg.to(receiver)
},
Err(e) => {
error!("Couldn't parse settings.billing.personnel_billing_emails email {}: {}", receiver, e);
}
}
}
let event = get_event(settings, event_id).unwrap();
let msg = msg.subject(format!("Einsatzabrechnung: {}", event.name))
.multipart(MultiPart::mixed().singlepart(attachement).singlepart(SinglePart::plain(String::from("Es wurde eine Einsatzabrechnung freigegeben. Sie befindet sich im Anhang dieser E-Mail.")))).unwrap();
mq.add_mail(msg);
}

View File

@ -0,0 +1,61 @@
use rocket::http::Status;
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::controller::groups::get_raw_groups;
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::modules::event_management::eventlist::EventList;
use crate::Settings;
#[get("/portal/eb/list")]
pub fn event_billing_list(cookie: SessionCookie, settings: &State<Settings>) -> Result<Template, Status> {
let member = match cookie.member {
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
Some(member) => member,
None => return Err(Status::Unauthorized),
};
if !member.has_permission(
crate::permissions::modules::event_billing::VIEW.to_string(),
) {
return Err(Status::Forbidden);
}
let header = Header {
html_language: "de".to_string(),
site_title: "Übersicht Einsatzabrechnung".to_string(),
stylesheets: vec![Stylesheet {
path: "/css/errms.css".to_string(),
}],
};
let footer = Footer {
scripts: vec![Script {
path: "/js/eb_list.js".to_string(),
}, Script {
path: "/js/search2.js".to_string(),
},Script {
path: "/js/pagination.js".to_string(),
}],
};
let mut sidebar = Sidebar::new(member.clone());
sidebar.event_management.active = true;
let groups = match get_raw_groups(settings){
Ok(groups) => groups,
Err(_e) => return Err(Status::InternalServerError)
};
let eventlist = EventList {
header,
footer,
sidebar,
caller: member.entity_id,
groups
};
Ok(Template::render("module_eb_list", eventlist))
}

View File

@ -110,7 +110,7 @@ pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) ->
res.push_str(&format!("Abrechnungssatz:,{},{},Pauschale:,{},€/Stunde:,{}\n", sanitize(billing_rate.name), sanitize(billing_rate.description.unwrap_or(String::from(""))), sanitize(format!("{}", billing_rate.lump_sum)), sanitize(format!("{}", billing_rate.payment_per_hour))));
}
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", String::from("Personalnummer"), String::from("Vorname"), String::from("Nachname"), String::from("Von"), String::from("Bis"), String::from("Stunden"), String::from("Pauschale"), String::from("Stundengeld"), String::from("Gesamt")));
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", String::from("Personalnummer"), String::from("Vorname"), String::from("Nachname"), String::from("Von"), String::from("Bis"), String::from("Stunden"), settings.billing.lump_sum_name, settings.billing.money_for_time_name, String::from("Gesamt")));
let position_instances = get_position_instances(settings, instance.instance_id)?;
@ -128,7 +128,7 @@ pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) ->
None => return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingTimes))
};
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", sanitize(member.personnel_number.unwrap_or(0).to_string()), sanitize(member.firstname), sanitize(member.lastname), sanitize(begin), sanitize(end), sanitize(personnel.fulfilled_time.to_string()), sanitize(personnel.money_from_lump_sum.to_string()), sanitize(convert(personnel.money_for_time.clone(), 2, ',', Some('.'))), sanitize(convert(personnel.money_from_lump_sum+personnel.money_for_time, 2, ',', Some('.')))));
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", sanitize(member.personnel_number.unwrap_or(0).to_string()), sanitize(member.firstname), sanitize(member.lastname), sanitize(begin), sanitize(end), sanitize(personnel.fulfilled_time.to_string()), sanitize(convert(personnel.money_from_lump_sum.clone(), 2, ',', Some('.'))), sanitize(convert(personnel.money_for_time.clone(), 2, ',', Some('.'))), sanitize(convert(personnel.money_from_lump_sum+personnel.money_for_time, 2, ',', Some('.')))));
}
None => {
return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingMember));

View File

@ -3,4 +3,5 @@ pub mod close_event;
pub mod edit_times;
pub mod approve;
pub mod edit_personnel_billing;
pub mod generate_billing_csv;
pub mod generate_billing_csv;
pub mod event_billing_list;