Add Products page backed by the CRM products API + storefront catalog picker
Deploy Ladill Merchant / deploy (push) Successful in 30s

- New Products page (merchant.products.*) with full CRUD proxied to the Ladill
  CRM products API via a new CrmClient + config/crm.php (owner-scoped, type=product).
- Sidebar gains a Products entry.
- The new storefront form loads the merchant's catalog: each shop/menu section
  gets an "Add from products…" picker that drops a CRM product in as an item
  (name, price, description). Catalog fetch is resilient — empty if CRM is down.

Wires CRM_API_URL + CRM_API_KEY_MERCHANT on the merchant env (matches CRM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
isaacclad
2026-06-24 07:32:58 +00:00
co-authored by Claude Opus 4.8
parent 08d3c99996
commit b19e2654a1
12 changed files with 435 additions and 9 deletions
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Merchant;
use App\Http\Controllers\Controller;
use App\Services\Crm\CrmClient;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* Products page fully backed by the Ladill CRM products API. The merchant's
* catalog lives in CRM (owner-scoped); this controller proxies CRUD to it.
*/
class ProductController extends Controller
{
private function crm(): CrmClient
{
return CrmClient::for((string) ladill_account()->public_id);
}
public function index(Request $request): View
{
$search = trim((string) $request->query('search', ''));
$response = $this->crm()->products([
'type' => 'product',
'per_page' => 100,
'search' => $search !== '' ? $search : null,
]);
return view('merchant.products.index', [
'products' => (array) ($response['data'] ?? []),
'search' => $search,
]);
}
public function create(): View
{
return view('merchant.products.create');
}
public function store(Request $request): RedirectResponse
{
$this->crm()->createProduct($this->payload($request));
return redirect()->route('merchant.products.index')->with('success', 'Product created.');
}
public function edit(string $product): View
{
return view('merchant.products.edit', ['product' => $this->crm()->product($product)]);
}
public function update(Request $request, string $product): RedirectResponse
{
$this->crm()->updateProduct($product, $this->payload($request));
return redirect()->route('merchant.products.index')->with('success', 'Product updated.');
}
public function destroy(string $product): RedirectResponse
{
$this->crm()->deleteProduct($product);
return redirect()->route('merchant.products.index')->with('success', 'Product deleted.');
}
/** @return array<string,mixed> */
private function payload(Request $request): array
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'sku' => ['nullable', 'string', 'max:80'],
'description' => ['nullable', 'string', 'max:5000'],
'unit_price' => ['nullable', 'numeric', 'min:0'],
'currency' => ['nullable', 'string', 'size:3'],
'tax_rate' => ['nullable', 'numeric', 'min:0', 'max:100'],
'active' => ['nullable', 'boolean'],
]);
return [
'name' => $data['name'],
'sku' => $data['sku'] ?? null,
'type' => 'product',
'description' => $data['description'] ?? null,
'unit_price_minor' => (int) round(((float) ($data['unit_price'] ?? 0)) * 100),
'currency' => strtoupper((string) ($data['currency'] ?? config('crm.default_currency', 'GHS'))),
'tax_rate' => (float) ($data['tax_rate'] ?? 0),
'active' => $request->boolean('active'),
];
}
}
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Merchant;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Services\Crm\CrmClient;
use App\Services\Qr\QrCodeManagerService;
use App\Services\Qr\QrImageGeneratorService;
use App\Services\Qr\QrPdfExporter;
@@ -59,9 +60,38 @@ class StorefrontController extends Controller
return view('merchant.storefronts.create', [
'requestedType' => $requestedType,
'prefill' => ($prefill['kind'] ?? null) === 'merchant_storefront' ? $prefill : null,
'catalog' => $this->catalogProducts(),
]);
}
/**
* Active products from the merchant's Ladill CRM catalog, shaped for the
* storefront editor's "add from catalog" picker. Resilient: returns [] if
* CRM is unreachable so the form still works.
*
* @return array<int,array{name:string,price:string,currency:string,description:string}>
*/
private function catalogProducts(): array
{
try {
$response = CrmClient::for((string) ladill_account()->public_id)
->products(['type' => 'product', 'active' => 1, 'per_page' => 200]);
} catch (\Throwable) {
return [];
}
return collect($response['data'] ?? [])
->map(fn ($p) => [
'name' => (string) ($p['name'] ?? ''),
'price' => number_format(((int) ($p['unit_price_minor'] ?? 0)) / 100, 2, '.', ''),
'currency' => strtoupper((string) ($p['currency'] ?? 'GHS')),
'description' => (string) ($p['description'] ?? ''),
])
->filter(fn ($p) => $p['name'] !== '')
->values()
->all();
}
public function store(Request $request): RedirectResponse
{
$account = ladill_account();
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Services\Crm;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
/**
* Thin client for the Ladill CRM internal API (crm.ladill.com/api). Authenticates
* with the per-service key (CRM_API_KEY_MERCHANT) and scopes every call to one
* platform account via the `owner` parameter (the user's public_id / OIDC sub).
*/
class CrmClient
{
public function __construct(private readonly string $owner) {}
public static function for(string $owner): self
{
return new self($owner);
}
public function products(array $filters = []): array
{
return $this->get('products', $filters);
}
public function product(int|string $id): array
{
return $this->get("products/{$id}");
}
public function createProduct(array $data): array
{
return $this->send('post', 'products', $data);
}
public function updateProduct(int|string $id, array $data): array
{
return $this->send('patch', "products/{$id}", $data);
}
public function deleteProduct(int|string $id): array
{
return $this->send('delete', "products/{$id}", []);
}
private function client(): PendingRequest
{
return Http::baseUrl((string) config('crm.url'))
->withToken((string) config('crm.key'))
->acceptJson()
->asJson()
->connectTimeout(10)
->timeout(20);
}
private function get(string $path, array $query = []): array
{
return $this->handle(fn () => $this->client()->get($path, [...array_filter($query, fn ($v) => $v !== null), 'owner' => $this->owner]));
}
private function send(string $method, string $path, array $data): array
{
return $this->handle(fn () => $this->client()->{$method}($path, [...$data, 'owner' => $this->owner]));
}
private function handle(callable $request): array
{
try {
$response = $request();
} catch (ConnectionException) {
throw ValidationException::withMessages([
'crm' => ['Could not reach the CRM service. Please try again in a moment.'],
]);
}
if ($response->failed()) {
abort($response->status() === 404 ? 404 : 502, 'CRM service error.');
}
return (array) $response->json();
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
return [
'url' => rtrim((string) env('CRM_API_URL', 'https://crm.ladill.com/api'), '/'),
'key' => env('CRM_API_KEY_MERCHANT'),
'default_currency' => env('CRM_DEFAULT_CURRENCY', 'GHS'),
];
@@ -0,0 +1,63 @@
@php
/** @var array<string,mixed> $product */
$product = $product ?? [];
$priceValue = old('unit_price', isset($product['unit_price_minor']) ? number_format(((int) $product['unit_price_minor']) / 100, 2, '.', '') : '');
@endphp
@if($errors->any())
<div class="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
<ul class="list-inside list-disc space-y-0.5">
@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach
</ul>
</div>
@endif
<div class="space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
<div>
<label class="block text-sm font-medium text-slate-700">Product name</label>
<input type="text" name="name" value="{{ old('name', $product['name'] ?? '') }}" required maxlength="255"
placeholder="e.g. Cotton T-shirt"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">SKU <span class="font-normal text-slate-400">(optional)</span></label>
<input type="text" name="sku" value="{{ old('sku', $product['sku'] ?? '') }}" maxlength="80"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Currency</label>
<select name="currency" class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach(['GHS', 'USD', 'NGN', 'KES'] as $code)
<option value="{{ $code }}" @selected(old('currency', $product['currency'] ?? config('crm.default_currency', 'GHS')) === $code)>{{ $code }}</option>
@endforeach
</select>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">Unit price</label>
<input type="number" name="unit_price" value="{{ $priceValue }}" min="0" step="0.01" placeholder="0.00"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Tax rate % <span class="font-normal text-slate-400">(optional)</span></label>
<input type="number" name="tax_rate" value="{{ old('tax_rate', $product['tax_rate'] ?? '') }}" min="0" max="100" step="0.01" placeholder="0"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Description <span class="font-normal text-slate-400">(optional)</span></label>
<textarea name="description" rows="3" maxlength="5000"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $product['description'] ?? '') }}</textarea>
</div>
<label class="flex items-center gap-2 text-sm text-slate-600">
<input type="hidden" name="active" value="0">
<input type="checkbox" name="active" value="1" @checked(old('active', $product['active'] ?? true)) class="rounded border-slate-300 text-indigo-600">
Active (available to add to storefronts)
</label>
</div>
@@ -0,0 +1,19 @@
<x-user-layout>
<x-slot name="title">New product</x-slot>
<div class="mx-auto max-w-2xl space-y-6">
<div>
<a href="{{ route('merchant.products.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
All products
</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">New product</h1>
<p class="mt-1 text-sm text-slate-500">Added to your Ladill CRM catalog and available on every storefront.</p>
</div>
<form method="post" action="{{ route('merchant.products.store') }}" class="space-y-5">
@csrf
@include('merchant.products._form', ['product' => []])
<button type="submit" class="btn-primary w-full">Create product</button>
</form>
</div>
</x-user-layout>
@@ -0,0 +1,20 @@
<x-user-layout>
<x-slot name="title">Edit product</x-slot>
<div class="mx-auto max-w-2xl space-y-6">
<div>
<a href="{{ route('merchant.products.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
All products
</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">Edit product</h1>
<p class="mt-1 text-sm text-slate-500">Changes sync to your Ladill CRM catalog.</p>
</div>
<form method="post" action="{{ route('merchant.products.update', $product['id']) }}" class="space-y-5">
@csrf
@method('PUT')
@include('merchant.products._form', ['product' => $product])
<button type="submit" class="btn-primary w-full">Save changes</button>
</form>
</div>
</x-user-layout>
@@ -0,0 +1,75 @@
<x-user-layout>
<x-slot name="title">Products</x-slot>
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-xl font-semibold text-slate-900">Products</h1>
<p class="mt-1 text-sm text-slate-500">Your product catalog, synced with Ladill CRM. Use these on any storefront.</p>
</div>
<x-btn.create :href="route('merchant.products.create')">New product</x-btn.create>
</div>
@if(session('success'))
<div class="rounded-xl border border-emerald-100 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">{{ session('success') }}</div>
@endif
<form method="get" class="flex max-w-sm items-center gap-2">
<input type="search" name="search" value="{{ $search }}" placeholder="Search products…"
class="w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
<button class="rounded-xl border border-slate-200 px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50">Search</button>
</form>
@if(empty($products))
<div class="rounded-2xl border border-dashed border-slate-200 bg-white px-6 py-12 text-center">
<p class="text-sm text-slate-500">{{ $search !== '' ? 'No products match your search.' : 'No products yet.' }}</p>
<a href="{{ route('merchant.products.create') }}" class="mt-3 inline-block text-sm font-semibold text-indigo-600 hover:text-indigo-800">Add your first product</a>
</div>
@else
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr>
<th class="px-4 py-3">Product</th>
<th class="px-4 py-3">SKU</th>
<th class="px-4 py-3 text-right">Price</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach($products as $p)
<tr class="hover:bg-slate-50/60">
<td class="px-4 py-3">
<p class="font-medium text-slate-900">{{ $p['name'] ?? '—' }}</p>
@if(!empty($p['description']))
<p class="mt-0.5 truncate text-xs text-slate-400">{{ \Illuminate\Support\Str::limit($p['description'], 60) }}</p>
@endif
</td>
<td class="px-4 py-3 text-slate-500">{{ $p['sku'] ?: '—' }}</td>
<td class="px-4 py-3 text-right font-medium text-slate-900">
{{ strtoupper($p['currency'] ?? 'GHS') }} {{ number_format(((int) ($p['unit_price_minor'] ?? 0)) / 100, 2) }}
</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {{ ($p['active'] ?? true) ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500' }}">
{{ ($p['active'] ?? true) ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-3">
<a href="{{ route('merchant.products.edit', $p['id']) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">Edit</a>
<form method="post" action="{{ route('merchant.products.destroy', $p['id']) }}"
onsubmit="return confirm('Delete this product?');">
@csrf
@method('DELETE')
<button class="text-sm font-medium text-red-500 hover:text-red-700">Delete</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</x-user-layout>
@@ -66,7 +66,7 @@
<p class="mt-1 text-xs text-slate-400">Private name to find this storefront in your dashboard.</p>
</div>
@include('merchant.storefronts.partials.editor-fields', ['content' => $prefillContent ?? []])
@include('merchant.storefronts.partials.editor-fields', ['content' => $prefillContent ?? [], 'catalog' => $catalog ?? []])
<button type="submit" class="btn-primary w-full">Create storefront</button>
</form>
@@ -1,6 +1,8 @@
@php
/** @var array<string,mixed> $content */
$content = $content ?? [];
/** @var array<int,array{name:string,price:string,currency:string,description:string}> $catalog */
$catalog = $catalog ?? [];
$defaultSections = [['name' => 'Products', 'items' => [['name' => '', 'description' => '', 'price' => '', 'image_path' => '']]]];
$seedSections = old('sections', ! empty($content['sections']) ? $content['sections'] : $defaultSections);
$seedServices = old('services', ! empty($content['services'])
@@ -117,7 +119,9 @@
<template x-if="type === 'menu'">
<div x-data="{
sections: @js($seedSections),
catalog: @js($catalog),
addItem(sIdx) { this.sections[sIdx].items.push({ name: '', description: '', price: '', image_path: '' }) },
addFromCatalog(sIdx, p) { if (p) this.sections[sIdx].items.push({ name: p.name, description: p.description || '', price: p.price, image_path: '' }) },
removeItem(sIdx, iIdx) { this.sections[sIdx].items.splice(iIdx, 1) },
removeSection(sIdx) { this.sections.splice(sIdx, 1) },
addSection() { this.sections.push({ name: 'New Section', items: [{ name: '', description: '', price: '', image_path: '' }] }) },
@@ -182,10 +186,19 @@
</div>
</div>
</template>
<div class="flex flex-wrap items-center gap-3">
<button type="button" @click="addItem(sIdx)" class="inline-flex items-center gap-1 text-xs font-semibold text-indigo-600 hover:text-indigo-800">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
Add item
</button>
<select x-show="catalog.length" @change="addFromCatalog(sIdx, catalog[$event.target.value]); $event.target.value = ''"
class="rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-600 focus:border-indigo-400 focus:outline-none">
<option value="">+ Add from products…</option>
<template x-for="(p, pIdx) in catalog" :key="pIdx">
<option :value="pIdx" x-text="p.name + ' — ' + p.currency + ' ' + p.price"></option>
</template>
</select>
</div>
</div>
</div>
</template>
@@ -200,7 +213,9 @@
<template x-if="type === 'shop'">
<div x-data="{
sections: @js($seedSections),
catalog: @js($catalog),
addItem(sIdx) { this.sections[sIdx].items.push({ name: '', description: '', price: '', image_path: '' }) },
addFromCatalog(sIdx, p) { if (p) this.sections[sIdx].items.push({ name: p.name, description: p.description || '', price: p.price, image_path: '' }) },
removeItem(sIdx, iIdx) { this.sections[sIdx].items.splice(iIdx, 1) },
removeSection(sIdx) { this.sections.splice(sIdx, 1) },
addSection() { this.sections.push({ name: 'New Category', items: [{ name: '', description: '', price: '', image_path: '' }] }) },
@@ -272,10 +287,19 @@
</div>
</div>
</template>
<div class="flex flex-wrap items-center gap-3">
<button type="button" @click="addItem(sIdx)" class="inline-flex items-center gap-1 text-xs font-semibold text-indigo-600 hover:text-indigo-800">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
Add product
</button>
<select x-show="catalog.length" @change="addFromCatalog(sIdx, catalog[$event.target.value]); $event.target.value = ''"
class="rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-600 focus:border-indigo-400 focus:outline-none">
<option value="">+ Add from products…</option>
<template x-for="(p, pIdx) in catalog" :key="pIdx">
<option :value="pIdx" x-text="p.name + ' — ' + p.currency + ' ' + p.price"></option>
</template>
</select>
</div>
</div>
</div>
</template>
@@ -10,6 +10,8 @@
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 11.2 3.05c.44-.44 1.15-.44 1.59 0L21.75 12M4.5 9.75v10.5a.75.75 0 0 0 .75.75H9.75v-6a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v6h4.5a.75.75 0 0 0 .75-.75V9.75" />'],
['name' => 'Storefronts', 'route' => route('merchant.storefronts.index'), 'active' => request()->routeIs('merchant.storefronts.*'),
'icon' => '<g transform="scale(1.7143)" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"><path d="M1.5 8.5V13C1.5 13.1326 1.55268 13.2598 1.64645 13.3536C1.74021 13.4473 1.86739 13.5 2 13.5H12C12.1326 13.5 12.2598 13.4473 12.3536 13.3536C12.4473 13.2598 12.5 13.1326 12.5 13V8.5"/><path d="M8 8.5V13.5"/><path d="M1.5 10H8"/><path d="M0.5 4L2 0.5H12L13.5 4H0.5Z"/><path d="M4.75 4V5C4.75 5.53043 4.53929 6.03914 4.16421 6.41421C3.78914 6.78929 3.28043 7 2.75 7H2.47C1.93957 7 1.43086 6.78929 1.05579 6.41421C0.680714 6.03914 0.47 5.53043 0.47 5V4"/><path d="M9.25 4V5C9.25 5.53043 9.03929 6.03914 8.66421 6.41421C8.28914 6.78929 7.78043 7 7.25 7H6.75C6.21957 7 5.71086 6.78929 5.33579 6.41421C4.96071 6.03914 4.75 5.53043 4.75 5V4"/><path d="M13.5 4V5C13.5 5.53043 13.2893 6.03914 12.9142 6.41421C12.5391 6.78929 12.0304 7 11.5 7H11.25C10.7196 7 10.2109 6.78929 9.83579 6.41421C9.46071 6.03914 9.25 5.53043 9.25 5V4"/></g>'],
['name' => 'Products', 'route' => route('merchant.products.index'), 'active' => request()->routeIs('merchant.products.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />'],
['name' => 'Orders', 'route' => route('merchant.orders.index'), 'active' => request()->routeIs('merchant.orders.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />'],
['name' => 'Payouts', 'route' => route('merchant.payouts'), 'active' => request()->routeIs('merchant.payouts'),
+8
View File
@@ -5,6 +5,7 @@ use App\Http\Controllers\WalletBalanceController;
use App\Http\Controllers\Merchant\OrdersController;
use App\Http\Controllers\Merchant\OverviewController;
use App\Http\Controllers\Merchant\PayoutsController;
use App\Http\Controllers\Merchant\ProductController;
use App\Http\Controllers\Merchant\StorefrontController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\Public\BookingController;
@@ -63,6 +64,13 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/storefronts/{storefront}/preview.png', [StorefrontController::class, 'preview'])->name('merchant.storefronts.preview');
Route::get('/storefronts/{storefront}/download/{format}', [StorefrontController::class, 'download'])->name('merchant.storefronts.download')->whereIn('format', ['png', 'svg', 'pdf']);
Route::get('/products', [ProductController::class, 'index'])->name('merchant.products.index');
Route::get('/products/create', [ProductController::class, 'create'])->name('merchant.products.create');
Route::post('/products', [ProductController::class, 'store'])->name('merchant.products.store');
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->name('merchant.products.edit');
Route::put('/products/{product}', [ProductController::class, 'update'])->name('merchant.products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('merchant.products.destroy');
Route::get('/orders', [OrdersController::class, 'index'])->name('merchant.orders.index');
Route::patch('/orders/{order}/status', [OrdersController::class, 'updateStatus'])->name('merchant.orders.update-status');
Route::patch('/bookings/{booking}/status', [OrdersController::class, 'updateBookingStatus'])->name('merchant.bookings.update-status');