587 lines
20 KiB
PHP
587 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Category;
|
|
use App\Models\MakerWorldCollection;
|
|
use App\Models\Product;
|
|
use App\Services\MakerWorldService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class MakerWorldController extends Controller
|
|
{
|
|
public function __construct(
|
|
public MakerWorldService $makerWorld
|
|
) {}
|
|
|
|
/**
|
|
* Display the MakerWorld importer & collections dashboard.
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
$isAuthenticated = $this->makerWorld->isAuthenticated();
|
|
$userProfile = $this->makerWorld->getUserProfile();
|
|
|
|
$tab = $request->query('tab', 'collections');
|
|
$models = [];
|
|
$apiCollections = [];
|
|
|
|
if ($tab === 'collections') {
|
|
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!");
|
|
}
|
|
}
|