Manipulating status and notes on invoices works.
This commit is contained in:
parent
9b2202e894
commit
9d9838b02d
|
|
@ -22,9 +22,9 @@ enum InvoiceStatus: string
|
|||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => 'gray',
|
||||
self::DRAFT => 'zinc',
|
||||
self::POSTED => 'green',
|
||||
self::VOID => 'zinc',
|
||||
self::VOID => 'red',
|
||||
self::PAID => 'blue',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
|
|
@ -16,6 +17,7 @@ class Invoice extends Model
|
|||
{
|
||||
static::creating(function (Invoice $invoice) {
|
||||
$invoice->invoice_number ??= static::generateInvoiceNumber();
|
||||
$invoice->uuid = (string) Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +51,17 @@ class Invoice extends Model
|
|||
'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
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,21 +1,176 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\InvoiceStatus;
|
||||
use App\Models\Client;
|
||||
use App\Models\Invoice;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
|
||||
new class extends Component {
|
||||
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
|
||||
{
|
||||
$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>
|
||||
<flux:heading size="xl">Edit Invoice</flux:heading>
|
||||
<flux:card>
|
||||
<flux:heading size="lg">Identifying Information</flux:heading>
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<flux:heading size="xl" class="mb-3">Edit Invoice</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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ new class extends Component {
|
|||
wire:click="sort('total')">
|
||||
Total
|
||||
</flux:table.column>
|
||||
<flux:table.column></flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
|
|
@ -103,6 +104,21 @@ new class extends Component {
|
|||
@endif
|
||||
</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>
|
||||
@endforeach
|
||||
|
|
|
|||
Loading…
Reference in New Issue