Files

604 lines
21 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\Support\Facades\Log;
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') {
// 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!");
}
}