465 lines
18 KiB
Vue
465 lines
18 KiB
Vue
<script setup>
|
|
import {computed, reactive, ref} from 'vue'
|
|
import {toast} from 'vue3-toastify'
|
|
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
|
|
import ShadowedBox from '../../../Views/Components/ShadowedBox.vue'
|
|
import ErrorText from '../../../Views/Components/ErrorText.vue'
|
|
import IbanInput from '../../../Views/Components/IbanInput.vue'
|
|
import TextResource from '../../../Views/Components/TextResource.vue'
|
|
import {useAjax} from '../../../../resources/js/components/ajaxHandler.js'
|
|
|
|
const {request} = useAjax()
|
|
|
|
/**
|
|
* `state` steuert die ganze Seite:
|
|
* open -- Formular für die Bankverbindung
|
|
* accepted -- Angaben liegen vor, nichts mehr zu tun
|
|
* unavailable -- Token unbekannt oder Erstattung abgebrochen
|
|
*/
|
|
const props = defineProps({
|
|
state: String,
|
|
message: String,
|
|
token: String,
|
|
name: String,
|
|
eventTitle: String,
|
|
eventEmail: String,
|
|
amount: String,
|
|
reason: String,
|
|
reasonNote: String,
|
|
acceptedAt: String,
|
|
donation: Boolean,
|
|
})
|
|
|
|
// Der Zustand wandert in ein ref, damit die Seite nach dem Absenden umschalten kann, ohne neu zu laden.
|
|
const state = ref(props.state)
|
|
|
|
// Ob gespendet wurde -- für den Abschlusstext. Nach dem Absenden setzt submit() den Wert selbst, ein
|
|
// Neuladen holt ihn vom Controller.
|
|
const donated = ref(props.donation === true)
|
|
|
|
/**
|
|
* Der Weg durch die Seite, nach dem Muster der Auslagenabrechnung (refund-data.vue): erst die
|
|
* Entscheidung, dann die Angaben dazu. Nichts wird gefragt, was für den gewählten Weg keine Rolle spielt.
|
|
*
|
|
* decision -- '' (noch nichts gewählt) | 'donation' (spenden) | 'payout' (auszahlen lassen)
|
|
* sameAccount -- nur bei 'payout': ob es das Konto der Ursprungszahlung ist
|
|
*/
|
|
const decision = ref('')
|
|
const sameAccount = ref(null)
|
|
|
|
const form = reactive({
|
|
accountOwner: '',
|
|
accountIban: '',
|
|
declarationAccepted: false,
|
|
accountDeclarationAccepted: false,
|
|
})
|
|
const errors = reactive({accountOwner: '', accountIban: '', declaration: '', accountDeclaration: ''})
|
|
const saving = ref(false)
|
|
|
|
/**
|
|
* Die Erklärung erscheint erst, wenn beide Kontoangaben stehen -- man bestätigt nichts, bevor man weiß,
|
|
* worüber. Dasselbe Vorgehen wie beim Erfassen einer Abrechnung (refund-data.vue).
|
|
*
|
|
* Dort wird zusätzlich auf `iban.length === 27` geprüft, also auf die Länge einer formatierten deutschen
|
|
* IBAN. Das bleibt hier bewusst weg: eine österreichische oder schweizerische käme sonst nie durch. Ob
|
|
* die IBAN stimmt, prüft der Server mit Prüfziffer und länderabhängiger Länge.
|
|
*/
|
|
const accountComplete = computed(
|
|
() => form.accountOwner.trim() !== '' && form.accountIban.trim() !== ''
|
|
)
|
|
|
|
const isDonation = computed(() => decision.value === 'donation')
|
|
|
|
/**
|
|
* Wechselt den Weg und nimmt dabei jedes Kreuz zurück.
|
|
*
|
|
* Ohne das Zurücksetzen stünde eine Erklärung als bestätigt da, die in diesem Weg gar nicht gezeigt
|
|
* wurde -- wer erst die Auszahlung ankreuzt und dann zur Spende wechselt, hätte den Verzicht nie gelesen.
|
|
*/
|
|
function choose(next) {
|
|
decision.value = next
|
|
sameAccount.value = null
|
|
form.declarationAccepted = false
|
|
form.accountDeclarationAccepted = false
|
|
Object.keys(errors).forEach((key) => (errors[key] = ''))
|
|
}
|
|
|
|
/** Zurück zur ersten Frage -- aus der Sackgasse „anderes Konto" führt der Weg zur Spende. */
|
|
function reset() {
|
|
choose('')
|
|
}
|
|
|
|
function validate() {
|
|
Object.keys(errors).forEach((key) => (errors[key] = ''))
|
|
|
|
errors.declaration = form.declarationAccepted ? '' : 'Bitte bestätige die Erklärung.'
|
|
|
|
if (!isDonation.value) {
|
|
errors.accountOwner = form.accountOwner.trim() ? '' : 'Bitte gib an, wem das Konto gehört.'
|
|
errors.accountIban = form.accountIban.trim() ? '' : 'Bitte gib die IBAN des Kontos ein.'
|
|
errors.accountDeclaration = form.accountDeclarationAccepted
|
|
? ''
|
|
: 'Bitte bestätige, dass es das Konto ist, von dem der Beitrag gezahlt wurde.'
|
|
}
|
|
|
|
return Object.values(errors).every((message) => message === '')
|
|
}
|
|
|
|
async function submit() {
|
|
if (!validate() || saving.value) return
|
|
|
|
saving.value = true
|
|
|
|
// Bei einer Spende geht keine Bankverbindung mit -- es gibt keine, und der Server erwartet auch keine.
|
|
const body = isDonation.value
|
|
? {donation: true, declarationAccepted: form.declarationAccepted}
|
|
: {
|
|
donation: false,
|
|
accountOwner: form.accountOwner,
|
|
accountIban: form.accountIban,
|
|
declarationAccepted: form.declarationAccepted,
|
|
accountDeclarationAccepted: form.accountDeclarationAccepted,
|
|
}
|
|
|
|
try {
|
|
const response = await request('/api/v1/participant-refund/' + props.token + '/accept', {
|
|
method: 'POST',
|
|
body,
|
|
})
|
|
|
|
if (!response) {
|
|
toast.error('Deine Angaben konnten nicht gespeichert werden. Bitte versuche es später erneut.')
|
|
return
|
|
}
|
|
|
|
// Feldbezogene Fehler kommen mit Status 200 als `error_types` zurück -- der HttpClient des
|
|
// Projekts verwirft Antworten mit Fehlerstatus.
|
|
if (response.status !== 'success') {
|
|
Object.keys(response.error_types ?? {}).forEach((key) => {
|
|
if (key in errors) errors[key] = response.error_types[key]
|
|
})
|
|
|
|
toast.error(response.message ?? 'Bitte prüfe deine Angaben.')
|
|
return
|
|
}
|
|
|
|
toast.success(response.message)
|
|
donated.value = isDonation.value
|
|
state.value = 'accepted'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout title="Rückerstattung">
|
|
<shadowed-box style="max-width: 640px; margin: 60px auto; padding: 24px;">
|
|
|
|
<template v-if="state === 'unavailable'">
|
|
<h2>Rückerstattung</h2>
|
|
<p class="hint">{{ props.message }}</p>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<h2>Rückerstattung deines Teilnahmebeitrags</h2>
|
|
|
|
<p>
|
|
Hallo {{ props.name }}, für deine Abmeldung von der Veranstaltung
|
|
<strong>{{ props.eventTitle }}</strong> wurde eine Rückerstattung freigegeben.
|
|
</p>
|
|
|
|
<table class="summary">
|
|
<tr>
|
|
<th>Betrag</th>
|
|
<td><strong>{{ props.amount }}</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th>Grund</th>
|
|
<td>{{ props.reason }}</td>
|
|
</tr>
|
|
<tr v-if="props.reasonNote">
|
|
<th>Anmerkung</th>
|
|
<td>{{ props.reasonNote }}</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<template v-if="state === 'accepted'">
|
|
<p v-if="donated" class="hint">
|
|
Du hast den Betrag gespendet<span v-if="props.acceptedAt"> (am {{ props.acceptedAt }})</span>
|
|
– vielen Dank. Es gibt nichts weiter zu tun. Stimmt etwas nicht, wende dich
|
|
bitte an die Aktionsleitung: {{ props.eventEmail }}
|
|
</p>
|
|
<p v-else class="hint">
|
|
Deine Angaben liegen uns vor<span v-if="props.acceptedAt"> (seit {{ props.acceptedAt }})</span>.
|
|
Die Überweisung veranlasst die Aktionsleitung. Stimmt etwas nicht, wende dich bitte
|
|
an sie: {{ props.eventEmail }}
|
|
</p>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<p class="hint">
|
|
Der Betrag steht fest und lässt sich hier nicht ändern. Hast du dazu Fragen, wende
|
|
dich bitte an die Aktionsleitung: {{ props.eventEmail }}
|
|
</p>
|
|
|
|
<form @submit.prevent="submit">
|
|
|
|
<!--
|
|
Erste Frage: Wer spendet, braucht kein Konto anzugeben. Deshalb steht sie
|
|
vor allem anderen.
|
|
-->
|
|
<h3>Möchtest du den Betrag stattdessen spenden?</h3>
|
|
|
|
<p class="choice-hint">
|
|
Spendest du, verbleibt der Betrag beim Verband und kommt unserer Arbeit
|
|
zugute. Wir brauchen dann keine Bankverbindung von dir.
|
|
</p>
|
|
|
|
<div class="choices">
|
|
<button
|
|
type="button"
|
|
class="choice"
|
|
:class="{active: decision === 'donation'}"
|
|
@click="choose('donation')"
|
|
>Ja, spenden</button>
|
|
<button
|
|
type="button"
|
|
class="choice"
|
|
:class="{active: decision === 'payout'}"
|
|
@click="choose('payout')"
|
|
>Nein, bitte erstatten</button>
|
|
</div>
|
|
|
|
<!-- Spendenweg: nur der Verzicht, keine Kontoangaben. -->
|
|
<template v-if="decision === 'donation'">
|
|
<div class="declaration">
|
|
<input
|
|
id="refund-donation-declaration"
|
|
v-model="form.declarationAccepted"
|
|
type="checkbox"
|
|
/>
|
|
<TextResource
|
|
text-name="CONFIRMATION_PARTICIPANT_REFUND_DONATION"
|
|
belongs-to="refund-donation-declaration"
|
|
/>
|
|
</div>
|
|
<ErrorText :message="errors.declaration" />
|
|
|
|
<button
|
|
v-if="form.declarationAccepted"
|
|
class="button"
|
|
type="submit"
|
|
:disabled="saving"
|
|
>
|
|
{{ saving ? 'Wird gespeichert…' : 'Betrag spenden' }}
|
|
</button>
|
|
</template>
|
|
|
|
<!--
|
|
Auszahlungsweg: Erstattet wird ausschließlich auf das Konto, von dem der
|
|
Beitrag kam. Das wird gefragt, bevor überhaupt eine IBAN eingegeben wird --
|
|
sonst tippt jemand ein Konto ab, das wir anschließend ablehnen müssen.
|
|
-->
|
|
<template v-else-if="decision === 'payout'">
|
|
<h3>Soll der Betrag auf das Konto erstattet werden, von dem der Beitrag
|
|
gezahlt wurde?</h3>
|
|
|
|
<div class="choices">
|
|
<button
|
|
type="button"
|
|
class="choice"
|
|
:class="{active: sameAccount === true}"
|
|
@click="sameAccount = true"
|
|
>Ja</button>
|
|
<button
|
|
type="button"
|
|
class="choice"
|
|
:class="{active: sameAccount === false}"
|
|
@click="sameAccount = false"
|
|
>Nein</button>
|
|
</div>
|
|
|
|
<template v-if="sameAccount === false">
|
|
<p class="hint">
|
|
So können wir den Betrag leider nicht erstatten: Zurück geht er nur auf
|
|
das Konto, von dem der Teilnahmebeitrag gezahlt wurde. Wurde er von
|
|
einem anderen Konto überwiesen – etwa dem eines Elternteils
|
|
–, gib bitte dieses an. Hilft das nicht weiter, wende dich bitte
|
|
an die Aktionsleitung: {{ props.eventEmail }}
|
|
</p>
|
|
|
|
<button type="button" class="button" @click="reset()">
|
|
Zurück zur Auswahl
|
|
</button>
|
|
</template>
|
|
|
|
<template v-else-if="sameAccount === true">
|
|
<div class="field">
|
|
<label for="account-owner">Kontoinhaber*in</label>
|
|
<input
|
|
id="account-owner"
|
|
v-model="form.accountOwner"
|
|
type="text"
|
|
class="form-input"
|
|
autocomplete="name"
|
|
/>
|
|
<ErrorText :message="errors.accountOwner" />
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="account-iban">IBAN</label>
|
|
<IbanInput id="account-iban" v-model="form.accountIban" class="form-input" />
|
|
<ErrorText :message="errors.accountIban" />
|
|
</div>
|
|
|
|
<!--
|
|
Die Erklärungen, die anschließend auf dem Eigenbeleg stehen. Sie
|
|
müssen hier gelesen und angekreuzt werden -- sonst schriebe der Beleg
|
|
dem Teili Zusicherungen zu, die er nie abgegeben hat.
|
|
-->
|
|
<template v-if="accountComplete">
|
|
<div class="declaration">
|
|
<input
|
|
id="refund-declaration"
|
|
v-model="form.declarationAccepted"
|
|
type="checkbox"
|
|
/>
|
|
<TextResource
|
|
text-name="CONFIRMATION_PARTICIPANT_REFUND"
|
|
belongs-to="refund-declaration"
|
|
/>
|
|
</div>
|
|
<ErrorText :message="errors.declaration" />
|
|
|
|
<div class="declaration">
|
|
<input
|
|
id="refund-account-declaration"
|
|
v-model="form.accountDeclarationAccepted"
|
|
type="checkbox"
|
|
/>
|
|
<TextResource
|
|
text-name="CONFIRMATION_PARTICIPANT_REFUND_ACCOUNT"
|
|
belongs-to="refund-account-declaration"
|
|
/>
|
|
</div>
|
|
<ErrorText :message="errors.accountDeclaration" />
|
|
|
|
<!-- Beim Speichern gesperrt statt ausgeblendet, sonst verschwände der
|
|
Knopf unter dem Finger. -->
|
|
<button
|
|
v-if="form.declarationAccepted && form.accountDeclarationAccepted"
|
|
class="button"
|
|
type="submit"
|
|
:disabled="saving"
|
|
>
|
|
{{ saving ? 'Wird gespeichert…' : 'Angaben absenden' }}
|
|
</button>
|
|
</template>
|
|
</template>
|
|
</template>
|
|
</form>
|
|
</template>
|
|
</template>
|
|
|
|
</shadowed-box>
|
|
</AppLayout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
h2 {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
h3 {
|
|
margin: 24px 0 12px;
|
|
}
|
|
|
|
.hint {
|
|
margin: 16px 0;
|
|
padding: 10px 12px;
|
|
border-left: 3px solid #f5c400;
|
|
background-color: #fffef5;
|
|
font-size: 0.9rem;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.summary {
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.summary th {
|
|
text-align: left;
|
|
padding: 6px 24px 6px 0;
|
|
color: #555;
|
|
font-weight: normal;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.summary td {
|
|
padding: 6px 0;
|
|
}
|
|
|
|
.choice-hint {
|
|
margin-bottom: 12px;
|
|
font-size: 0.9rem;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.choices {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.choice {
|
|
flex: 1;
|
|
padding: 10px 12px;
|
|
border: 1px solid #d8dde6;
|
|
border-radius: 0;
|
|
background: #ffffff;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.choice.active {
|
|
border-color: #1a4799;
|
|
background: #eef2fa;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.field {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.field label {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
font-size: 0.9rem;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.field .form-input {
|
|
width: 100%;
|
|
}
|
|
|
|
.declaration {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
margin: 20px 0 8px;
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.declaration input {
|
|
margin-top: 3px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.declaration :deep(label) {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|