Kundenportal Vertragsanfragen ergänzen
This commit is contained in:
@@ -10,12 +10,27 @@ const toast = useToast()
|
||||
|
||||
const customer = ref<any | null>(null)
|
||||
const contracts = ref<any[]>([])
|
||||
const contracttypes = ref<any[]>([])
|
||||
const invoices = ref<any[]>([])
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const expandedInvoiceId = ref<number | null>(null)
|
||||
const downloadingInvoiceId = ref<number | null>(null)
|
||||
const activeTab = ref("0")
|
||||
const contractChangeModalOpen = ref(false)
|
||||
const cancellationModalOpen = ref(false)
|
||||
const submittingContractRequest = ref(false)
|
||||
const selectedContract = ref<any | null>(null)
|
||||
|
||||
const contractChangeForm = reactive({
|
||||
contracttype: null as number | null,
|
||||
message: ""
|
||||
})
|
||||
|
||||
const cancellationForm = reactive({
|
||||
requestedEndDate: "",
|
||||
message: ""
|
||||
})
|
||||
|
||||
const customerForm = reactive({
|
||||
name: "",
|
||||
@@ -112,6 +127,58 @@ function formatCurrency(value: number) {
|
||||
return useCurrency(value)
|
||||
}
|
||||
|
||||
function formatBoolean(value: boolean | null | undefined) {
|
||||
if (typeof value !== "boolean") return "-"
|
||||
return value ? "Ja" : "Nein"
|
||||
}
|
||||
|
||||
function formatValue(value: any) {
|
||||
if (value === null || typeof value === "undefined" || value === "") return "-"
|
||||
|
||||
if (typeof value === "boolean") return formatBoolean(value)
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (Object.keys(value).length === 0) return "-"
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function maskIban(value?: string | null) {
|
||||
if (!value) return "-"
|
||||
|
||||
const clean = value.replace(/\s+/g, "")
|
||||
if (clean.length <= 4) return clean
|
||||
|
||||
const country = clean.slice(0, 2)
|
||||
const lastTwo = clean.slice(-2)
|
||||
const maskedLength = Math.max(clean.length - 4, 0)
|
||||
const masked = "*".repeat(maskedLength).replace(/(.{4})/g, "$1 ").trim()
|
||||
|
||||
return `${country} ${masked} ${lastTwo}`.replace(/\s+/g, " ").trim()
|
||||
}
|
||||
|
||||
function getContactLabel(contact: any) {
|
||||
if (!contact) return "-"
|
||||
if (typeof contact !== "object") return String(contact)
|
||||
return contact.fullName || [contact.firstName, contact.lastName].filter(Boolean).join(" ") || contact.email || contact.id || "-"
|
||||
}
|
||||
|
||||
function openContractChangeRequest(contract: any) {
|
||||
selectedContract.value = contract
|
||||
contractChangeForm.contracttype = contract.contracttype?.id || null
|
||||
contractChangeForm.message = ""
|
||||
contractChangeModalOpen.value = true
|
||||
}
|
||||
|
||||
function openCancellationRequest(contract: any) {
|
||||
selectedContract.value = contract
|
||||
cancellationForm.requestedEndDate = contract.endDate ? dayjs(contract.endDate).format("YYYY-MM-DD") : ""
|
||||
cancellationForm.message = ""
|
||||
cancellationModalOpen.value = true
|
||||
}
|
||||
|
||||
function getInvoiceAmount(invoice: any) {
|
||||
return useSum().getCreatedDocumentSum(invoice, invoices.value)
|
||||
}
|
||||
@@ -141,6 +208,48 @@ async function downloadInvoice(invoice: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitContractChangeRequest() {
|
||||
if (!selectedContract.value?.id || !contractChangeForm.contracttype) return
|
||||
|
||||
submittingContractRequest.value = true
|
||||
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/portal/contracts/${selectedContract.value.id}/change-request`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
contracttype: contractChangeForm.contracttype,
|
||||
message: contractChangeForm.message
|
||||
}
|
||||
})
|
||||
|
||||
contractChangeModalOpen.value = false
|
||||
toast.add({ title: "Ihre Anfrage wurde übermittelt." })
|
||||
} finally {
|
||||
submittingContractRequest.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCancellationRequest() {
|
||||
if (!selectedContract.value?.id || !cancellationForm.requestedEndDate) return
|
||||
|
||||
submittingContractRequest.value = true
|
||||
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/portal/contracts/${selectedContract.value.id}/cancellation-request`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
requestedEndDate: cancellationForm.requestedEndDate,
|
||||
message: cancellationForm.message
|
||||
}
|
||||
})
|
||||
|
||||
cancellationModalOpen.value = false
|
||||
toast.add({ title: "Ihre Anfrage wurde übermittelt." })
|
||||
} finally {
|
||||
submittingContractRequest.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPortalData() {
|
||||
if (!portalCustomerId.value) {
|
||||
loading.value = false
|
||||
@@ -150,15 +259,17 @@ async function loadPortalData() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const [customerRecord, contractRows, invoiceRows] = await Promise.all([
|
||||
const [customerRecord, contractRows, invoiceRows, contracttypeRows] = await Promise.all([
|
||||
useEntities("customers").selectSingle(portalCustomerId.value),
|
||||
useEntities("contracts").select("*, contracttype(id,name)", "startDate", true),
|
||||
useEntities("createddocuments").select("*, files(*), statementallocations(*), contract(id,name,contractNumber)", "documentDate", true)
|
||||
useEntities("createddocuments").select("*, files(*), statementallocations(*), contract(id,name,contractNumber)", "documentDate", true),
|
||||
useEntities("contracttypes").select("*", "name", true)
|
||||
])
|
||||
|
||||
customer.value = customerRecord
|
||||
contracts.value = (contractRows || []).filter((item: any) => !item.archived)
|
||||
invoices.value = (invoiceRows || []).filter((item: any) => !item.archived)
|
||||
contracttypes.value = (contracttypeRows || []).filter((item: any) => !item.archived)
|
||||
fillFormFromCustomer(customerRecord)
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -472,24 +583,149 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-pencil-square"
|
||||
class="justify-center"
|
||||
@click="openContractChangeRequest(contract)"
|
||||
>
|
||||
Änderung anfragen
|
||||
</UButton>
|
||||
<UButton
|
||||
color="red"
|
||||
variant="soft"
|
||||
icon="i-heroicons-document-minus"
|
||||
class="justify-center"
|
||||
@click="openCancellationRequest(contract)"
|
||||
>
|
||||
Kündigung anfragen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Laufzeit</p>
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Vertragstyp</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.startDate) }} bis {{ formatDate(contract.endDate) }}
|
||||
{{ contract.contracttype?.name || "Nicht hinterlegt" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Abrechnung</p>
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Ansprechpartner</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ getContactLabel(contract.contact) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Aktiv</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatBoolean(contract.active) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Wiederkehrend</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatBoolean(contract.recurring) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Startdatum</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Enddatum</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.endDate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Unterschrieben am</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.signDate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Laufzeit</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.duration || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Zahlungsart</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.paymentType || "Nicht hinterlegt" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Abrechnungsintervall</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.billingInterval || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Rechnungsversand</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.invoiceDispatch || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Kontoinhaber</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.bankingOwner || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Bank</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.bankingName || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">IBAN</p>
|
||||
<p class="mt-1 break-all text-sm font-medium text-neutral-900">
|
||||
{{ maskIban(contract.bankingIban) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">BIC</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.bankingBIC || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">SEPA-Referenz</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ contract.sepaRef || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">SEPA-Datum</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.sepaDate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Erstellt am</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Aktualisiert am</p>
|
||||
<p class="mt-1 text-sm font-medium text-neutral-900">
|
||||
{{ formatDate(contract.updatedAt) }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="contract.notes" class="rounded-xl bg-white p-3 sm:col-span-2">
|
||||
<p class="text-xs uppercase tracking-wide text-neutral-400">Notizen</p>
|
||||
<p class="mt-1 whitespace-pre-wrap break-words text-sm font-medium text-neutral-900">
|
||||
{{ contract.notes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="contract.notes" class="mt-4 text-sm text-neutral-600">
|
||||
{{ contract.notes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -498,5 +734,104 @@ onMounted(async () => {
|
||||
</UCard>
|
||||
</div>
|
||||
</UContainer>
|
||||
|
||||
<UModal v-model:open="contractChangeModalOpen">
|
||||
<template #content>
|
||||
<div class="space-y-5 p-6">
|
||||
<div>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.2em] text-primary-600">Vertrag ändern</p>
|
||||
<h3 class="mt-2 text-xl font-semibold text-neutral-900">Änderung anfragen</h3>
|
||||
<p class="mt-1 text-sm text-neutral-500">
|
||||
Ihre Anfrage wird an unser Team übermittelt. Der Vertrag wird dadurch noch nicht geändert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">Gewünschter Vertragstyp</label>
|
||||
<USelectMenu
|
||||
v-model="contractChangeForm.contracttype"
|
||||
:items="contracttypes"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
class="w-full"
|
||||
placeholder="Vertragstyp auswählen"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">Nachricht optional</label>
|
||||
<UTextarea
|
||||
v-model="contractChangeForm.message"
|
||||
class="w-full"
|
||||
:rows="4"
|
||||
placeholder="Ergänzende Hinweise zur gewünschten Änderung"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="neutral" variant="ghost" @click="contractChangeModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
:loading="submittingContractRequest"
|
||||
:disabled="!contractChangeForm.contracttype"
|
||||
@click="submitContractChangeRequest"
|
||||
>
|
||||
Anfrage senden
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UModal v-model:open="cancellationModalOpen">
|
||||
<template #content>
|
||||
<div class="space-y-5 p-6">
|
||||
<div>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.2em] text-red-600">Kündigung</p>
|
||||
<h3 class="mt-2 text-xl font-semibold text-neutral-900">Kündigung anfragen</h3>
|
||||
<p class="mt-1 text-sm text-neutral-500">
|
||||
Ihre Kündigungsanfrage wird dokumentiert und intern geprüft.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">Gewünschtes Kündigungsdatum</label>
|
||||
<UInput
|
||||
v-model="cancellationForm.requestedEndDate"
|
||||
type="date"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">Nachricht optional</label>
|
||||
<UTextarea
|
||||
v-model="cancellationForm.message"
|
||||
class="w-full"
|
||||
:rows="4"
|
||||
placeholder="Optionaler Grund oder weitere Hinweise"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="neutral" variant="ghost" @click="cancellationModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
color="red"
|
||||
:loading="submittingContractRequest"
|
||||
:disabled="!cancellationForm.requestedEndDate"
|
||||
@click="submitCancellationRequest"
|
||||
>
|
||||
Anfrage senden
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user