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
|
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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
<?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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue