Manipulating status and notes on invoices works.

This commit is contained in:
Matt Young 2026-01-28 14:29:33 -06:00
parent 9b2202e894
commit 9d9838b02d
5 changed files with 220 additions and 8 deletions

View File

@ -22,9 +22,9 @@ enum InvoiceStatus: string
public function color(): string public function color(): string
{ {
return match ($this) { return match ($this) {
self::DRAFT => 'gray', self::DRAFT => 'zinc',
self::POSTED => 'green', self::POSTED => 'green',
self::VOID => 'zinc', self::VOID => 'red',
self::PAID => 'blue', self::PAID => 'blue',
}; };
} }

View File

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Invoice extends Model class Invoice extends Model
{ {
@ -16,6 +17,7 @@ class Invoice extends Model
{ {
static::creating(function (Invoice $invoice) { static::creating(function (Invoice $invoice) {
$invoice->invoice_number ??= static::generateInvoiceNumber(); $invoice->invoice_number ??= static::generateInvoiceNumber();
$invoice->uuid = (string) Str::uuid();
}); });
} }
@ -49,6 +51,17 @@ class Invoice extends Model
'sent_at' => 'date', 'sent_at' => 'date',
]; ];
/**
* Get the route key for the model.
* This tells Laravel to use the 'uuid' column for route model binding.
*
* @return string
*/
public function getRouteKeyName(): string
{
return 'uuid';
}
public function client(): BelongsTo public function client(): BelongsTo
{ {
return $this->belongsTo(Client::class); return $this->belongsTo(Client::class);

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->uuid('uuid')->after('id')->unique();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
};

View File

@ -1,21 +1,176 @@
<?php <?php
use App\Enums\InvoiceStatus;
use App\Models\Client;
use App\Models\Invoice;
use Livewire\Component; use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
new class extends Component
{ new class extends Component {
public $invoice; public $invoice;
#[Validate('required|exists:clients,id')]
public ?int $client_id = null;
#[Validate('nullable|string')]
public ?string $notes = null;
#[Validate('nullable|string')]
public ?string $internal_notes = null;
public function mount($invoice = null): void public function mount($invoice = null): void
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client_id = $invoice?->client_id;
$this->notes = $invoice?->notes;
$this->internal_notes = $invoice?->internal_notes;
}
public function updateClient(): void
{
$this->validate([
'client_id' => 'required|exists:clients,id'
]);
$this->invoice->update(['client_id' => $this->client_id]);
}
public function updateNotes(): void
{
$this->validate([
'notes' => 'nullable|string',
'internal_notes' => 'nullable|string'
]);
$this->invoice->update([
'notes' => $this->notes,
'internal_notes' => $this->internal_notes,
]);
}
public function setStatus($newStatus): void
{
$updatedValue = match ($newStatus) {
'posted' => InvoiceStatus::POSTED,
'draft' => InvoiceStatus::DRAFT,
'void' => InvoiceStatus::VOID,
'paid' => InvoiceStatus::PAID,
default => $this->invoice->status
};
$this->invoice->update([
'status' => $updatedValue,
]);
if ($newStatus === 'posted') {
$this->invoice->update([
'invoice_date' => now(),
'due_date' => now()->addDays(30),
]);
}
}
#[Computed]
public function clients()
{
return Client::where('status', 'active')->orderBy('abbreviation')->get();
} }
}; };
?> ?>
<div> <div>
<flux:heading size="xl">Edit Invoice</flux:heading> <div class="flex justify-between items-center mb-8">
<flux:card> <flux:heading size="xl" class="mb-3">Edit Invoice</flux:heading>
<flux:heading size="lg">Identifying Information</flux:heading> <div>
@if($this->invoice->status === InvoiceStatus::DRAFT)
<flux:button variant="primary" color="red" wire:click="setStatus('void')">Void Invoice</flux:button>
<flux:button variant="primary" color="green" wire:click="setStatus('posted')">Post Invoice</flux:button>
@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="amber" wire:click="setStatus('draft')">Un-Post Invoice</flux:button>
@elseif($this->invoice->status === InvoiceStatus::VOID)
<flux:button variant="primary" color="blue" wire:click="setStatus('draft')">Restore Invoice</flux:button>
@endif
</div>
</div>
<flux:card class="bg-gray-50">
<div class="grid grid-cols-3 gap-4">
<div>
<div class="flex gap-3">
<flux:heading size="md">ID</flux:heading>
<flux:text>{{ $invoice->id }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">Invoice Number</flux:heading>
<flux:text>{{ $invoice->invoice_number }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">UUID</flux:heading>
<flux:text>{{ $invoice->uuid }}</flux:text>
</div>
</div>
<div>
@if($invoice->status !== InvoiceStatus::DRAFT)
<div class="flex gap-3">
<flux:heading size="md">Client</flux:heading>
<flux:text>{{ $invoice->client->name }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">Invoice Date</flux:heading>
<flux:text>{{ $invoice->invoice_date?->format('m/d/Y') }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">Due Date</flux:heading>
<flux:text>{{ $invoice->due_date?->format('m/d/Y') }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">Sent Date</flux:heading>
<flux:text>{{ $invoice->sent_at?->format('m/d/Y') }}</flux:text>
</div>
@else
<flux:select wire:model="client_id" wire:change="updateClient" label="Client"
placeholder="Choose client..."
:disabled="$invoice->status !== InvoiceStatus::DRAFT">
@foreach($this->clients as $client)
<flux:select.option :value="$client->id">{{ $client->name }}</flux:select.option>
@endforeach
</flux:select>
@endif
</div>
<div>
<div class="flex gap-3">
<flux:heading size="md">Status</flux:heading>
<flux:badge color="{{ $invoice->status->color() }}" size="sm">
{{ $invoice->status->label() }}
</flux:badge>
</div>
<div class="flex gap-3">
<flux:heading size="md">Created</flux:heading>
<flux:text>{{ $invoice->created_at->local()->format('m/d/Y | h:m:s') }}</flux:text>
</div>
<div class="flex gap-3">
<flux:heading size="md">Last Updated</flux:heading>
<flux:text>{{ $invoice->updated_at->local()->format('m/d/Y | h:m:s') }}</flux:text>
</div>
</div>
</div>
</flux:card> </flux:card>
<form wire:submit="updateNotes" x-data="{ dirty: false }" x-on:submit="dirty = false">
<flux:card class="bg-gray-50 mt-8">
<div class="grid grid-cols-2 gap-4">
<flux:textarea wire:model="notes" label="Notes" placeholder="Add notes..." rows="5"
x-on:input="dirty = true"/>
<flux:textarea wire:model="internal_notes" label="Internal Notes" placeholder="Add internal notes..."
rows="5" x-on:input="dirty = true"/>
</div>
<div class="text-right" x-show="dirty" x-cloak>
<flux:button type="submit" variant="primary" class="mt-4">Save Notes</flux:button>
</div>
</flux:card>
</form>
</div> </div>

View File

@ -72,6 +72,7 @@ new class extends Component {
wire:click="sort('total')"> wire:click="sort('total')">
Total Total
</flux:table.column> </flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns> </flux:table.columns>
<flux:table.rows> <flux:table.rows>
@ -103,6 +104,21 @@ new class extends Component {
@endif @endif
</flux:table.cell> </flux:table.cell>
<flux:table.cell>{{ formatMoney($invoice->total) }}</flux:table.cell> <flux:table.cell>{{ formatMoney($invoice->total) }}</flux:table.cell>
<flux:table.cell>
<flux:dropdown position="bottom" align="start">
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom"></flux:button>
<flux:navmenu>
<flux:menu.group heading="{{ $invoice->invoice_number }}">
<flux:menu.separator></flux:menu.separator>
<flux:navmenu.item
href="{{ route('invoices.edit', $invoice) }}"
icon="pencil">Edit Invoice</flux:navmenu.item>
</flux:menu.group>
</flux:navmenu>
</flux:dropdown>
</flux:table.cell>
</flux:table.row> </flux:table.row>
@endforeach @endforeach