Send invoices by email.

This commit is contained in:
2025-01-09 11:18:12 +01:00
parent 8da6da471d
commit 6235112f74
19 changed files with 468 additions and 23 deletions

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Mail\InvoiceMail;
use App\Models\Invoice;
use App\TenantMail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Mail\Mailer;
class MailController extends Controller
{
/**
* The Mailer used for sending Tenant Mails defined by options.
* @var Mailer
*/
protected Mailer $mailer;
/**
* Set the TenantMail::class as mailer for further usage
*/
public function __construct()
{
$this->mailer = TenantMail::get();
}
/**
* Send the invoice mail for the given request.
*/
public function sendInvoice(Request $request): JsonResponse
{
$invoice = Invoice::find($request->id);
$invoiceMail = new InvoiceMail($invoice);
$invoiceMail->subject($request->Subject);
$invoiceMail->body = $request->Body;
try {
$this->mailer->to($request->To)
->cc($request->Cc)
->Bcc($request->Bcc)
->send($invoiceMail);
} catch (\Exception $exception) {
return response()->json(['status' => 'error', 'message' => $exception->getMessage()], 500);
}
$invoice->update(['status' => 'sent']);
return response()->json($invoice);
}
}

View File

@@ -8,12 +8,38 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class EController extends Controller class EController extends Controller
{ {
/**
* Return an invoice as download for the specified resource.
*/
public function downloadInvoice(int $id): StreamedResponse public function downloadInvoice(int $id): StreamedResponse
{ {
$invoice = Invoice::find($id); $invoice = Invoice::find($id);
$taxes = self::buildTaxes($invoice->items);
return response()->streamDownload(function () use ($invoice, $taxes) {
echo view('xml.invoice', ['invoice' => $invoice, 'options' => Option::optionsAsObject(), 'taxes' => $taxes]);
}, 'test.xml', ['Content-Type' => 'application/xml']);
}
/**
* Return a rendered xml invoice as string to build an email attachment from it for the specified resource.
*/
public function attachInvoice(int $id): string
{
$invoice = Invoice::find($id);
$taxes = self::buildTaxes($invoice->items);
return view('xml.invoice', ['invoice' => $invoice, 'options' => Option::optionsAsObject(), 'taxes' => $taxes])->render();
}
/**
* Build taxes for the given invoice items grouped by tax.
*/
private static function buildTaxes($items): array
{
$taxes = []; $taxes = [];
foreach ($invoice->items as $item) { foreach ($items as $item) {
if (!isset($taxes[$item->tax])) { if (!isset($taxes[$item->tax])) {
$taxes[$item->tax] = ['tax' => 0, 'taxable' => 0]; $taxes[$item->tax] = ['tax' => 0, 'taxable' => 0];
} }
@@ -21,9 +47,6 @@ class EController extends Controller
$taxes[$item->tax]['taxable'] += $item->price * $item->amount; $taxes[$item->tax]['taxable'] += $item->price * $item->amount;
} }
return response()->streamDownload(function () use ($invoice, $taxes) { return $taxes;
echo view('xml.invoice', ['invoice' => $invoice, 'options' => Option::optionsAsObject(), 'taxes' => $taxes]);
}, 'test.xml', ['Content-Type' => 'application/xml']);
} }
} }

View File

@@ -3,14 +3,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Invoice; use App\Models\Invoice;
use Illuminate\Http\Request; use Illuminate\Contracts\View\View;
class InvoiceController extends Controller class InvoiceController extends Controller
{ {
/** /**
* Display a listing of the resource. * Display a listing of the resource.
*/ */
public function index() public function index(): View
{ {
return view('invoice.index'); return view('invoice.index');
} }
@@ -18,7 +18,7 @@ class InvoiceController extends Controller
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
*/ */
public function create() public function create(): View
{ {
return view('invoice.create'); return view('invoice.create');
} }
@@ -26,7 +26,7 @@ class InvoiceController extends Controller
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
public function show(Invoice $invoice) public function show(Invoice $invoice): View
{ {
return view('invoice.show', ['invoice' => $invoice]); return view('invoice.show', ['invoice' => $invoice]);
} }
@@ -38,4 +38,14 @@ class InvoiceController extends Controller
{ {
// //
} }
/**
* Show the form for sending the specified invoice.
*/
public function mail(int $id): View
{
$invoice = Invoice::find($id);
return view('invoice.mail', ['invoice' => $invoice]);
}
} }

