Creating Participation refunds
This commit is contained in:
@@ -9,6 +9,7 @@ import {format} from "date-fns";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
|
||||
import DialableTelephoneNumber from "../../../../Views/Components/DialableTelephoneNumber.vue";
|
||||
import ErrorText from "../../../../Views/Components/ErrorText.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -29,7 +30,7 @@ const props = defineProps({
|
||||
|
||||
const today = format(new Date(), "yyyy-MM-dd");
|
||||
|
||||
const { request } = useAjax();
|
||||
const { request, download } = useAjax();
|
||||
|
||||
const searchTerms = reactive({});
|
||||
const selectedStatuses = reactive({});
|
||||
@@ -44,6 +45,18 @@ const mailCompose = ref(false);
|
||||
|
||||
const openCancelDialog = ref(false);
|
||||
const openPartialPaymentDialogSwitch = ref(false);
|
||||
const openRefundDialogSwitch = ref(false);
|
||||
|
||||
// Der Erstattungsdialog. Betrag und Grund werden hier gesetzt; die Bankverbindung erfasst der Teili
|
||||
// selbst über den Link, den die Freigabe ihm schickt.
|
||||
const refundForm = reactive({amount: '', reason: '', reasonNote: ''});
|
||||
const refundErrors = reactive({amount: '', reason: '', reasonNote: ''});
|
||||
const refundReasons = ref([]);
|
||||
const refundSaving = ref(false);
|
||||
|
||||
const selectedRefundReason = computed(
|
||||
() => refundReasons.value.find(r => r.value === refundForm.reason) ?? null
|
||||
);
|
||||
|
||||
defineEmits(['showParticipantDetails', 'markCocExisting', 'paymentComplete'])
|
||||
|
||||
@@ -292,6 +305,104 @@ async function execPartialPayment() {
|
||||
openPartialPaymentDialogSwitch.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstattung freigeben.
|
||||
*
|
||||
* Der Betrag ist mit dem vor, was der Teili gezahlt hat -- das ist der Regelfall; Abzüge trägt die
|
||||
* Aktionsleitung von Hand ein.
|
||||
*/
|
||||
async function openRefundDialog(participant) {
|
||||
showParticipant.value = participant;
|
||||
|
||||
refundForm.amount = participant.amountPaid?.short ?? '';
|
||||
refundForm.reason = '';
|
||||
refundForm.reasonNote = '';
|
||||
refundErrors.amount = '';
|
||||
refundErrors.reason = '';
|
||||
refundErrors.reasonNote = '';
|
||||
|
||||
if (refundReasons.value.length === 0) {
|
||||
const reasons = await request('/api/v1/core/retrieve-refund-reasons', {method: 'GET'});
|
||||
refundReasons.value = reasons ?? [];
|
||||
}
|
||||
|
||||
openRefundDialogSwitch.value = true;
|
||||
}
|
||||
|
||||
function validateRefund() {
|
||||
const amount = Number((refundForm.amount ?? '').replace(',', '.'));
|
||||
const paid = Number(showParticipant.value?.amountPaidValue ?? 0);
|
||||
|
||||
refundErrors.amount = '';
|
||||
refundErrors.reason = '';
|
||||
refundErrors.reasonNote = '';
|
||||
|
||||
if (!refundForm.amount || !(amount > 0)) {
|
||||
refundErrors.amount = 'Bitte gib einen Betrag größer als 0 ein.';
|
||||
} else if (amount > paid + 0.005) {
|
||||
refundErrors.amount = 'Mehr als der gezahlte Beitrag kann nicht erstattet werden.';
|
||||
}
|
||||
|
||||
if (!refundForm.reason) {
|
||||
refundErrors.reason = 'Bitte wähle einen Grund aus.';
|
||||
} else if (selectedRefundReason.value?.requiresNote && !refundForm.reasonNote.trim()) {
|
||||
refundErrors.reasonNote = 'Bitte erläutere den Grund.';
|
||||
}
|
||||
|
||||
return !refundErrors.amount && !refundErrors.reason && !refundErrors.reasonNote;
|
||||
}
|
||||
|
||||
async function execRefund() {
|
||||
if (!validateRefund() || refundSaving.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
refundSaving.value = true;
|
||||
|
||||
try {
|
||||
const data = await request('/api/v1/participant-refund/' + showParticipant.value.identifier + '/release', {
|
||||
method: "POST",
|
||||
body: {
|
||||
amount: refundForm.amount,
|
||||
reason: refundForm.reason,
|
||||
reasonNote: refundForm.reasonNote,
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.status === 'success') {
|
||||
toast.success(data.message);
|
||||
// Reaktiv statt per getElementById: die Meta-Zeile hängt am Vorgang und wechselt mit ihm.
|
||||
showParticipant.value.refund = data.refund;
|
||||
openRefundDialogSwitch.value = false;
|
||||
} else {
|
||||
toast.error(data?.message ?? 'Die Erstattung konnte nicht freigegeben werden.');
|
||||
}
|
||||
} finally {
|
||||
refundSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function execCancelRefund(participant) {
|
||||
const data = await request('/api/v1/participant-refund/' + participant.refund.token + '/cancel', {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (data?.status === 'success') {
|
||||
toast.success(data.message);
|
||||
participant.refund = null;
|
||||
} else {
|
||||
toast.error(data?.message ?? 'Die Erstattung konnte nicht abgebrochen werden.');
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadRefundDocument(participant) {
|
||||
const ok = await download('/api/v1/participant-refund/' + participant.refund.token + '/document');
|
||||
|
||||
if (!ok) {
|
||||
toast.error('Der Beleg konnte nicht erstellt werden.');
|
||||
}
|
||||
}
|
||||
|
||||
const mailToType = ref('')
|
||||
const recipientIdentifier = ref('')
|
||||
|
||||
@@ -395,6 +506,27 @@ function mailToGroup(groupKey) {
|
||||
<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>
|
||||
|
||||
<!-- Erstattung: erst der Einstieg, danach der Zustand des Vorgangs. -->
|
||||
<template v-if="participant.unregistered">
|
||||
<span
|
||||
v-if="!participant.refund && Number(participant.amountPaidValue ?? 0) > 0"
|
||||
class="link"
|
||||
style="color: #3cb62e;"
|
||||
@click="openRefundDialog(participant)"
|
||||
> | Beitrag erstatten</span>
|
||||
|
||||
<template v-else-if="participant.refund?.status === 'pending'">
|
||||
| <strong>Erstattung offen:</strong> {{ participant.refund.amount }},
|
||||
wartet auf Bankverbindung
|
||||
<span class="link" style="color: #da7070;" @click="execCancelRefund(participant)">Abbrechen</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="participant.refund?.status === 'accepted'">
|
||||
| <strong>Erstattet:</strong> {{ participant.refund.amount }}
|
||||
<span class="link" @click="downloadRefundDocument(participant)">Beleg</span>
|
||||
</template>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -460,6 +592,48 @@ function mailToGroup(groupKey) {
|
||||
<button class="button" @click="execPartialPayment()">Teilbetrag buchen</button>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
:show="openRefundDialogSwitch"
|
||||
title="Beitrag erstatten"
|
||||
width="480px"
|
||||
@close="openRefundDialogSwitch = false"
|
||||
>
|
||||
<p class="refund-intro">
|
||||
{{ showParticipant?.fullname }} hat
|
||||
<strong>{{ showParticipant?.amountPaid?.readable }}</strong> gezahlt. Nach der Freigabe erhält
|
||||
der Teili eine E-Mail und trägt seine Bankverbindung selbst ein.
|
||||
</p>
|
||||
|
||||
<div class="refund-field">
|
||||
<label for="refund_amount">Zu erstattender Betrag</label>
|
||||
<div>
|
||||
<AmountInput id="refund_amount" v-model="refundForm.amount" style="width: 100px !important;" /> Euro
|
||||
</div>
|
||||
<ErrorText :message="refundErrors.amount" />
|
||||
</div>
|
||||
|
||||
<div class="refund-field">
|
||||
<label for="refund_reason">Grund</label>
|
||||
<select id="refund_reason" v-model="refundForm.reason" class="form-input">
|
||||
<option value="">Bitte auswählen …</option>
|
||||
<option v-for="reason in refundReasons" :key="reason.value" :value="reason.value">
|
||||
{{ reason.label }}
|
||||
</option>
|
||||
</select>
|
||||
<ErrorText :message="refundErrors.reason" />
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRefundReason?.requiresNote" class="refund-field">
|
||||
<label for="refund_reason_note">Erläuterung</label>
|
||||
<textarea id="refund_reason_note" v-model="refundForm.reasonNote" class="form-input" rows="3"></textarea>
|
||||
<ErrorText :message="refundErrors.reasonNote" />
|
||||
</div>
|
||||
|
||||
<button class="button" :disabled="refundSaving" @click="execRefund()">
|
||||
{{ refundSaving ? 'Wird freigegeben…' : 'Erstattung freigeben' }}
|
||||
</button>
|
||||
</Modal>
|
||||
|
||||
<FullScreenModal
|
||||
:show="mailCompose"
|
||||
title="E-Mail senden"
|
||||
@@ -473,6 +647,28 @@ function mailToGroup(groupKey) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.refund-intro {
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.refund-field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.refund-field label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.refund-field select,
|
||||
.refund-field textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.participants-table {
|
||||
width: 95%;
|
||||
margin: 20px auto;
|
||||
|
||||
Reference in New Issue
Block a user