Add HL3D branding, Etsy Live/Draft paging and Projects lifecycle
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s

Replace the old touchmark/PNG identity with the supplied lockup and
monogram, segmented role-aware navigation, Live/Draft listing filters
with generation-based cache invalidation, and admin Projects covering
immutable file revisions, listing live pointers, and exact-decimal
filament/packaging/shipping costing. Additive schema only; no Etsy
publication. Isolated clone tests: 301 tests, 1162 assertions.
This commit is contained in:
2026-11-08 12:50:56 +00:00
parent c8c5822166
commit d4928789c4
83 changed files with 5808 additions and 84 deletions
+30 -4
View File
@@ -1,12 +1,38 @@
@extends('layouts.app')
@section('title', 'Etsy shop')
@section('content')
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Sales</p><h2 class="mt-2 text-2xl font-bold">{{ $connection?->shop_name ?? 'Connect your Etsy shop' }}</h2><p class="mt-1 text-slate-400">Paid orders awaiting dispatch and active listings.</p></div>@if($configured)<form method="post" action="{{ route('etsy.connect') }}">@csrf<button class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">{{ $connection ? 'Reconnect Etsy' : 'Connect Etsy' }}</button></form>@else<a href="{{ route('settings.index') }}" class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Set up Etsy</a>@endif</div>
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Sales</p><h2 class="mt-2 text-2xl font-bold">{{ $connection?->shop_name ?? 'Connect your Etsy shop' }}</h2><p class="mt-1 text-slate-400">Paid orders awaiting dispatch and listings by state.</p></div>@if($configured)<form method="post" action="{{ route('etsy.connect') }}">@csrf<button class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">{{ $connection ? 'Reconnect Etsy' : 'Connect Etsy' }}</button></form>@else<a href="{{ route('settings.index') }}" class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Set up Etsy</a>@endif</div>
@if(! $configured)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">Add your Etsy app key and shared secret in <a class="underline" href="{{ route('settings.index') }}">Settings</a>, then connect your shop.</div>@endif
@if($integrationError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $integrationError }}</div>@endif
@if($stateError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $stateError }}</div>@endif
@if($pageError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $pageError }}</div>@endif
@if($receiptsError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $receiptsError }}</div>@endif
@if($listingsError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $listingsError }}</div>@endif
@if($connection)
<div class="grid gap-4 sm:grid-cols-2"><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Awaiting dispatch</p><p class="mt-3 text-4xl font-bold">{{ count($receipts) }}</p></div><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Active listings shown</p><p class="mt-3 text-4xl font-bold">{{ count($listings) }}</p></div></div>
<div class="grid gap-4 sm:grid-cols-2"><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Awaiting dispatch</p><p class="mt-3 text-4xl font-bold">{{ count($receipts) }}</p></div><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">{{ ucfirst($state === 'active' ? 'Live' : 'Draft') }} listings total</p><p class="mt-3 text-4xl font-bold">{{ $listingCount }}</p></div></div>
<section class="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-900"><h3 class="p-6 text-lg font-semibold">Orders to dispatch</h3><table class="w-full min-w-[650px] text-left text-sm"><thead class="border-y border-slate-800 text-slate-400"><tr><th class="p-4">Receipt</th><th class="p-4">Buyer</th><th class="p-4">Placed</th><th class="p-4">Total</th></tr></thead><tbody class="divide-y divide-slate-800">@forelse($receipts as $receipt)<tr><td class="p-4 font-medium">#{{ $receipt['receipt_id'] ?? '—' }}</td><td class="p-4">{{ $receipt['name'] ?? $receipt['buyer_email'] ?? 'Buyer' }}</td><td class="p-4">{{ isset($receipt['create_timestamp']) ? \Illuminate\Support\Carbon::createFromTimestamp($receipt['create_timestamp'])->format('d M Y') : '—' }}</td><td class="p-4">{{ isset($receipt['grandtotal']['amount']) ? ($receipt['grandtotal']['currency_code'] ?? 'GBP').' '.number_format((float) $receipt['grandtotal']['amount'] / max(1, (int) ($receipt['grandtotal']['divisor'] ?? 100)), 2) : '—' }}</td></tr>@empty<tr><td colspan="4" class="p-6 text-slate-400">No paid orders awaiting dispatch.</td></tr>@endforelse</tbody></table></section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><h3 class="text-lg font-semibold">Active listings</h3><div class="mt-4 divide-y divide-slate-800">@forelse($listings as $listing)<div class="flex items-center justify-between gap-4 py-3"><span>{{ $listing['title'] ?? 'Untitled listing' }}</span><span class="shrink-0 text-sm text-slate-400">{{ $listing['quantity'] ?? 0 }} available</span></div>@empty<p class="py-3 text-sm text-slate-400">No listings returned.</p>@endforelse</div></section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<h3 class="text-lg font-semibold">Listings</h3>
<div class="flex gap-2 text-sm" role="tablist" aria-label="Listing state">
<a href="{{ route('etsy.index', ['state' => 'active']) }}" role="tab" aria-selected="{{ $state === 'active' ? 'true' : 'false' }}" @class(['rounded-lg px-3 py-2', 'bg-brand/10 text-brand' => $state === 'active', 'text-slate-400 hover:text-white' => $state !== 'active'])>Live</a>
<a href="{{ route('etsy.index', ['state' => 'draft']) }}" role="tab" aria-selected="{{ $state === 'draft' ? 'true' : 'false' }}" @class(['rounded-lg px-3 py-2', 'bg-brand/10 text-brand' => $state === 'draft', 'text-slate-400 hover:text-white' => $state !== 'draft'])>Drafts</a>
</div>
</div>
@php
$snapshotAge = is_array($listingSnapshot ?? null) && ! empty($listingSnapshot['fetched_at'])
? \Illuminate\Support\Carbon::createFromTimestamp($listingSnapshot['fetched_at'])->diffForHumans()
: null;
$snapshotStale = is_array($listingSnapshot ?? null) && ! empty($listingSnapshot['stale']);
@endphp
<p class="mt-3 text-xs text-slate-500">Showing {{ count($listings) }} of {{ $listingCount }} {{ $state === 'active' ? 'live' : 'draft' }} listings.{{ $snapshotAge ? ' Snapshot '.$snapshotAge.($snapshotStale ? ' · cached after a failed refresh, not asserted live' : '').'.' : '' }}</p>
<div class="mt-4 divide-y divide-slate-800">@forelse($listings as $listing)<div class="flex items-center justify-between gap-4 py-3"><span>{{ $listing['title'] ?? 'Untitled listing' }}</span><span class="shrink-0 text-sm text-slate-400">{{ $listing['quantity'] ?? 0 }} available</span></div>@empty<p class="py-3 text-sm text-slate-400">No {{ $state === 'active' ? 'live' : 'draft' }} listings returned.</p>@endforelse</div>
@if($listingCount > $perPage || $page > 1)
<nav class="mt-4 flex items-center justify-between text-sm text-slate-400" aria-label="Listings pagination">
@if($page > 1)<a href="{{ route('etsy.index', ['page' => $page - 1, 'state' => $state]) }}" class="hover:text-white">&larr; Previous</a>@else<span></span>@endif
<span>Page {{ $page }}</span>
@if($listingCount > $page * $perPage)<a href="{{ route('etsy.index', ['page' => $page + 1, 'state' => $state]) }}" class="hover:text-white">Next &rarr;</a>@else<span></span>@endif
</nav>
@endif
</section>
@endif
@endsection