Implement MakerWorld collections sync every 10 minutes and Artikel Beheer staging workflow

This commit is contained in:
2026-11-01 15:39:10 +01:00
parent 121ff624de
commit 2d78d80934
20 changed files with 1984 additions and 675 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
@extends('layouts.admin')
@section('title', 'Artikel Beheer (Geïmporteerde Modellen) | Jouwdriedee.nl Beheer')
@section('content')
<div class="py-8 bg-slate-950 text-slate-100 min-h-screen" x-data="artikelBeheerApp()">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<!-- Top Header & Breadcrumb -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pb-6 border-b border-slate-800">
<div>
<div class="flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse"></span>
<span>Staging & Curation</span>
<span>&bull;</span>
<span>Geïmporteerde Modellen</span>
</div>
<h1 class="text-2xl sm:text-3xl font-black text-white flex items-center space-x-3">
<span class="p-2 bg-gradient-to-tr from-amber-400 to-orange-500 rounded-2xl text-slate-950 shadow-lg shadow-amber-400/20">
<i data-lucide="layers" class="w-6 h-6"></i>
</span>
<span>Artikel Beheer</span>
</h1>
<p class="mt-1 text-sm text-slate-400">
Beoordeel automatisch binnengehaalde MakerWorld modellen. Stel de gewenste categorie en verkoopprijs in en publiceer ze met 1 klik naar de webshop!
</p>
</div>
<!-- Header Quick Actions -->
<div class="flex items-center space-x-3">
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-4 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-200 text-xs font-bold transition flex items-center space-x-2 border border-slate-800">
<i data-lucide="refresh-cw" class="w-4 h-4 text-amber-400"></i>
<span>Collecties & Auto-Sync</span>
</a>
<a href="{{ route('admin.products.index') }}" class="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold transition flex items-center space-x-2 border border-slate-700">
<i data-lucide="box" class="w-4 h-4"></i>
<span>Actieve Producten</span>
</a>
</div>
</div>
<!-- Notification Alerts -->
@if(session('success'))
<div class="p-4 rounded-2xl bg-emerald-950/60 border border-emerald-500/40 text-emerald-200 text-sm flex items-center space-x-3">
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-400 shrink-0"></i>
<div class="font-bold">{{ session('success') }}</div>
</div>
@endif
@if(session('error'))
<div class="p-4 rounded-2xl bg-rose-950/60 border border-rose-500/40 text-rose-200 text-sm flex items-center space-x-3">
<i data-lucide="x-circle" class="w-5 h-5 text-rose-400 shrink-0"></i>
<div class="font-bold">{{ session('error') }}</div>
</div>
@endif
<!-- Filter & Search Toolbar -->
<div class="bg-slate-900 rounded-3xl p-4 sm:p-5 border border-slate-800 flex flex-col md:flex-row md:items-center justify-between gap-4">
<form method="GET" action="{{ route('admin.makerworld.staging') }}" class="flex flex-wrap items-center gap-3 flex-1">
<!-- Search input -->
<div class="relative min-w-[240px] flex-1">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-500">
<i data-lucide="search" class="w-4 h-4"></i>
</div>
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek op titel, maker of model-ID..."
class="w-full pl-9 pr-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white text-xs placeholder-slate-500 focus:ring-amber-400"
>
</div>
<!-- Collection dropdown filter -->
<div class="w-48">
<select name="collection_id" onchange="this.form.submit()" class="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
<option value="">Alle collecties</option>
@foreach($collections as $col)
<option value="{{ $col->id }}" {{ request('collection_id') == $col->id ? 'selected' : '' }}>
{{ $col->name }} ({{ $col->items_count }})
</option>
@endforeach
</select>
</div>
<button type="submit" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold transition">
Filteren
</button>
@if(request()->hasAny(['q', 'collection_id']))
<a href="{{ route('admin.makerworld.staging') }}" class="text-xs text-rose-400 hover:underline font-bold">
Filters wissen
</a>
@endif
</form>
<div class="flex items-center space-x-3 text-xs">
<span class="px-3 py-1.5 rounded-xl bg-amber-400/10 text-amber-300 border border-amber-400/30 font-black">
{{ $totalStaged }} concepten wachten op beoordeling
</span>
</div>
</div>
<!-- Bulk Selection Action Bar -->
<div x-show="selectedIds.length > 0" x-cloak class="p-4 rounded-2xl bg-amber-400 text-slate-950 flex flex-col md:flex-row items-center justify-between gap-4 shadow-xl">
<div class="flex items-center space-x-2 font-black text-xs uppercase tracking-wider">
<i data-lucide="check-square" class="w-5 h-5"></i>
<span x-text="selectedIds.length + ' artikelen geselecteerd'"></span>
</div>
<form action="{{ route('admin.makerworld.staging.bulk-publish') }}" method="POST" class="flex flex-wrap items-center gap-3">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<div class="flex items-center space-x-2">
<label class="text-xs font-black uppercase">Categorie:</label>
<select name="category_id" required class="px-3 py-1.5 bg-slate-950 text-white rounded-xl text-xs font-bold">
@foreach($categories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div class="flex items-center space-x-2">
<label class="text-xs font-black uppercase">Prijs-marge %:</label>
<input type="number" name="custom_markup" placeholder="0" class="w-16 px-2 py-1.5 bg-slate-950 text-white rounded-xl text-xs font-bold">
</div>
<button type="submit" class="px-4 py-2 bg-slate-950 text-amber-400 hover:bg-slate-900 font-black text-xs rounded-xl shadow-md transition flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Publiceer Selectie naar Webshop</span>
</button>
</form>
<form action="{{ route('admin.makerworld.staging.bulk-reject') }}" method="POST" onsubmit="return confirm('Weet je zeker dat je de geselecteerde concepten wilt verwijderen?');">
@csrf
<template x-for="id in selectedIds" :key="id">
<input type="hidden" name="product_ids[]" :value="id">
</template>
<button type="submit" class="px-3 py-2 bg-rose-600 hover:bg-rose-700 text-white font-bold text-xs rounded-xl transition flex items-center space-x-1">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Verwijderen</span>
</button>
</form>
</div>
<!-- Select All Trigger -->
@if($stagedProducts->isNotEmpty())
<div class="flex items-center justify-between text-xs text-slate-400 px-2">
<button type="button" @click="toggleSelectAll()" class="hover:text-amber-400 font-bold transition flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span x-text="selectedIds.length === {{ $stagedProducts->count() }} ? 'Deselecteer alles' : 'Selecteer alle {{ $stagedProducts->count() }} modellen op deze pagina'"></span>
</button>
<span>Pagina {{ $stagedProducts->currentPage() }} van {{ $stagedProducts->lastPage() }}</span>
</div>
@endif
<!-- Staged Products Grid -->
@if($stagedProducts->isEmpty())
<div class="text-center py-20 bg-slate-900/60 rounded-3xl border border-slate-800 p-8">
<div class="w-20 h-20 rounded-3xl bg-slate-800 flex items-center justify-center text-slate-400 mx-auto mb-4">
<i data-lucide="sparkles" class="w-10 h-10 text-amber-400"></i>
</div>
<h3 class="text-xl font-black text-white">Geen concepten in de wachtrij!</h3>
<p class="text-xs text-slate-400 max-w-md mx-auto mt-2">
Alle binnengehaalde modellen zijn al beoordeeld en gepubliceerd, of er zijn nog geen nieuwe modellen toegevoegd aan je gevolgde collecties.
</p>
<div class="mt-6 flex items-center justify-center space-x-3">
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-5 py-2.5 rounded-xl bg-amber-400 text-slate-950 font-black text-xs uppercase tracking-wider shadow-lg shadow-amber-400/20 transition">
Bekijk Collecties & Auto-Sync
</a>
</div>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($stagedProducts as $product)
@php
$printInfo = $product->print_info ?? [];
@endphp
<div class="bg-slate-900 rounded-3xl border border-slate-800 overflow-hidden flex flex-col justify-between shadow-lg hover:border-slate-700 transition relative">
<!-- Top Image Preview & Gallery -->
<div>
<div class="relative aspect-4/3 bg-slate-950 overflow-hidden group">
<img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover">
<!-- Selection Checkbox -->
<div class="absolute top-3 left-3 z-10">
<label class="flex items-center justify-center w-7 h-7 rounded-xl bg-slate-900/90 backdrop-blur border border-white/20 cursor-pointer shadow-md hover:scale-105 transition">
<input type="checkbox" value="{{ $product->id }}" x-model="selectedIds" class="w-4 h-4 text-amber-500 rounded focus:ring-0">
</label>
</div>
<!-- Draft Badge -->
<div class="absolute top-3 right-3 flex flex-col items-end gap-1.5">
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider bg-amber-400 text-slate-950 shadow-md">
Concept
</span>
@if($product->makerworldCollection)
<span class="px-2 py-0.5 rounded-md text-[9px] font-bold bg-slate-950/80 text-amber-300 border border-amber-400/20 backdrop-blur truncate max-w-[150px]">
{{ $product->makerworldCollection->name }}
</span>
@endif
</div>
<!-- MakerWorld Link -->
<a href="https://makerworld.com/nl/models/{{ $product->makerworld_id }}" target="_blank" class="absolute bottom-3 right-3 p-2 rounded-xl bg-slate-950/80 hover:bg-slate-900 text-slate-300 hover:text-white border border-white/10 backdrop-blur transition" title="Bekijk origineel op MakerWorld">
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
</a>
</div>
<!-- Photo Thumbnails Strip (All Pulled Pictures) -->
@if(!empty($product->gallery_images) && count($product->gallery_images) > 0)
<div class="px-4 py-2 bg-slate-950 border-b border-slate-800/80 flex items-center space-x-2 overflow-x-auto">
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wider shrink-0">Foto's:</span>
<img src="{{ $product->image_url }}" alt="" class="w-9 h-9 rounded-lg object-cover border-2 border-amber-400 shrink-0">
@foreach(array_slice($product->gallery_images, 0, 6) as $galleryPic)
<img src="{{ $galleryPic }}" alt="" class="w-9 h-9 rounded-lg object-cover border border-slate-800 shrink-0 hover:border-slate-600 transition" loading="lazy">
@endforeach
@if(count($product->gallery_images) > 6)
<span class="text-[10px] text-slate-400 font-bold shrink-0">+{{ count($product->gallery_images) - 6 }} meer</span>
@endif
</div>
@endif
<!-- Print Specifications Strip -->
<div class="grid grid-cols-3 gap-1 px-4 py-2.5 bg-slate-950/40 border-b border-slate-800/60 text-center text-xs">
<div>
<span class="text-[10px] font-bold text-slate-500 block uppercase">Printtijd</span>
<span class="font-black text-slate-200">
{{ $product->print_time_hours ? $product->print_time_hours . ' uur' : '-' }}
</span>
</div>
<div>
<span class="text-[10px] font-bold text-slate-500 block uppercase">Gewicht</span>
<span class="font-black text-slate-200">
{{ $product->weight_grams ? $product->weight_grams . ' gram' : '-' }}
</span>
</div>
<div>
<span class="text-[10px] font-bold text-amber-400 block uppercase">Adviesprijs</span>
<span class="font-black text-amber-400">
{{ $product->formatted_price }}
</span>
</div>
</div>
<!-- Meta Details -->
<div class="p-4 space-y-2">
<div class="text-[11px] text-slate-400 flex items-center justify-between">
<span class="truncate">Maker: <strong class="text-slate-300">{{ $product->makerworld_author ?? 'MakerWorld Maker' }}</strong></span>
<span class="font-mono text-slate-500 text-[10px]">#{{ $product->makerworld_id }}</span>
</div>
</div>
</div>
<!-- Curation / Publish Form (Inline) -->
<form action="{{ route('admin.makerworld.staging.publish', $product) }}" method="POST" class="p-4 bg-slate-950 border-t border-slate-800 space-y-3">
@csrf
<!-- Editable Product Name -->
<div>
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Producttitel</label>
<input
type="text"
name="name"
value="{{ old('name', $product->name) }}"
required
class="w-full px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400"
>
</div>
<div class="grid grid-cols-2 gap-2">
<!-- Category selector -->
<div>
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Categorie</label>
<select name="category_id" required class="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ $product->category_id == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
</div>
<!-- Retail Price -->
<div>
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Verkoopprijs (€)</label>
<input
type="number"
step="0.01"
name="base_price"
value="{{ old('base_price', $product->base_price) }}"
required
class="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-amber-300 text-xs font-black focus:ring-amber-400"
>
</div>
</div>
<!-- Action Buttons -->
<div class="pt-2 flex items-center justify-between gap-2">
<button type="submit" class="flex-1 py-2 px-3 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-black rounded-xl text-xs uppercase tracking-wider shadow-md shadow-emerald-500/20 transition flex items-center justify-center space-x-1">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Publiceer naar Webshop</span>
</button>
<button
type="submit"
form="reject-form-{{ $product->id }}"
class="p-2 rounded-xl text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 border border-slate-800 transition"
title="Concept afwijzen & verwijderen"
>
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</form>
<!-- Standalone Delete Form for this product -->
<form id="reject-form-{{ $product->id }}" action="{{ route('admin.makerworld.staging.reject', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit concept wilt verwijderen?');" class="hidden">
@csrf
@method('DELETE')
</form>
</div>
@endforeach
</div>
<!-- Pagination -->
<div class="mt-8">
{{ $stagedProducts->links() }}
</div>
@endif
</div>
</div>
<script>
function artikelBeheerApp() {
return {
selectedIds: [],
allIds: @json($stagedProducts->pluck('id')->toArray()),
toggleSelectAll() {
if (this.selectedIds.length === this.allIds.length) {
this.selectedIds = [];
} else {
this.selectedIds = [...this.allIds];
}
}
}
}
</script>
@endsection
+23 -2
View File
@@ -44,6 +44,19 @@
<span>Producten</span>
</a>
@php
$draftBadgeCount = \App\Models\Product::where('status', 'draft')->count();
@endphp
<a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}">
<i data-lucide="layers" class="w-4 h-4"></i>
<span>Artikel Beheer</span>
@if($draftBadgeCount > 0)
<span class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] font-black {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-slate-950 text-amber-400' : 'bg-amber-400 text-slate-950' }}">
{{ $draftBadgeCount }}
</span>
@endif
</a>
<a href="{{ route('admin.categories.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.categories.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}">
<i data-lucide="folder-tree" class="w-4 h-4"></i>
<span>Categorieën</span>
@@ -54,9 +67,9 @@
<span>Bestellingen</span>
</a>
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}">
<a href="{{ route('admin.makerworld.index') }}" class="px-3 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('admin.makerworld.index*') ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-300 hover:text-white hover:bg-slate-800' }}">
<i data-lucide="download-cloud" class="w-4 h-4"></i>
<span>MakerWorld</span>
<span>MakerWorld Sync</span>
</a>
</nav>
</div>
@@ -85,6 +98,14 @@
<a href="{{ route('admin.products.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.products.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}">
📦 Producten
</a>
<a href="{{ route('admin.makerworld.staging') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.makerworld.staging*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}">
⚡ Artikel Beheer (Geïmporteerd)
@if(($draftBadgeCount ?? 0) > 0)
<span class="ml-2 px-1.5 py-0.5 rounded-full text-xs font-black bg-amber-400 text-slate-950">
{{ $draftBadgeCount }} nieuw
</span>
@endif
</a>
<a href="{{ route('admin.categories.index') }}" class="block px-3 py-2 rounded-xl {{ request()->routeIs('admin.categories.*') ? 'bg-amber-400 text-slate-950' : 'text-slate-300' }}">
📁 Categorieën
</a>