From 4878f750bd69659a021e46cc8895df699b0145d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 18 Apr 2026 23:42:39 +0200 Subject: [PATCH] Improved handling for new invoices --- .../CreateInvoice/CreateInvoiceCommand.php | 21 +++++++ .../CreateInvoice/CreateInvoiceRequest.php | 3 + .../CreateInvoiceReceiptCommand.php | 4 +- .../Controllers/NewInvoiceController.php | 49 +++++++-------- .../invoiceDetails/DistanceAllowance.vue | 5 ++ .../newInvoice/expense-accounting.vue | 1 + .../Views/Partials/newInvoice/refund-data.vue | 6 +- .../newInvoice/travel-expense-accounting.vue | 14 ++++- .../InvoiceMailsNewInvoiceMail.php | 60 +++++++++++++++++++ app/Models/Invoice.php | 1 + app/Models/Tenant.php | 1 + app/Repositories/CostUnitRepository.php | 21 ++++++- app/Resources/InvoiceResource.php | 13 +++- .../2026_01_30_140001_create_tenants.php | 1 + .../2026_01_30_140010_create_invoices.php | 1 + .../emails/invoices/new_invoice.blade.php | 37 ++++++++++++ 16 files changed, 206 insertions(+), 32 deletions(-) create mode 100644 app/Mail/InvoiceMails/InvoiceMailsNewInvoiceMail.php create mode 100644 resources/views/emails/invoices/new_invoice.blade.php diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php index fc55c6f..d9f83c2 100644 --- a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php @@ -3,7 +3,10 @@ namespace App\Domains\Invoice\Actions\CreateInvoice; use App\Enumerations\InvoiceStatus; +use App\Mail\InvoiceMails\InvoiceMailsNewInvoiceMail; +use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail; use App\Models\Invoice; +use Illuminate\Support\Facades\Mail; class CreateInvoiceCommand { private CreateInvoiceRequest $request; @@ -36,6 +39,7 @@ class CreateInvoiceCommand { 'amount' => $this->request->totalAmount, 'distance' => $this->request->distance, 'travel_direction' => $this->request->travelRoute, + 'travel_reason' => $this->request->travelReason, 'passengers' => $this->request->passengers, 'transportation' => $this->request->transportations, 'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null, @@ -46,6 +50,23 @@ class CreateInvoiceCommand { $response->invoice = $invoice; } + if ($this->request->costUnit->mail_on_new) { + $recipients = [app('tenant')->email_finance]; + + foreach ($this->request->costUnit->treasurers()->get() as $treasurer) { + if (!in_array($treasurer->email, $recipients)) { + $recipients[] = $treasurer->email; + } + } + + foreach ($recipients as $recipient) { + Mail::to($recipient)->send(new InvoiceMailsNewInvoiceMail( + invoice: $invoice, + costUnit: $this->request->costUnit, + )); + } + } + return $response; } diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php index cc88a39..e27eadf 100644 --- a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php @@ -22,6 +22,7 @@ class CreateInvoiceRequest { public float $totalAmount; public bool $isDonation; public ?int $userId; + public ?string $travelReason; public function __construct( @@ -41,6 +42,7 @@ class CreateInvoiceRequest { ?int $distance = null, ?int $passengers = null, ?int $transportations, + ?string $travelReason = null, ) { $this->costUnit = $costUnit; @@ -59,6 +61,7 @@ class CreateInvoiceRequest { $this->totalAmount = $totalAmount; $this->isDonation = $isDonation; $this->userId = $userId; + $this->travelReason = $travelReason; if ($accountIban === 'undefined') { $this->accountIban = null; diff --git a/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php index 642f5a9..d5342bb 100644 --- a/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php +++ b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php @@ -84,6 +84,7 @@ class CreateInvoiceReceiptCommand { $travelPartTemplate = <<Reiseweg:%1\$s + Grund der Reise:%6\$s Gesamtlänge der Strecke:%2\$s km x %3\$s / km Materialtransport:%4\$s Mitfahrende im PKW:%5\$s @@ -95,7 +96,8 @@ HTML; $invoiceReadable['distance'], $invoiceReadable['distanceAllowance'], $invoiceReadable['transportation'], - $invoiceReadable['passengers'] + $invoiceReadable['passengers'], + $invoiceReadable['travelReason'] , ); $invoiceTravelPart = 'Kosten für ÖPNV:' . $invoiceReadable['amount'] . '';; diff --git a/app/Domains/Invoice/Controllers/NewInvoiceController.php b/app/Domains/Invoice/Controllers/NewInvoiceController.php index d8613a1..25ad83d 100644 --- a/app/Domains/Invoice/Controllers/NewInvoiceController.php +++ b/app/Domains/Invoice/Controllers/NewInvoiceController.php @@ -64,31 +64,32 @@ class NewInvoiceController extends CommonController { case InvoiceType::INVOICE_TYPE_TRAVELLING: if ($uploadedFile !== null) { - $amount = Amount::fromString($request->get('amount'))->getAmount(); + $amount = Amount::fromString($request->input('amount'))->getAmount(); $distance = null; } else { - $distance = Amount::fromString($request->get('amount'))->getRoundedAmount(); + $distance = Amount::fromString($request->input('amount'))->getRoundedAmount(); $amount = $distance * $costUnit->distance_allowance; } $createInvoiceRequest = new CreateInvoiceRequest( $costUnit, - $request->get('name'), + $request->input('name'), InvoiceType::INVOICE_TYPE_TRAVELLING, $amount, $uploadedFile, - 'donation' === $request->get('decision') ? true : false, + 'donation' === $request->input('decision') ? true : false, $this->users->getCurrentUserDetails()['userId'], - $request->get('contactEmail'), - $request->get('telephone'), - $request->get('accountOwner'), - $request->get('accountIban'), + $request->input('email'), + $request->input('telephone'), + $request->input('accountOwner'), + $request->input('accountIban'), null, - $request->get('otherText'), + $request->input('otherText'), $distance, - $request->get('havePassengers'), - $request->get('materialTransportation'), + $request->input('havePassengers'), + $request->input('materialTransportation'), + $request->input('travelReason'), ); break; @@ -96,21 +97,22 @@ class NewInvoiceController extends CommonController { default: $createInvoiceRequest = new CreateInvoiceRequest( $costUnit, - $request->get('name'), + $request->input('name'), $invoiceType, - Amount::fromString($request->get('amount'))->getAmount(), + Amount::fromString($request->input('amount'))->getAmount(), $uploadedFile, - 'donation' === $request->get('decision') ? true : false, + 'donation' === $request->input('decision') ? true : false, $this->users->getCurrentUserDetails()['userId'], - $request->get('contactEmail'), - $request->get('telephone'), - $request->get('accountOwner'), - $request->get('accountIban'), - $request->get('otherText'), + $request->input('email'), + $request->input('telephone'), + $request->input('accountOwner'), + $request->input('accountIban'), + $request->input('otherText'), null, null, - $request->get('havePassengers'), - $request->get('materialTransportation'), + $request->input('havePassengers'), + $request->input('materialTransportation'), + null ); break; @@ -124,15 +126,10 @@ class NewInvoiceController extends CommonController { 'success' ); - return response()->json([ 'status' => 'success', 'message' => 'Alright' ]); } - - - - dd($request->all()); } } diff --git a/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue b/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue index 34fd61f..7ca8eff 100644 --- a/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue +++ b/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue @@ -17,6 +17,11 @@ const props = defineProps({ {{props.invoice.travelRoute}} + + Grund der Reise + {{props.invoice.travelReason}} + + Gesamte Wegstrecke {{props.invoice.distance}} km diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue b/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue index d58c907..724106a 100644 --- a/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue +++ b/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue @@ -109,6 +109,7 @@ function handleFileChange(event) { :userIban="data.userIban" :userAccountOwner="data.userAccountOwner" :receipt="receipt" + travelReason="" @close="finalStep = false" /> diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue b/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue index 5f42d40..c48da1c 100644 --- a/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue +++ b/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue @@ -25,9 +25,10 @@ const props = defineProps({ userIban: String, havePassengers: Number, materialTransportation: Boolean, + travelReason: String, }) -console.log(props.receipt) +console.log(props) const finalStep = ref(true) const userName = ref(props.userName) @@ -49,6 +50,8 @@ async function sendData() { errorMsg.value = '' success.value = false + console.log(props) + const formData = new FormData() formData.append('name', userName.value) @@ -61,6 +64,7 @@ async function sendData() { formData.append('accountIban', userIban.value) formData.append('havePassengers', props.havePassengers ? 1 : 0) formData.append('materialTransportation', props.materialTransportation ? 1 : 0) + formData.append('travelReason', props.travelReason) if (props.receipt) { formData.append('receipt', props.receipt) diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue b/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue index 440d515..af1f6f2 100644 --- a/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue +++ b/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue @@ -18,6 +18,7 @@ const data = defineProps({ const { request } = useAjax(); const distanceAllowance = ref(null); const travelDirection = ref(null); +const travelReason = ref(null); const have_receipt = ref('') const havePassengers = ref(false); const materialTransportation = ref(false); @@ -57,7 +58,16 @@ function handleFileChange(event) { />

-
+
+ Was war der Grund für deine Reise? + +


+ +
Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg @@ -92,6 +102,7 @@ function handleFileChange(event) { :userIban="data.userIban" :userAccountOwner="data.userAccountOwner" :receipt="receipt" + :travelReason="travelReason" @close="finalStep = false" />
@@ -142,6 +153,7 @@ function handleFileChange(event) { :userTelephone="data.userTelephone" :userIban="data.userIban" :userAccountOwner="data.userAccountOwner" + :travelReason="travelReason" @close="finalStep = false" /> diff --git a/app/Mail/InvoiceMails/InvoiceMailsNewInvoiceMail.php b/app/Mail/InvoiceMails/InvoiceMailsNewInvoiceMail.php new file mode 100644 index 0000000..8169cd5 --- /dev/null +++ b/app/Mail/InvoiceMails/InvoiceMailsNewInvoiceMail.php @@ -0,0 +1,60 @@ +costUnit->name, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'emails.invoices.new_invoice', + with: [ + 'costUnitName' => $this->costUnit->name, + 'invoiceAmount' => Amount::fromString($this->invoice->amount)->toString(), + 'invoiceType' => $this->invoice->invoiceType()->name, + ], + ); + } + + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index a048a8d..77d356a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -58,6 +58,7 @@ class Invoice extends InstancedModel 'comment', 'changes', 'travel_direction', + 'travel_reason', 'passengers', 'transportation', 'document_filename', diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index a541737..1efd913 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -33,6 +33,7 @@ class Tenant extends CommonModel 'slug', 'name', 'email', + 'email_finance', 'url', 'account_name', 'account_iban', diff --git a/app/Repositories/CostUnitRepository.php b/app/Repositories/CostUnitRepository.php index 8ca9416..5c60cf9 100644 --- a/app/Repositories/CostUnitRepository.php +++ b/app/Repositories/CostUnitRepository.php @@ -132,7 +132,7 @@ class CostUnitRepository { if (strlen($costUnit->name) > 15) { $costUnit->name = substr($costUnit->name, 8, 13) . '...'; } - $costUnit->totalAmount = $this->sumupAmounts($costUnit)->toString(); + $costUnit->totalAmount = $this->sumupUnhandledAmounts($costUnit)->toString(); $returnData[] = $costUnit; } @@ -174,8 +174,27 @@ class CostUnitRepository { $amount->addAmount(Amount::fromString($invoice->amount)); } return $amount; + } + public function sumupUnhandledAmounts(CostUnit $costUnit, bool $donatedAmount = false) : Amount { + $amount = new Amount(0, ''); + foreach ($costUnit->invoices()->get() as $invoice) { + if (!in_array($invoice->status, + [InvoiceStatus::INVOICE_STATUS_NEW, InvoiceStatus::INVOICE_STATUS_APPROVED])) { + continue; + } + + if ( + ($donatedAmount && !$invoice->donation) || + (!$donatedAmount && $invoice->donation) + ) { + continue; + } + + $amount->addAmount(Amount::fromString($invoice->amount)); + } + return $amount; } public function countInvoices(CostUnit $costUnit) : array { diff --git a/app/Resources/InvoiceResource.php b/app/Resources/InvoiceResource.php index a3fdcc4..f26fb74 100644 --- a/app/Resources/InvoiceResource.php +++ b/app/Resources/InvoiceResource.php @@ -5,6 +5,7 @@ namespace App\Resources; use App\Enumerations\InvoiceStatus; use App\Enumerations\InvoiceType; use App\Models\Invoice; +use App\Models\User; use App\ValueObjects\Amount; class InvoiceResource { @@ -20,6 +21,13 @@ class InvoiceResource { if ($this->invoice->invoiceType()->slug === InvoiceType::INVOICE_TYPE_OTHER) { $returnData['invoiceType'] .= ' (' . $this->invoice->type_other . ')'; } + $approvedBy = '--'; + $approvedAt = '--'; + + if ($this->invoice->approved_at !== null) { + $approvedBy = User::findOrFail($this->invoice->approved_by)->getOfficialName(); + $approvedAt = \DateTime::createFromFormat('Y-m-d H:i:s', $this->invoice->approved_at)->format('d.m.Y H:i'); + } $returnData['invoiceTypeShort'] = $this->invoice->invoiceType()->name; $returnData['costUnitName'] = $this->invoice->costUnit()->first()->name; @@ -40,6 +48,7 @@ class InvoiceResource { $returnData['changes'] = $this->invoice->changes ?? '--'; $returnData['deniedReason'] = $this->invoice->denied_reason ?? '--'; $returnData['travelDirection'] = $this->invoice->travel_direction ?? '--'; + $returnData['travelReason'] = $this->invoice->travel_reason ?? '--'; $returnData['distance'] = $this->invoice->distance; $returnData['distanceAllowance'] = new Amount($this->invoice->costUnit()->first()->distance_allowance, '')->toString(); $returnData['passengers'] = $this->invoice->passengers ? 'Ja' : 'Nein'; @@ -50,8 +59,8 @@ class InvoiceResource { $returnData['internalType'] = $this->invoice->type; $returnData['donationText'] = $this->invoice->donation ? 'Ja' : 'Nein'; $returnData['createdAt'] = \DateTime::createFromFormat('Y-m-d H:i:s', $this->invoice->created_at)->format('d.m.Y H:i'); - $returnData['approvedAt'] = '--'; - $returnData['approvedBy'] = '--'; + $returnData['approvedAt'] = $approvedAt; + $returnData['approvedBy'] = $approvedBy; if ($this->invoice->status === InvoiceStatus::INVOICE_STATUS_APPROVED) { $returnData['approvedAt'] = \DateTime::createFromFormat('Y-m-d H:i:s', $this->invoice->approved_at)->format('d.m.Y H:i'); diff --git a/database/migrations/2026_01_30_140001_create_tenants.php b/database/migrations/2026_01_30_140001_create_tenants.php index 3cdcf41..93697e9 100644 --- a/database/migrations/2026_01_30_140001_create_tenants.php +++ b/database/migrations/2026_01_30_140001_create_tenants.php @@ -11,6 +11,7 @@ return new class extends Migration { $table->string('slug')->unique(); $table->string('name'); $table->string('email'); + $table->string('email_finance'); $table->string('url'); $table->string('account_name'); $table->string('account_iban'); diff --git a/database/migrations/2026_01_30_140010_create_invoices.php b/database/migrations/2026_01_30_140010_create_invoices.php index dabe3a0..49e5189 100644 --- a/database/migrations/2026_01_30_140010_create_invoices.php +++ b/database/migrations/2026_01_30_140010_create_invoices.php @@ -40,6 +40,7 @@ return new class extends Migration { $table->string('comment')->nullable(); $table->longText('changes')->nullable(); $table->string('travel_direction')->nullable(); + $table->string('travel_reason')->nullable(); $table->boolean('passengers')->nullable(); $table->boolean('transportation')->nullable(); $table->string('document_filename')->nullable(); diff --git a/resources/views/emails/invoices/new_invoice.blade.php b/resources/views/emails/invoices/new_invoice.blade.php new file mode 100644 index 0000000..fc1c60f --- /dev/null +++ b/resources/views/emails/invoices/new_invoice.blade.php @@ -0,0 +1,37 @@ + + + +

Hallo,

+

+ für die Kostenstelle "{{$costUnitName}}" ist die Benachrichtigung für neue Abrechnungen aktiviert.
+ Soeben wurde eine neue Abrechnung erfasst: +

+ + + + + + + + + + + +
+ Betrag: + + {{$invoiceAmount}} +
+ Grund der Abrechnung: + + {{$invoiceType}} +
+ + +

+ Weitere Details zur Abrechnung wurden aus Datenschutzgründen der Mail nicht hinzugefügt.
+ Bitte öffne die Kostenstelle "{{ $costUnitName }}" im Abrechnungssystem mareike, um weitere Informationen zu erhalten und die Abrechnung bearbeiten zu können. +

+ +