View File

@@ -9,10 +9,29 @@ use Illuminate\Http\Response;
class PdfController extends Controller class PdfController extends Controller
{ {
/**
* Return an invoice as download for the specified resource.
*/
public function downloadInvoice(int $invoice_id): Response public function downloadInvoice(int $invoice_id): Response
{
return $this->buildInvoicePdf($invoice_id)->stream();
}
/**
* Return a rendered pdf invoice as string to build an email attachment from it for the specified resource.
*/
public function attachInvoice(int $invoice_id): string
{
return $this->buildInvoicePdf($invoice_id)->output();
}
/**
* Render the pdf view for the given invoice.
*/
protected function buildInvoicePdf(int $invoice_id): \Barryvdh\DomPDF\PDF
{ {
$invoice = Invoice::find($invoice_id); $invoice = Invoice::find($invoice_id);
return Pdf::loadView('pdfs.invoice', ['invoice' => $invoice->load(['address', 'delivery']), 'options' => Option::optionsAsObject()])->stream(); return Pdf::loadView('pdfs.invoice', ['invoice' => $invoice->load(['address', 'delivery']), 'options' => Option::optionsAsObject()]);
} }
} }

63
app/Mail/InvoiceMail.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
namespace App\Mail;
use App\Http\Controllers\EController;
use App\Http\Controllers\PdfController;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Queue\SerializesModels;
class InvoiceMail extends Mailable
{
use Queueable, SerializesModels;
public string $body {
set {
$this->body = $value;
}
}
public string $pdf;
public string $xml;
/**
* Create a new message instance.
*/
public function __construct(protected Invoice $invoice)
{
$pdf = new PdfController();
$this->pdf = $pdf->attachInvoice($invoice->id);
$xml = new EController();
$this->xml = $xml->attachInvoice($invoice->id);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
html: 'mail.invoice.sent',
with: ['html' => $this->body],
);
}
/**
* Get the attachments for the message.
*/
public function attachments(): array
{
return [
Attachment::fromData(fn() => $this->pdf, __('invoice.Invoice') . '_' . $this->invoice->number . '.pdf')
->withMime('application/pdf'),
Attachment::fromData(fn() => $this->xml, __('invoice.Invoice') . '_' . $this->invoice->number . '.xml')
->withMime('application/xml'),
];
}
}

View File

@@ -32,9 +32,20 @@ class Invoice extends Model
*/ */
protected $appends = [ protected $appends = [
'created', 'created',
'number' 'number',
'localized_state'
]; ];
/**
* Get the invoice state as translated string.
*
* @return string
*/
public function getLocalizedStateAttribute(): string
{
return __('invoice.state_' . $this->status);
}
/** /**
* Get the invoice number formatted * Get the invoice number formatted
*/ */

32
app/TenantMail.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App;
use App\Http\Option;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Facades\Mail;
class TenantMail
{
/**
* Build a tenant-mailer from options
*
* @return Mailer
*/
public static function get(): Mailer
{
$options = Option::optionsAsObject();
$mailer = Mail::build([
'transport' => $options->mail_transport,
'host' => $options->mail_host,
'port' => $options->mail_port,
'encryption' => (property_exists($options, 'mail_encryption')) ? $options->mail_encryption : null,
'username' => (property_exists($options, 'mail_encryption')) ? $options->mail_username : null,
'password' => (property_exists($options, 'mail_encryption')) ? $options->mail_password : null,
]);
$mailer->alwaysFrom($options->email, $options->representative);
return $mailer;
}
}

View File

@@ -33,5 +33,10 @@ return [
'Email Password Reset Link' => 'Email Passwort-Reset Link', 'Email Password Reset Link' => 'Email Passwort-Reset Link',
'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.' => 'Passwort vergessen? Kein Problem. Teilen Sie uns einfach Ihre E-Mail-Adresse mit und wir senden Ihnen per E-Mail einen Link zum Zurücksetzen Ihres Passworts, mit dem Sie ein neues Passwort auswählen können.', 'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.' => 'Passwort vergessen? Kein Problem. Teilen Sie uns einfach Ihre E-Mail-Adresse mit und wir senden Ihnen per E-Mail einen Link zum Zurücksetzen Ihres Passworts, mit dem Sie ein neues Passwort auswählen können.',
'Date' => 'Datum', 'Date' => 'Datum',
'Email-To' => 'An',
'Email-Bcc' => 'Bcc',
'Email-Cc' => 'Cc',
'Email-Subject' => 'Betreff',
'Email-Body' => 'Nachricht',
]; ];

