Operation processes on invoices

This commit is contained in:
2026-02-13 00:11:51 +01:00
parent 882752472e
commit fd403f8520
44 changed files with 1635 additions and 42 deletions

View File

@@ -18,7 +18,7 @@ class ChangeCostUnitTreasurersCommand {
$this->request->costUnit->resetTreasurers();
foreach ($this->request->treasurers as $treasurer) {
$this->request->costUnit->tresurers()->attach($treasurer);
$this->request->costUnit->treasurers()->attach($treasurer);
}
$this->request->costUnit->save();

View File

@@ -9,8 +9,6 @@ use Illuminate\Http\Request;
class ListController extends CommonController {
public function __invoke() {
$inertiaProvider = new InertiaProvider('CostUnit/List', [
'cost_unit_id' => 1
]);

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class OpenController extends CommonController {
public function __invoke(int $costUnitId) {
$inertiaProvider = new InertiaProvider('CostUnit/Open', [
'costUnitId' => $costUnitId
]);
return $inertiaProvider->render();
}
public function listInvoices(int $costUnitId, string $invoiceStatus) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$invoices = $this->invoices->getByStatus($costUnit, $invoiceStatus);
return response()->json([
'status' => 'success',
'costUnit' => $costUnit,
'invoices' => $invoices,
'endpoint' => $invoiceStatus,
]);
}
}

View File

@@ -4,6 +4,7 @@ use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
use App\Domains\CostUnit\Controllers\EditController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\CostUnit\Controllers\OpenController;
use App\Domains\CostUnit\Controllers\TreasurersEditController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
@@ -21,6 +22,11 @@ Route::prefix('api/v1')
Route::prefix('/{costUnitId}') ->group(function () {
Route::get('/invoice-list/{invoiceStatus}', [OpenController::class, 'listInvoices']);
Route::post('/close', [ChangeStateController::class, 'close']);
Route::post('/open', [ChangeStateController::class, 'open']);
Route::post('/archive', [ChangeStateController::class, 'archive']);

View File

@@ -1,6 +1,7 @@
<?php
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\CostUnit\Controllers\OpenController;
use App\Domains\UserManagement\Controllers\EmailVerificationController;
use App\Domains\UserManagement\Controllers\LoginController;
use App\Domains\UserManagement\Controllers\LogOutController;
@@ -14,6 +15,9 @@ Route::middleware(IdentifyTenant::class)->group(function () {
Route::middleware(['auth'])->group(function () {
Route::get('/create', [CreateController::class, 'showForm']);
Route::get('/list', ListController::class);
Route::get('/{costUnitId}/', OpenController::class);
});

View File

@@ -6,10 +6,8 @@ import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListCostUnits from "./Partials/ListCostUnits.vue";
const props = defineProps({
message: String,

View File

@@ -0,0 +1,67 @@
<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/ListInvoices.vue";
const props = defineProps({
costUnitId: Number
})
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Nichtexportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/approved",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Exportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/exported",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Abgelehnte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/denied",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Abrechnungen">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />
</shadowed-box>
</AppLayout>
</template>

View File

@@ -46,7 +46,7 @@ const costUnit = ref(null)
const { data, loading, error, request, download } = useAjax()
if (props.deep_jump_id > 0) {
open_invoice_list(props.deep_jump_id, 'new', props.deep_jump_id_sub)
// open_invoice_list(props.deep_jump_id, 'new', props.deep_jump_id_sub)
}
async function costUnitDetails(costUnitId) {
@@ -79,17 +79,8 @@ async function editTreasurers(costUnitId) {
}
}
async function open_invoice_list(cost_unit_id, endpoint, invoice_id) {
const url = '' // `/wp-json/mareike/invoices/list-${endpoint}?invoice_id=${invoice_id}&cost_unit_id=${cost_unit_id}
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) throw new Error('Fehler beim Laden')
invoices.value = await response.json()
current_cost_unit.value = cost_unit_id
invoice_id = invoice_id
showInvoiceList.value = true
} catch (err) {
}
function loadInvoices(cost_unit_id) {
window.location.href = '/cost-unit/' + cost_unit_id;
}
async function denyNewRequests(costUnitId) {
@@ -182,7 +173,7 @@ async function export_payouts(cost_unit_id) {
<th>Unbearbeitet</th>
<td>{{ costUnit.countNewInvoices }}</td>
<td rowspan="4" style="vertical-align: top;">
<input v-if="!costUnit.archived" type="button" value="Abrechnungen bearbeiten" />
<input v-if="!costUnit.archived" type="button" value="Abrechnungen bearbeiten" @click="loadInvoices(costUnit.id)" />
<input v-else type="button" value="Abrechnungen einsehen" />
<br />
<input v-if="!costUnit.archived" type="button" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />

View File

@@ -0,0 +1,80 @@
<script setup>
import Icon from "../../../../Views/Components/Icon.vue";
import InvoiceDetails from "../../../Invoice/Views/Partials/invoiceDetails/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/cost-unit/" + props.data.costUnit.id + "/invoice-list/" + 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.costUnit.name}}</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

@@ -24,7 +24,7 @@ onMounted(async () => {
</tr>
<tr v-for="costUnit in costUnits.openCostUnits">
<td>{{costUnit.name}}</td>
<td><a :href="'/cost-unit/' + costUnit.id" class="link">{{costUnit.name}}</a></td>
<td>{{costUnit.new_invoices_count}}</td>
<td>{{costUnit.approved_invoices_count}}</td>
<td>{{costUnit.totalAmount}}</td>

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
use App\Enumerations\InvoiceStatus;
class ChangeStatusCommand {
private ChangeStatusRequest $request;
public function __construct(ChangeStatusRequest $request) {
$this->request = $request;
}
public function execute() : ChangeStatusResponse {
$response = new ChangeStatusResponse();
switch ($this->request->status) {
case InvoiceStatus::INVOICE_STATUS_APPROVED:
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_APPROVED;
$this->request->invoice->approved_by = auth()->user()->id;
$this->request->invoice->approved_at = now();
break;
case InvoiceStatus::INVOICE_STATUS_DENIED:
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DENIED;
$this->request->invoice->denied_by = auth()->user()->id;
$this->request->invoice->denied_at = now();
$this->request->invoice->denied_reason = $this->request->comment;
break;
case InvoiceStatus::INVOICE_STATUS_NEW:
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_NEW;
$this->request->invoice->approved_by = null;
$this->request->invoice->approved_at = null;
$this->request->invoice->denied_by = null;
$this->request->invoice->denied_at = null;
$this->request->invoice->comment = $this->request->invoice->denied_reason;
$this->request->invoice->denied_reason = null;
break;
}
if ($this->request->invoice->save()) {
$response->success = true;
}
return $response;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
use App\Models\Invoice;
class ChangeStatusRequest {
public Invoice $invoice;
public string $status;
public ?string $comment;
public function __construct(Invoice $invoice, string $status, ?string $comment = null) {
$this->invoice = $invoice;
$this->status = $status;
$this->comment = $comment;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
class ChangeStatusResponse {
public bool $success;
function __construct() {
$this->success = false;
}
}

View File

@@ -38,7 +38,7 @@ class CreateInvoiceCommand {
'travel_direction' => $this->request->travelRoute,
'passengers' => $this->request->passengers,
'transportation' => $this->request->transportations,
'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->path : null,
'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null,
]);
if ($invoice !== null) {

View File

@@ -59,6 +59,11 @@ class CreateInvoiceRequest {
$this->totalAmount = $totalAmount;
$this->isDonation = $isDonation;
$this->userId = $userId;
if ($accountIban === 'undefined') {
$this->accountIban = null;
$this->accountOwner = null;
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domains\Invoice\Actions\UpdateInvoice;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
use App\Enumerations\InvoiceStatus;
class UpdateInvoiceCommand {
private UpdateInvoiceRequest $request;
public function __construct(UpdateInvoiceRequest $request) {
$this->request = $request;
}
public function execute() : UpdateInvoiceResponse {
$response = new UpdateInvoiceResponse();
$this->request->invoice->amount = $this->request->amount->getAmount();
$this->request->invoice->cost_unit_id = $this->request->costUnit->id;
$this->request->invoice->type = $this->request->invoiceType;
$this->request->invoice->comment = $this->request->comment;
$this->request->invoice->save();
$request = new ChangeStatusRequest($this->request->invoice, InvoiceStatus::INVOICE_STATUS_APPROVED);
$changeStatusCommand = new ChangeStatusCommand($request);
$changeStatusCommand->execute();
return $response;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Invoice\Actions\UpdateInvoice;
use App\Enumerations\InvoiceType;
use App\Models\CostUnit;
use App\Models\Invoice;
use App\ValueObjects\Amount;
class UpdateInvoiceRequest {
public string $comment;
public string $invoiceType;
public CostUnit $costUnit;
public Invoice $invoice;
public Amount $amount;
public function __construct(Invoice $invoice, string $comment, string $invoiceType, CostUnit $costUnit, Amount $amount) {
$this->comment = $comment;
$this->invoiceType = $invoiceType;
$this->costUnit = $costUnit;
$this->invoice = $invoice;
$this->amount = $amount;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\Invoice\Actions\UpdateInvoice;
use App\Models\Invoice;
class UpdateInvoiceResponse {
public bool $success;
public ?Invoice $invoice;
public function __construct() {
$this->success = false;
$this->invoice = null;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Domains\Invoice\Controllers;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class ChangeStateController extends CommonController
{
public function __invoke(int $invoiceId, string $newState) : JsonResponse {
$invoice = $this->invoices->getAsTreasurer($invoiceId);
if ($invoice === null) {
return response()->json([]);
}
$comment = request()->get('reason') ?? null;
$changeStatusRequest = new ChangeStatusRequest($invoice, $newState, $comment);
$changeStatusCommand = new ChangeStatusCommand($changeStatusRequest);
if ($changeStatusCommand->execute()->success) {
return response()->json(['status' => 'success']);
}
return response()->json([]);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Domains\Invoice\Controllers;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceCommand;
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
use App\Domains\Invoice\Actions\UpdateInvoice\UpdateInvoiceCommand;
use App\Domains\Invoice\Actions\UpdateInvoice\UpdateInvoiceRequest;
use App\Enumerations\CostUnitType;
use App\Enumerations\InvoiceStatus;
use App\Resources\InvoiceResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use App\ValueObjects\InvoiceFile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EditController extends CommonController{
public function copyInvoice(int $invoiceId) : JsonResponse{
$invoice = $this->invoices->getAsTreasurer($invoiceId);
if ($invoice === null) {
return response()->json([]);
}
$receiptfile = null;
if ($invoice->document_filename !== null) {
$receiptfile = new InvoiceFile();
$receiptfile->filename = $invoice->document_filename;
$receiptfile->fullPath = $invoice->document_filename;
}
$createInvoiceRequest = new CreateInvoiceRequest(
$invoice->costUnit()->first(),
$invoice->contact_name,
$invoice->type,
$invoice->amount,
$receiptfile,
$invoice->donation,
$invoice->user_id,
$invoice->contact_email,
$invoice->contact_phone,
$invoice->contact_bank_owner,
$invoice->contact_bank_iban,
$invoice->type_other,
$invoice->travel_direction,
$invoice->distance,
$invoice->passengers,
$invoice->transportation
);
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
$newInvoice = $invoiceCreationCommand->execute()->invoice;
$invoiceDenyRequest = new ChangeStatusRequest($invoice,InvoiceStatus::INVOICE_STATUS_DENIED, 'Abrechnungskorrektur in Rechnungsnummer #' . $newInvoice->invoice_number . ' erstellt.');
$invoiceDenyCommand = new ChangeStatusCommand($invoiceDenyRequest);
$invoiceDenyCommand->execute();
$runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB);
$currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT);
return response()->json([
'invoice' => new InvoiceResource($invoice)->toArray(),
'status' => 'success',
'costUnits' => array_merge($runningJobs, $currentEvents),
]);
}
public function updateInvoice(int $invoiceId, Request $request) : JsonResponse {
$invoice = $this->invoices->getAsTreasurer($invoiceId);
if ($invoice === null) {
return response()->json([]);
}
$modifyData = $request->get('invoiceData');
$newAmount = Amount::fromString($modifyData['amount']);
$amountLeft = Amount::fromString($invoice->amount);
$amountLeft->subtractAmount($newAmount);
$newCostUnit = $this->costUnits->getById($modifyData['cost_unit'],true);
$updateInvoiceRequest = new UpdateInvoiceRequest(
$invoice,
$modifyData['reason_of_correction'] ?? 'Abrechnungskorrektur',
$modifyData['type_internal'],
$newCostUnit,
$newAmount
);
$updateInvoiceCommand = new UpdateInvoiceCommand($updateInvoiceRequest);
$updateInvoiceCommand->execute();
$newInvoice = null;
if (isset($modifyData['duplicate']) && $modifyData['duplicate'] === true) {
$receiptfile = null;
if ($invoice->document_filename !== null) {
$receiptfile = new InvoiceFile();
$receiptfile->filename = $invoice->document_filename;
$receiptfile->fullPath = $invoice->document_filename;
}
$createInvoiceRequest = new CreateInvoiceRequest(
$invoice->costUnit()->first(),
$invoice->contact_name,
$invoice->type,
$amountLeft->getAmount(),
$receiptfile,
$invoice->donation,
$invoice->user_id,
$invoice->contact_email,
$invoice->contact_phone,
$invoice->contact_bank_owner,
$invoice->contact_bank_iban,
$invoice->type_other,
$invoice->travel_direction,
$invoice->distance,
$invoice->passengers,
$invoice->transportation
);
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
$newInvoice = $invoiceCreationCommand->execute()->invoice;
}
$useInvoice = $newInvoice ?? $invoice;
$do_copy = $newInvoice !== null ? true : false;
return response()->json([
'invoice' => new InvoiceResource($useInvoice)->toArray(),
'do_copy' => $do_copy,
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Domains\Invoice\Controllers;
use App\Enumerations\CostUnitType;
use App\Resources\InvoiceResource;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ShowInvoiceController extends CommonController {
public function __invoke(int $invoiceId) : JsonResponse{
$invoice = $this->invoices->getAsTreasurer($invoiceId);
$runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB);
$currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT);
return response()->json([
'invoice' => new InvoiceResource($invoice)->toArray(),
'costUnits' => array_merge($runningJobs, $currentEvents),
]);
}
public function showReceipt(int $invoiceId): BinaryFileResponse
{
$invoice = $this->invoices->getAsTreasurer($invoiceId);
if (null === $invoice) {
abort(404, 'Datei nicht gefunden');
}
if (null === $invoice->document_filename) {
abort(404, 'Datei nicht gefunden');
}
$path = $invoice->document_filename;
// Pfad zur Datei
$fullPath = 'private/' . $path;
if (!Storage::exists($path)) {
abort(404, 'Datei nicht gefunden');
}
return response()->file(storage_path('app/' . $fullPath), [
'Content-Type' => 'application/pdf'
]);
}
}

View File

@@ -2,20 +2,27 @@
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\Invoice\Controllers\ChangeStateController;
use App\Domains\Invoice\Controllers\EditController;
use App\Domains\Invoice\Controllers\NewInvoiceController;
use App\Domains\Invoice\Controllers\ShowInvoiceController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('api/v1/invoice')->group(function () {
Route::post('/new/{costUnitId}/{invoiceType}', [NewInvoiceController::class, 'saveInvoice']);
Route::middleware(['auth'])->group(function () {
Route::get('/details/{invoiceId}', ShowInvoiceController::class);
Route::get('/showReceipt/{invoiceId}', [ShowInvoiceController::class, 'showReceipt']);
Route::post('/details/{invoiceId}/change-state/{newState}', ChangeStateController::class);
Route::post('/details/{invoiceId}/copy', [EditController::class, 'copyInvoice']);
Route::post('/details/{invoiceId}/update', [EditController::class, 'updateInvoice']);
Route::get('/create', [CreateController::class, 'showForm']);
});

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,116 @@
<script setup>
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
import DistanceAllowance from "./DistanceAllowance.vue";
import {onMounted, reactive} from 'vue';
const props = defineProps({
newInvoice: {
type: Object,
required: true
},
costUnits: {
type: Object,
required: true
}
})
const emit = defineEmits(['submit', 'cancel'])
const formData = reactive({
type_internal: props.newInvoice.internalType || '',
cost_unit: props.newInvoice.costUnitId || '',
amount: props.newInvoice.amountPlain || '',
reason_of_correction: '',
})
const submitForm = () => {
emit('submit', formData)
}
const invoiceTypeCollection = reactive({
invoiceTypes: {}
});
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-invoice-types-all');
const data = await response.json();
Object.assign(invoiceTypeCollection, data);
});
</script>
<template>
<form @submit.prevent="submitForm">
<table style="width: 100%; font-family: sans-serif;">
<tr>
<td style="width: 150px;">Rechnungstyp:</td>
<td style="width: 450px;">
<select v-model="formData.type_internal" class="width-half-full">
<option v-for="invoiceType in invoiceTypeCollection.invoiceTypes" :value="invoiceType.slug">{{invoiceType.name}}</option>
</select>
</td>
</tr>
<tr>
<td>Kostenstelle:</td>
<td>
<select v-model="formData.cost_unit" class="width-half-full">
<option v-for="costUnit in props.costUnits" :value="costUnit.id">{{costUnit.name}}</option>
</select>
</td>
</tr>
<tr>
<td>Gesamtbetrag:</td>
<td>
<input type="text" v-model="formData.amount" class="width-small" /> Euro
</td>
</tr>
<tr>
<td>Grund der Korrektur:</td>
<td>
<input type="text" v-model="formData.reason_of_correction" class="width-half-full" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" v-model="formData.duplicate" id="mareike_correct_invoice_duplicate" />
<label for="mareike_correct_invoice_duplicate">Kopie zur Weiterbearbeitung erstellen</label>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Speichern und freigeben" class="button mareike-accept-button" />
</td>
</tr>
</table>
</form>
<br /><br />
<PdfViewer :url="'/api/v1/invoice/showReceipt/' + props.newInvoice.id" v-if="props.newInvoice.documentFilename !== null" />
<DistanceAllowance v-else :invoice="props.newInvoice" />
</template>
<style>
.width-full {
width: 100%;
}
.width-almost-full {
width: calc(100% - 75px);
}
.width-half-full {
width: 50%;
}
.width-small {
width: 100px;
}
</style>

View File

@@ -0,0 +1,117 @@
<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">
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('accept')"
class="button mareike-button mareike-accept-button"
>
Abrechnung annehmen
</button>
<button v-if="props.data.status === 'denied' && modeShow"
@click="emit('reopen')"
class="button mareike-button mareike-accept-button"
>
Abrechnung zur Wiedervorlage öffnen
</button>
<br />
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('fix')"
class="button mareike-button mareike-fix-button"
>
Abrechnung ablehnen und korrigieren
</button><br />
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('deny')"
class="button mareike-button mareike-deny-button"
>
Abrechnung ablehnen
</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,285 @@
<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 EditInvoicePartial from "./EditInvoice.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 denyInvoice() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/denied", {
method: "POST",
body: {
reason: document.getElementById('deny_invoice_reason').value
}
});
if (data.status === 'success') {
toast.success('Abrechnung wurde abgelehnt.');
} else {
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
}
denyInvoiceDialog.value = false;
close();
}
async function reopenInvoice() {
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/new", {
method: "POST",
});
if (data.status === 'success') {
toast.success('Die Abrechnung wurde zur erneuten Bearbeitung vorgelegt');
} else {
toast.error('Beim Bearbeiten ist ein Fehler aufgetreten.');
}
close();
}
</script>
<template>
<FullScreenModal
:show="showInvoice"
title="Abrechnungsdetails"
@close="emit('close')"
>
<Header :data="props.data"
@accept="acceptInvoice"
@deny="openDenyInvoiceDialog"
@fix="openInvoiceFixDialog"
@reopen="reopenInvoice"
:modeShow="modeShow"
/>
<ShowInvoicePartial
v-if="modeShow"
:data="props.data"
@accept="acceptInvoice"
@deny="openDenyInvoiceDialog"
@fix="openInvoiceFixDialog"
/>
<EditInvoicePartial
v-else
:newInvoice="newInvoice"
:costUnits="costUnits"
@accept="acceptInvoice"
@deny="openDenyInvoiceDialog"
@fix="openInvoiceFixDialog"
@update="updateInvoice"
@submit="updateInvoice"
/>
</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,21 @@
<script setup>
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
import DistanceAllowance from "./DistanceAllowance.vue";
const props = defineProps({
data: {
type: Object,
required: true
}
})
console.log(props.data)
const emit = defineEmits(["accept", "deny", "fix"])
</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>

View File

@@ -10,7 +10,10 @@ class InvoiceStatus extends CommonModel
public const INVOICE_STATUS_APPROVED = 'approved';
public const INVOICE_STATUS_DENIED = 'denied';
public const INVOICE_STATUS_EXPORTED = 'exported';
public const INVOICE_STAUTS_DELETED = 'deleted';
public const INVOICE_STATUS_DELETED = 'deleted';
public const INVOICE_META_STATUS_NO_PAYOUT = 'no_payout';
public const INVOICE_META_STATUS_DONATED = 'donated';
protected $table = 'invoice_status';

View File

@@ -29,13 +29,13 @@ class CostUnit extends InstancedModel
'archived',
];
public function tresurers() : BelongsToMany{
public function treasurers() : BelongsToMany{
return $this->belongsToMany(User::class, 'cost_unit_treasurers', 'cost_unit_id', 'user_id')
->withTimestamps();
}
public function resetTreasurers() {
$this->tresurers()->detach();
$this->treasurers()->detach();
$this->save();
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType;
use App\Scopes\InstancedModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -75,4 +76,8 @@ class Invoice extends InstancedModel
public function status() : BelongsTo {
return $this->belongsTo(InvoiceStatus::class, 'status')->first();
}
public function invoiceType() : InvoiceType {
return $this->belongsTo(InvoiceType::class, 'type', 'slug')->first();
}
}

View File

@@ -24,6 +24,28 @@ class GlobalDataProvider {
]);
}
public function getAllInvoiceTypes() : JsonResponse {
$invoiceTypes = [];
foreach (InvoiceType::all() as $invoiceType) {
if (
$invoiceType->slug === InvoiceType::INVOICE_TYPE_OTHER
) {
continue;
}
$invoiceTypes[] = [
'slug' => $invoiceType->slug,
'name' => $invoiceType->name
];
}
$invoiceTypes[] = ['slug' => InvoiceType::INVOICE_TYPE_OTHER, 'name' => 'Sonstige Kosten'];
return response()->json([
'invoiceTypes' => $invoiceTypes
]);
}
public function getInvoiceTypes() : JsonResponse {
$invoiceTypes = [];
foreach (InvoiceType::all() as $invoiceType) {

View File

@@ -85,7 +85,7 @@ class CostUnitRepository {
$visibleCostUnits = [];
/** @var CostUnit $costUnit */
foreach (Costunit::where($criteria)->get() as $costUnit) {
if ($costUnit->tresurers()->where('user_id', $user->id)->exists() || $canSeeAll || $disableAccessCheck) {
if ($costUnit->treasurers()->where('user_id', $user->id)->exists() || $canSeeAll || $disableAccessCheck) {
if ($forDisplay) {
$visibleCostUnits[] = new CostUnitResource($costUnit)->toArray(request());
} else {
@@ -134,7 +134,7 @@ class CostUnitRepository {
return $returnData;
}
public function sumupAmounts(CostUnit $costUnit) : Amount {
public function sumupAmounts(CostUnit $costUnit, bool $donatedAmount = false) : Amount {
$amount = new Amount(0, '');
foreach ($costUnit->invoices()->get() as $invoice) {
@@ -142,9 +142,57 @@ class CostUnitRepository {
continue;
}
if (
($donatedAmount && !$invoice->donation) ||
(!$donatedAmount && $invoice->donation)
) {
continue;
}
$amount->addAmount(Amount::fromString($invoice->amount));
}
return $amount;
}
public function countInvoices(CostUnit $costUnit) : array {
$returnData = [
InvoiceStatus::INVOICE_STATUS_NEW => 0,
InvoiceStatus::INVOICE_STATUS_APPROVED => 0,
InvoiceStatus::INVOICE_STATUS_DENIED => 0,
InvoiceStatus::INVOICE_META_STATUS_NO_PAYOUT => 0,
InvoiceStatus::INVOICE_META_STATUS_DONATED => 0
];
foreach ($costUnit->invoices()->get() as $invoice) {
if ($invoice->status === InvoiceStatus::INVOICE_STATUS_DELETED) {
continue;
}
if( $invoice->status === InvoiceStatus::INVOICE_STATUS_DENIED) {
$returnData[InvoiceStatus::INVOICE_STATUS_DENIED]++;
continue;
}
switch ($invoice->status) {
case InvoiceStatus::INVOICE_STATUS_NEW:
$returnData[InvoiceStatus::INVOICE_STATUS_NEW]++;
break;
case InvoiceStatus::INVOICE_STATUS_APPROVED:
$returnData[InvoiceStatus::INVOICE_STATUS_APPROVED]++;
break;
case InvoiceStatus::INVOICE_STATUS_EXPORTED:
if ($invoice->donation) {
$returnData[InvoiceStatus::INVOICE_META_STATUS_DONATED]++;
} else {
if ($invoice->contact_bank_iban === null) {
$returnData[InvoiceStatus::INVOICE_META_STATUS_NO_PAYOUT]++;
}
}
break;
}
}
return $returnData;
}
}

View File

@@ -3,8 +3,12 @@
namespace App\Repositories;
use App\Enumerations\InvoiceStatus;
use App\Enumerations\UserRole;
use App\Models\CostUnit;
use App\Models\Invoice;
use App\Resources\InvoiceResource;
use App\ValueObjects\Amount;
use Illuminate\Database\Eloquent\Collection;
class InvoiceRepository {
public function getMyInvoicesWidget() : array {
@@ -34,4 +38,40 @@ class InvoiceRepository {
return $invoices;
}
public function getByStatus(CostUnit $costUnit, string $status) : array {
$returnData = [];
foreach ($costUnit->invoices()->where('status', $status)->get() as $invoice) {
$returnData[] = new InvoiceResource($invoice)->toArray();
};
return $returnData;
}
public function getAsTreasurer(int $invoiceId) : ?Invoice {
$invoice = Invoice::where('id', $invoiceId)->first();
if ($invoice === null) {
return null;
}
$isTreasurer = $invoice->costUnit()->first()->treasurers()->where('user_id', auth()->user()->id)->exists();
if ($isTreasurer) {
return $invoice;
}
$user = auth()->user();
if ($user->user_role_main === UserRole::USER_ROLE_ADMIN) {
return $invoice;
}
if (app('tenant')->slug === 'lv' && $user->user_role_main === UserRole::USER_ROLE_GROUP_LEADER) {
return $invoice;
}
if (app('tenant')->slug !== 'lv' && $user->local_group === app('tenant')->slug && $user->user_role_local_group === UserRole::USER_ROLE_GROUP_LEADER) {
return $invoice;
}
return null;
}
}

View File

@@ -2,7 +2,9 @@
namespace App\Resources;
use App\Enumerations\InvoiceStatus;
use App\Models\CostUnit;
use App\Repositories\CostUnitRepository;
use App\ValueObjects\Amount;
class CostUnitResource {
@@ -13,13 +15,18 @@ class CostUnitResource {
}
public function toArray($request) {
$totalAmount = 0;
$donatedAmount = 0;
$costUnitRepository = new CostUnitRepository();
$countNewInvoices = 0;
$countApprovedInvoices = 0;
$countDonatedInvoices = 0;
$countDeniedInvoices = 0;
$totalAmount = $costUnitRepository->sumupAmounts($this->costUnit)->getAmount();
$donatedAmount = $costUnitRepository->sumupAmounts($this->costUnit, true)->getAmount();
$countInvoices = $costUnitRepository->countInvoices($this->costUnit);
$countNewInvoices = $countInvoices[InvoiceStatus::INVOICE_STATUS_NEW];
$countApprovedInvoices = $countInvoices[InvoiceStatus::INVOICE_STATUS_APPROVED];
$countDonatedInvoices = $countInvoices[InvoiceStatus::INVOICE_META_STATUS_DONATED];
$countDeniedInvoices = $countInvoices[InvoiceStatus::INVOICE_STATUS_DENIED];
$data = array_merge(
@@ -33,7 +40,7 @@ class CostUnitResource {
'countApprovedInvoices' => $countApprovedInvoices,
'countDonatedInvoices' => $countDonatedInvoices,
'countDeniedInvoices' => $countDeniedInvoices,
'treasurers' => $this->costUnit->tresurers()->get()->map(fn($user) => new UserResource($user))->toArray(),
'treasurers' => $this->costUnit->treasurers()->get()->map(fn($user) => new UserResource($user))->toArray(),
]);

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Resources;
use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType;
use App\Models\Invoice;
use App\ValueObjects\Amount;
class InvoiceResource {
private Invoice $invoice;
function __construct(Invoice $invoice) {
$this->invoice = $invoice;
}
public function toArray() {
$returnData = [];
$returnData['invoiceType'] = $this->invoice->invoiceType()->name;
if ($this->invoice->invoiceType()->slug === InvoiceType::INVOICE_TYPE_OTHER) {
$returnData['invoiceType'] .= ' (' . $this->invoice->type_other . ')';
}
$returnData['costUnitName'] = $this->invoice->costUnit()->first()->name;
$returnData['invoiceNumber'] = $this->invoice->invoice_number;
$returnData['contactName'] = $this->invoice->contact_name;
$returnData['contactEmail'] = $this->invoice->contact_email ?? '--';
$returnData['contactPhone'] = $this->invoice->contact_phone ?? '--';
$returnData['amount'] = Amount::fromString($this->invoice->amount, ' Euro')->toString();
$returnData['id'] = $this->invoice->id;
$returnData['donation'] = $this->invoice->donation;
$returnData['alreadyPaid'] = !$this->invoice->donation && null === $this->invoice->contact_bank_iban;
$returnData['accountOwner'] = $this->invoice->contact_bank_owner ?? '--';
$returnData['accountIban'] = $this->invoice->contact_bank_iban ?? '--';
$returnData['status'] = $this->invoice->status;
$returnData['documentFilename'] = $this->invoice->document_filename;
$returnData['readableStatus'] = $this->getReadableStatus();
$returnData['comment'] = $this->invoice->comment ?? '--';
$returnData['changes'] = $this->invoice->changes ?? '--';
$returnData['deniedReason'] = $this->invoice->denied_reason ?? '--';
$returnData['travelDirection'] = $this->invoice->travel_direction ?? '--';
$returnData['distance'] = $this->invoice->distance;
$returnData['distanceAllowance'] = new Amount($this->invoice->costUnit()->first()->distance_allowance, '')->toString();
$returnData['passengers'] = $this->invoice->passengers ? 'Ja' : 'Nein';
$returnData['transportation'] = $this->invoice->transportation ? 'Ja' : 'Nein';
$returnData['travelRoute'] = $this->invoice->travel_direction;
$returnData['costUnitId'] = $this->invoice->cost_unit_id;
$returnData['amountPlain'] = new Amount($this->invoice->amount, '')->toString();
$returnData['internalType'] = $this->invoice->type;
return $returnData;
}
private function getReadableStatus() : string {
switch ($this->invoice->status) {
case InvoiceStatus::INVOICE_STATUS_NEW:
return 'Unbearbeitet';
case InvoiceStatus::INVOICE_STATUS_APPROVED:
return 'Nicht exportiert';
case InvoiceStatus::INVOICE_STATUS_DENIED:
return 'Abgelehnt';
case InvoiceStatus::INVOICE_STATUS_EXPORTED:
return 'Exportiert';
}
return $this->invoice->status;
}
}

View File

@@ -40,4 +40,8 @@ class Amount {
public function addAmount(Amount $amount) : void {
$this->amount += $amount->getAmount();
}
public function subtractAmount(Amount $amount) : void {
$this->amount -= $amount->getAmount();
}
}

View File

@@ -0,0 +1,146 @@
<template>
<teleport to="body">
<transition name="fade">
<div
v-if="show"
class="full-screen-modal-overlay"
@click.self="close"
>
<transition name="scale">
<div
v-if="show"
ref="modalRef"
class="full-screen-modal-content"
tabindex="-1"
>
<div class="full-screen-modal-body">
<slot />
<span @click="close" class="dashicons dashicons-dismiss full-screen-modal-close"></span>
</div>
</div>
</transition>
</div>
</transition>
</teleport>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
const props = defineProps({
show: Boolean
})
const emit = defineEmits(['close'])
const modalRef = ref(null)
function close() {
emit('close')
}
// ESC-Key & Focus-Trap
function handleKeyDown(e) {
if (e.key === 'Escape') {
close()
}
if (e.key === 'Tab' && modalRef.value) {
const focusable = modalRef.value.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
// Body-Scroll sperren
watch(
() => props.show,
async (newVal) => {
if (newVal) {
document.body.style.overflow = 'hidden'
await nextTick()
modalRef.value?.focus()
} else {
document.body.style.overflow = ''
}
}
)
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
document.body.style.overflow = ''
})
</script>
<style scoped>
.full-screen-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.full-screen-modal-content {
background: white;
border-radius: 12px;
position: absolute;
top: 30px;
bottom: 30px;
left: 30px;
right: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
outline: none;
display: flex;
flex-direction: column;
}
.full-screen-modal-body {
flex: 1;
overflow-y: auto;
padding: 20px;
position: relative;
}
.full-screen-modal-close {
position: absolute;
top: 15px;
right: 15px;
cursor: pointer;
font-size: 22px;
}
/* Animation */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.scale-enter-active,
.scale-leave-active {
transition: transform 0.25s ease, opacity 0.25s ease;
}
.scale-enter-from,
.scale-leave-to {
transform: scale(0.98);
opacity: 0;
}
</style>

View File

@@ -7,8 +7,17 @@ const props = defineProps({
name: { type: String, required: true },
})
if (SolidIcons[`fa${props.name.charAt(0).toUpperCase()}${props.name.slice(1)}`]) {
library.add(SolidIcons[`fa${props.name.charAt(0).toUpperCase()}${props.name.slice(1)}`])
function toPascalCase(str) {
return str
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
}
const iconName = `fa${toPascalCase(props.name)}`
if (SolidIcons[iconName]) {
library.add(SolidIcons[iconName])
}
</script>

View File

@@ -0,0 +1,46 @@
<script setup>
import { ref, onMounted } from "vue"
import { VuePDF, usePDF } from '@tato30/vue-pdf'
const props = defineProps({
url: String
})
const page = ref(1)
const { pdf, pages } = usePDF(props.url)
</script>
<template>
<div>
<div class="mareike-invoice-pdfview-controls">
<button @click="page = page > 1 ? page - 1 : page" class="button mareike-button page-switch-button">
Vorherige Seite
</button>
<span>{{ page }} / {{ pages }}</span>
<button @click="page = page < pages ? page + 1 : page" class="button mareike-button page-switch-button">
Nächste Seite
</button>
<VuePDF :pdf="pdf" :page="page" class="mareike_pdv_view" fitParent="true" cale="0.9" height="90"/>
</div>
</div>
</template>
<style scoped>
.page-switch-button {
padding: 8px 20px !important;
width: 200px !important;
margin: 0 10px !important;
}
.mareike-invoice-pdfview-controls {
position: relative;
top: -50px;
text-align: center;
}
</style>

View File

@@ -25,6 +25,7 @@
"@inertiajs/progress": "^0.2.7",
"@inertiajs/vue3": "^2.3.12",
"vue": "^3.5.27",
"vue3-toastify": "^0.2.8"
"vue3-toastify": "^0.2.8",
"@tato30/vue-pdf": "^2.0.0"
}
}

View File

@@ -153,6 +153,7 @@ th:after {
.widget-content-item td,
.widget-content-item th {
font-size: 10pt;
padding: 4px 0;
}
.widget-content-item label {

View File

@@ -66,5 +66,6 @@ input[type="submit"]:hover {
.link {
cursor: pointer;
text-decoration: underline;
color: #47a0d8;
font-weight: bold;
color: #2a7caf;
}

View File

@@ -39,3 +39,27 @@ fieldset legend {
background-color: #ffffff;
border-radius: 10px;
}
.invoice-list-table {
width: 90%;
margin: 20px auto;
}
.invoice-list-table td {
border-bottom: 1px solid #c3c4c7;
padding: 10px;
}
.invoice-list-table tr td:first-child {
border-left: 1px solid #c3c4c7;
}
.invoice-list-table tr td:last-child {
border-right: 1px solid #c3c4c7;
}
.invoice-list-table tr:first-child td {
background: linear-gradient(to bottom, #fff, #f6f7f7);
}

View File

@@ -26,6 +26,7 @@ Route::middleware(IdentifyTenant::class)->group(function () {
Route::get('/retrieve-text-resource/{textResource}', [GlobalDataProvider::class, 'getTextResourceText']);
Route::get('/retrieve-messages', [GlobalDataProvider::class, 'getMessages']);
Route::get('/retrieve-invoice-types', [GlobalDataProvider::class, 'getInvoiceTypes']);
Route::get('/retrieve-invoice-types-all', [GlobalDataProvider::class, 'getAllInvoiceTypes']);
});