makerWorld->isAuthenticated(); $userProfile = $this->makerWorld->getUserProfile(); $tab = $request->query('tab', 'collections'); $models = []; $apiCollections = []; if ($tab === 'collections') { // Check of actieve collecties toe zijn aan synchronisatie (> 5 minuten geleden of nog nooit gesynchroniseerd) $dueCollections = MakerWorldCollection::where('is_active_sync', true) ->where(function ($q) { $q->whereNull('last_synced_at') ->orWhere('last_synced_at', '<=', now()->subMinutes(5)); }) ->get(); foreach ($dueCollections as $dueCol) { try { $this->makerWorld->syncCollection($dueCol); } catch (\Throwable $e) { Log::warning("Auto-sync error for collection #{$dueCol->id}: {$e->getMessage()}"); } } if ($isAuthenticated) { $apiCollections = $this->makerWorld->getFavoritesCollections(); } } elseif ($tab === 'favorites' && $isAuthenticated) { $models = $this->makerWorld->getFavorites(); } elseif ($tab === 'published' && $isAuthenticated) { $models = $this->makerWorld->getPublished(); } elseif ($tab === 'community') { $models = $this->makerWorld->getCommunityShowcase(); } $trackedCollections = MakerWorldCollection::withCount(['products', 'draftProducts', 'publishedProducts']) ->latest() ->get(); $stagedCount = Product::where('status', 'draft')->count(); $categories = Category::orderBy('name')->get(); return view('admin.makerworld.index', [ 'isAuthenticated' => $isAuthenticated, 'userProfile' => $userProfile, 'currentTab' => $tab, 'models' => $models, 'apiCollections' => $apiCollections, 'trackedCollections' => $trackedCollections, 'stagedCount' => $stagedCount, 'categories' => $categories, ]); } /** * Display the "Artikel Beheer" staging area where imported models wait for review. */ public function staging(Request $request): View { $tab = $request->query('tab', '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')) { $query->where('makerworld_collection_id', $request->input('collection_id')); } if ($request->filled('q')) { $q = $request->input('q'); $query->where(function ($sub) use ($q) { $sub->where('name', 'like', "%{$q}%") ->orWhere('makerworld_author', 'like', "%{$q}%") ->orWhere('makerworld_id', 'like', "%{$q}%"); }); } $stagedProducts = $query->latest()->paginate(24)->withQueryString(); $categories = Category::orderBy('name')->get(); $collections = MakerWorldCollection::all(); $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' )); } /** * Add a MakerWorld collection to be tracked and auto-synced. */ public function addCollection(Request $request): RedirectResponse { $validated = $request->validate([ 'collection_input' => 'required|string', 'is_active_sync' => 'nullable|boolean', 'sync_now' => 'nullable|boolean', ]); $input = trim($validated['collection_input']); $parsed = $this->makerWorld->parseCollectionIdentifier($input); $collectionId = $parsed['id']; $userHandle = $parsed['handle'] ?? 'user_1718037659'; // If user entered a user collections URL or handle without specific collection ID if ($parsed['type'] === 'user_collections' && ! $collectionId) { // Attempt to fetch collection list for user $userCollections = $this->makerWorld->getFavoritesCollections(); if (! empty($userCollections)) { $addedCount = 0; foreach ($userCollections as $c) { $cId = (string) ($c['id'] ?? ''); if ($cId !== '') { $collection = MakerWorldCollection::firstOrCreate( ['makerworld_collection_id' => $cId], [ 'name' => $c['title'] ?? 'MakerWorld Collectie #'.$cId, 'description' => $c['description'] ?? null, 'cover_url' => is_array($c['designCover'] ?? null) ? ($c['designCover'][0] ?? null) : ($c['cover'] ?? null), 'user_handle' => $userHandle, 'source_url' => $input, 'is_active_sync' => true, 'sync_interval_minutes' => 10, ] ); $this->makerWorld->syncCollection($collection); $addedCount++; } } return redirect()->route('admin.makerworld.index', ['tab' => 'collections']) ->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']) ->with('success', "Collectie van @{$userHandle} succesvol gekoppeld! Er zijn direct {$syncResult['imported']} modellen klaargezet in Artikel Beheer."); } if (! $collectionId) { return redirect()->route('admin.makerworld.index', ['tab' => 'collections']) ->with('error', 'Kon geen geldige MakerWorld collectie ID of URL herkennen.'); } // Fetch collection details from MakerWorld $details = $this->makerWorld->getCollectionDetails($collectionId); $name = $details['title'] ?? 'MakerWorld Collectie #'.$collectionId; $cover = is_array($details['designCover'] ?? null) ? ($details['designCover'][0] ?? null) : ($details['cover'] ?? null); $desc = $details['description'] ?? null; $collection = MakerWorldCollection::updateOrCreate( ['makerworld_collection_id' => (string) $collectionId], [ 'name' => $name, 'description' => $desc, 'cover_url' => $cover, 'user_handle' => $userHandle, 'source_url' => $input, 'is_active_sync' => $request->boolean('is_active_sync', true), 'sync_interval_minutes' => 10, 'raw_metadata' => $details, ] ); $msg = "Collectie '{$collection->name}' is succesvol toegevoegd!"; if ($request->boolean('sync_now', true)) { $syncResult = $this->makerWorld->syncCollection($collection); $msg .= " Direct {$syncResult['imported']} modellen binnengehaald als concept voor Artikel Beheer."; } return redirect()->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', $msg); } /** * Toggle active 10-minute sync for a collection. */ public function toggleCollectionSync(MakerWorldCollection $collection): RedirectResponse { $collection->update([ 'is_active_sync' => ! $collection->is_active_sync, ]); $statusStr = $collection->is_active_sync ? 'geactiveerd (elke 10 min)' : 'gepauzeerd'; return redirect()->back() ->with('success', "Automatische synchronisatie voor '{$collection->name}' is {$statusStr}."); } /** * Trigger immediate synchronization for a collection. */ public function syncCollectionNow(MakerWorldCollection $collection): RedirectResponse { $result = $this->makerWorld->syncCollection($collection); if ($result['error']) { return redirect()->back() ->with('error', "Synchronisatie mislukt: {$result['error']}"); } return redirect()->back() ->with('success', "Collectie '{$collection->name}' gesynchroniseerd: {$result['imported']} nieuwe modellen binnengehaald, {$result['updated']} bijgewerkt."); } /** * Remove a tracked collection. */ public function deleteCollection(MakerWorldCollection $collection): RedirectResponse { $name = $collection->name; $collection->delete(); return redirect()->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', "Collectie '{$name}' is verwijderd uit de synchronisatie."); } /** * Publish a single staged model to the active webshop catalog. */ public function publishStaged(Request $request, Product $product): RedirectResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', 'category_id' => 'required|exists:categories,id', 'base_price' => 'required|numeric|min:0.5', 'short_description' => 'nullable|string|max:500', 'is_featured' => 'nullable|boolean', ]); $product->update([ 'name' => $validated['name'], 'category_id' => $validated['category_id'], 'base_price' => $validated['base_price'], 'short_description' => $validated['short_description'] ?? $product->short_description, 'is_featured' => $request->boolean('is_featured', false), 'status' => 'published', 'is_active' => true, ]); return redirect()->back() ->with('success', "Artikel '{$product->name}' is succesvol goedgekeurd en gepubliceerd in de webshop!"); } /** * Bulk publish selected staged models. */ public function bulkPublishStaged(Request $request): RedirectResponse { $validated = $request->validate([ 'product_ids' => 'required|array', 'product_ids.*' => 'integer|exists:products,id', 'category_id' => 'required|exists:categories,id', 'custom_markup' => 'nullable|numeric', ]); $products = Product::whereIn('id', $validated['product_ids']) ->where('status', 'draft') ->get(); $count = 0; foreach ($products as $p) { $price = $p->base_price; if (! empty($validated['custom_markup']) && $validated['custom_markup'] > 0) { $price = round($price * (1 + ($validated['custom_markup'] / 100)), 2); } $p->update([ 'category_id' => $validated['category_id'], 'base_price' => $price, 'status' => 'published', 'is_active' => true, ]); $count++; } return redirect()->back() ->with('success', "{$count} artikelen succesvol gepubliceerd in de webshop!"); } /** * Reject and delete a staged model. */ public function rejectStaged(Product $product): RedirectResponse { $name = $product->name; $product->delete(); return redirect()->back() ->with('success', "Concept '{$name}' is verwijderd."); } /** * Bulk reject staged models. */ public function bulkRejectStaged(Request $request): RedirectResponse { $validated = $request->validate([ 'product_ids' => 'required|array', 'product_ids.*' => 'integer|exists:products,id', ]); $count = Product::whereIn('id', $validated['product_ids']) ->where('status', 'draft') ->delete(); return redirect()->back() ->with('success', "{$count} concepten zijn verwijderd."); } /** * Authenticate with MakerWorld credentials. */ public function login(Request $request): RedirectResponse { $validated = $request->validate([ 'account' => 'required|string', 'password' => 'required|string', ]); $result = $this->makerWorld->loginWithCredentials( $validated['account'], $validated['password'] ); if ($result['status'] === 'success') { return redirect() ->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', $result['message']); } if ($result['status'] === '2fa_required') { return redirect() ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('warning', $result['message']) ->with('makerworld_2fa_account', $result['account']); } return redirect() ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('error', $result['message']); } /** * Submit email verification code for 2FA. */ public function verifyCode(Request $request): RedirectResponse { $validated = $request->validate([ 'account' => 'required|string', 'code' => 'required|string|size:6', ]); $result = $this->makerWorld->verifyLoginCode( $validated['account'], $validated['code'] ); if ($result['status'] === 'success') { return redirect() ->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', $result['message']); } return redirect() ->route('admin.makerworld.index', ['tab' => 'auth']) ->with('error', $result['message']) ->with('makerworld_2fa_account', $validated['account']); } /** * Resend verification code for 2FA manually on request. */ public function resendCode(Request $request): RedirectResponse { $validated = $request->validate([ 'account' => 'required|string', ]); $sent = $this->makerWorld->sendEmailVerificationCode($validated['account']); return redirect() ->route('admin.makerworld.index', ['tab' => 'auth']) ->with($sent ? 'success' : 'error', $sent ? 'Er is een nieuwe verificatiecode verstuurd naar je e-mail.' : 'Kon verificatiecode niet opnieuw verzenden.') ->with('makerworld_2fa_account', $validated['account']); } /** * Save MakerWorld session / bearer token directly. */ public function saveToken(Request $request): RedirectResponse { $validated = $request->validate([ 'token' => 'required|string|min:10', ]); $this->makerWorld->setAccessToken($validated['token']); return redirect() ->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', 'MakerWorld token succesvol opgeslagen en verbonden!'); } /** * Disconnect MakerWorld account. */ public function disconnect(): RedirectResponse { $this->makerWorld->logout(); return redirect() ->route('admin.makerworld.index', ['tab' => 'collections']) ->with('success', 'MakerWorld account succesvol ontkoppeld.'); } /** * Ajax preview endpoint for single model by URL or ID. */ public function preview(Request $request): JsonResponse { $input = $request->query('query'); if (! $input) { return response()->json(['error' => 'Geen model URL of ID opgegeven.'], 400); } $id = $this->makerWorld->parseModelId($input); if (! $id) { return response()->json(['error' => 'Ongeldige MakerWorld URL of model-ID.'], 400); } $details = $this->makerWorld->getDesignDetails($id); if (! $details) { return response()->json(['error' => "Model #{$id} kon niet worden opgehaald bij MakerWorld."], 404); } $formatted = $this->makerWorld->formatDesignForImport($details); $alreadyImported = Product::where('makerworld_id', $id)->first(); return response()->json([ 'model' => $formatted, 'raw' => $details, 'is_imported' => (bool) $alreadyImported, 'existing_product' => $alreadyImported, ]); } /** * Import a single model into shop. */ public function import(Request $request): RedirectResponse|JsonResponse { $validated = $request->validate([ 'makerworld_id' => 'required|integer', 'category_id' => 'nullable|exists:categories,id', 'custom_price' => 'nullable|numeric|min:0.5', 'custom_name' => 'nullable|string|max:255', 'is_active' => 'nullable|boolean', 'status' => 'nullable|string|in:draft,published', ]); $details = $this->makerWorld->getDesignDetails($validated['makerworld_id']); if (! $details) { $msg = "Model #{$validated['makerworld_id']} kon niet worden opgehaald."; if ($request->wantsJson()) { return response()->json(['error' => $msg], 404); } return redirect()->back()->with('error', $msg); } $status = $validated['status'] ?? ($request->boolean('is_active', true) ? 'published' : 'draft'); $product = $this->makerWorld->importDesign( $details, $validated['category_id'] ?? null, $validated['custom_price'] ?? null, $validated['custom_name'] ?? null, $status === 'published', $status ); $msg = "Model \"{$product->name}\" is succesvol toegevoegd!"; if ($request->wantsJson()) { return response()->json([ 'success' => true, 'message' => $msg, 'product' => $product, ]); } return redirect()->back()->with('success', $msg); } /** * Batch import multiple selected models. */ public function batchImport(Request $request): RedirectResponse { $validated = $request->validate([ 'ids' => 'required|array', 'ids.*' => 'integer', 'category_id' => 'nullable|exists:categories,id', 'is_active' => 'nullable|boolean', ]); $count = 0; $isActive = $request->boolean('is_active', true); $categoryId = $validated['category_id'] ?? null; foreach ($validated['ids'] as $id) { $details = $this->makerWorld->getDesignDetails($id); if ($details) { $this->makerWorld->importDesign( $details, $categoryId, null, null, $isActive, $isActive ? 'published' : 'draft' ); $count++; } } return redirect()->back()->with('success', "{$count} modellen succesvol geïmporteerd!"); } }