View File

@@ -44,5 +44,12 @@ return [
'Color' => 'Farbe', 'Color' => 'Farbe',
'Company logo' => 'Firmenlogo', 'Company logo' => 'Firmenlogo',
'Activity' => 'Tätigkeitsfeld', 'Activity' => 'Tätigkeitsfeld',
'Mail delivery' => 'E-Mail Versand',
'Mail transport' => 'Versand Typ',
'Mail host' => 'Server Adresse',
'Mail port' => 'Port',
'Mail encryption' => 'Verschlüsselung',
'Mail username' => 'Benutzername',
'Mail password' => 'Passwort',
]; ];

View File

@@ -19,5 +19,6 @@ return [
'SaveAndContinue' => 'Speichern und Weiter', 'SaveAndContinue' => 'Speichern und Weiter',
'Saved' => 'Gespeichert', 'Saved' => 'Gespeichert',
'Cancel' => 'Abbrechen', 'Cancel' => 'Abbrechen',
'Send' => 'Senden',
]; ];

View File

@@ -43,6 +43,13 @@ return [
'Item total price short' => 'G-Preis', 'Item total price short' => 'G-Preis',
'Net long' => 'Netto zuzüglich MwSt.', 'Net long' => 'Netto zuzüglich MwSt.',
'Gross long' => 'Gesamtpreis inkl. gesetzlicher MwSt.', 'Gross long' => 'Gesamtpreis inkl. gesetzlicher MwSt.',
'Final sentence' => 'Bitte überweisen Sie den fälligen Rechnungsbetrag in Höhe von :sum bis spätestens :date auf das unten genannte Konto.' 'Final sentence' => 'Bitte überweisen Sie den fälligen Rechnungsbetrag in Höhe von :sum bis spätestens :date auf das unten genannte Konto.',
'state_created' => 'Erstellt',
'state_sent' => 'Gesendet',
'Mail' => 'E-Mail',
'Send email to your customer with attachments.' => 'E-Mail mit Anlagen an den Kunden versenden.',
'Invoice body' => 'Sehr geehrte Kundin, sehr geehrter Kunde\n\nim Anhang erhalten Sie die Rechnung :invoice_number.\n\nMit freundlichen Grüßen',
'Send Pdf' => 'Rechnung als Pdf versenden',
'Send Xml' => 'Rechnung als Xml versenden',
]; ];

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" {{ $attributes->merge(['class' => 'size-8 p-1']) }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -55,18 +55,22 @@
<div class="w-1/6" x-text="invoice.number"></div> <div class="w-1/6" x-text="invoice.number"></div>
<div class="w-1/6" x-text="invoice.address.name"></div> <div class="w-1/6" x-text="invoice.address.name"></div>
<div class="w-1/4" x-text="invoice.address.email"></div> <div class="w-1/4" x-text="invoice.address.email"></div>
<div class="w-1/12" x-text="invoice.status"></div> <div class="w-1/12" x-text="invoice.localized_state"></div>
<div class="w-1/6 text-right" x-text="invoice.sum"></div> <div class="w-1/6 text-right" x-text="invoice.sum + ' €'"></div>
<div class="w-1/6 text-right" x-text="invoice.created"></div> <div class="w-1/6 text-right" x-text="invoice.created"></div>
</summary> </summary>
</details> </details>
</template> </template>
<div class="grid grid-cols-2 border-t-2"> <div class="w-full border-t-2"></div>
<div>{{ __('invoice.Sum') }}</div>
<div x-text="sum"></div> <div class="w-1/2 grid grid-cols-2">
<div>{{ __('invoice.Net') }}</div>
<div class="text-right" x-text="(sum - tax).toFixed(2) + ' €'"></div>
<div>{{ __('invoice.Tax') }}</div> <div>{{ __('invoice.Tax') }}</div>
<div x-text="tax"></div> <div class="text-right" x-text="tax + ' €'"></div>
<div class="font-bold">{{ __('invoice.Sum') }}</div>
<div class="font-bold text-right" x-text="sum + ' €'"></div>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,113 @@
<x-app-layout>
<x-slot name="header">
<div class="flex flex-row w-full">
<h2 class="grow font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('invoice.Invoice') }} {{ $invoice->number }}
</h2>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<!-- Customer data -->
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w" x-data="mailForm">
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('invoice.Mail') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __("invoice.Send email to your customer with attachments.") }}
</p>
</header>
<form class="mt-6 space-y-6" @submit.prevent="">
<div class="flex flex-row items-center">
<x-input-label for="To" :value="__('common.Email-To')" class="w-1/4"/>
<x-text-input id="To" name="To" type="email" class="mt-1 block w-full"
:value="old('To')" required multiple autocomplete="To"
x-model="data.To"/>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Cc" :value="__('common.Email-Cc')" class="w-1/4"/>
<x-text-input id="Cc" name="Cc" type="email" class="mt-1 block w-full"
:value="old('Cc')" multiple autocomplete="Cc"
x-model="data.Cc"/>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Bcc" :value="__('common.Email-Bcc')" class="w-1/4"/>
<x-text-input id="Bcc" name="Bcc" type="email" class="mt-1 block w-full"
:value="old('Bcc')" multiple autocomplete="Bcc"
x-model="data.Bcc"/>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Subject" :value="__('common.Email-Subject')" class="w-1/4"/>
<x-text-input id="Subject" name="Subject" type="text" class="mt-1 block w-full"
:value="old('Subject')" required autocomplete="Subject"
x-model="data.Subject"/>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Body" :value="__('common.Email-Body')" class="w-1/4"/>
<textarea rows="10" id="Body" name="Body" class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm mt-1 block w-full"
required autocomplete="Body"
x-model="data.Body"></textarea>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Pdf" :value="__('invoice.Send Pdf')" class="w-1/4"/>
<x-text-input id="Pdf" name="Pdf" x-model="data.Pdf" type="checkbox"/>
<x-pdf-icon class="text-gray-800 cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/pdf-download', '_blank', 'popup=true')"/>
</div>
<div class="flex flex-row items-center">
<x-input-label for="Xml" :value="__('invoice.Send Xml')" class="w-1/4"/>
<x-text-input id="Xml" name="Xml" x-model="data.Xml" type="checkbox"/>
<x-e-icon class="cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/xml-download', '_blank', 'popup=true')"/>
</div>
<div class="flex items-center gap-4" x-show="!sent">
<x-primary-button @click="submit">{{ __('form.Send') }}</x-primary-button>
</div>
</form>
</section>
</div>
</div>
</div>
</div>
</x-app-layout>
<script>
function mailForm() {
return {
data: {
id: {{ $invoice->id }},
To: '{{ $invoice->customer->email }}',
Cc: '{{ $invoice->address->email }}',
Bcc: '',
Subject: '{{ __('invoice.Invoice') . ' ' . $invoice->number }}',
Body: '{{ __('invoice.Invoice body', ['invoice_number' => $invoice->number]) }}',
Pdf: true,
Xml: true,
},
sent: false,
submit() {
this.sent = true;
axios.post('/sendInvoice', this.data)
.then(function (response) {
console.log(response);
}).catch(function(error) {
console.log(error);
})
console.log(this.data);
}
}
}
</script>

