Managing own invoices

This commit is contained in:
2026-02-13 12:38:48 +01:00
parent ab711109a7
commit f468814a2f
15 changed files with 715 additions and 21 deletions

View File

@@ -0,0 +1,58 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListInvoices from "./Partials/myInvoiceDetails/ListInvoices.vue";
const props = defineProps({
currentStatus: Number,
})
console.log(props.currentStatus)
const initialCostUnitId = props.cost_unit_id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/invoice/my-invoices/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Freigegebene Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/invoice/my-invoices/approved",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Abgelehnte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/invoice/my-invoices/denied",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Meine Abrechnungen">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :subTabIndex=props.currentStatus />
</shadowed-box>
</AppLayout>
</template>

View File

@@ -0,0 +1,78 @@
<script setup>
const props = defineProps({
invoice: {
type: Object,
default: () => ({})
}
})
</script>
<template>
<table class="travel_allowance">
<tr><td colspan="2">
Abrechnung einer Reisekostenpauschale
</td></tr>
<tr>
<th>Reiseroute</th>
<td>{{props.invoice.travelRoute}}</td>
</tr>
<tr>
<th>Gesamte Wegstrecke</th>
<td>{{props.invoice.distance}} km</td>
</tr>
<tr>
<th>Kilometerpauschale</th>
<td>{{props.invoice.distanceAllowance}} Euro / km</td>
</tr>
<tr>
<th>Gesamtbetrag</th>
<td style="font-weight: bold">{{props.invoice.amount}}</td>
</tr>
<tr>
<th>Marterialtransport</th>
<td>{{props.invoice.transportation}}</td>
</tr>
<tr>
<th>Hat Personen mitgenommen</th>
<td>{{props.invoice.passengers}}</td>
</tr>
</table>
</template>
<style scoped>
.travel_allowance {
border-spacing: 0;
}
.travel_allowance tr th {
width: 300px !important;
border-left: 1px solid #ccc;
padding-left: 20px;
}
.travel_allowance tr td,
.travel_allowance tr th {
font-family: sans-serif;
line-height: 1.8em;
border-bottom: 1px solid #ccc;
padding: 10px;
}
.travel_allowance tr td:last-child {
border-right: 1px solid #ccc;
}
.travel_allowance tr:first-child td:first-child {
margin-bottom: 10px;
font-weight: bold;
background: linear-gradient(to bottom, #fff, #f6f7f7);
border-top: 1px solid #ccc;
border-left: 1px solid #ccc !important;
}
</style>

View File

@@ -0,0 +1,100 @@
<script setup>
const props = defineProps({
data: {
type: Object,
required: true
},
modeShow: {
type: Boolean,
required: true,
}
})
const emit = defineEmits(["accept", "deny", "fix", "reopen"])
</script>
<template>
<span id="invoice_details_header">
<table>
<tr>
<td>Name:</td>
<td v-if="modeShow">{{props.data.contactName}}</td>
<td v-else style="width: 300px;">{{props.data.contactName}}</td>
<td v-if="modeShow" style="width: 250px;">Kostenstelle</td>
<td v-else style="width: 300px;">Kostensatelle (ursprünglich)</td>
<td>{{props.data.costUnitName}}</td>
<td rowspan="4" style="vertical-align: top;">
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('reject')"
class="button mareike-button mareike-deny-button"
>
Abrechnung zurückziehen
</button>
<button v-if="props.data.status === 'denied' && modeShow"
@click="emit('delete')"
class="button mareike-button mareike-deny-button"
>
Abrechnung Endgültig löschen
</button>
</td>
</tr>
<!-- Rest der Tabelle bleibt unverändert -->
<tr>
<td>E-Mail:</td>
<td>{{props.data.contactEmail}}</td>
<td>
Abrechnungsnummer
<label v-if="!modeShow"> (ursprünglich)</label>:
</td>
<td>{{props.data.invoiceNumber}}</td>
</tr>
<tr>
<td>Telefon:</td>
<td>{{props.data.contactPhone}}</td>
<td>
Abrechnungstyp
<label v-if="!modeShow"> (Ursprünglich)</label>:
</td>
<td>{{props.data.invoiceType}}</td>
</tr>
<tr>
<td>Kontoinhaber*in:</td>
<td>{{props.data.accountOwner}}</td>
<td>Gesamtbetrag
<label v-if="!modeShow"> (Ursprünglich)</label>:
</td>
<td><strong>{{props.data.amount}}</strong></td>
</tr>
<tr>
<td>IBAN:</td>
<td>{{props.data.accountIban}}</td>
<td>Buchungsinformationen:</td>
<td v-if="props.data.donation">Als Spende gebucht</td>
<td v-else-if="props.data.alreadyPaid">Beleg ohne Auszahlung</td>
<td v-else>Klassische Auszahlung</td>
</tr>
<tr>
<td>Status:</td>
<td>{{props.data.readableStatus}}</td>
<td>Anmerkungen:</td>
<td>
<span v-if="props.data.status === 'denied'">
{{props.data.deniedReason}}
</span>
<span v-else>{{props.data.comment}}</span>
</td>
</tr>
</table>
</span>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,264 @@
<script setup>
import FullScreenModal from "../../../../../Views/Components/FullScreenModal.vue";
import {ref} from "vue";
import Modal from "../../../../../Views/Components/Modal.vue";
import { useAjax } from "../../../../../../resources/js/components/ajaxHandler.js";
import ShowInvoicePartial from "./ShowInvoice.vue";
import Header from "./Header.vue";
import {toast} from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
}, showInvoice: Boolean
})
const showInvoice = ref(props.showInvoice)
const emit = defineEmits(["close"])
const denyInvoiceDialog = ref(false)
const { data, loading, error, request } = useAjax()
const modeShow = ref(true)
const costUnits = ref(null)
const newInvoice = ref(null)
async function acceptInvoice() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/approved", {
method: "POST",
});
if (data.status === 'success') {
toast.success('Abrechnung wurde freigegeben.');
} else {
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
}
close();
}
function close() {
emit('reload')
emit('close')
}
async function updateInvoice(formData) {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/update", {
method: "POST",
body: {
invoiceData: formData
}
});
if (!data.do_copy) {
modeShow.value = true;
toast.success('Die Koreektur der Abrechnung wurde gespeichert.');
close();
} else {
modeShow.value = true;
newInvoice.value = data.invoice;
props.data.id = data.invoice.id;
reloadInvoiceFixDialog()
}
}
async function reloadInvoiceFixDialog() {
const data = await request("api/v1/invoice/details/" + props.data.id, {
method: "GET",
});
newInvoice.value = data.invoice;
props.data.id = data.invoice.id;
costUnits.value = data.costUnits;
props.data.id = data.invoice.id;
modeShow.value = false;
toast.success('Die Abrechnung wurde gespeichert und eine neue Abrechnung wurde erstellt.');
}
async function openInvoiceFixDialog() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/copy", {
method: "POST",
body: {
}
});
if (data.status === 'success') {
costUnits.value = data.costUnits;
newInvoice.value = data.invoice;
props.data.id = data.invoice.id;
modeShow.value = false;
}
}
function openDenyInvoiceDialog() {
denyInvoiceDialog.value = true;
}
async function rejectInvoice() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/denied", {
method: "POST",
body: {
reason: 'Von Antragssteller*in zurückgezogen'
}
});
if (data.status === 'success') {
toast.success('Abrechnung wurde zurückgezogen.');
} else {
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
}
denyInvoiceDialog.value = false;
close();
}
async function deleteInvoice() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/deleted", {
method: "POST",
});
if (data.status === 'success') {
toast.success('Die Abrechnung wurde gelöscht.');
} else {
toast.error('Beim Bearbeiten ist ein Fehler aufgetreten.');
}
close();
}
</script>
<template>
<FullScreenModal
:show="showInvoice"
title="Abrechnungsdetails"
@close="emit('close')"
>
<Header :data="props.data"
@reject="rejectInvoice"
@delete="deleteInvoice"
:modeShow="modeShow"
/>
<ShowInvoicePartial :data="props.data" />
</FullScreenModal>
<Modal title="Abrechnung ablehnen" :show="denyInvoiceDialog" @close="denyInvoiceDialog = false" >
Begründung:
<textarea class="mareike-textarea" style="width: 100%; height: 100px; margin-top: 10px;" id="deny_invoice_reason" />
<input type="button" class="mareike-button mareike-deny-invoice-button" value="Abrechnung ablehnen" @click="denyInvoice" />
</Modal>
</template>
<style>
.mareike-deny-invoice-button {
width: 150px !important;
margin-top: 10px;
}
#invoice_details_header{
font-weight: bold;
font-size: 12pt;
line-height: 1.8em;
width: 100%;
}
#invoice_details_header table {
border-style: solid;
border-width: 1px;
border-radius: 10px !important;
width: 98%;
border-color: #c0c0c0;
box-shadow: 5px 5px 10px #c0c0c0;
margin-bottom: 75px;
font-weight: normal;
}
#invoice_details_header table tr td:first-child {
padding-right: 50px;
width: 175px;
}
#invoice_details_header table tr td:nth-child(2) {
padding-right: 25px;
}
#invoice_details_header table tr td:nth-child(3) {
padding-right: 50px;
width: 100px;
vertical-align: top;
}
#invoice_details_body {
height: 400px;
overflow: auto;
}
#invoice_details_body table tr:nth-child(1) td,
#invoice_details_body table tr:nth-child(2) td,
#invoice_details_body table tr:nth-child(3) td,
#invoice_details_body table tr:nth-child(4) td,
#invoice_details_body table tr:nth-child(6) td{
vertical-align: top;
height: 20px;
}
#invoice_details_body table tr:nth-child(5) td {
height: 50px;
}
#invoice_details_body table {
width: 100%;
}
#invoice_details_body table tr:nth-child(1) td:first-child {
padding-right: 50px;
}
#invoice_details_body table tr:nth-child(1) td:nth-child(2),
#invoice_details_body table tr:nth-child(1) td:nth-child(3)
{
width: 250px;
}
.mareike-accept-button {
background-color: #36c054 !important;
color: #ffffff !important;
}
.mareike-deny-button {
background-color: #ee4b5c !important;
color: #ffffff !important;
}
.mareike-fix-button {
background-color: #d3d669 !important;
color: #67683c !important;
}
.mareike-button {
padding: 5px 25px !important;
font-size: 11pt !important;
margin-bottom: 10px;
width: 100%;
padding-right: 2px;
}
</style>

