Product management is working.

This commit is contained in:
Matt Young 2026-01-28 05:01:16 -06:00
parent a1c7ee43f7
commit 17de29ec91
6 changed files with 260 additions and 1 deletions

View File

@ -0,0 +1,66 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component {
#[Validate('required|string|max:50|unique:products,sku')]
public string $sku = '';
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('nullable|string|max:1000')]
public string $description = '';
#[Validate('required|numeric|min:0')]
public string $price = '';
#[Validate('boolean')]
public bool $active = true;
public function save(): void
{
$this->validate();
Product::create([
'sku' => $this->sku,
'name' => $this->name,
'description' => $this->description ?: null,
'price' => $this->price,
'active' => $this->active,
]);
$this->reset();
Flux::modal('create-product')->close();
$this->dispatch('product-created');
}
};
?>
<div>
<flux:modal.trigger name="create-product">
<flux:button icon="plus" variant="primary">
New Product
</flux:button>
</flux:modal.trigger>
<flux:modal name="create-product" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Create Product</flux:heading>
<flux:input label="SKU" wire:model="sku" />
<flux:input label="Name" wire:model="name" />
<flux:textarea label="Description" wire:model="description" rows="3" />
<flux:input label="Price" wire:model="price" type="number" step="0.01" min="0" />
<flux:switch label="Active" wire:model="active" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Create</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -0,0 +1,86 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\On;
use Flux\Flux;
new class extends Component {
public ?int $productId = null;
#[Validate('required|string|max:50')]
public string $sku = '';
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('nullable|string|max:1000')]
public string $description = '';
#[Validate('required|numeric|min:0')]
public string $price = '';
#[Validate('boolean')]
public bool $active = true;
#[On('edit-product')]
public function edit(int $productId): void
{
$this->productId = $productId;
$product = Product::findOrFail($productId);
$this->sku = $product->sku;
$this->name = $product->name;
$this->description = $product->description ?? '';
$this->price = (string) $product->getRawOriginal('price') / 100;
$this->active = $product->active;
$this->resetValidation();
Flux::modal('edit-product')->show();
}
public function save(): void
{
$this->validate([
'sku' => 'required|string|max:50|unique:products,sku,' . $this->productId,
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'price' => 'required|numeric|min:0',
'active' => 'boolean',
]);
$product = Product::findOrFail($this->productId);
$product->update([
'sku' => $this->sku,
'name' => $this->name,
'description' => $this->description ?: null,
'price' => $this->price,
'active' => $this->active,
]);
$this->reset();
Flux::modal('edit-product')->close();
$this->dispatch('product-updated');
}
};
?>
<div>
<flux:modal name="edit-product" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Edit Product</flux:heading>
<flux:input label="SKU" wire:model="sku" />
<flux:input label="Name" wire:model="name" />
<flux:textarea label="Description" wire:model="description" rows="3" />
<flux:input label="Price" wire:model="price" type="number" step="0.01" min="0" />
<flux:switch label="Active" wire:model="active" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Save</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -0,0 +1,93 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $sortBy = 'name';
public string $sortDirection = 'asc';
public function sort($column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
#[On('product-created')]
#[On('product-updated')]
public function refresh(): void {}
#[Computed]
public function products()
{
return Product::orderBy($this->sortBy, $this->sortDirection)->paginate(10);
}
};
?>
<!--suppress RequiredAttributes -->
<div>
<flux:table :paginate="$this->products">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'sku'" :direction="$sortDirection"
wire:click="sort('sku')">
SKU
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection"
wire:click="sort('name')">
Name
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'description'" :direction="$sortDirection"
wire:click="sort('description')">
Description
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'price'" :direction="$sortDirection"
wire:click="sort('price')">
Price
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'active'" :direction="$sortDirection"
wire:click="sort('active')">
Status
</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach($this->products as $product)
<flux:table.row :key="$product->id">
<flux:table.cell>{{ $product->sku }}</flux:table.cell>
<flux:table.cell>{{ $product->name }}</flux:table.cell>
<flux:table.cell class="max-w-xs truncate">{{ $product->description }}</flux:table.cell>
<flux:table.cell>{{ $product->price }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$product->active ? 'green' : 'zinc'">
{{ $product->active ? 'Active' : 'Inactive' }}
</flux:badge>
</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="{{ $product->sku }}">
<flux:menu.separator></flux:menu.separator>
<flux:menu.item wire:click="$dispatch('edit-product', { productId: {{ $product->id }} })" icon="pencil">Edit</flux:menu.item>
</flux:menu.group>
</flux:navmenu>
</flux:dropdown>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@ -20,10 +20,14 @@
{{ __('Clients') }}
</flux:sidebar.item>
<flux:sidebar.item icon="user" :href="route('contacts')" :current="request()->routeIs('clients.*')" wire:navigate>
<flux:sidebar.item icon="user" :href="route('contacts')" :current="request()->routeIs('contacts.*')" wire:navigate>
{{ __('Contacts') }}
</flux:sidebar.item>
<flux:sidebar.item icon="archive-box" :href="route('products')" :current="request()->routeIs('products.*')" wire:navigate>
{{ __('Products') }}
</flux:sidebar.item>
</flux:sidebar.group>
</flux:sidebar.nav>

View File

@ -0,0 +1,9 @@
<x-layouts::app :title="__('Products')">
<div class="max-w-7xl mx-auto space-y-4">
<div class="flex justify-end">
<livewire:create-product />
</div>
<livewire:product-list />
<livewire:edit-product />
</div>
</x-layouts::app>

View File

@ -11,6 +11,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::view('dashboard', 'dashboard')->name('dashboard');
Route::view('clients', 'clients.index')->name('clients');
Route::view('contacts', 'contacts.index')->name('contacts');
Route::view('products', 'products.index')->name('products');
});
// Route::view('dashboard', 'dashboard')