View File

@@ -5,6 +5,7 @@
{{ __('invoice.Invoice') }} {{ $invoice->number }} {{ __('invoice.Invoice') }} {{ $invoice->number }}
</h2> </h2>
<p class="relative flex flex-row"> <p class="relative flex flex-row">
<x-mail-icon class="cursor-pointer mr-4" onclick="window.location.href = '{{ route('invoice.mail', $invoice->id) }}'"/>
<x-pdf-icon class="text-gray-800 cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/pdf-download', '_blank', 'popup=true')"/> <x-pdf-icon class="text-gray-800 cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/pdf-download', '_blank', 'popup=true')"/>
<x-e-icon class="cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/xml-download', '_blank', 'popup=true')"/> <x-e-icon class="cursor-pointer" onclick="window.open('/invoice/{{ $invoice->id }}/xml-download', '_blank', 'popup=true')"/>
</p> </p>

View File

@@ -0,0 +1,3 @@
<body>
{!! nl2br($html) !!}
</body>

View File

@@ -199,7 +199,7 @@
</div> </div>
</div> </div>
<!-- correspondence --> <!-- Correspondence -->
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w"> <div class="max-w">
<section> <section>
@@ -233,6 +233,95 @@
</div> </div>
</div> </div>
<!-- Mail derlivery -->
<!--
'transport' => $options->mail_transport,
'host' => $options->mail_host,
'port' => $options->mail_port,
'encryption' => $options->mail_encryption,
'username' => $options->mail_username,
'password' => $options->mail_password,
-->
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w">
<section>
<details>
<summary class="text-lg font-medium text-gray-900 dark:text-gray-100 cursor-pointer">
{{ __('configuration.Mail delivery') }}
</summary>
<form class="mt-6 space-y-2" @submit.prevent="">
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_transport"
:value="__('configuration.Mail transport')"/>
<x-text-input id="mail_transport" name="mail_transport" type="text"
class="mt-1 block w-full"
:value="old('mail_transport')" autofocus
placeholder="smtp"
autocomplete="mail_transport"
x-model="options.mail_transport"/>
</div>
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_host"
:value="__('configuration.Mail host')"/>
<x-text-input id="mail_host" name="mail_host" type="text"
class="mt-1 block w-full"
:value="old('mail_host')" autofocus
placeholder="mail.example.com"
autocomplete="mail_host"
x-model="options.mail_host"/>
</div>
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_port"
:value="__('configuration.Mail port')"/>
<x-text-input id="mail_port" name="mail_port" type="text"
class="mt-1 block w-full"
:value="old('mail_port')" autofocus
placeholder="587"
autocomplete="mail_port"
x-model="options.mail_port"/>
</div>
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_encryption"
:value="__('configuration.Mail encryption')"/>
<x-text-input id="mail_encryption" name="mail_encryption" type="text"
class="mt-1 block w-full"
:value="old('mail_encryption')" autofocus
placeholder="tls"
autocomplete="mail_encryption"
x-model="options.mail_encryption"/>
</div>
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_username"
:value="__('configuration.Mail username')"/>
<x-text-input id="mail_username" name="mail_username" type="text"
class="mt-1 block w-full"
:value="old('mail_username')" autofocus
placeholder="Username"
autocomplete="mail_username"
x-model="options.mail_username"/>
</div>
<div class="flex flex-row items-center">
<x-input-label class="w-1/4" for="mail_password"
:value="__('configuration.Mail password')"/>
<x-text-input id="mail_password" name="mail_password" type="password"
class="mt-1 block w-full"
:value="old('mail_password')" autofocus
autocomplete="mail_password"
x-model="options.mail_password"/>
</div>
</form>
</details>
</section>
</div>
</div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<x-primary-button @click="submit">{{ __('form.Save') }}</x-primary-button> <x-primary-button @click="submit">{{ __('form.Save') }}</x-primary-button>
</div> </div>

