Build real shop/menu/booking storefronts (replace church giving UI).
Deploy Ladill Merchant / deploy (push) Successful in 1m1s

The storefront create/edit/manage UI and the public storefront page were
give's church-donation flow relabeled. Replace with genuine merchant
storefronts, reusing the QR-core manager + validator (which already build
sections/services content):

- Merchant create: type picker (shop/menu/booking) + per-type editors in a
  shared partial (products/menu items with prices; bookable services with
  days/hours). x-if per type so inputs never collide across types.
- StorefrontController store/update/create now delegate to QrCodeManagerService
  for all three types (was hardcoded church org_type/denomination/collection).
- Storefront show = QR preview + download + live toggle + delete + full editor.
- QrCodeManagerService: shop/menu/booking are free (no QR-wallet gate); drop
  the duplicate church TYPE_SHOP arm in hasContentChanges.
- Public: new storefront catalog+cart view for shop/menu (posts items[] to the
  existing order/Pay flow); route shop+menu to it. Booking already had a real
  public page; church TYPE_SHOP landing branch retired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
isaacclad
2026-06-10 10:24:39 +00:00
co-authored by Claude Opus 4.8
parent de5bd16219
commit 8b68fad82c
8 changed files with 674 additions and 186 deletions
@@ -7,6 +7,7 @@ use App\Models\QrCode;
use App\Services\Qr\QrCodeManagerService;
use App\Services\Qr\QrImageGeneratorService;
use App\Services\Qr\QrPdfExporter;
use App\Support\Qr\QrTypeCatalog;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
@@ -42,9 +43,14 @@ class StorefrontController extends Controller
]);
}
public function create(): View
public function create(Request $request): View
{
return view('merchant.storefronts.create');
$requestedType = (string) $request->query('type', QrCode::TYPE_SHOP);
if (! QrTypeCatalog::isValid($requestedType)) {
$requestedType = QrCode::TYPE_SHOP;
}
return view('merchant.storefronts.create', ['requestedType' => $requestedType]);
}
public function store(Request $request): RedirectResponse
@@ -53,28 +59,17 @@ class StorefrontController extends Controller
$request->validate([
'label' => ['required', 'string', 'max:120'],
'name' => ['required', 'string', 'max:120'],
'org_type' => ['required', 'in:church,school,mosque,ngo,club'],
'denomination' => ['nullable', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:2000'],
'phone' => ['nullable', 'string', 'max:30'],
'email' => ['nullable', 'email', 'max:200'],
'website' => ['nullable', 'url', 'max:2048'],
'address' => ['nullable', 'string', 'max:500'],
'service_times' => ['nullable', 'string', 'max:200'],
'brand_color' => ['nullable', 'string', 'max:20'],
'collection_types' => ['nullable', 'array', 'min:1'],
'collection_types.*' => ['string', 'max:80'],
'church_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'],
'church_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'],
'type' => ['required', 'in:'.implode(',', QrTypeCatalog::storefrontTypes())],
'menu_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'],
'menu_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'],
'item_images' => ['nullable', 'array'],
'item_images.*' => ['array'],
'item_images.*.*' => ['image', 'max:4096'],
]);
$data = array_merge($request->all(), [
'type' => QrCode::TYPE_SHOP,
'currency' => 'GHS',
'accepts_payment' => true,
'church_logo' => $request->file('church_logo'),
'church_cover' => $request->file('church_cover'),
'menu_logo' => $request->file('menu_logo'),
'menu_cover' => $request->file('menu_cover'),
]);
try {
@@ -83,7 +78,8 @@ class StorefrontController extends Controller
return back()->withInput()->with('error', $e->getMessage());
}
return redirect()->route('merchant.storefronts.show', $qrCode)->with('success', 'Storefront created.');
return redirect()->route('merchant.storefronts.show', $qrCode)
->with('success', QrTypeCatalog::label($qrCode->type).' storefront created.');
}
public function show(QrCode $storefront): View
@@ -102,28 +98,18 @@ class StorefrontController extends Controller
$request->validate([
'label' => ['sometimes', 'string', 'max:120'],
'name' => ['sometimes', 'string', 'max:120'],
'org_type' => ['sometimes', 'in:church,school,mosque,ngo,club'],
'denomination' => ['nullable', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:2000'],
'phone' => ['nullable', 'string', 'max:30'],
'email' => ['nullable', 'email', 'max:200'],
'website' => ['nullable', 'url', 'max:2048'],
'address' => ['nullable', 'string', 'max:500'],
'service_times' => ['nullable', 'string', 'max:200'],
'brand_color' => ['nullable', 'string', 'max:20'],
'collection_types' => ['nullable', 'array', 'min:1'],
'collection_types.*' => ['string', 'max:80'],
'is_active' => ['sometimes', 'boolean'],
'church_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'],
'church_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'],
'menu_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'],
'menu_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'],
'item_images' => ['nullable', 'array'],
'item_images.*' => ['array'],
'item_images.*.*' => ['image', 'max:4096'],
]);
$data = array_merge($request->all(), [
'accepts_payment' => true,
'is_active' => $request->boolean('is_active', $storefront->is_active),
'church_logo' => $request->file('church_logo'),
'church_cover' => $request->file('church_cover'),
'menu_logo' => $request->file('menu_logo'),
'menu_cover' => $request->file('menu_cover'),
]);
try {