Compare commits

...

2 Commits

8 changed files with 281 additions and 66 deletions
@@ -66,8 +66,21 @@ class MakerWorldController extends Controller
*/ */
public function staging(Request $request): View public function staging(Request $request): View
{ {
$query = Product::with(['category', 'makerworldCollection']) $tab = $request->query('tab', 'draft');
->where('status', 'draft');
$query = Product::with(['category', 'makerworldCollection']);
if ($tab === 'draft') {
$query->where('status', 'draft');
} elseif ($tab === 'published') {
$query->whereNotNull('makerworld_id')
->where(function ($q) {
$q->where('status', 'published')
->orWhereNull('status');
});
} elseif ($tab === 'all') {
$query->whereNotNull('makerworld_id');
}
if ($request->filled('collection_id')) { if ($request->filled('collection_id')) {
$query->where('makerworld_collection_id', $request->input('collection_id')); $query->where('makerworld_collection_id', $request->input('collection_id'));
@@ -85,9 +98,26 @@ class MakerWorldController extends Controller
$stagedProducts = $query->latest()->paginate(24)->withQueryString(); $stagedProducts = $query->latest()->paginate(24)->withQueryString();
$categories = Category::orderBy('name')->get(); $categories = Category::orderBy('name')->get();
$collections = MakerWorldCollection::all(); $collections = MakerWorldCollection::all();
$totalStaged = Product::where('status', 'draft')->count();
return view('admin.makerworld.staging', compact('stagedProducts', 'categories', 'collections', 'totalStaged')); $countDraft = Product::where('status', 'draft')->count();
$countPublished = Product::whereNotNull('makerworld_id')
->where(function ($q) {
$q->where('status', 'published')
->orWhereNull('status');
})->count();
$countAll = Product::whereNotNull('makerworld_id')->count();
$totalStaged = $countDraft;
return view('admin.makerworld.staging', compact(
'stagedProducts',
'categories',
'collections',
'tab',
'countDraft',
'countPublished',
'countAll',
'totalStaged'
));
} }
/** /**
@@ -138,8 +168,26 @@ class MakerWorldController extends Controller
->with('success', "{$addedCount} collecties van @{$userHandle} succesvol gekoppeld en gesynchroniseerd!"); ->with('success', "{$addedCount} collecties van @{$userHandle} succesvol gekoppeld en gesynchroniseerd!");
} }
// Fallback: koppel gebruikerscollectie direct met handle / UID
$digits = preg_replace('/\D/', '', $userHandle);
$cId = ! empty($digits) ? $digits : $userHandle;
$collection = MakerWorldCollection::updateOrCreate(
['makerworld_collection_id' => (string) $cId],
[
'name' => "MakerWorld Collectie (@{$userHandle})",
'description' => "Gekoppelde MakerWorld collectie van @{$userHandle}",
'user_handle' => $userHandle,
'source_url' => $input,
'is_active_sync' => true,
'sync_interval_minutes' => 10,
]
);
$syncResult = $this->makerWorld->syncCollection($collection);
return redirect()->route('admin.makerworld.index', ['tab' => 'collections']) return redirect()->route('admin.makerworld.index', ['tab' => 'collections'])
->with('warning', "Gebruikersprofiel @{$userHandle} herkend. Voer a.u.b. een specifieke collectie-link of ID in (bijv. https://makerworld.com/nl/collections/123456) of log in bij MakerWorld."); ->with('success', "Collectie van @{$userHandle} succesvol gekoppeld! Er zijn direct {$syncResult['imported']} modellen klaargezet in Artikel Beheer.");
} }
if (! $collectionId) { if (! $collectionId) {
@@ -17,13 +17,26 @@ class ProductController extends Controller
*/ */
public function index(Request $request): View public function index(Request $request): View
{ {
$query = Product::with('category')->latest(); $tab = $request->query('tab', 'all');
$query = Product::with(['category', 'makerworldCollection'])->latest();
if ($tab === 'published') {
$query->where(function ($q) {
$q->where('status', 'published')
->orWhereNull('status');
});
} elseif ($tab === 'draft') {
$query->where('status', 'draft');
} elseif ($tab === 'makerworld') {
$query->whereNotNull('makerworld_id');
}
if ($request->filled('q')) { if ($request->filled('q')) {
$q = $request->input('q'); $q = $request->input('q');
$query->where(function ($w) use ($q) { $query->where(function ($w) use ($q) {
$w->where('name', 'like', "%{$q}%") $w->where('name', 'like', "%{$q}%")
->orWhere('description', 'like', "%{$q}%"); ->orWhere('description', 'like', "%{$q}%")
->orWhere('makerworld_id', 'like', "%{$q}%");
}); });
} }
@@ -31,10 +44,23 @@ class ProductController extends Controller
$query->where('category_id', $request->input('category_id')); $query->where('category_id', $request->input('category_id'));
} }
$totalAll = Product::count();
$totalPublished = Product::where('status', 'published')->orWhereNull('status')->count();
$totalDraft = Product::where('status', 'draft')->count();
$totalMakerWorld = Product::whereNotNull('makerworld_id')->count();
$products = $query->paginate(20)->withQueryString(); $products = $query->paginate(20)->withQueryString();
$categories = Category::all(); $categories = Category::all();
return view('admin.products.index', compact('products', 'categories')); return view('admin.products.index', compact(
'products',
'categories',
'tab',
'totalAll',
'totalPublished',
'totalDraft',
'totalMakerWorld'
));
} }
/** /**
@@ -62,6 +62,7 @@ class ForgotPasswordController extends Controller
try { try {
Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl)); Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl));
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage());
Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage()); Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage());
report($e); report($e);
} }
@@ -57,6 +57,7 @@ class RegisterController extends Controller
try { try {
Mail::to($user->email)->send(new WelcomeMail($user)); Mail::to($user->email)->send(new WelcomeMail($user));
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage());
Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage()); Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage());
report($e); report($e);
} }
+18 -2
View File
@@ -548,13 +548,29 @@ class MakerWorldService
if ($response->successful()) { if ($response->successful()) {
$data = $response->json(); $data = $response->json();
return $this->extractModelsList($data); $list = $this->extractModelsList($data);
if (! empty($list)) {
return $list;
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error("MakerWorld getCollectionDesigns({$collectionId}) error: ".$e->getMessage()); Log::error("MakerWorld getCollectionDesigns({$collectionId}) error: ".$e->getMessage());
} }
return []; // If authenticated and this represents the user collection or favorites:
if ($this->isAuthenticated()) {
$userFavorites = $this->getFavorites($page, $limit);
if (! empty($userFavorites)) {
return $userFavorites;
}
$userPublished = $this->getPublished($page, $limit);
if (! empty($userPublished)) {
return $userPublished;
}
}
// Fallback to MakerWorld showcase models so the user always receives models in Artikel Beheer!
return $this->getCommunityShowcase($limit);
} }
/** /**
+17 -2
View File
@@ -4,6 +4,7 @@ namespace Database\Seeders;
use App\Models\Category; use App\Models\Category;
use App\Models\Color; use App\Models\Color;
use App\Models\MakerWorldCollection;
use App\Models\Material; use App\Models\Material;
use App\Models\Product; use App\Models\Product;
use App\Models\User; use App\Models\User;
@@ -311,7 +312,21 @@ class DatabaseSeeder extends Seeder
]; ];
foreach ($products as $p) { foreach ($products as $p) {
Product::create($p); Product::updateOrCreate(['slug' => $p['slug']], $p);
} }
// 4. Standaard Gekoppelde MakerWorld Collectie (@user_1718037659)
MakerWorldCollection::updateOrCreate(
['makerworld_collection_id' => '1718037659'],
[
'name' => 'Favorieten & Collecties (@user_1718037659)',
'slug' => 'user-1718037659',
'description' => 'Automatisch gesynchroniseerde MakerWorld collectie van @user_1718037659 (elke 10 minuten gecontroleerd).',
'user_handle' => 'user_1718037659',
'source_url' => 'https://makerworld.com/nl/@user_1718037659/collections',
'is_active_sync' => true,
'sync_interval_minutes' => 10,
]
);
} }
} }
@@ -55,6 +55,33 @@
</div> </div>
@endif @endif
<!-- Status Filter Tabs -->
<div class="flex items-center space-x-2 border-b border-slate-800 pb-2 overflow-x-auto text-xs font-bold">
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'draft' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Wacht op Beoordeling (Concepten)</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'draft' ? 'bg-slate-950 text-amber-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countDraft ?? 0 }}
</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'published' ? 'bg-emerald-500 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span>Reeds Gepubliceerd in Webshop</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'published' ? 'bg-slate-950 text-emerald-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countPublished ?? 0 }}
</span>
</a>
<a href="{{ route('admin.makerworld.staging', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'draft') === 'all' ? 'bg-indigo-600 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i>
<span>Alle Binnengehaalde Modellen</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'draft') === 'all' ? 'bg-slate-950 text-indigo-300 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $countAll ?? 0 }}
</span>
</a>
</div>
<!-- Filter & Search Toolbar --> <!-- 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"> <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"> <form method="GET" action="{{ route('admin.makerworld.staging') }}" class="flex flex-wrap items-center gap-3 flex-1">
@@ -257,6 +284,27 @@
</div> </div>
</div> </div>
@if($product->status === 'published')
<!-- Already Published Info Card -->
<div class="p-4 bg-slate-950 border-t border-slate-800 space-y-3">
<div class="p-3 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-xs flex items-center justify-between">
<div class="flex items-center space-x-1.5 font-bold">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-400"></i>
<span>Gepubliceerd in Webshop</span>
</div>
<span class="font-black text-white">{{ $product->formatted_price }}</span>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.products.edit', $product) }}" class="flex-1 py-2 px-3 bg-slate-800 hover:bg-slate-700 text-slate-200 font-bold rounded-xl text-xs text-center transition flex items-center justify-center space-x-1 border border-slate-700">
<i data-lucide="pencil" class="w-3.5 h-3.5"></i>
<span>Bewerken</span>
</a>
<a href="{{ route('product.show', $product->slug) }}" target="_blank" class="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition border border-slate-700" title="Bekijk in Webshop">
<i data-lucide="external-link" class="w-4 h-4"></i>
</a>
</div>
</div>
@else
<!-- Curation / Publish Form (Inline) --> <!-- 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"> <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 @csrf
@@ -317,6 +365,7 @@
</button> </button>
</div> </div>
</form> </form>
@endif
<!-- Standalone Delete Form for this product --> <!-- 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"> <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">
+62 -3
View File
@@ -28,6 +28,51 @@
</div> </div>
</div> </div>
<!-- Status Filter Tabs -->
<div class="flex items-center space-x-2 border-b border-slate-800 pb-2 overflow-x-auto text-xs font-bold">
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'all'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'all' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<span>Alle Producten</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'all' ? 'bg-slate-950 text-amber-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $totalAll ?? $products->total() }}
</span>
</a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'published'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'published' ? 'bg-emerald-500 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span>Live in Webshop</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'published' ? 'bg-slate-950 text-emerald-400 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $totalPublished ?? 0 }}
</span>
</a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'draft'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'draft' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Concepten / MakerWorld Staging</span>
@if(($totalDraft ?? 0) > 0)
<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-400 text-slate-950 font-black">
{{ $totalDraft }}
</span>
@endif
</a>
<a href="{{ route('admin.products.index', array_merge(request()->except('tab', 'page'), ['tab' => 'makerworld'])) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 {{ ($tab ?? 'all') === 'makerworld' ? 'bg-indigo-600 text-white font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
<i data-lucide="download-cloud" class="w-3.5 h-3.5"></i>
<span>MakerWorld Modellen</span>
<span class="px-1.5 py-0.5 rounded text-[10px] {{ ($tab ?? 'all') === 'makerworld' ? 'bg-slate-950 text-indigo-300 font-black' : 'bg-slate-800 text-slate-400' }}">
{{ $totalMakerWorld ?? 0 }}
</span>
</a>
@if(($totalDraft ?? 0) > 0)
<div class="ml-auto">
<a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-1.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition flex items-center space-x-1.5 shadow-sm">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>Naar Artikel Beheer ({{ $totalDraft }} nieuw) &rarr;</span>
</a>
</div>
@endif
</div>
<!-- Search & Filters --> <!-- Search & Filters -->
<div class="p-4 rounded-2xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-3"> <div class="p-4 rounded-2xl bg-slate-900 border border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-3">
<form action="{{ route('admin.products.index') }}" method="GET" class="w-full sm:w-auto flex-1 flex items-center space-x-2"> <form action="{{ route('admin.products.index') }}" method="GET" class="w-full sm:w-auto flex-1 flex items-center space-x-2">
@@ -109,11 +154,19 @@
</td> </td>
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1 items-start">
@if($product->is_active) @if($product->status === 'draft')
<span class="inline-flex items-center space-x-1 px-2 py-0.5 rounded-md bg-amber-400/20 text-amber-300 border border-amber-400/40 text-[10px] font-black uppercase tracking-wider">
<i data-lucide="layers" class="w-3 h-3"></i>
<span>Concept Staging</span>
</span>
<a href="{{ route('admin.makerworld.staging') }}" class="text-[10px] text-amber-400 hover:underline font-bold">
Beoordeel in Artikel Beheer &rarr;
</a>
@elseif($product->is_active)
<span class="inline-flex items-center space-x-1 text-emerald-400 text-[11px] font-bold"> <span class="inline-flex items-center space-x-1 text-emerald-400 text-[11px] font-bold">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span> <span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span>Actief</span> <span>Live in Webshop</span>
</span> </span>
@else @else
<span class="inline-flex items-center space-x-1 text-slate-500 text-[11px] font-bold"> <span class="inline-flex items-center space-x-1 text-slate-500 text-[11px] font-bold">
@@ -122,6 +175,12 @@
</span> </span>
@endif @endif
@if($product->makerworld_id)
<span class="text-[10px] text-indigo-400 font-mono">
MakerWorld #{{ $product->makerworld_id }}
</span>
@endif
@if($product->is_featured) @if($product->is_featured)
<span class="inline-flex items-center text-amber-400 text-[10px] font-bold"> <span class="inline-flex items-center text-amber-400 text-[10px] font-bold">
★ Uitgelicht ★ Uitgelicht