View File

@@ -0,0 +1,80 @@
<script setup>
import Icon from "../../../../../Views/Components/Icon.vue";
import InvoiceDetails from "./InvoiceDetails.vue";
import { useAjax } from "../../../../../../resources/js/components/ajaxHandler.js";
import {ref} from "vue";
const props = defineProps({
data: Object
})
const { request } = useAjax()
const invoice = ref(null)
const show_invoice = ref(false)
const localData = ref(props.data)
async function openInvoiceDetails(invoiceId) {
const url = '/api/v1/invoice/details/' + invoiceId
try {
const response = await fetch(url, { method: 'GET' })
const result = await response.json()
invoice.value = result.invoice
show_invoice.value = true
} catch (err) {
console.error('Error fetching invoices:', err)
}
}
async function reload() {
const url = "/api/v1/invoice/my-invoices/" + props.data.endpoint
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) throw new Error('Fehler beim Laden')
const result = await response.json()
localData.value = result
} catch (err) {
console.error('Error fetching invoices:', err)
}
}
</script>
<template>
<table v-if="localData.invoices.length > 0" class="invoice-list-table">
<tr>
<td colspan="6">{{props.data.title}}</td>
</tr>
<tr v-for="invoice in localData.invoices" :id="'invoice_' + invoice.id">
<td>{{invoice.invoiceNumber}}</td>
<td>{{invoice.invoiceType}}</td>
<td>
{{invoice.amount}}
</td>
<td style="width: 150px;">
<Icon v-if="invoice.donation" name="hand-holding-dollar" style="color: #ffffff; background-color: green" />
<Icon v-if="invoice.alreadyPaid" name="comments-dollar" style="color: #ffffff; background-color: green" />
</td>
<td>
{{invoice.contactName}}<br />
<label v-if="invoice.contactEmail !== '--'">{{invoice.contactEmail}}<br /></label>
<label v-if="invoice.contactPhone !== '--'">{{invoice.contactPhone}}<br /></label>
</td>
<td>
<input type="button" value="Abrechnung Anzeigen" @click="openInvoiceDetails(invoice.id)" />
</td>
</tr>
</table>
<p v-else>Es sind keine Abrechnungen in dieser Kategorie vorhanden.</p>
<InvoiceDetails :data="invoice" :show-invoice="show_invoice" v-if="show_invoice" @close="show_invoice = false; reload()" />
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,19 @@
<script setup>
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
import DistanceAllowance from "./DistanceAllowance.vue";
const props = defineProps({
data: {
type: Object,
required: true
}
})
</script>
<template>
<span id="invoice_details_body">
<PdfViewer :url="'/api/v1/invoice/showReceipt/' + props.data.id" v-if="props.data.documentFilename !== null" />
<DistanceAllowance v-else :invoice="props.data" />
</span>
</template>