File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/products/form.blade.php

    <flux:heading size="xl" class="mb-6">{{ $product ? 'Edit product' : 'New product' }}</flux:heading>

    @if (session('success'))
        <flux:callout variant="success" icon="check-circle" class="mb-4" heading="{{ session('success') }}"></flux:callout>
    @endif

    <form wire:submit="save" class="flex flex-col gap-6">

            <flux:heading size="lg" class="mb-4">Details</flux:heading>

                <flux:input label="Title" wire:model="title" required />
                <flux:input label="Handle (URL slug)" wire:model="handle" placeholder="Auto-generated if empty" />
                <flux:textarea label="Description" wire:model="descriptionHtml" rows="5" />

                    <flux:input label="Vendor" wire:model="vendor" />
                    <flux:input label="Type" wire:model="productType" />

                <flux:select label="Status" wire:model="status">
                    <flux:select.option value="draft">Draft</flux:select.option>
                    <flux:select.option value="active">Active</flux:select.option>
                    <flux:select.option value="archived">Archived</flux:select.option>
                </flux:select>

            <flux:heading size="lg" class="mb-4">Pricing & inventory</flux:heading>

                <flux:input type="number" label="Price (in cents)" wire:model="priceAmount" />
                <flux:input type="number" label="Quantity on hand" wire:model="quantityOnHand" />

            <flux:button type="button" variant="ghost" href="{{ route('admin.products.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


Write

<?php

namespace App\Livewire\Admin\Orders;

use App\Models\Order;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    use WithPagination;

    #[Url(as: 'status')]
    public string $status = 'all';

    public function render()
    {
        $orders = Order::query()
            ->when($this->status !== 'all', fn ($q) => $q->where('financial_status', $this->status))
            ->orderByDesc('placed_at')
            ->paginate(20);

        return view('livewire.admin.orders.index', compact('orders'))->title('Orders');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Orders/Index.php

<?php

namespace App\Livewire\Admin\Orders;

use App\Models\Order;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    use WithPagination;

    #[Url(as: 'status')]
    public string $status = 'all';

    public function render()
    {
        $orders = Order::query()
            ->when($this->status !== 'all', fn ($q) => $q->where('financial_status', $this->status))
            ->orderByDesc('placed_at')
            ->paginate(20);

        return view('livewire.admin.orders.index', compact('orders'))->title('Orders');
    }
}


    <flux:heading size="xl">Orders</flux:heading>

    <flux:select wire:model.live="status" class="w-fit">
        <flux:select.option value="all">All</flux:select.option>
        <flux:select.option value="pending">Pending</flux:select.option>
        <flux:select.option value="authorized">Authorized</flux:select.option>
        <flux:select.option value="paid">Paid</flux:select.option>
        <flux:select.option value="refunded">Refunded</flux:select.option>
    </flux:select>

        @if ($orders->isEmpty())