View File

@@ -29,6 +29,7 @@ Route::group(['as' => 'api.'], function () {
Route::apiResource('/invoice.item', InvoiceItemController::class)->shallow(); Route::apiResource('/invoice.item', InvoiceItemController::class)->shallow();
Route::get('/option', [OptionController::class, 'index'])->name('option.index'); Route::get('/option', [OptionController::class, 'index'])->name('option.index');
Route::post('/option', [OptionController::class, 'store'])->name('option.store'); Route::post('/option', [OptionController::class, 'store'])->name('option.store');
Route::post('/sendInvoice', [\App\Http\Controllers\Api\MailController::class, 'sendInvoice'])->name('sendInvoice');
}); });
}); });

View File

@@ -28,6 +28,7 @@ Route::middleware('auth')->group(function () {
Route::get('/option', [OptionController::class, 'index'])->name('option.index'); Route::get('/option', [OptionController::class, 'index'])->name('option.index');
Route::get('/invoice/{id}/pdf-download', [PdfController::class, 'downloadInvoice'])->name('invoice.pdfDownload'); Route::get('/invoice/{id}/pdf-download', [PdfController::class, 'downloadInvoice'])->name('invoice.pdfDownload');
Route::get('/invoice/{id}/xml-download', [EController::class, 'downloadInvoice'])->name('invoice.eDownload'); Route::get('/invoice/{id}/xml-download', [EController::class, 'downloadInvoice'])->name('invoice.eDownload');
Route::get('/invoice/{id}/mail', [InvoiceController::class, 'mail'])->name('invoice.mail');
}); });
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';