Add funcitons to email invoices.

This commit is contained in:
Matt Young 2026-01-30 21:06:42 -06:00
parent 3c96f639fa
commit ad4cccdcd3
4 changed files with 188 additions and 2 deletions

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

@ -0,0 +1,37 @@
<?php
namespace App\Mail;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class InvoiceMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Invoice $invoice
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Invoice {$this->invoice->invoice_number} - Payment Requested",
);
}
public function content(): Content
{
return new Content(
view: 'emails.invoice',
with: [
'invoice' => $this->invoice,
'invoiceUrl' => route('invoices.show', $this->invoice),
],
);
}
}

View File

@ -24,7 +24,7 @@ new class extends Component {
{ {
$this->validate(); $this->validate();
Invoice::create([ $invoice = Invoice::create([
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'status' => $this->status, 'status' => $this->status,
'notes' => $this->notes, 'notes' => $this->notes,
@ -34,6 +34,8 @@ new class extends Component {
$this->reset(); $this->reset();
Flux::modal('create-invoice')->close(); Flux::modal('create-invoice')->close();
$this->dispatch('invoice-created'); $this->dispatch('invoice-created');
$this->redirect(route('invoices.edit', $invoice), navigate: true);
} }
#[Computed] #[Computed]

View File

@ -1,12 +1,14 @@
<?php <?php
use App\Enums\InvoiceStatus; use App\Enums\InvoiceStatus;
use App\Mail\InvoiceMail;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use Illuminate\Support\Facades\Mail;
use Livewire\Component; use Livewire\Component;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component { new class extends Component {
public $invoice; public $invoice;
@ -72,6 +74,56 @@ new class extends Component {
$this->dispatch('invoice-status-changed'); $this->dispatch('invoice-status-changed');
} }
public function sendToPrimaryContact(): void
{
$primaryContact = $this->invoice->client->primary_contact;
if (!$primaryContact || !$primaryContact->email) {
Flux::toast(
text: 'No primary contact with email address found.',
variant: 'danger',
);
return;
}
Mail::to($primaryContact->email)->send(new InvoiceMail($this->invoice));
if ($this->invoice->sent_at === null) {
$this->invoice->update(['sent_at' => now()]);
}
Flux::toast(
text: "Invoice sent to {$primaryContact->full_name}.",
variant: 'success',
);
}
public function sendToAllContacts(): void
{
$contacts = $this->invoice->client->contacts->filter(fn($c) => $c->email);
if ($contacts->isEmpty()) {
Flux::toast(
text: 'No contacts with email addresses found.',
variant: 'danger',
);
return;
}
foreach ($contacts as $contact) {
Mail::to($contact->email)->send(new InvoiceMail($this->invoice));
}
if ($this->invoice->sent_at === null) {
$this->invoice->update(['sent_at' => now()]);
}
Flux::toast(
text: "Invoice sent to {$contacts->count()} contact(s).",
variant: 'success',
);
}
#[Computed] #[Computed]
public function clients() public function clients()
{ {
@ -90,6 +142,8 @@ new class extends Component {
@elseif($this->invoice->status === InvoiceStatus::POSTED) @elseif($this->invoice->status === InvoiceStatus::POSTED)
<flux:button variant="primary" color="red" wire:click="setStatus('void')">Void Invoice</flux:button> <flux:button variant="primary" color="red" wire:click="setStatus('void')">Void Invoice</flux:button>
<flux:button variant="primary" color="amber" wire:click="setStatus('draft')">Un-Post Invoice</flux:button> <flux:button variant="primary" color="amber" wire:click="setStatus('draft')">Un-Post Invoice</flux:button>
<flux:button variant="primary" wire:click="sendToPrimaryContact" wire:loading.attr="disabled">Send to Primary Contact</flux:button>
<flux:button variant="primary" wire:click="sendToAllContacts" wire:loading.attr="disabled">Send to All Contacts</flux:button>
@elseif($this->invoice->status === InvoiceStatus::VOID) @elseif($this->invoice->status === InvoiceStatus::VOID)
<flux:button variant="primary" color="blue" wire:click="setStatus('draft')">Restore Invoice</flux:button> <flux:button variant="primary" color="blue" wire:click="setStatus('draft')">Restore Invoice</flux:button>
@endif @endif

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice {{ $invoice->invoice_number }}</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 8px;">
<h1 style="color: #1a1a1a; margin-top: 0; font-size: 24px;">Invoice {{ $invoice->invoice_number }}</h1>
<p style="margin-bottom: 20px;">Dear {{ $invoice->client->primary_contact?->first_name ?? 'Valued Customer' }},</p>
<p>Please find below a summary of your invoice from eBandroom.</p>
<div style="background-color: #ffffff; padding: 20px; border-radius: 6px; margin: 20px 0; border: 1px solid #e9ecef;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;">Invoice Number:</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600;">{{ $invoice->invoice_number }}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;">Invoice Date:</td>
<td style="padding: 8px 0; text-align: right;">{{ $invoice->invoice_date?->format('F j, Y') }}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;">Due Date:</td>
<td style="padding: 8px 0; text-align: right;">{{ $invoice->due_date?->format('F j, Y') }}</td>
</tr>
</table>
<hr style="border: none; border-top: 1px solid #e9ecef; margin: 15px 0;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid #e9ecef;">
<th style="padding: 10px 0; text-align: left; color: #666; font-weight: 500;">Description</th>
<th style="padding: 10px 0; text-align: right; color: #666; font-weight: 500;">Amount</th>
</tr>
</thead>
<tbody>
@foreach($invoice->lines as $line)
<tr>
<td style="padding: 10px 0;">
{{ $line->product?->name ?? $line->description }}
@if($line->description && $line->product)
<br><span style="color: #666; font-size: 14px;">{{ $line->description }}</span>
@endif
</td>
<td style="padding: 10px 0; text-align: right;">${{ number_format($line->amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
<hr style="border: none; border-top: 2px solid #e9ecef; margin: 15px 0;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; font-weight: 600; font-size: 18px;">Total Due:</td>
<td style="padding: 8px 0; text-align: right; font-weight: 600; font-size: 18px; color: #2563eb;">${{ number_format($invoice->balance_due, 2) }}</td>
</tr>
</table>
</div>
@if($invoice->notes)
<div style="background-color: #fff3cd; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #ffc107;">
<strong>Notes:</strong><br>
{{ $invoice->notes }}
</div>
@endif
<p style="margin: 25px 0;">To view your complete invoice or pay online, please click the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $invoiceUrl }}" style="display: inline-block; background-color: #2563eb; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-weight: 600;">View Invoice & Pay Online</a>
</div>
<p style="color: #666; font-size: 14px; margin-top: 30px;">
If you have any questions about this invoice, please don't hesitate to contact us.
</p>
<p style="margin-bottom: 0;">
Thank you for your business,<br>
<strong>{{ config('app.name') }}</strong>
</p>
</div>
<div style="text-align: center; padding: 20px; color: #999; font-size: 12px;">
<p>This email was sent regarding invoice {{ $invoice->invoice_number }}.</p>
</div>
</body>
</html>