No orders yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Order</flux:table.column>
                    <flux:table.column>Date</flux:table.column>
                    <flux:table.column>Customer</flux:table.column>
                    <flux:table.column>Payment</flux:table.column>
                    <flux:table.column>Fulfillment</flux:table.column>
                    <flux:table.column>Total</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($orders as $order)
                        <flux:table.row>
                            <flux:table.cell>
                                {{ $order->order_number }}
                            </flux:table.cell>
                            <flux:table.cell>{{ $order->placed_at?->format('Y-m-d H:i') }}</flux:table.cell>
                            <flux:table.cell>{{ $order->email }}</flux:table.cell>
                            <flux:table.cell>
                                <flux:badge size="sm" :color="$order->financial_status->value === 'paid' ? 'emerald' : 'zinc'">{{ $order->financial_status->value }}</flux:badge>
                            </flux:table.cell>
                            <flux:table.cell>
                                <flux:badge size="sm" :color="$order->fulfillment_status->value === 'fulfilled' ? 'emerald' : 'zinc'">{{ $order->fulfillment_status->value }}</flux:badge>
                            </flux:table.cell>
                            <flux:table.cell>{{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}</flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif

{{ $orders->links() }}


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/orders/index.blade.php

    <flux:heading size="xl">Orders</flux:heading>

    <flux:select wire:model.live="status" class="w-fit">
        <flux:select.option value="all">All</flux:select.option>
        <flux:select.option value="pending">Pending</flux:select.option>
        <flux:select.option value="authorized">Authorized</flux:select.option>
        <flux:select.option value="paid">Paid</flux:select.option>
        <flux:select.option value="refunded">Refunded</flux:select.option>
    </flux:select>

        @if ($orders->isEmpty())

No orders yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Order</flux:table.column>
                    <flux:table.column>Date</flux:table.column>
                    <flux:table.column>Customer</flux:table.column>
                    <flux:table.column>Payment</flux:table.column>
                    <flux:table.column>Fulfillment</flux:table.column>
                    <flux:table.column>Total</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($orders as $order)
                        <flux:table.row>
                            <flux:table.cell>
                                {{ $order->order_number }}
                            </flux:table.cell>
                            <flux:table.cell>{{ $order->placed_at?->format('Y-m-d H:i') }}</flux:table.cell>
                            <flux:table.cell>{{ $order->email }}</flux:table.cell>
                            <flux:table.cell>
                                <flux:badge size="sm" :color="$order->financial_status->value === 'paid' ? 'emerald' : 'zinc'">{{ $order->financial_status->value }}</flux:badge>
                            </flux:table.cell>
                            <flux:table.cell>
                                <flux:badge size="sm" :color="$order->fulfillment_status->value === 'fulfilled' ? 'emerald' : 'zinc'">{{ $order->fulfillment_status->value }}</flux:badge>
                            </flux:table.cell>
                            <flux:table.cell>{{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}</flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif

{{ $orders->links() }}


<?php

namespace App\Livewire\Admin\Orders;

use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Models\Fulfillment;
use App\Models\FulfillmentLine;
use App\Models\Order;
use App\Models\Refund;
use App\Services\Orders\OrderService;
use App\Services\Payments\PaymentProvider;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Show extends Component
{
    public Order $order;

    public ?int $refundAmount = null;

    public string $trackingCompany = 'DHL';

    public string $trackingNumber = '';

    public function mount(Order $order): void
    {
        $this->order = $order->load('lines.variant.product', 'payments', 'refunds', 'fulfillments.lines', 'customer');
    }

    public function fulfillAll(): void
    {
        $fulfillment = Fulfillment::create([
            'order_id' => $this->order->id,
            'status' => FulfillmentShipmentStatus::Shipped->value,
            'tracking_company' => $this->trackingCompany ?: null,
            'tracking_number' => $this->trackingNumber ?: null,
            'shipped_at' => now(),
            'created_at' => now(),
        ]);

        foreach ($this->order->lines as $line) {
            FulfillmentLine::create([
                'fulfillment_id' => $fulfillment->id,
                'order_line_id' => $line->id,
                'quantity' => $line->quantity,
            ]);
        }

        $this->order->update(['fulfillment_status' => FulfillmentStatus::Fulfilled->value]);
        $this->order->refresh()->load('fulfillments.lines');
        session()->flash('success', 'Order fulfilled.');
    }

    public function refund(OrderService $orderService, PaymentProvider $paymentProvider): void
    {
        $amount = (int) ($this->refundAmount ?? $this->order->total_amount);
        if ($amount <= 0) {
            return;
        }

        $payment = $this->order->payments()->latest()->first();
        $result = $paymentProvider->refund($this->order, $amount, $payment?->id);

        Refund::create([
            'order_id' => $this->order->id,
            'payment_id' => $payment?->id,
            'amount' => $amount,
            'reason' => 'admin_refund',
            'status' => $result->succeeded ? 'succeeded' : 'failed',
            'provider_refund_id' => $result->providerPaymentId,
            'created_at' => now(),
        ]);

        $totalRefunded = (int) $this->order->refunds()->where('status', 'succeeded')->sum('amount') + $amount;
        $financial = $totalRefunded >= $this->order->total_amount ? 'refunded' : 'partially_refunded';
        $this->order->update(['financial_status' => $financial]);
        $this->order->refresh()->load('refunds');

        session()->flash('success', 'Refund processed.');
    }

    public function cancel(OrderService $orderService): void
    {
        $orderService->cancel($this->order);
        $this->order->refresh();
        session()->flash('success', 'Order cancelled.');
    }

    public function render()
    {
        return view('livewire.admin.orders.show')->title($this->order->order_number);
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Orders/Show.php

<?php

namespace App\Livewire\Admin\Orders;

use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Models\Fulfillment;
use App\Models\FulfillmentLine;
use App\Models\Order;
use App\Models\Refund;
use App\Services\Orders\OrderService;
use App\Services\Payments\PaymentProvider;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Show extends Component
{
    public Order $order;

    public ?int $refundAmount = null;

    public string $trackingCompany = 'DHL';

    public string $trackingNumber = '';

    public function mount(Order $order): void
    {
        $this->order = $order->load('lines.variant.product', 'payments', 'refunds', 'fulfillments.lines', 'customer');
    }

    public function fulfillAll(): void
    {
        $fulfillment = Fulfillment::create([
            'order_id' => $this->order->id,
            'status' => FulfillmentShipmentStatus::Shipped->value,
            'tracking_company' => $this->trackingCompany ?: null,
            'tracking_number' => $this->trackingNumber ?: null,
            'shipped_at' => now(),
            'created_at' => now(),
        ]);

        foreach ($this->order->lines as $line) {
            FulfillmentLine::create([
                'fulfillment_id' => $fulfillment->id,
                'order_line_id' => $line->id,
                'quantity' => $line->quantity,
            ]);
        }

        $this->order->update(['fulfillment_status' => FulfillmentStatus::Fulfilled->value]);
        $this->order->refresh()->load('fulfillments.lines');
        session()->flash('success', 'Order fulfilled.');
    }

    public function refund(OrderService $orderService, PaymentProvider $paymentProvider): void
    {
        $amount = (int) ($this->refundAmount ?? $this->order->total_amount);
        if ($amount <= 0) {
            return;
        }

        $payment = $this->order->payments()->latest()->first();
        $result = $paymentProvider->refund($this->order, $amount, $payment?->id);

        Refund::create([
            'order_id' => $this->order->id,
            'payment_id' => $payment?->id,
            'amount' => $amount,
            'reason' => 'admin_refund',
            'status' => $result->succeeded ? 'succeeded' : 'failed',
            'provider_refund_id' => $result->providerPaymentId,
            'created_at' => now(),
        ]);

        $totalRefunded = (int) $this->order->refunds()->where('status', 'succeeded')->sum('amount') + $amount;
        $financial = $totalRefunded >= $this->order->total_amount ? 'refunded' : 'partially_refunded';
        $this->order->update(['financial_status' => $financial]);
        $this->order->refresh()->load('refunds');

        session()->flash('success', 'Refund processed.');
    }

    public function cancel(OrderService $orderService): void
    {
        $orderService->cancel($this->order);
        $this->order->refresh();
        session()->flash('success', 'Order cancelled.');
    }

    public function render()
    {
        return view('livewire.admin.orders.show')->title($this->order->order_number);
    }
}


        <flux:heading size="xl">{{ $order->order_number }}</flux:heading>

            <flux:badge :color="$order->financial_status->value === 'paid' ? 'emerald' : 'zinc'">{{ $order->financial_status->value }}</flux:badge>
            <flux:badge :color="$order->fulfillment_status->value === 'fulfilled' ? 'emerald' : 'zinc'">{{ $order->fulfillment_status->value }}</flux:badge>

    @if (session('success'))
        <flux:callout variant="success" icon="check-circle" heading="{{ session('success') }}"></flux:callout>
    @endif

                <flux:heading size="lg" class="mb-4">Items</flux:heading>

                    @foreach ($order->lines as $line)

{{ $line->title_snapshot }}

SKU: {{ $line->sku_snapshot }} · qty {{ $line->quantity }}

{{ $order->currency }} {{ number_format($line->total_amount / 100, 2) }}

                    @endforeach

Subtotal{{ $order->currency }} {{ number_format($order->subtotal_amount / 100, 2) }}

                    @if ($order->discount_amount > 0)
Discount−{{ $order->currency }} {{ number_format($order->discount_amount / 100, 2) }}
@endif

Shipping{{ $order->currency }} {{ number_format($order->shipping_amount / 100, 2) }}

Tax{{ $order->currency }} {{ number_format($order->tax_amount / 100, 2) }}

Total{{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}

            @if ($order->fulfillments->isNotEmpty())

                    <flux:heading size="lg" class="mb-4">Fulfillments</flux:heading>
                    @foreach ($order->fulfillments as $f)

{{ $f->tracking_company }} · {{ $f->tracking_number ?: 'no tracking' }}

{{ $f->shipped_at?->format('Y-m-d H:i') }}

                            <flux:badge :color="$f->status->value === 'shipped' ? 'emerald' : 'zinc'">{{ $f->status->value }}</flux:badge>

                    @endforeach

            @endif

            @if ($order->refunds->isNotEmpty())

                    <flux:heading size="lg" class="mb-4">Refunds</flux:heading>
                    @foreach ($order->refunds as $r)

                            {{ $r->created_at?->format('Y-m-d H:i') }} · {{ $r->reason }}
                            {{ $order->currency }} {{ number_format($r->amount / 100, 2) }}

                    @endforeach

            @endif

                <flux:heading size="lg" class="mb-3">Customer</flux:heading>

{{ $order->customer?->name ?? 'Guest' }}

{{ $order->email }}

                <flux:heading size="lg" class="mb-3">Addresses</flux:heading>

Shipping

                    @php($s = $order->shipping_address_json)
                    @if ($s)

{{ $s['first_name'] ?? '' }} {{ $s['last_name'] ?? '' }}

{{ $s['address1'] ?? '' }}

{{ $s['zip'] ?? '' }} {{ $s['city'] ?? '' }}

{{ $s['country'] ?? '' }}

                    @endif

                <flux:heading size="lg" class="mb-3">Actions</flux:heading>

                    @if ($order->fulfillment_status->value !== 'fulfilled' && $order->status->value !== 'cancelled')

                            <flux:input size="sm" label="Tracking company" wire:model="trackingCompany" />
                            <flux:input size="sm" label="Tracking number" wire:model="trackingNumber" />
                            <flux:button size="sm" variant="primary" wire:click="fulfillAll">Mark fulfilled</flux:button>

                    @endif

                    @if ($order->financial_status->value === 'paid')

                            <flux:input type="number" size="sm" label="Refund amount (cents)" wire:model="refundAmount" placeholder="Full refund" />
                            <flux:button size="sm" variant="ghost" wire:click="refund">Refund</flux:button>

                    @endif

                    @if ($order->status->value === 'open')
                        <flux:button size="sm" variant="danger" wire:click="cancel" wire:confirm="Cancel this order?">Cancel order</flux:button>
                    @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/orders/show.blade.php

        <flux:heading size="xl">{{ $order->order_number }}</flux:heading>

            <flux:badge :color="$order->financial_status->value === 'paid' ? 'emerald' : 'zinc'">{{ $order->financial_status->value }}</flux:badge>
            <flux:badge :color="$order->fulfillment_status->value === 'fulfilled' ? 'emerald' : 'zinc'">{{ $order->fulfillment_status->value }}</flux:badge>

    @if (session('success'))
        <flux:callout variant="success" icon="check-circle" heading="{{ session('success') }}"></flux:callout>
    @endif

                <flux:heading size="lg" class="mb-4">Items</flux:heading>

                    @foreach ($order->lines as $line)

{{ $line->title_snapshot }}

SKU: {{ $line->sku_snapshot }} · qty {{ $line->quantity }}

{{ $order->currency }} {{ number_format($line->total_amount / 100, 2) }}

                    @endforeach

Subtotal{{ $order->currency }} {{ number_format($order->subtotal_amount / 100, 2) }}

                    @if ($order->discount_amount > 0)
Discount−{{ $order->currency }} {{ number_format($order->discount_amount / 100, 2) }}
@endif

Shipping{{ $order->currency }} {{ number_format($order->shipping_amount / 100, 2) }}

Tax{{ $order->currency }} {{ number_format($order->tax_amount / 100, 2) }}

Total{{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}

            @if ($order->fulfillments->isNotEmpty())

                    <flux:heading size="lg" class="mb-4">Fulfillments</flux:heading>
                    @foreach ($order->fulfillments as $f)

{{ $f->tracking_company }} · {{ $f->tracking_number ?: 'no tracking' }}

{{ $f->shipped_at?->format('Y-m-d H:i') }}

                            <flux:badge :color="$f->status->value === 'shipped' ? 'emerald' : 'zinc'">{{ $f->status->value }}</flux:badge>

                    @endforeach

            @endif

            @if ($order->refunds->isNotEmpty())

                    <flux:heading size="lg" class="mb-4">Refunds</flux:heading>
                    @foreach ($order->refunds as $r)

                            {{ $r->created_at?->format('Y-m-d H:i') }} · {{ $r->reason }}
                            {{ $order->currency }} {{ number_format($r->amount / 100, 2) }}

                    @endforeach

            @endif

                <flux:heading size="lg" class="mb-3">Customer</flux:heading>

{{ $order->customer?->name ?? 'Guest' }}

{{ $order->email }}

                <flux:heading size="lg" class="mb-3">Addresses</flux:heading>

Shipping

                    @php($s = $order->shipping_address_json)
                    @if ($s)

{{ $s['first_name'] ?? '' }} {{ $s['last_name'] ?? '' }}

{{ $s['address1'] ?? '' }}

{{ $s['zip'] ?? '' }} {{ $s['city'] ?? '' }}

{{ $s['country'] ?? '' }}

                    @endif

                <flux:heading size="lg" class="mb-3">Actions</flux:heading>

                    @if ($order->fulfillment_status->value !== 'fulfilled' && $order->status->value !== 'cancelled')

                            <flux:input size="sm" label="Tracking company" wire:model="trackingCompany" />
                            <flux:input size="sm" label="Tracking number" wire:model="trackingNumber" />
                            <flux:button size="sm" variant="primary" wire:click="fulfillAll">Mark fulfilled</flux:button>

                    @endif

                    @if ($order->financial_status->value === 'paid')

                            <flux:input type="number" size="sm" label="Refund amount (cents)" wire:model="refundAmount" placeholder="Full refund" />
                            <flux:button size="sm" variant="ghost" wire:click="refund">Refund</flux:button>

                    @endif

                    @if ($order->status->value === 'open')
                        <flux:button size="sm" variant="danger" wire:click="cancel" wire:confirm="Cancel this order?">Cancel order</flux:button>
                    @endif


Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Symbol '$orderService' is declared but not used.


Your mission is to implement an entire shop system based on the specifications im specs/*. You must do in one go without stopping. You must use team mode! You must test everything via Pest (unit, and…


Now all remaining admin modules (customers, collections, inventory, discounts, pages, settings, navigation).


<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    use WithPagination;

    public string $search = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function render()
    {
        $customers = Customer::query()
            ->withCount('orders')
            ->withSum('orders as lifetime_value_amount', 'total_amount')
            ->when($this->search !== '', fn ($q) => $q->where('email', 'like', '%'.$this->search.'%')->orWhere('name', 'like', '%'.$this->search.'%'))
            ->orderByDesc('id')
            ->paginate(20);

        return view('livewire.admin.customers.index', compact('customers'))->title('Customers');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Customers/Index.php

<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    use WithPagination;

    public string $search = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function render()
    {
        $customers = Customer::query()
            ->withCount('orders')
            ->withSum('orders as lifetime_value_amount', 'total_amount')
            ->when($this->search !== '', fn ($q) => $q->where('email', 'like', '%'.$this->search.'%')->orWhere('name', 'like', '%'.$this->search.'%'))
            ->orderByDesc('id')
            ->paginate(20);

        return view('livewire.admin.customers.index', compact('customers'))->title('Customers');
    }
}


    <flux:heading size="xl">Customers</flux:heading>

    <flux:input wire:model.live.debounce.300ms="search" placeholder="Search by name or email..." icon="magnifying-glass" />

        @if ($customers->isEmpty())

No customers yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Name</flux:table.column>
                    <flux:table.column>Email</flux:table.column>
                    <flux:table.column>Orders</flux:table.column>
                    <flux:table.column>LTV</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($customers as $customer)
                        <flux:table.row>
                            <flux:table.cell>{{ $customer->name ?? 'No name' }}</flux:table.cell>
                            <flux:table.cell>{{ $customer->email }}</flux:table.cell>
                            <flux:table.cell>{{ $customer->orders_count }}</flux:table.cell>
                            <flux:table.cell>{{ number_format((int) ($customer->lifetime_value_amount ?? 0) / 100, 2) }}</flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif

{{ $customers->links() }}


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/customers/index.blade.php

    <flux:heading size="xl">Customers</flux:heading>

    <flux:input wire:model.live.debounce.300ms="search" placeholder="Search by name or email..." icon="magnifying-glass" />

        @if ($customers->isEmpty())

No customers yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Name</flux:table.column>
                    <flux:table.column>Email</flux:table.column>
                    <flux:table.column>Orders</flux:table.column>
                    <flux:table.column>LTV</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($customers as $customer)
                        <flux:table.row>
                            <flux:table.cell>{{ $customer->name ?? 'No name' }}</flux:table.cell>
                            <flux:table.cell>{{ $customer->email }}</flux:table.cell>
                            <flux:table.cell>{{ $customer->orders_count }}</flux:table.cell>
                            <flux:table.cell>{{ number_format((int) ($customer->lifetime_value_amount ?? 0) / 100, 2) }}</flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif

{{ $customers->links() }}


<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Show extends Component
{
    public Customer $customer;

    public function mount(Customer $customer): void
    {
        $this->customer = $customer->load('addresses', 'orders');
    }

    public function render()
    {
        return view('livewire.admin.customers.show')->title($this->customer->name ?? $this->customer->email);
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Customers/Show.php

<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Show extends Component
{
    public Customer $customer;

    public function mount(Customer $customer): void
    {
        $this->customer = $customer->load('addresses', 'orders');
    }

    public function render()
    {
        return view('livewire.admin.customers.show')->title($this->customer->name ?? $this->customer->email);
    }
}


    <flux:heading size="xl">{{ $customer->name ?? $customer->email }}</flux:heading>

            <flux:heading size="lg" class="mb-3">Profile</flux:heading>

Email

{{ $customer->email }}

Marketing opt-in

{{ $customer->marketing_opt_in ? 'Yes' : 'No' }}

Joined

{{ $customer->created_at?->format('Y-m-d') }}

            <flux:heading size="lg" class="mb-3">Orders</flux:heading>
            @if ($customer->orders->isEmpty())

No orders.

            @else

                    @foreach ($customer->orders as $order)

                            {{ $order->order_number }}
                            {{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}

                    @endforeach

            @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/customers/show.blade.php

    <flux:heading size="xl">{{ $customer->name ?? $customer->email }}</flux:heading>

            <flux:heading size="lg" class="mb-3">Profile</flux:heading>

Email

{{ $customer->email }}

Marketing opt-in

{{ $customer->marketing_opt_in ? 'Yes' : 'No' }}

Joined

{{ $customer->created_at?->format('Y-m-d') }}

            <flux:heading size="lg" class="mb-3">Orders</flux:heading>
            @if ($customer->orders->isEmpty())

No orders.

            @else

                    @foreach ($customer->orders as $order)

                            {{ $order->order_number }}
                            {{ $order->currency }} {{ number_format($order->total_amount / 100, 2) }}

                    @endforeach

            @endif


<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $collections = Collection::query()->withCount('products')->orderByDesc('updated_at')->get();

        return view('livewire.admin.collections.index', compact('collections'))->title('Collections');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Collections/Index.php

<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $collections = Collection::query()->withCount('products')->orderByDesc('updated_at')->get();

        return view('livewire.admin.collections.index', compact('collections'))->title('Collections');
    }
}


        <flux:heading size="xl">Collections</flux:heading>
        <flux:button href="{{ route('admin.collections.create') }}" variant="primary" icon="plus" wire:navigate>New collection</flux:button>

        @if ($collections->isEmpty())

No collections yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Title</flux:table.column>
                    <flux:table.column>Type</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column>Products</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($collections as $collection)
                        <flux:table.row>
                            <flux:table.cell>
{{ $collection->title }}
</flux:table.cell>
                            <flux:table.cell>{{ $collection->type->value }}</flux:table.cell>
                            <flux:table.cell><flux:badge size="sm" :color="$collection->status->value === 'active' ? 'emerald' : 'zinc'">{{ $collection->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell>{{ $collection->products_count }}</flux:table.cell>
                            <flux:table.cell>
                                <flux:button size="xs" variant="ghost" href="{{ route('admin.collections.edit', $collection) }}" wire:navigate>Edit</flux:button>
                            </flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/collections/index.blade.php

        <flux:heading size="xl">Collections</flux:heading>
        <flux:button href="{{ route('admin.collections.create') }}" variant="primary" icon="plus" wire:navigate>New collection</flux:button>

        @if ($collections->isEmpty())

No collections yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Title</flux:table.column>
                    <flux:table.column>Type</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column>Products</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($collections as $collection)
                        <flux:table.row>
                            <flux:table.cell>
{{ $collection->title }}
</flux:table.cell>
                            <flux:table.cell>{{ $collection->type->value }}</flux:table.cell>
                            <flux:table.cell><flux:badge size="sm" :color="$collection->status->value === 'active' ? 'emerald' : 'zinc'">{{ $collection->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell>{{ $collection->products_count }}</flux:table.cell>
                            <flux:table.cell>
                                <flux:button size="xs" variant="ghost" href="{{ route('admin.collections.edit', $collection) }}" wire:navigate>Edit</flux:button>
                            </flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use App\Models\Product;
use App\Support\HandleGenerator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Collection $collection = null;

    #[Validate('required|string|max:255')]
    public string $title = '';

    public string $handle = '';

    public string $descriptionHtml = '';

    public string $status = 'active';

    public string $type = 'manual';

    public array $productIds = [];

    public function mount(?Collection $collection = null): void
    {
        if ($collection?->exists) {
            $this->collection = $collection;
            $this->title = $collection->title;
            $this->handle = $collection->handle;
            $this->descriptionHtml = (string) $collection->description_html;
            $this->status = $collection->status->value;
            $this->type = $collection->type->value;
            $this->productIds = $collection->products()->pluck('products.id')->toArray();
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $storeId = app('current_store')->id;
        $data = [
            'store_id' => $storeId,
            'title' => $this->title,
            'handle' => $this->handle ?: HandleGenerator::generate($this->title, 'collections', $storeId, $this->collection?->id),
            'description_html' => $this->descriptionHtml ?: null,
            'status' => $this->status,
            'type' => $this->type,
        ];

        if ($this->collection) {
            $this->collection->update($data);
        } else {
            $this->collection = Collection::create($data);
        }

        $this->collection->products()->sync($this->productIds);

        session()->flash('success', 'Collection saved.');

        return $this->redirect(route('admin.collections.edit', $this->collection), navigate: true);
    }

    public function render()
    {
        $availableProducts = Product::query()->orderBy('title')->get(['id', 'title', 'handle']);

        return view('livewire.admin.collections.form', compact('availableProducts'))
            ->title($this->collection ? 'Edit collection' : 'New collection');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Collections/Form.php

<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use App\Models\Product;
use App\Support\HandleGenerator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Collection $collection = null;

    #[Validate('required|string|max:255')]
    public string $title = '';

    public string $handle = '';

    public string $descriptionHtml = '';

    public string $status = 'active';

    public string $type = 'manual';

    public array $productIds = [];

    public function mount(?Collection $collection = null): void
    {
        if ($collection?->exists) {
            $this->collection = $collection;
            $this->title = $collection->title;
            $this->handle = $collection->handle;
            $this->descriptionHtml = (string) $collection->description_html;
            $this->status = $collection->status->value;
            $this->type = $collection->type->value;
            $this->productIds = $collection->products()->pluck('products.id')->toArray();
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $storeId = app('current_store')->id;
        $data = [
            'store_id' => $storeId,
            'title' => $this->title,
            'handle' => $this->handle ?: HandleGenerator::generate($this->title, 'collections', $storeId, $this->collection?->id),
            'description_html' => $this->descriptionHtml ?: null,
            'status' => $this->status,
            'type' => $this->type,
        ];

        if ($this->collection) {
            $this->collection->update($data);
        } else {
            $this->collection = Collection::create($data);
        }

        $this->collection->products()->sync($this->productIds);

        session()->flash('success', 'Collection saved.');

        return $this->redirect(route('admin.collections.edit', $this->collection), navigate: true);
    }

    public function render()
    {
        $availableProducts = Product::query()->orderBy('title')->get(['id', 'title', 'handle']);

        return view('livewire.admin.collections.form', compact('availableProducts'))
            ->title($this->collection ? 'Edit collection' : 'New collection');
    }
}


    <flux:heading size="xl" class="mb-6">{{ $collection ? 'Edit collection' : 'New collection' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" icon="check-circle" class="mb-4" heading="{{ session('success') }}"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4">

                <flux:input label="Title" wire:model="title" required />
                <flux:input label="Handle" wire:model="handle" placeholder="Auto-generated if empty" />
                <flux:textarea label="Description" wire:model="descriptionHtml" rows="4" />
                <flux:select label="Status" wire:model="status">
                    <flux:select.option value="draft">Draft</flux:select.option>
                    <flux:select.option value="active">Active</flux:select.option>
                    <flux:select.option value="archived">Archived</flux:select.option>
                </flux:select>

            <flux:heading size="lg" class="mb-3">Products</flux:heading>

                @foreach ($availableProducts as $p)

                        <input type="checkbox" wire:model="productIds" value="{{ $p->id }}" class="rounded">
                        {{ $p->title }}
                        {{ $p->handle }}

                @endforeach

            <flux:button type="button" variant="ghost" href="{{ route('admin.collections.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/collections/form.blade.php

    <flux:heading size="xl" class="mb-6">{{ $collection ? 'Edit collection' : 'New collection' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" icon="check-circle" class="mb-4" heading="{{ session('success') }}"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4">

                <flux:input label="Title" wire:model="title" required />
                <flux:input label="Handle" wire:model="handle" placeholder="Auto-generated if empty" />
                <flux:textarea label="Description" wire:model="descriptionHtml" rows="4" />
                <flux:select label="Status" wire:model="status">
                    <flux:select.option value="draft">Draft</flux:select.option>
                    <flux:select.option value="active">Active</flux:select.option>
                    <flux:select.option value="archived">Archived</flux:select.option>
                </flux:select>

            <flux:heading size="lg" class="mb-3">Products</flux:heading>

                @foreach ($availableProducts as $p)

                        <input type="checkbox" wire:model="productIds" value="{{ $p->id }}" class="rounded">
                        {{ $p->title }}
                        {{ $p->handle }}

                @endforeach

            <flux:button type="button" variant="ghost" href="{{ route('admin.collections.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


<?php

namespace App\Livewire\Admin\Inventory;

use App\Models\InventoryItem;
use App\Services\Inventory\InventoryService;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public array $adjustments = [];

    public function adjust(int $itemId, InventoryService $service): void
    {
        $item = InventoryItem::find($itemId);
        if (! $item) {
            return;
        }

        $new = (int) ($this->adjustments[$itemId] ?? $item->quantity_on_hand);
        $service->adjustOnHand($item, $new);
        session()->flash('success', 'Inventory updated.');
    }

    public function render()
    {
        $items = InventoryItem::query()->with('variant.product')->get();

        return view('livewire.admin.inventory.index', compact('items'))->title('Inventory');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Inventory/Index.php

<?php

namespace App\Livewire\Admin\Inventory;

use App\Models\InventoryItem;
use App\Services\Inventory\InventoryService;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public array $adjustments = [];

    public function adjust(int $itemId, InventoryService $service): void
    {
        $item = InventoryItem::find($itemId);
        if (! $item) {
            return;
        }

        $new = (int) ($this->adjustments[$itemId] ?? $item->quantity_on_hand);
        $service->adjustOnHand($item, $new);
        session()->flash('success', 'Inventory updated.');
    }

    public function render()
    {
        $items = InventoryItem::query()->with('variant.product')->get();

        return view('livewire.admin.inventory.index', compact('items'))->title('Inventory');
    }
}


    <flux:heading size="xl">Inventory</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}"></flux:callout>
    @endif

        @if ($items->isEmpty())

No inventory records yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Product</flux:table.column>
                    <flux:table.column>SKU</flux:table.column>
                    <flux:table.column>On hand</flux:table.column>
                    <flux:table.column>Reserved</flux:table.column>
                    <flux:table.column>Available</flux:table.column>
                    <flux:table.column>Update</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($items as $item)
                        <flux:table.row>
                            <flux:table.cell>{{ $item->variant?->product?->title }}</flux:table.cell>
                            <flux:table.cell>{{ $item->variant?->sku }}</flux:table.cell>
                            <flux:table.cell>{{ $item->quantity_on_hand }}</flux:table.cell>
                            <flux:table.cell>{{ $item->quantity_reserved }}</flux:table.cell>
                            <flux:table.cell>{{ $item->available() }}</flux:table.cell>
                            <flux:table.cell>

                                    <input type="number" wire:model="adjustments.{{ $item->id }}" class="w-20 rounded border border-zinc-300 px-2 py-1 text-sm dark:border-zinc-700 dark:bg-zinc-900" />
                                    <flux:button size="xs" wire:click="adjust({{ $item->id }})">Set</flux:button>

                            </flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/inventory/index.blade.php

    <flux:heading size="xl">Inventory</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}"></flux:callout>
    @endif

        @if ($items->isEmpty())

No inventory records yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Product</flux:table.column>
                    <flux:table.column>SKU</flux:table.column>
                    <flux:table.column>On hand</flux:table.column>
                    <flux:table.column>Reserved</flux:table.column>
                    <flux:table.column>Available</flux:table.column>
                    <flux:table.column>Update</flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($items as $item)
                        <flux:table.row>
                            <flux:table.cell>{{ $item->variant?->product?->title }}</flux:table.cell>
                            <flux:table.cell>{{ $item->variant?->sku }}</flux:table.cell>
                            <flux:table.cell>{{ $item->quantity_on_hand }}</flux:table.cell>
                            <flux:table.cell>{{ $item->quantity_reserved }}</flux:table.cell>
                            <flux:table.cell>{{ $item->available() }}</flux:table.cell>
                            <flux:table.cell>

                                    <input type="number" wire:model="adjustments.{{ $item->id }}" class="w-20 rounded border border-zinc-300 px-2 py-1 text-sm dark:border-zinc-700 dark:bg-zinc-900" />
                                    <flux:button size="xs" wire:click="adjust({{ $item->id }})">Set</flux:button>

                            </flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


<?php

namespace App\Livewire\Admin\Discounts;

use App\Models\Discount;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $discounts = Discount::query()->orderByDesc('created_at')->get();

        return view('livewire.admin.discounts.index', compact('discounts'))->title('Discounts');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Index.php

<?php

namespace App\Livewire\Admin\Discounts;

use App\Models\Discount;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $discounts = Discount::query()->orderByDesc('created_at')->get();

        return view('livewire.admin.discounts.index', compact('discounts'))->title('Discounts');
    }
}


        <flux:heading size="xl">Discounts</flux:heading>
        <flux:button href="{{ route('admin.discounts.create') }}" variant="primary" icon="plus" wire:navigate>New discount</flux:button>

        @if ($discounts->isEmpty())

No discounts yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Code</flux:table.column>
                    <flux:table.column>Value</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column>Usage</flux:table.column>
                    <flux:table.column>Expires</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($discounts as $discount)
                        <flux:table.row>
                            <flux:table.cell>
{{ $discount->code ?? '—' }}
</flux:table.cell>
                            <flux:table.cell>
                                @switch($discount->value_type->value)
                                    @case('percent') {{ $discount->value_amount }}% off @break
                                    @case('fixed') {{ number_format($discount->value_amount / 100, 2) }} off @break
                                    @case('free_shipping') Free shipping @break
                                @endswitch
                            </flux:table.cell>
                            <flux:table.cell><flux:badge size="sm" :color="$discount->status->value === 'active' ? 'emerald' : 'zinc'">{{ $discount->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell>{{ $discount->usage_count }} / {{ $discount->usage_limit ?? '∞' }}</flux:table.cell>
                            <flux:table.cell>{{ $discount->ends_at?->format('Y-m-d') ?? '—' }}</flux:table.cell>
                            <flux:table.cell><flux:button size="xs" variant="ghost" href="{{ route('admin.discounts.edit', $discount) }}" wire:navigate>Edit</flux:button></flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/index.blade.php

        <flux:heading size="xl">Discounts</flux:heading>
        <flux:button href="{{ route('admin.discounts.create') }}" variant="primary" icon="plus" wire:navigate>New discount</flux:button>

        @if ($discounts->isEmpty())

No discounts yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Code</flux:table.column>
                    <flux:table.column>Value</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column>Usage</flux:table.column>
                    <flux:table.column>Expires</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($discounts as $discount)
                        <flux:table.row>
                            <flux:table.cell>
{{ $discount->code ?? '—' }}
</flux:table.cell>
                            <flux:table.cell>
                                @switch($discount->value_type->value)
                                    @case('percent') {{ $discount->value_amount }}% off @break
                                    @case('fixed') {{ number_format($discount->value_amount / 100, 2) }} off @break
                                    @case('free_shipping') Free shipping @break
                                @endswitch
                            </flux:table.cell>
                            <flux:table.cell><flux:badge size="sm" :color="$discount->status->value === 'active' ? 'emerald' : 'zinc'">{{ $discount->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell>{{ $discount->usage_count }} / {{ $discount->usage_limit ?? '∞' }}</flux:table.cell>
                            <flux:table.cell>{{ $discount->ends_at?->format('Y-m-d') ?? '—' }}</flux:table.cell>
                            <flux:table.cell><flux:button size="xs" variant="ghost" href="{{ route('admin.discounts.edit', $discount) }}" wire:navigate>Edit</flux:button></flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


<?php

namespace App\Livewire\Admin\Discounts;

use App\Models\Discount;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Discount $discount = null;

    #[Validate('required|string|max:50')]
    public string $code = '';

    public string $type = 'code';

    public string $valueType = 'percent';

    public int $valueAmount = 10;

    public ?string $startsAt = null;

    public ?string $endsAt = null;

    public ?int $usageLimit = null;

    public string $status = 'active';

    public function mount(?Discount $discount = null): void
    {
        if ($discount?->exists) {
            $this->discount = $discount;
            $this->code = (string) $discount->code;
            $this->type = $discount->type->value;
            $this->valueType = $discount->value_type->value;
            $this->valueAmount = $discount->value_amount;
            $this->startsAt = $discount->starts_at?->format('Y-m-d');
            $this->endsAt = $discount->ends_at?->format('Y-m-d');
            $this->usageLimit = $discount->usage_limit;
            $this->status = $discount->status->value;
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $data = [
            'store_id' => app('current_store')->id,
            'type' => $this->type,
            'code' => strtoupper($this->code),
            'value_type' => $this->valueType,
            'value_amount' => $this->valueAmount,
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt,
            'usage_limit' => $this->usageLimit,
            'status' => $this->status,
            'rules_json' => [],
        ];

        if ($this->discount) {
            $this->discount->update($data);
        } else {
            $this->discount = Discount::create($data);
        }

        session()->flash('success', 'Discount saved.');

        return $this->redirect(route('admin.discounts.edit', $this->discount), navigate: true);
    }

    public function render()
    {
        return view('livewire.admin.discounts.form')
            ->title($this->discount ? 'Edit discount' : 'New discount');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Form.php

<?php

namespace App\Livewire\Admin\Discounts;

use App\Models\Discount;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Discount $discount = null;

    #[Validate('required|string|max:50')]
    public string $code = '';

    public string $type = 'code';

    public string $valueType = 'percent';

    public int $valueAmount = 10;

    public ?string $startsAt = null;

    public ?string $endsAt = null;

    public ?int $usageLimit = null;

    public string $status = 'active';

    public function mount(?Discount $discount = null): void
    {
        if ($discount?->exists) {
            $this->discount = $discount;
            $this->code = (string) $discount->code;
            $this->type = $discount->type->value;
            $this->valueType = $discount->value_type->value;
            $this->valueAmount = $discount->value_amount;
            $this->startsAt = $discount->starts_at?->format('Y-m-d');
            $this->endsAt = $discount->ends_at?->format('Y-m-d');
            $this->usageLimit = $discount->usage_limit;
            $this->status = $discount->status->value;
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $data = [
            'store_id' => app('current_store')->id,
            'type' => $this->type,
            'code' => strtoupper($this->code),
            'value_type' => $this->valueType,
            'value_amount' => $this->valueAmount,
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt,
            'usage_limit' => $this->usageLimit,
            'status' => $this->status,
            'rules_json' => [],
        ];

        if ($this->discount) {
            $this->discount->update($data);
        } else {
            $this->discount = Discount::create($data);
        }

        session()->flash('success', 'Discount saved.');

        return $this->redirect(route('admin.discounts.edit', $this->discount), navigate: true);
    }

    public function render()
    {
        return view('livewire.admin.discounts.form')
            ->title($this->discount ? 'Edit discount' : 'New discount');
    }
}


    <flux:heading size="xl" class="mb-6">{{ $discount ? 'Edit discount' : 'New discount' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Code" wire:model="code" required />

            <flux:select label="Value type" wire:model="valueType">
                <flux:select.option value="percent">Percent</flux:select.option>
                <flux:select.option value="fixed">Fixed (cents)</flux:select.option>
                <flux:select.option value="free_shipping">Free shipping</flux:select.option>
            </flux:select>
            <flux:input type="number" label="Value amount" wire:model="valueAmount" />

            <flux:input type="date" label="Starts" wire:model="startsAt" />
            <flux:input type="date" label="Ends" wire:model="endsAt" />

        <flux:input type="number" label="Usage limit (blank for unlimited)" wire:model="usageLimit" />
        <flux:select label="Status" wire:model="status">
            <flux:select.option value="draft">Draft</flux:select.option>
            <flux:select.option value="active">Active</flux:select.option>
            <flux:select.option value="disabled">Disabled</flux:select.option>
        </flux:select>

            <flux:button type="button" variant="ghost" href="{{ route('admin.discounts.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/discounts/form.blade.php

    <flux:heading size="xl" class="mb-6">{{ $discount ? 'Edit discount' : 'New discount' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Code" wire:model="code" required />

            <flux:select label="Value type" wire:model="valueType">
                <flux:select.option value="percent">Percent</flux:select.option>
                <flux:select.option value="fixed">Fixed (cents)</flux:select.option>
                <flux:select.option value="free_shipping">Free shipping</flux:select.option>
            </flux:select>
            <flux:input type="number" label="Value amount" wire:model="valueAmount" />

            <flux:input type="date" label="Starts" wire:model="startsAt" />
            <flux:input type="date" label="Ends" wire:model="endsAt" />

        <flux:input type="number" label="Usage limit (blank for unlimited)" wire:model="usageLimit" />
        <flux:select label="Status" wire:model="status">
            <flux:select.option value="draft">Draft</flux:select.option>
            <flux:select.option value="active">Active</flux:select.option>
            <flux:select.option value="disabled">Disabled</flux:select.option>
        </flux:select>

            <flux:button type="button" variant="ghost" href="{{ route('admin.discounts.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


<?php

namespace App\Livewire\Admin\Pages;

use App\Models\Page;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $pages = Page::query()->orderByDesc('updated_at')->get();

        return view('livewire.admin.pages.index', compact('pages'))->title('Pages');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Pages/Index.php

<?php

namespace App\Livewire\Admin\Pages;

use App\Models\Page;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public function render()
    {
        $pages = Page::query()->orderByDesc('updated_at')->get();

        return view('livewire.admin.pages.index', compact('pages'))->title('Pages');
    }
}


        <flux:heading size="xl">Pages</flux:heading>
        <flux:button href="{{ route('admin.pages.create') }}" variant="primary" icon="plus" wire:navigate>New page</flux:button>

        @if ($pages->isEmpty())

No pages yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Title</flux:table.column>
                    <flux:table.column>Handle</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($pages as $page)
                        <flux:table.row>
                            <flux:table.cell>{{ $page->title }}</flux:table.cell>
                            <flux:table.cell>/{{ $page->handle }}</flux:table.cell>
                            <flux:table.cell><flux:badge size="sm">{{ $page->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell><flux:button size="xs" variant="ghost" href="{{ route('admin.pages.edit', $page) }}" wire:navigate>Edit</flux:button></flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/pages/index.blade.php

        <flux:heading size="xl">Pages</flux:heading>
        <flux:button href="{{ route('admin.pages.create') }}" variant="primary" icon="plus" wire:navigate>New page</flux:button>

        @if ($pages->isEmpty())

No pages yet.

        @else
            <flux:table>
                <flux:table.columns>
                    <flux:table.column>Title</flux:table.column>
                    <flux:table.column>Handle</flux:table.column>
                    <flux:table.column>Status</flux:table.column>
                    <flux:table.column></flux:table.column>
                </flux:table.columns>
                <flux:table.rows>
                    @foreach ($pages as $page)
                        <flux:table.row>
                            <flux:table.cell>{{ $page->title }}</flux:table.cell>
                            <flux:table.cell>/{{ $page->handle }}</flux:table.cell>
                            <flux:table.cell><flux:badge size="sm">{{ $page->status->value }}</flux:badge></flux:table.cell>
                            <flux:table.cell><flux:button size="xs" variant="ghost" href="{{ route('admin.pages.edit', $page) }}" wire:navigate>Edit</flux:button></flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        @endif


<?php

namespace App\Livewire\Admin\Pages;

use App\Models\Page;
use App\Support\HandleGenerator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Page $page = null;

    #[Validate('required|string|max:255')]
    public string $title = '';

    public string $handle = '';

    public string $bodyHtml = '';

    public string $status = 'published';

    public function mount(?Page $page = null): void
    {
        if ($page?->exists) {
            $this->page = $page;
            $this->title = $page->title;
            $this->handle = $page->handle;
            $this->bodyHtml = (string) $page->body_html;
            $this->status = $page->status->value;
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $storeId = app('current_store')->id;
        $data = [
            'store_id' => $storeId,
            'title' => $this->title,
            'handle' => $this->handle ?: HandleGenerator::generate($this->title, 'pages', $storeId, $this->page?->id),
            'body_html' => $this->bodyHtml ?: null,
            'status' => $this->status,
            'published_at' => $this->status === 'published' ? now() : null,
        ];

        if ($this->page) {
            $this->page->update($data);
        } else {
            $this->page = Page::create($data);
        }

        session()->flash('success', 'Page saved.');

        return $this->redirect(route('admin.pages.edit', $this->page), navigate: true);
    }

    public function render()
    {
        return view('livewire.admin.pages.form')->title($this->page ? 'Edit page' : 'New page');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Pages/Form.php

<?php

namespace App\Livewire\Admin\Pages;

use App\Models\Page;
use App\Support\HandleGenerator;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Form extends Component
{
    public ?Page $page = null;

    #[Validate('required|string|max:255')]
    public string $title = '';

    public string $handle = '';

    public string $bodyHtml = '';

    public string $status = 'published';

    public function mount(?Page $page = null): void
    {
        if ($page?->exists) {
            $this->page = $page;
            $this->title = $page->title;
            $this->handle = $page->handle;
            $this->bodyHtml = (string) $page->body_html;
            $this->status = $page->status->value;
        }
    }

    public function save(): mixed
    {
        $this->validate();

        $storeId = app('current_store')->id;
        $data = [
            'store_id' => $storeId,
            'title' => $this->title,
            'handle' => $this->handle ?: HandleGenerator::generate($this->title, 'pages', $storeId, $this->page?->id),
            'body_html' => $this->bodyHtml ?: null,
            'status' => $this->status,
            'published_at' => $this->status === 'published' ? now() : null,
        ];

        if ($this->page) {
            $this->page->update($data);
        } else {
            $this->page = Page::create($data);
        }

        session()->flash('success', 'Page saved.');

        return $this->redirect(route('admin.pages.edit', $this->page), navigate: true);
    }

    public function render()
    {
        return view('livewire.admin.pages.form')->title($this->page ? 'Edit page' : 'New page');
    }
}


    <flux:heading size="xl" class="mb-6">{{ $page ? 'Edit page' : 'New page' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Title" wire:model="title" required />
        <flux:input label="Handle" wire:model="handle" placeholder="Auto-generated if empty" />
        <flux:textarea label="Body (HTML)" wire:model="bodyHtml" rows="10" />
        <flux:select label="Status" wire:model="status">
            <flux:select.option value="draft">Draft</flux:select.option>
            <flux:select.option value="published">Published</flux:select.option>
            <flux:select.option value="archived">Archived</flux:select.option>
        </flux:select>

            <flux:button type="button" variant="ghost" href="{{ route('admin.pages.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/pages/form.blade.php

    <flux:heading size="xl" class="mb-6">{{ $page ? 'Edit page' : 'New page' }}</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Title" wire:model="title" required />
        <flux:input label="Handle" wire:model="handle" placeholder="Auto-generated if empty" />
        <flux:textarea label="Body (HTML)" wire:model="bodyHtml" rows="10" />
        <flux:select label="Status" wire:model="status">
            <flux:select.option value="draft">Draft</flux:select.option>
            <flux:select.option value="published">Published</flux:select.option>
            <flux:select.option value="archived">Archived</flux:select.option>
        </flux:select>

            <flux:button type="button" variant="ghost" href="{{ route('admin.pages.index') }}" wire:navigate>Cancel</flux:button>
            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


<?php

namespace App\Livewire\Admin\Navigation;

use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public string $itemLabel = '';

    public string $itemUrl = '';

    public ?int $menuId = null;

    public function addItem(): void
    {
        if (! $this->menuId || $this->itemLabel === '') {
            return;
        }

        NavigationItem::create([
            'menu_id' => $this->menuId,
            'type' => 'link',
            'label' => $this->itemLabel,
            'url' => $this->itemUrl ?: '/',
            'position' => NavigationItem::where('menu_id', $this->menuId)->count(),
        ]);

        $this->itemLabel = '';
        $this->itemUrl = '';
    }

    public function deleteItem(int $id): void
    {
        NavigationItem::where('id', $id)->delete();
    }

    public function render()
    {
        $menus = NavigationMenu::query()->with('items')->get();

        if (! $this->menuId && $menus->isNotEmpty()) {
            $this->menuId = $menus->first()->id;
        }

        return view('livewire.admin.navigation.index', compact('menus'))->title('Navigation');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Navigation/Index.php

<?php

namespace App\Livewire\Admin\Navigation;

use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Index extends Component
{
    public string $itemLabel = '';

    public string $itemUrl = '';

    public ?int $menuId = null;

    public function addItem(): void
    {
        if (! $this->menuId || $this->itemLabel === '') {
            return;
        }

        NavigationItem::create([
            'menu_id' => $this->menuId,
            'type' => 'link',
            'label' => $this->itemLabel,
            'url' => $this->itemUrl ?: '/',
            'position' => NavigationItem::where('menu_id', $this->menuId)->count(),
        ]);

        $this->itemLabel = '';
        $this->itemUrl = '';
    }

    public function deleteItem(int $id): void
    {
        NavigationItem::where('id', $id)->delete();
    }

    public function render()
    {
        $menus = NavigationMenu::query()->with('items')->get();

        if (! $this->menuId && $menus->isNotEmpty()) {
            $this->menuId = $menus->first()->id;
        }

        return view('livewire.admin.navigation.index', compact('menus'))->title('Navigation');
    }
}


    <flux:heading size="xl">Navigation</flux:heading>
    @if ($menus->isEmpty())
        <flux:callout>No navigation menus yet. Seed demo data to create the Main menu.</flux:callout>
    @else
        <flux:select wire:model.live="menuId" class="w-fit">
            @foreach ($menus as $menu)
                <flux:select.option value="{{ $menu->id }}">{{ $menu->title }}</flux:select.option>
            @endforeach
        </flux:select>

        @php($activeMenu = $menus->firstWhere('id', $menuId))
        @if ($activeMenu)

                <flux:heading size="lg" class="mb-4">Items in {{ $activeMenu->title }}</flux:heading>
                @if ($activeMenu->items->isEmpty())

No items yet.

                @else

                        @foreach ($activeMenu->items as $item)

{{ $item->label }} → {{ $item->url }}

                                <flux:button size="xs" variant="ghost" wire:click="deleteItem({{ $item->id }})">Remove</flux:button>

                        @endforeach

                @endif

                <form wire:submit="addItem" class="mt-4 flex items-end gap-2">
                    <flux:input size="sm" label="Label" wire:model="itemLabel" />
                    <flux:input size="sm" label="URL" wire:model="itemUrl" placeholder="/shop" />
                    <flux:button type="submit" size="sm" variant="primary">Add</flux:button>
                </form>

        @endif
    @endif


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/navigation/index.blade.php

    <flux:heading size="xl">Navigation</flux:heading>
    @if ($menus->isEmpty())
        <flux:callout>No navigation menus yet. Seed demo data to create the Main menu.</flux:callout>
    @else
        <flux:select wire:model.live="menuId" class="w-fit">
            @foreach ($menus as $menu)
                <flux:select.option value="{{ $menu->id }}">{{ $menu->title }}</flux:select.option>
            @endforeach
        </flux:select>

        @php($activeMenu = $menus->firstWhere('id', $menuId))
        @if ($activeMenu)

                <flux:heading size="lg" class="mb-4">Items in {{ $activeMenu->title }}</flux:heading>
                @if ($activeMenu->items->isEmpty())

No items yet.

                @else

                        @foreach ($activeMenu->items as $item)

{{ $item->label }} → {{ $item->url }}

                                <flux:button size="xs" variant="ghost" wire:click="deleteItem({{ $item->id }})">Remove</flux:button>

                        @endforeach

                @endif

                <form wire:submit="addItem" class="mt-4 flex items-end gap-2">
                    <flux:input size="sm" label="Label" wire:model="itemLabel" />
                    <flux:input size="sm" label="URL" wire:model="itemUrl" placeholder="/shop" />
                    <flux:button type="submit" size="sm" variant="primary">Add</flux:button>
                </form>

        @endif
    @endif


<?php

namespace App\Livewire\Admin\Settings;

use App\Models\ShippingRate;
use App\Models\ShippingZone;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Shipping extends Component
{
    public string $zoneName = '';

    public string $zoneCountries = 'DE,AT,CH';

    public ?int $zoneId = null;

    public string $rateName = '';

    public int $rateAmount = 499;

    public function addZone(): void
    {
        if ($this->zoneName === '') {
            return;
        }

        ShippingZone::create([
            'store_id' => app('current_store')->id,
            'name' => $this->zoneName,
            'countries_json' => array_map('trim', explode(',', $this->zoneCountries)),
            'regions_json' => [],
        ]);

        $this->zoneName = '';
        $this->zoneCountries = 'DE,AT,CH';
    }

    public function addRate(int $zoneId): void
    {
        if ($this->rateName === '') {
            return;
        }

        ShippingRate::create([
            'zone_id' => $zoneId,
            'name' => $this->rateName,
            'type' => 'flat',
            'config_json' => ['amount' => $this->rateAmount],
            'is_active' => true,
        ]);

        $this->rateName = '';
        $this->rateAmount = 499;
    }

    public function deleteRate(int $rateId): void
    {
        ShippingRate::where('id', $rateId)->delete();
    }

    public function render()
    {
        $zones = ShippingZone::query()->with('rates')->get();

        return view('livewire.admin.settings.shipping', compact('zones'))->title('Shipping');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Settings/Shipping.php

<?php

namespace App\Livewire\Admin\Settings;

use App\Models\ShippingRate;
use App\Models\ShippingZone;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Shipping extends Component
{
    public string $zoneName = '';

    public string $zoneCountries = 'DE,AT,CH';

    public ?int $zoneId = null;

    public string $rateName = '';

    public int $rateAmount = 499;

    public function addZone(): void
    {
        if ($this->zoneName === '') {
            return;
        }

        ShippingZone::create([
            'store_id' => app('current_store')->id,
            'name' => $this->zoneName,
            'countries_json' => array_map('trim', explode(',', $this->zoneCountries)),
            'regions_json' => [],
        ]);

        $this->zoneName = '';
        $this->zoneCountries = 'DE,AT,CH';
    }

    public function addRate(int $zoneId): void
    {
        if ($this->rateName === '') {
            return;
        }

        ShippingRate::create([
            'zone_id' => $zoneId,
            'name' => $this->rateName,
            'type' => 'flat',
            'config_json' => ['amount' => $this->rateAmount],
            'is_active' => true,
        ]);

        $this->rateName = '';
        $this->rateAmount = 499;
    }

    public function deleteRate(int $rateId): void
    {
        ShippingRate::where('id', $rateId)->delete();
    }

    public function render()
    {
        $zones = ShippingZone::query()->with('rates')->get();

        return view('livewire.admin.settings.shipping', compact('zones'))->title('Shipping');
    }
}


    <flux:heading size="xl">Shipping</flux:heading>

    <form wire:submit="addZone" class="flex items-end gap-2 rounded-xl bg-white p-4 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input size="sm" label="Zone name" wire:model="zoneName" />
        <flux:input size="sm" label="Countries (CSV)" wire:model="zoneCountries" />
        <flux:button type="submit" size="sm" variant="primary">Add zone</flux:button>
    </form>

    @foreach ($zones as $zone)

                <flux:heading size="lg">{{ $zone->name }}</flux:heading>
                <flux:subheading>{{ implode(', ', $zone->countries_json ?? []) }}</flux:subheading>

                @foreach ($zone->rates as $rate)

{{ $rate->name }}

{{ $rate->type->value }}

                            {{ $currentStore->default_currency }} {{ number_format(($rate->config_json['amount'] ?? 0) / 100, 2) }}
                            <flux:button size="xs" variant="ghost" wire:click="deleteRate({{ $rate->id }})">Remove</flux:button>

                @endforeach

            <form wire:submit="addRate({{ $zone->id }})" class="mt-3 flex items-end gap-2">
                <flux:input size="sm" label="Rate name" wire:model="rateName" />
                <flux:input size="sm" type="number" label="Amount (cents)" wire:model="rateAmount" />
                <flux:button type="submit" size="sm">Add rate</flux:button>
            </form>

    @endforeach


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/settings/shipping.blade.php

    <flux:heading size="xl">Shipping</flux:heading>

    <form wire:submit="addZone" class="flex items-end gap-2 rounded-xl bg-white p-4 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input size="sm" label="Zone name" wire:model="zoneName" />
        <flux:input size="sm" label="Countries (CSV)" wire:model="zoneCountries" />
        <flux:button type="submit" size="sm" variant="primary">Add zone</flux:button>
    </form>

    @foreach ($zones as $zone)

                <flux:heading size="lg">{{ $zone->name }}</flux:heading>
                <flux:subheading>{{ implode(', ', $zone->countries_json ?? []) }}</flux:subheading>

                @foreach ($zone->rates as $rate)

{{ $rate->name }}

{{ $rate->type->value }}

                            {{ $currentStore->default_currency }} {{ number_format(($rate->config_json['amount'] ?? 0) / 100, 2) }}
                            <flux:button size="xs" variant="ghost" wire:click="deleteRate({{ $rate->id }})">Remove</flux:button>

                @endforeach

            <form wire:submit="addRate({{ $zone->id }})" class="mt-3 flex items-end gap-2">
                <flux:input size="sm" label="Rate name" wire:model="rateName" />
                <flux:input size="sm" type="number" label="Amount (cents)" wire:model="rateAmount" />
                <flux:button type="submit" size="sm">Add rate</flux:button>
            </form>

    @endforeach


<?php

namespace App\Livewire\Admin\Settings;

use App\Models\TaxSettings;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Taxes extends Component
{
    public string $mode = 'manual';

    public float $defaultRate = 0.19;

    public bool $pricesIncludeTax = false;

    public function mount(): void
    {
        $settings = TaxSettings::find(app('current_store')->id);
        if ($settings) {
            $this->mode = $settings->mode->value;
            $this->defaultRate = (float) ($settings->config_json['default_rate'] ?? 0.19);
            $this->pricesIncludeTax = $settings->prices_include_tax;
        }
    }

    public function save(): void
    {
        TaxSettings::updateOrCreate(
            ['store_id' => app('current_store')->id],
            [
                'mode' => $this->mode,
                'provider' => null,
                'prices_include_tax' => $this->pricesIncludeTax,
                'config_json' => ['default_rate' => $this->defaultRate],
            ],
        );

        session()->flash('success', 'Tax settings saved.');
    }

    public function render()
    {
        return view('livewire.admin.settings.taxes')->title('Taxes');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Settings/Taxes.php

<?php

namespace App\Livewire\Admin\Settings;

use App\Models\TaxSettings;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class Taxes extends Component
{
    public string $mode = 'manual';

    public float $defaultRate = 0.19;

    public bool $pricesIncludeTax = false;

    public function mount(): void
    {
        $settings = TaxSettings::find(app('current_store')->id);
        if ($settings) {
            $this->mode = $settings->mode->value;
            $this->defaultRate = (float) ($settings->config_json['default_rate'] ?? 0.19);
            $this->pricesIncludeTax = $settings->prices_include_tax;
        }
    }

    public function save(): void
    {
        TaxSettings::updateOrCreate(
            ['store_id' => app('current_store')->id],
            [
                'mode' => $this->mode,
                'provider' => null,
                'prices_include_tax' => $this->pricesIncludeTax,
                'config_json' => ['default_rate' => $this->defaultRate],
            ],
        );

        session()->flash('success', 'Tax settings saved.');
    }

    public function render()
    {
        return view('livewire.admin.settings.taxes')->title('Taxes');
    }
}


    <flux:heading size="xl" class="mb-6">Taxes</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:select label="Mode" wire:model="mode">
            <flux:select.option value="manual">Manual</flux:select.option>
            <flux:select.option value="provider">Provider</flux:select.option>
        </flux:select>
        <flux:input type="number" step="0.001" label="Default rate (e.g. 0.19 for 19%)" wire:model="defaultRate" />
        <flux:checkbox label="Prices include tax" wire:model="pricesIncludeTax" />

            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/settings/taxes.blade.php

    <flux:heading size="xl" class="mb-6">Taxes</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:select label="Mode" wire:model="mode">
            <flux:select.option value="manual">Manual</flux:select.option>
            <flux:select.option value="provider">Provider</flux:select.option>
        </flux:select>
        <flux:input type="number" step="0.001" label="Default rate (e.g. 0.19 for 19%)" wire:model="defaultRate" />
        <flux:checkbox label="Prices include tax" wire:model="pricesIncludeTax" />

            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


<?php

namespace App\Livewire\Admin\Settings;

use App\Models\Store;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class General extends Component
{
    public string $storeName = '';

    public string $defaultCurrency = 'EUR';

    public string $defaultLocale = 'en';

    public string $timezone = 'Europe/Berlin';

    public function mount(): void
    {
        $store = app('current_store');
        $this->storeName = $store->name;
        $this->defaultCurrency = $store->default_currency;
        $this->defaultLocale = $store->default_locale;
        $this->timezone = $store->timezone;
    }

    public function save(): void
    {
        Store::query()->where('id', app('current_store')->id)->update([
            'name' => $this->storeName,
            'default_currency' => $this->defaultCurrency,
            'default_locale' => $this->defaultLocale,
            'timezone' => $this->timezone,
        ]);

        session()->flash('success', 'General settings saved.');
    }

    public function render()
    {
        return view('livewire.admin.settings.general')->title('General settings');
    }
}


File created successfully at: /Users/fabianwesner/Herd/shop/app/Livewire/Admin/Settings/General.php

<?php

namespace App\Livewire\Admin\Settings;

use App\Models\Store;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('components.layouts.admin')]
class General extends Component
{
    public string $storeName = '';

    public string $defaultCurrency = 'EUR';

    public string $defaultLocale = 'en';

    public string $timezone = 'Europe/Berlin';

    public function mount(): void
    {
        $store = app('current_store');
        $this->storeName = $store->name;
        $this->defaultCurrency = $store->default_currency;
        $this->defaultLocale = $store->default_locale;
        $this->timezone = $store->timezone;
    }

    public function save(): void
    {
        Store::query()->where('id', app('current_store')->id)->update([
            'name' => $this->storeName,
            'default_currency' => $this->defaultCurrency,
            'default_locale' => $this->defaultLocale,
            'timezone' => $this->timezone,
        ]);

        session()->flash('success', 'General settings saved.');
    }

    public function render()
    {
        return view('livewire.admin.settings.general')->title('General settings');
    }
}


    <flux:heading size="xl" class="mb-6">General settings</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Store name" wire:model="storeName" />

            <flux:input label="Currency" wire:model="defaultCurrency" />
            <flux:input label="Locale" wire:model="defaultLocale" />
            <flux:input label="Timezone" wire:model="timezone" />

            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


File created successfully at: /Users/fabianwesner/Herd/shop/resources/views/livewire/admin/settings/general.blade.php

    <flux:heading size="xl" class="mb-6">General settings</flux:heading>
    @if (session('success'))
        <flux:callout variant="success" heading="{{ session('success') }}" class="mb-4"></flux:callout>
    @endif
    <form wire:submit="save" class="flex flex-col gap-4 rounded-xl bg-white p-6 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
        <flux:input label="Store name" wire:model="storeName" />

            <flux:input label="Currency" wire:model="defaultCurrency" />
            <flux:input label="Locale" wire:model="defaultLocale" />
            <flux:input label="Timezone" wire:model="timezone" />

            <flux:button type="submit" variant="primary">Save</flux:button>

    </form>


Phase 6: Customer accounts

Customer login, register, forgot password, dashboard, orders list, order detail, addresses. Livewire components for storefront account.

Phase 7: Admin panel

Admin Dashboard, Products (list/create/edit), Collections, Inventory, Orders, Customers, Discounts, Settings (shipping, taxes, general), Pages, Navigation, Themes.

Phase 1: Foundation — migrations, enums, models, tenancy, auth

Create config updates, all foundation migrations (organizations, stores, store_domains, store_users, store_settings, customers, customer_addresses), enums, models with BelongsToStore trait + StoreScope, ResolveStore middleware, admin auth login/logout, customer auth provider.

Phase 2: Catalog — products, variants, inventory, collections, media

Migrations + models + services for products, product_options, product_option_values, product_variants, variant_option_values, inventory_items, collections, collection_products, product_media. Includes ProductService, InventoryService, HandleGenerator.

Phase 3: Themes + storefront layout

Theme, theme_files, theme_settings, pages, navigation_menus, navigation_items migrations and models. Blade storefront layout + Livewire components (Home, Collections, Products, Cart, Search, Pages).

Phase 8-10: Search (FTS), analytics, apps/webhooks stubs

SQLite FTS5 search index, analytics event tracking + daily rollups, apps/installations/webhook_subscriptions/webhook_deliveries tables and admin UI.

Phase 4: Cart, checkout, discounts, shipping, taxes

Migrations + models for carts, cart_lines, checkouts, shipping_zones, shipping_rates, tax_settings, discounts. Services: CartService, PricingService, ShippingService, TaxService, DiscountService.

Phase 5: Payments, orders, fulfillment

Migrations + models for orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. MockPaymentProvider, OrderService, CheckoutCompletionService, FulfillmentService, RefundService.

Phase 11-12: Seeders, Pest tests, Playwright E2E

Database seeders (demo store, demo products, demo orders). Pest unit + feature tests for each module. Playwright browser tests for customer journey + admin operations.


Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.

Undefined method 'title'.


Now the routes file and the big demo seeder.