539 lines
20 KiB
Vue
539 lines
20 KiB
Vue
<script setup>
|
|
import { computed, reactive, ref } from "vue";
|
|
import Modal from "../../../../Views/Components/Modal.vue";
|
|
import ParticipantData from "./ParticipantData.vue";
|
|
import MailCompose from "./MailCompose.vue";
|
|
import {toast} from "vue3-toastify";
|
|
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
|
|
import {format, getDay, getMonth, getYear} from "date-fns";
|
|
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
|
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
|
|
|
|
const props = defineProps({
|
|
data: {
|
|
type: Object,
|
|
default: () => ({
|
|
localGroups: {},
|
|
participants: {},
|
|
event: {},
|
|
listType: '',
|
|
}),
|
|
},
|
|
});
|
|
|
|
const today = format(new Date(), "yyyy-MM-dd");
|
|
|
|
const { request } = useAjax();
|
|
|
|
const searchTerms = reactive({});
|
|
const selectedStatuses = reactive({});
|
|
|
|
const event = computed(() => props.data?.event ?? {});
|
|
const participantGroups = computed(() => props.data?.participants ?? {});
|
|
const showParticipantDetails = ref(false);
|
|
const showParticipant = ref(null);
|
|
const editMode = ref(false);
|
|
|
|
const mailCompose = ref(false);
|
|
|
|
const openCancelDialog = ref(false);
|
|
const openPartialPaymentDialogSwitch = ref(false);
|
|
|
|
defineEmits(['showParticipantDetails', 'markCocExisting', 'paymentComplete'])
|
|
|
|
function openParticipantDetails(input) {
|
|
showParticipantDetails.value = true;
|
|
showParticipant.value = input;
|
|
editMode.value = false;
|
|
}
|
|
|
|
async function saveParticipant(formData) {
|
|
if (!showParticipant.value?.identifier) {
|
|
return;
|
|
}
|
|
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/update', {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message ?? 'Die Änderungen wurden gespichert. Die Liste wird beim nächsten Neuladen neu generiert.');
|
|
|
|
document.getElementById('participant-' + data.participant.identifier + '-fullname').innerHTML = data.participant.fullname;
|
|
document.getElementById('participant-' + data.participant.identifier + '-birthday').innerText = data.participant.birthday;
|
|
document.getElementById('participant-' + data.participant.identifier + '-age').innerText = data.participant.age;
|
|
document.getElementById('participant-' + data.participant.identifier + '-localgroup').innerText = data.participant.localgroup;
|
|
document.getElementById('participant-' + data.participant.identifier + '-arrival').innerText = data.participant.arrival;
|
|
document.getElementById('participant-' + data.participant.identifier + '-departure').innerText = data.participant.departure;
|
|
document.getElementById('participant-' + data.participant.identifier + '-email_1').innerText = data.participant.email_1;
|
|
document.getElementById('participant-' + data.participant.identifier + '-email_2').innerText = data.participant.email_2 ?? '--';
|
|
document.getElementById('participant-' + data.participant.identifier + '-phone_1').innerText = data.participant.phone_1;
|
|
document.getElementById('participant-' + data.participant.identifier + '-phone_2').innerText = data.participant.phone_2 ?? '--';
|
|
|
|
if (data.cocChanged) {
|
|
document.getElementById('participant-' + data.identifier + '-coc-status').innerText = data.coc.statusText;
|
|
document.getElementById('participant-' + data.identifier + '-coc-action').style.display=data.coc.action;
|
|
document.getElementById('participant-' + data.identifier + '-name').className = data.coc.class;
|
|
}
|
|
|
|
if (data.amountChanged) {
|
|
document.getElementById('participant-' + data.identifier + '-payment').removeAttribute('class');
|
|
document.getElementById('participant-' + data.identifier + '-paid').innerText = data.amount.paid;
|
|
document.getElementById('participant-' + data.identifier + '-expected').innerText = data.amount.expected;
|
|
document.getElementById('participant-' + data.identifier + '-actions').style.display=data.amount.actions;
|
|
document.getElementById('participant-' + data.identifier + '-payment').className = data.amount.class;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
editMode.value = false;
|
|
} else {
|
|
toast.error(data.message ?? 'Speichern fehlgeschlagen');
|
|
}
|
|
}
|
|
|
|
|
|
const getGroupEntries = computed(() => {
|
|
return Object.entries(participantGroups.value ?? {});
|
|
});
|
|
|
|
const getAgeCounts = (participants) => {
|
|
const buckets = {
|
|
'0-5': 0,
|
|
'6-11': 0,
|
|
'12-15': 0,
|
|
'16-17': 0,
|
|
'18-27': 0,
|
|
'27+': 0,
|
|
};
|
|
|
|
(participants ?? []).forEach((participant) => {
|
|
const age = Number(participant?.age);
|
|
|
|
if (!Number.isFinite(age)) {
|
|
return;
|
|
}
|
|
|
|
if (age >= 0 && age <= 5) {
|
|
buckets['0-5']++;
|
|
} else if (age <= 11) {
|
|
buckets['6-11']++;
|
|
} else if (age <= 15) {
|
|
buckets['12-15']++;
|
|
} else if (age <= 17) {
|
|
buckets['16-17']++;
|
|
} else if (age <= 27) {
|
|
buckets['18-27']++;
|
|
} else {
|
|
buckets['27+']++;
|
|
}
|
|
});
|
|
|
|
return buckets;
|
|
};
|
|
|
|
const getSearchText = (participant) => {
|
|
return [
|
|
participant?.firstname,
|
|
participant?.lastname,
|
|
participant?.nickname,
|
|
participant?.email_1,
|
|
participant?.email_2,
|
|
participant?.phone_1,
|
|
participant?.phone_2,
|
|
participant?.local_group_string,
|
|
participant?.participation_type_string,
|
|
participant?.efz_status_string,
|
|
participant?.amount_string,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")
|
|
.toLowerCase();
|
|
};
|
|
|
|
const getFilteredParticipants = (groupKey, participants) => {
|
|
const searchTerm = (searchTerms[groupKey] ?? "").trim().toLowerCase();
|
|
|
|
return (participants ?? []).filter((participant) => {
|
|
const matchesSearch =
|
|
!searchTerm ||
|
|
getSearchText(participant).includes(searchTerm);
|
|
|
|
|
|
return matchesSearch;
|
|
});
|
|
};
|
|
|
|
const getRowClass = (participant) => {
|
|
if (participant?.unregistered_at) {
|
|
return "bg-gray-50 text-gray-500";
|
|
}
|
|
|
|
if (
|
|
Number(participant?.amount?.amount ?? participant?.amount ?? 0) !==
|
|
Number(participant?.amount_paid?.amount ?? participant?.amount_paid ?? 0)
|
|
) {
|
|
return "bg-red-50";
|
|
}
|
|
|
|
return "";
|
|
};
|
|
|
|
async function paymentComplete(participant) {
|
|
const data = await request('/api/v1/event/participant/' + participant.identifier + '/payment-complete', {
|
|
method: "POST",
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message);
|
|
document.getElementById('participant-' + participant.identifier + '-payment').removeAttribute('class');
|
|
document.getElementById('participant-' + participant.identifier + '-paid').innerText = participant.amountExpected.readable;
|
|
document.getElementById('participant-' + participant.identifier + '-actions').style.display='none';
|
|
} else {
|
|
toast.error(data.message);
|
|
|
|
}
|
|
}
|
|
|
|
async function markCocExisting(participant) {
|
|
const data = await request('/api/v1/event/participant/' + participant.identifier + '/mark-coc-existing', {
|
|
method: "POST",
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message);
|
|
document.getElementById('participant-' + participant.identifier + '-coc-status').innerText = 'Gültig';
|
|
document.getElementById('participant-' + participant.identifier + '-coc-action').style.display='none';
|
|
document.getElementById('participant-' + participant.identifier + '-name').removeAttribute('class');
|
|
} else {
|
|
toast.error(data.message);
|
|
|
|
}
|
|
}
|
|
|
|
function openCancelParticipationDialog(participant) {
|
|
showParticipant.value = participant;
|
|
openCancelDialog.value = true;
|
|
}
|
|
|
|
async function execCancelParticipation() {
|
|
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/signoff', {
|
|
method: "POST",
|
|
body: {
|
|
cancel_date: document.getElementById('cancel_date').value,
|
|
},
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message);
|
|
|
|
document.getElementById('participant-' + data.identifier + '-common').style.display = 'none';
|
|
document.getElementById('participant-' + data.identifier + '-meta').style.display = 'none';
|
|
|
|
} else {
|
|
toast.error(data.message);
|
|
|
|
}
|
|
openCancelDialog.value = false;
|
|
}
|
|
|
|
async function execResignonParticipant(participant) {
|
|
const data = await request('/api/v1/event/participant/' + participant.identifier + '/re-signon', {
|
|
method: "POST",
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message);
|
|
|
|
document.getElementById('participant-' + data.identifier + '-common').style.display = 'none';
|
|
document.getElementById('participant-' + data.identifier + '-meta').style.display = 'none';
|
|
|
|
} else {
|
|
toast.error(data.message);
|
|
|
|
}
|
|
openCancelDialog.value = false;
|
|
}
|
|
|
|
function openPartialPaymentDialog(participant) {
|
|
showParticipant.value = participant;
|
|
openPartialPaymentDialogSwitch.value = true;
|
|
}
|
|
|
|
async function execPartialPayment() {
|
|
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/partial-payment', {
|
|
method: "POST",
|
|
body: {
|
|
amount: document.getElementById('partial_payment_amount').value,
|
|
},
|
|
});
|
|
|
|
if (data.status === 'success') {
|
|
toast.success(data.message);
|
|
|
|
document.getElementById('participant-' + data.identifier + '-payment').removeAttribute('class');
|
|
document.getElementById('participant-' + data.identifier + '-paid').innerText = data.amount.paid;
|
|
document.getElementById('participant-' + data.identifier + '-expected').innerText = data.amount.expected;
|
|
document.getElementById('participant-' + data.identifier + '-actions').style.display=data.amount.actions;
|
|
document.getElementById('participant-' + data.identifier + '-payment').className = data.amount.class;
|
|
} else {
|
|
toast.error(data.message);
|
|
|
|
}
|
|
|
|
openPartialPaymentDialogSwitch.value = false;
|
|
}
|
|
|
|
const mailToType = ref('')
|
|
const recipientIdentifier = ref('')
|
|
|
|
function mailToGroup(groupKey) {
|
|
recipientIdentifier.value = groupKey;
|
|
mailToType.value = props.data.listType;
|
|
mailCompose.value = true
|
|
}
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<h2>{{ event?.name ?? "Veranstaltung" }}</h2>
|
|
<div :key="groupKey">
|
|
<div>
|
|
<table style="width: 95%; margin: 20px auto; border-collapse: collapse;" v-for="[groupKey, participants] in getGroupEntries">
|
|
<thead>
|
|
<tr>
|
|
<th colspan="4" style="background: linear-gradient(to bottom, #fff, #f6f7f7); font-weight: bold">
|
|
{{ groupKey }} ({{ participants.length }} Personen)
|
|
</th>
|
|
</tr>
|
|
<tr style="background: linear-gradient(to bottom, #fff, #f6f7f7);">
|
|
<th>Name</th>
|
|
<th>Beitrag</th>
|
|
<th>E-Mail-Adresse</th>
|
|
<th>Telefon</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
<template
|
|
v-for="participant in getFilteredParticipants(groupKey, participants)"
|
|
:key="participant.id"
|
|
>
|
|
<tr :class="getRowClass(participant)" :id="'participant-' + participant.identifier + '-common'">
|
|
<td :id="'participant-' + participant.identifier +'-name'"
|
|
style="width: 300px;"
|
|
:class="participant.efz_status === 'checked_invalid' ? 'efz-invalid' :
|
|
participant.efz_status === 'not_checked' ? 'efz-not-checked' : ''">
|
|
<div :id="'participant-' + participant.identifier +'-fullname'" v-html="participant.fullname" /><br />
|
|
Geburtsdatum: <label :id="'participant-' + participant.identifier +'-birthday'">{{ participant.birthday }}</label><br />
|
|
Alter: <label :id="'participant-' + participant.identifier +'-age'">{{ participant.age }}</label> Jahre<br />
|
|
|
|
|
|
<span>eFZ-Status: <label :id="'participant-' + participant.identifier +'-coc-status'">{{ participant.efzStatusReadable }}</label></span>
|
|
<span :id="'participant-' + participant.identifier +'-coc-action'" v-if="participant.efz_status !== 'checked_valid' && participant.efz_status !== 'not_required'" class="link" style="color: #3cb62e; font-size: 11pt;" @click="markCocExisting(participant)">Vorhanden?</span>
|
|
</td>
|
|
|
|
<td :id="'participant-' + participant.identifier +'-payment'" :class="participant.amount_left_value != 0 && !participant.unregistered ? 'not-paid' : ''" style="width: 275px; '">
|
|
Gezahlt: <label :id="'participant-' + participant.identifier + '-paid'">{{ participant?.amountPaid.readable }}</label> /<br />
|
|
Gesamt: <label :id="'participant-' + participant.identifier + '-expected'">{{ participant?.amountExpected.readable }}</label>
|
|
<br /><br />
|
|
<span v-if="participant.amount_left_value != 0 && !participant.unregistered" :id="'participant-' + participant.identifier + '-actions'">
|
|
<span class="link" style="font-size:10pt;" @click="paymentComplete(participant)">Zahlung buchen</span>
|
|
<span class="link" style="font-size:10pt;" @click="openPartialPaymentDialog(participant)">Teilzahlung buchen</span>
|
|
</span>
|
|
</td>
|
|
|
|
<td>
|
|
<label :id="'participant-' + participant.identifier +'-email_1'" class="block-label">{{ participant?.email_1 ?? "-" }}</label>
|
|
<label :id="'participant-' + participant.identifier +'-email_2'" class="block-label">{{ participant.email_2 }}</label>
|
|
</td>
|
|
|
|
<td>
|
|
<label :id="'participant-' + participant.identifier +'-phone_1'" class="block-label">{{ participant?.phone_1 }}</label>
|
|
<label :id="'participant-' + participant.identifier +'-phone_2'" class="block-label">{{ participant?.phone_2 }}</label>
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
<tr class="participant-meta-row" :id="'participant-' + participant.identifier + '-meta'">
|
|
<td colspan="5" style="height: 15px !important; font-size: 9pt; background-color: #ffffff; border-top-style: none;">
|
|
<label :id="'participant-' + participant.identifier +'-localgroup'">
|
|
{{ participant?.localgroup ?? "-" }}
|
|
</label> |
|
|
<strong> Anreise: </strong>
|
|
<label :id="'participant-' + participant.identifier +'-arrival'">
|
|
{{ participant?.arrival ?? "-" }}
|
|
</label>|
|
|
<strong> Abreise: </strong>
|
|
<label :id="'participant-' + participant.identifier +'-departure'">
|
|
{{ participant?.departure ?? "-" }}
|
|
</label> |
|
|
<label v-if="!participant.unregistered">
|
|
<strong> Angemeldet am: </strong>{{ participant?.registerDate ?? "-" }}
|
|
</label>
|
|
<label v-else>
|
|
<strong> Abgemeldet am: </strong>{{ participant?.unregisteredAt ?? "-" }}
|
|
</label> |
|
|
<span class="link" @click="openParticipantDetails(participant)">Details</span> |
|
|
<span class="link">E-Mail senden</span> |
|
|
<span @click="openCancelParticipationDialog(participant)" v-if="!participant.unregistered" class="link" style="color: #da7070;">Abmelden</span>
|
|
<span v-else class="link" @click="execResignonParticipant(participant)" style="color: #3cb62e;">Wieder anmelden</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
<tr>
|
|
<td colspan="3" style="font-weight: normal;">
|
|
0 - 5 Jahre: <strong>{{ getAgeCounts(participants)['0-5'] ?? 0 }}</strong> |
|
|
6-11 Jahre: <strong>{{ getAgeCounts(participants)['6-11'] ?? 0 }}</strong> |
|
|
12-15 Jahre: <strong>{{ getAgeCounts(participants)['12-15'] ?? 0 }}</strong> |
|
|
16 - 17 Jahre: <strong>{{ getAgeCounts(participants)['16-17'] ?? 0 }}</strong> |
|
|
18 - 27 Jahre: <strong>{{ getAgeCounts(participants)['18-27'] ?? 0 }}</strong> |
|
|
27 Jahre und älter: <strong>{{ getAgeCounts(participants)['27+'] ?? 0 }}</strong>
|
|
</td>
|
|
<td>
|
|
<input type="button" class="button" @click="mailToGroup(groupKey)" value="E-Mail an Gruppe senden" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal
|
|
:show="showParticipantDetails"
|
|
title="Anmeldedetails ansehen"
|
|
@close="showParticipantDetails = false;"
|
|
>
|
|
<ParticipantData
|
|
@cancelParticipation="openCancelParticipationDialog"
|
|
:participant="showParticipant"
|
|
:editMode="editMode"
|
|
:event="event"
|
|
@editParticipant="editMode = true"
|
|
@saveParticipant="saveParticipant"
|
|
@paymentComplete="paymentComplete"
|
|
@markCocExisting="markCocExisting"
|
|
@closeParticipantDetails="showParticipantDetails = false" />
|
|
</Modal>
|
|
|
|
<Modal
|
|
:show="openCancelDialog"
|
|
title="Anmeldung stornieren"
|
|
width="350px"
|
|
@close="openCancelDialog = false"
|
|
>
|
|
Datum der Abmeldung:
|
|
<input type="date" style="margin-top: 10px;" id="cancel_date" :value="today" />
|
|
<br /><br />
|
|
<button class="button" @click="execCancelParticipation()">Abmeldung durchführen</button>
|
|
</Modal>
|
|
|
|
<Modal
|
|
:show="openPartialPaymentDialogSwitch"
|
|
title="Teilbetragszahlung"
|
|
width="350px"
|
|
@close="openPartialPaymentDialogSwitch = false"
|
|
>
|
|
Gesamtbetrag der Zahlung:
|
|
<AmountInput type="text" v-model="showParticipant.amountExpected.short" style="margin-top: 10px; width: 100px !important;" id="partial_payment_amount" /> Euro /
|
|
{{showParticipant.amountExpected.readable}} Euro
|
|
<br /><br />
|
|
<button class="button" @click="execPartialPayment()">Teilbetrag buchen</button>
|
|
</Modal>
|
|
|
|
<FullScreenModal
|
|
:show="mailCompose"
|
|
title="E-Mail senden"
|
|
@close="mailCompose = false"
|
|
>
|
|
<MailCompose @closeComposer="mailCompose = false" :event="event" :mailToType="mailToType" :recipientIdentifier="recipientIdentifier" />
|
|
</FullScreenModal>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
<style scoped>
|
|
table {
|
|
margin-bottom: 60px !important;
|
|
}
|
|
|
|
tr {
|
|
vertical-align: top;
|
|
}
|
|
|
|
tr td {
|
|
height: 80px;
|
|
padding: 10px;
|
|
padding-top: 20px;
|
|
font-size: 11pt;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
tr th {
|
|
height: 40px;
|
|
padding-left: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
tr th:after {
|
|
content: "";
|
|
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: #f9fafb;
|
|
border-style: solid;
|
|
border-width: 0 1px;
|
|
border-color: #e5e7eb;
|
|
}
|
|
|
|
|
|
|
|
tr:nth-child(odd) {
|
|
background-color: #ffffff;
|
|
border-style: solid;
|
|
border-width: 5px 1px 0 1px;
|
|
border-color: #e5e7eb;
|
|
}
|
|
|
|
tr:first-child {
|
|
border-width: 1px 1px 0 1px;
|
|
}
|
|
|
|
tr:last-child {
|
|
border-width: 0 1px 1px 1px;
|
|
}
|
|
|
|
tr:last-child td {
|
|
background: linear-gradient(to bottom, #fff, #f6f7f7); font-weight: bold;
|
|
height: 30px;
|
|
}
|
|
|
|
.button {
|
|
display: block;
|
|
font-size: 10pt;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.not-paid {
|
|
color: #ff0000; background-color: #ffe6e6;
|
|
}
|
|
|
|
.efz-invalid {
|
|
color: #ff0000; background-color: #ffe6e6;
|
|
}
|
|
|
|
.efz-not-checked {
|
|
color: #8D914BFF; background-color: #F4E99EFF;
|
|
}
|
|
|
|
.block-label {
|
|
display: block;
|
|
}
|
|
</style>
|