EinsatzOnline/resources/js/event_billing.js

780 lines
33 KiB
JavaScript

let searchParams;
$(document).ready(async function () {
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 pending_requests = [];
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();
} else if (pathname === "/portal/eb/personnel_billing") {
await personnel_billing_view.setup();
} else if (pathname === "/portal/eb/approve") {
await approve_view.setup();
}
};
let run_pending_requests = async function (success_callback) {
//Run all pending requests:
await Promise.all(pending_requests.map(req => $.ajax(req))).then(async function (values) {
pending_requests = []; //Clear pending requests
let error_occured = false;
for (let i = 0; i < values.length; i++) {
let val = values[i];
if (!is_ok(val)) {
error_occured = true;
}
}
if (!error_occured) {
success_callback();
}
}, function () {
//Failure
});
};
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];
});
let progressbar = {};
progressbar.billing_states = billing_states;
progressbar.event_id = event.entity_id;
$(".nav-progressbar").html(templates.progressbar(progressbar));
$(".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 >= 7) {
$(".progress_personnel_billing").addClass("active");
if(event_min_billing_state) {
for (let state of billing_states) {
$(".progress_approve_" + state.entity_id).addClass("active");
if (state.entity_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 = await $.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(res == null){
return null;
}
if(typeof res !== 'object'){
return res;
}
if('error' in res){
return null;
}else{
return res;
}
};
let load_templates = async function () {
const progressbar = $.get("/templates/progressbar.hbs");
const eb_action_buttons = $.get("/templates/eb_action_buttons.hbs");
await Promise.all([progressbar, eb_action_buttons]).then(function (res) {
templates.progressbar = Handlebars.compile(res[0]);
templates.eb_action_buttons = Handlebars.compile(res[1]);
});
};
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 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 (instances) {
let queue = [];
for (let instance of instances) {
queue.push(load_positions_for_instance_new(instance.instance_id));
}
await Promise.all(queue).then(function (res) {
for (let i = 0; i < instances.length; i++) {
instances[i].positions = res[i];
}
});
};
let update_position_instance = function (data) {
pending_requests.push(({
type: "PATCH",
url: "/api/events/position_instances/" + data.position_instance_id,
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data),
}));
};
//TODO: only call if instance really changed
let save_eu_instance = function (data) {
pending_requests.push(({
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.");
},
}));
};
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 >= 7) {
if (!event_min_billing_state) {
$(".goto_approve_" + billing_states[0].entity_id).show();
}
//Show next approval step (billing_state) button
for (let i = 0; i < billing_states.length - 1; i++) {
if (billing_states[i].entity_id === event_min_billing_state) {
$(".goto_approve_" + billing_states[i+1].entity_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");
const new_position_instance = $.get("/templates/eb_eu_instance_card_new_position_instance.hbs");
await Promise.all([eu_instance_card, new_position_instance]).then(function (res) {
templates.eu_instance_card = Handlebars.compile(res[0]);
templates.new_position_instance = Handlebars.compile(res[1]);
Handlebars.registerPartial('new_position_instance', templates.new_position_instance);
});
};
let setup = async function () {
await load_local_templates();
eu_instances = await load_eu_instances(event.entity_id);
await load_eu_instance_positions(eu_instances);
$(".eu_instances_container").empty();
for (let instance of eu_instances) {
if (instance.planned_start_time && instance.planned_end_time) {
instance.planned_time = combine_start_end_time(instance.planned_start_time, instance.planned_end_time);
}
for (let position of instance.positions) {
if (position.taken_by) {
let member = await get_member(position.taken_by);
position.taken_by_member = member.firstname + " " + member.lastname;
}
}
$(".eu_instances_container").append(templates.eu_instance_card(instance));
}
Search2.setup(); //Setup searchbars
$(".eb_eu_instance_save_btn").off("click").on("click", save);
//Apply remove/restore position_instance button listeners
$(".remove_position_instance_button").off("click").on("click", delete_position_btn);
$(".restore_position_instance_button").off("click").on("click", restore_position_btn);
$(".approve_times_btn").off("click").on("click", approve_times);
$(".qsf").off("change").on("change", function () { //Show save button if instance changed
$(this).closest(".position_instance").addClass("modified");
$(this).closest(".instance").find(".eb_eu_instance_save_btn").show();
});
let add_new_position_instance_listener = function(){
$(this).closest(".new_position_instances").append(templates.new_position_instance);
$(".new_position_instance").off("click");
$(".new_position_instance:last-child").on("click", add_new_position_instance_listener);
$(".remove_new_position_instance_button").off("click").on("click", function () {
$(this).closest(".card").remove();
$(".new_position_instance").off("click");
$(".new_position_instance:last-child").on("click", add_new_position_instance_listener);
});
}
$(".new_position_instance:last-child").off("click").on("click", function(){
add_new_position_instance_listener(this);
});
$(".remove_new_position_instance_button").off("click").on("click", function () {
$(this).closest(".card").remove();
});
};
let save = async function () {
//TODO: use global save_pending_requests()
//TODO: send requests after each step. Otherwise request to delete something get's executed before request to edit same thing
pending_requests = [];
let parent = $(this).closest(".instance");
let save_btn = $(this);
//Save basic instance data:
let instance_patch = {};
instance_patch.instance_id = parent.data("instance-id") || undefined; //undefined = do not transmit at all
instance_patch.real_start_time = parent.find(".eb_eu_instance_real_start_time").val() || null; //null = set to null in database
instance_patch.real_end_time = parent.find(".eb_eu_instance_real_end_time").val() || null;
save_eu_instance(instance_patch);
//Save newly added positions:
let npi = $(".new_position_instance");
for (let i = 0; i < npi.length; i++) {
let new_pos = $(npi[i]);
let position = new_pos.find(".search_new_position_instance_position").data("value-id") || undefined;
let member = new_pos.find(".search_new_position_instance_member").data("value-id") || undefined;
console.log("member:" + member + " position:" + position);
//Skip new position if position or member isn't set
// TODO: simplify
if (!position || !member || position.trim() === "" || member.trim() === "") {
//Report error if new position missing either position or member (but not both)
if ((!position || position.trim() === "") && member) {
alert("Der neuen Personalposition fehlt die Positionsangabe!");
return;
}
if ((!member || member.trim() === "") && position) {
alert("Der neuen Personalposition fehlt ein Mitglied!");
return;
}
//Skip empty new positions
continue;
}
let new_position_instance = {};
new_position_instance.instance_id = parent.data("instance-id") || undefined;
new_position_instance.position_id = position || undefined;
new_position_instance.taken_by = member || undefined;
new_position_instance.real_start_time = new_pos.find(".new_position_instance_real_start_time").val() || undefined;
new_position_instance.real_end_time = new_pos.find(".new_position_instance_real_end_time").val() || undefined;
add_position_to_position_instances(new_position_instance);
}
//Remove position instances which are marked for removal:
let to_removed = $(".marked-for-delete");
for (i = 0; i < to_removed.length; i++) {
let element = $(to_removed[i]);
let position_instance_id = element.closest(".position_instance").data("position-instance-id");
console.log("removing: " + position_instance_id);
delete_position_instance(position_instance_id);
to_removed[i].remove();
}
//Update changed position instances:
let to_updated = $(".position_instance.modified");
for (i = 0; i < to_updated.length; i++) {
let element = $(to_updated[i]);
let update_set = {};
update_set.position_instance_id = element.data("position-instance-id") || undefined;
update_set.taken_by = element.find(".search_instance_position_instance").data("value-id") || null;
update_set.real_start_time = element.find(".eb_eu_instance_position_real_start_time").val() || null;
update_set.real_end_time = element.find(".eb_eu_instance_position_real_end_time").val() || null;
console.log(update_set);
update_position_instance(update_set);
}
//Run all pending requests:
await Promise.all(pending_requests.map(req => $.ajax(req))).then(async function (values) {
let error_occured = false;
for (let i = 0; i < values.length; i++) {
let val = values[i];
if (!is_ok(val)) {
error_occured = true;
}
}
if (!error_occured) {
save_btn.hide();
parent.find(".marked-for-delete").remove();
parent.find(".modified").removeClass("modified");
}
}, function () {
//Failure
});
pending_requests = []; //Clear pending requests
};
let add_position_to_position_instances = function (data) {
pending_requests.push(({
type: "POST",
url: "/api/events/instances/" + data.instance_id + "/positions/" + data.position_id,
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data),
}));
};
let delete_position_instance = function (position_instance_id) {
pending_requests.push(({
type: "DELETE",
url: "/api/events/position_instances/" + position_instance_id,
contentType: 'application/json',
timeout: 3000,
}));
};
let delete_position_btn = function () {
let position = $(this).closest(".card");
$(this).closest(".instance").find(".eb_eu_instance_save_btn").show(); //Show save button
//Hide delete button and show restore button
$(this).hide();
position.find(".restore_position_instance_button").show();
$(position).addClass("marked-for-delete");
};
let restore_position_btn = function () {
let position = $(this).closest(".card");
//Hide restore button and show delete button
$(this).hide();
position.find(".remove_position_instance_button").show();
$(position).removeClass("marked-for-delete");
};
let approve_times = async function () {
let save_btns = $(".eb_eu_instance_save_btn:visible");
if (save_btns.length !== 0) {
alert("Es gibt ungespeicherte Änderungen. Bitte speichere alle Änderungen bevor zu die Einsatzzeiten bestätigst.");
} else {
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)) {
setTimeout(function(){
window.location.href = "/portal/eb/personnel_billing?event_id=" + event.entity_id;
}, 500);
} else {
throw res;
}
}
};
return {
setup
}
}());
let personnel_billing_view = (function () {
let eu_instances = {};
let setup = async function () {
await load_local_templates();
eu_instances = await load_eu_instances();
await load_eu_instance_positions(eu_instances);
let billing_rates = await load_personnel_billing_rates();
$(".eu_instances_container").empty();
for (let instance of eu_instances) {
if (instance.planned_start_time && instance.planned_end_time) {
instance.planned_time = combine_start_end_time(instance.planned_start_time, instance.planned_end_time);
}
if (instance.real_start_time && instance.real_end_time) {
instance.real_time = combine_start_end_time(instance.real_start_time, instance.real_end_time);
}
instance.billing_rates = JSON.parse(JSON.stringify(billing_rates)); //Clone array (not copy with reference) to set selected rate
for (let rate of instance.billing_rates) {
if (rate.billing_rate_id === instance.billing_rate_id) {
rate.selected = true;
}
}
for (let position of instance.positions) {
if (position.taken_by) {
let member = await get_member(position.taken_by);
position.taken_by_member = member.firstname + " " + member.lastname;
}
position.real_time = combine_start_end_time(position.real_start_time, position.real_end_time);
if (position.billable === null) {
//If billable is set to null in database set it to true (default) in UI
position.billable = true;
}
}
$(".eu_instances_container").append(templates.personnel_instance_card(instance));
//When non-billable is active disable billable and manual billing checkboxes
$(".instance_select_billing_rate").each(function () {
if ($(this).val() === "non-billable") {
$(this).closest(".instance").find(".eb_personnel_position_instance_is_billable").prop("checked", false).prop("disabled", true);
$(this).closest(".instance").find(".eb_personnel_position_instance_manual_billing_check").prop("checked", false).prop("disabled", true);
}
})
console.log(instance); //TODO: remove when testing done
}
$(".instance_select_billing_rate").on("change", function () {
let val = $(this).val();
let instance = $(this).closest(".instance");
if (val === "non-billable") {
instance.find(".eb_personnel_position_instance_is_billable").prop("checked", false).prop("disabled", true);
instance.find(".eb_personnel_position_instance_manual_billing_check").prop("checked", false).prop("disabled", true);
} else {
instance.find(".eb_personnel_position_instance_is_billable").prop("disabled", false);
instance.find(".eb_personnel_position_instance_manual_billing_check").prop("disabled", false);
}
});
$(".eb_personnel_position_instance_manual_billing_check").on("change", function () {
let personal_instance = $(this).closest(".position_instance");
if ($(this).prop("checked")) {
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time_container").show();
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum_container").show();
} else {
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time_container").hide();
personal_instance.find(".eb_personnel_position_instance_manual_billing_money_for_time").val("0.00");
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum_container").hide();
personal_instance.find(".eb_personnel_position_instance_manual_billing_lump_sum").val("0.00");
}
});
$(".qsf").on("change", function () {
let instance = $(this).closest(".instance");
instance.find(".eb_personnel_instance_save_btn").show();
$(this).addClass("modified");
});
$(".eb_personnel_instance_save_btn").on("click", save);
$(".check_personnel_billing_btn").on("click", async function () {
await finish_personnel_billing();
window.location = "/portal/eb/approve?event_id=" + event.entity_id + "&stage=" + billing_states[0].entity_id;
})
};
let load_local_templates = async function () {
const personnel_instance_card = $.get("/templates/eb_personnel_instance_card.hbs");
await Promise.all([personnel_instance_card]).then(function (res) {
templates.personnel_instance_card = Handlebars.compile(res[0]);
});
};
let load_personnel_billing_rates = async function () {
const res = $.ajax({
url: '/api/personnel_billing_rates',
type: 'GET',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
if (is_ok(res)) {
return res;
}
};
let save = async function () {
let instance = $(this).closest(".instance");
pending_requests = [];
//Save Instance:
let instance_patch = {};
instance_patch.instance_id = instance.data("instance-id") || undefined;
let billing_rate = instance.find(".instance_select_billing_rate").val();
//Remove personnel billing entries if billing_rate was set to non-billable
if (billing_rate === "non-billable") {
instance.find(".position_instance").each(function () {
remove_personnel_billing_entry($(this).data("position-instance-id"));
});
instance_patch.billing_rate_id = null;
} else {
instance_patch.billing_rate_id = billing_rate;
}
save_eu_instance(instance_patch);
run_pending_requests(function () {
});
//Save position instances:
for (let position_instance of instance.find(".position_instance")) {
let position_instance_id = $(position_instance).data("position-instance-id") || undefined;
//Patch eu_position_instance table
let position_instance_patch = {};
position_instance_patch.position_instance_id = position_instance_id;
if (billing_rate === "non-billable") {
position_instance_patch.billable = false;
} else {
position_instance_patch.billable = $(position_instance).find(".eb_personnel_position_instance_is_billable").prop("checked");
}
update_position_instance(position_instance_patch)
}
run_pending_requests(function () {
});
for (let position_instance of instance.find(".position_instance")) {
let position_instance_id = $(position_instance).data("position-instance-id") || undefined;
if ($(position_instance).find(".eb_personnel_position_instance_manual_billing_check").prop("checked")) {
//manual billing
let personnel_billing_patch = {};
personnel_billing_patch.position_instance_id = position_instance_id;
personnel_billing_patch.money_from_lump_sum = $(position_instance).find(".eb_personnel_position_instance_manual_billing_lump_sum").val() || undefined;
personnel_billing_patch.money_for_time = $(position_instance).find(".eb_personnel_position_instance_manual_billing_money_for_time").val() || undefined;
add_personnel_billing_entry_manual(personnel_billing_patch);
} else {
if (billing_rate !== "non-billable" && $(position_instance).find(".eb_personnel_position_instance_is_billable").prop("checked")) {
//Add personnel_billing entry. Overwrites existing entries
add_personnel_billing_entry(position_instance_id, billing_rate);
}
}
}
run_pending_requests(function () {
instance.find(".eb_personnel_instance_save_btn").hide();
instance.find(".modified").removeClass("modified");
});
};
let remove_personnel_billing_entry = function (position_instance_id) {
pending_requests.push(({
type: "DELETE",
url: "/api/personnel_billing/" + position_instance_id,
contentType: 'application/json',
timeout: 3000,
}));
};
let add_personnel_billing_entry = function (position_instance_id2, billing_rate_id2) {
let data = {
position_instance_id: position_instance_id2,
billing_rate_id: billing_rate_id2
};
pending_requests.push(({
type: "POST",
url: "/api/personnel_billing/",
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data)
}));
};
let add_personnel_billing_entry_manual = function (data) {
pending_requests.push(({
type: "POST",
url: "/api/personnel_billing/",
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data),
}));
};
let finish_personnel_billing = async function () {
const res = $.ajax({
url: '/api/events/' + event.entity_id + '/finish_personnel_billing',
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 approve_view = (function () {
let setup = async function () {
await load_local_templates();
let instances = await load_eu_instances();
await load_eu_instance_positions(instances);
let total_sum = 0;
for (let instance of instances) {
instance.instance_total_sum = 0;
for (let pos of instance.positions) {
let val = await load_personnel_billing(pos.position_instance_id);
if (val) {
val.total_money = Number(val.total_money).toFixed(2);
instance.instance_total_sum = Number(Number(instance.instance_total_sum) + Number(val.total_money)).toFixed(2);
val.money_from_lump_sum = Number(val.money_from_lump_sum).toFixed(2);
val.money_for_time = Number(val.money_for_time).toFixed(2);
pos.personnel_billing = val;
}
if (pos.taken_by) {
let member = await get_member(pos.taken_by);
pos.taken_by_member = member.firstname + " " + member.lastname;
}
pos.real_time = combine_start_end_time(pos.real_start_time, pos.real_end_time);
}
total_sum = Number(Number(total_sum) + Number(instance.instance_total_sum)).toFixed(2);
$(".eu_approve_summary").append(templates.approve_instance_card(instance));
}
console.log("Gesamt gesamt:" + total_sum);
$(".eu_approve_summary").append(templates.approve_total_sum({total_sum}));
$(".approve_btn").on("click", async function () {
let current_state = searchParams.get('stage') || undefined;
if (is_ok(await approve(current_state))) {
window.location = "/portal/eb/event?event_id=" + event.entity_id;
}
})
};
let load_local_templates = async function () {
const approve_instance_card = $.get("/templates/eb_approve_instance_card.hbs");
const approve_total_sum = $.get("/templates/eb_approve_total_sum.hbs");
await Promise.all([approve_instance_card, approve_total_sum]).then(function (res) {
templates.approve_instance_card = Handlebars.compile(res[0]);
templates.approve_total_sum = Handlebars.compile(res[1]);
});
};
let load_personnel_billing = async function (position_instance_id) {
const data = await $.ajax({
url: '/api/personnel_billing/' + position_instance_id,
type: 'GET',
contentType: 'application/json',
timeout: 3000,
});
if (!is_ok(data)) {
show_error(data)
} else {
return data;
}
};
let approve = async function (stage_id) {
const res = $.ajax({
url: '/api/events/' + event.entity_id + '/approve?stage=' + stage_id,
type: 'PUT',
contentType: 'application/json',
timeout: 3000,
error: function () {
alert("Es ist ein Fehler aufgetreten!");
}
});
return res;
}
return {
setup
}
}());
return {
setup
}
}());