diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index cb94715..b8783b4 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -8,6 +8,7 @@ use App\Models\Product; use App\Models\Tag; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\Rule; @@ -121,21 +122,34 @@ class ProductController extends Controller $validated['user_id'] = Auth::id(); $validated['status'] = 'pending'; - // Handle thumbnail upload - if ($request->hasFile('thumbnail')) { - $path = $request->file('thumbnail')->store('thumbnails', 'public'); - $validated['thumbnail'] = $path; + $created = []; + $obsolete = []; + + try { + $product = DB::transaction(function () use ($request, $validated, &$created, &$obsolete) { + // Handle thumbnail upload + if ($request->hasFile('thumbnail')) { + $path = $request->file('thumbnail')->store('thumbnails', 'public'); + $created[] = $path; + $validated['thumbnail'] = $path; + } + + $product = Product::create($validated); + + // Attach tags if provided + if ($request->has('tags')) { + $product->tags()->attach($request->tags); + } + + $this->syncDetails($request, $product, true, $created, $obsolete); + + return $product; + }); + } catch (\Throwable $e) { + Storage::disk('public')->delete($created); + throw $e; } - $product = Product::create($validated); - - // Attach tags if provided - if ($request->has('tags')) { - $product->tags()->attach($request->tags); - } - - $this->syncDetails($request, $product); - return redirect()->route('products.show', $product) ->with('success', 'Product submitted successfully! It will be reviewed by our moderators.'); } @@ -193,23 +207,39 @@ class ProductController extends Controller $validated = array_diff_key($validated, Product::BADGES); } - // Handle thumbnail upload - if ($request->hasFile('thumbnail')) { - // Delete old thumbnail if exists - if ($product->thumbnail) { - Storage::disk('public')->delete($product->thumbnail); - } + // Only the submitter and admins manage the team + $manageMembers = Auth::id() === $product->user_id || Auth::user()->isAdmin(); - $path = $request->file('thumbnail')->store('thumbnails', 'public'); - $validated['thumbnail'] = $path; + $created = []; + $obsolete = []; + + try { + DB::transaction(function () use ($request, $product, $validated, $manageMembers, &$created, &$obsolete) { + // Handle thumbnail upload + if ($request->hasFile('thumbnail')) { + if ($product->thumbnail) { + $obsolete[] = $product->thumbnail; + } + + $path = $request->file('thumbnail')->store('thumbnails', 'public'); + $created[] = $path; + $validated['thumbnail'] = $path; + } + + $product->update($validated); + + $this->syncDetails($request, $product, $manageMembers, $created, $obsolete); + + // Sync tags (will remove old tags and add new ones) + $product->tags()->sync($request->tags ?? []); + }); + } catch (\Throwable $e) { + Storage::disk('public')->delete($created); + throw $e; } - $product->update($validated); - - $this->syncDetails($request, $product); - - // Sync tags (will remove old tags and add new ones) - $product->tags()->sync($request->tags ?? []); + // Old files are only removed once the changes are committed + Storage::disk('public')->delete($obsolete); return redirect()->route('products.show', $product) ->with('success', 'Product updated successfully!'); @@ -231,7 +261,7 @@ class ProductController extends Controller 'remove_screenshots.*' => 'integer', 'plans' => 'nullable|array', 'plans.*.name' => 'required|string|max:100', - 'plans.*.price' => 'required|numeric|min:0|max:99999', + 'plans.*.price' => 'required|numeric|min:0|max:99999|decimal:0,2', 'plans.*.period' => ['required', Rule::in(PricingPlan::PERIODS)], 'plans.*.description' => 'nullable|string|max:255', 'members' => 'nullable|array', @@ -242,33 +272,39 @@ class ProductController extends Controller /** * Sync related product detail data (badges are stored on the product itself). + * Must run inside a transaction: newly stored files are added to $created, + * files to remove after commit are added to $obsolete. */ - protected function syncDetails(Request $request, Product $product): void + protected function syncDetails(Request $request, Product $product, bool $manageMembers, array &$created, array &$obsolete): void { - // The submitted plan list replaces all existing plans - $product->pricingPlans()->delete(); - foreach (array_values((array) $request->input('plans', [])) as $i => $plan) { - $product->pricingPlans()->create([ - 'name' => $plan['name'], - 'price' => $plan['price'], - 'period' => $plan['period'], - 'description' => $plan['description'] ?? null, - 'position' => $i, - ]); + // The submitted plan list replaces all existing plans (a missing sentinel means the form could not send one) + if ($request->has('plans_present') || $product->wasRecentlyCreated) { + $product->pricingPlans()->delete(); + foreach (array_values((array) $request->input('plans', [])) as $i => $plan) { + $product->pricingPlans()->create([ + 'name' => $plan['name'], + 'price' => $plan['price'], + 'period' => $plan['period'], + 'description' => $plan['description'] ?? null, + 'position' => $i, + ]); + } } // The submitted member list replaces all existing members - $members = []; - foreach ((array) $request->input('members', []) as $member) { - $members[$member['user_id']] = ['role' => $member['role']]; + if ($manageMembers && ($request->has('members_present') || $product->wasRecentlyCreated)) { + $members = []; + foreach ((array) $request->input('members', []) as $member) { + $members[$member['user_id']] = ['role' => $member['role']]; + } + $product->members()->sync($members); } - $product->members()->sync($members); // Only rows belonging to this product may be removed $ids = array_filter((array) $request->input('remove_screenshots', [])); if ($ids) { - $product->screenshots()->whereIn('id', $ids)->get()->each(function ($shot) { - Storage::disk('public')->delete($shot->path); + $product->screenshots()->whereIn('id', $ids)->get()->each(function ($shot) use (&$obsolete) { + $obsolete[] = $shot->path; $shot->delete(); }); } @@ -276,8 +312,10 @@ class ProductController extends Controller if ($request->hasFile('screenshots')) { $position = ($product->screenshots()->max('position') ?? -1) + 1; foreach ($request->file('screenshots') as $file) { + $path = $file->store('screenshots', 'public'); + $created[] = $path; $product->screenshots()->create([ - 'path' => $file->store('screenshots', 'public'), + 'path' => $path, 'position' => $position++, ]); } @@ -291,17 +329,16 @@ class ProductController extends Controller { Gate::authorize('delete-product', $product); - // Delete thumbnail if exists + $files = $product->screenshots->pluck('path')->all(); if ($product->thumbnail) { - Storage::disk('public')->delete($product->thumbnail); - } - - foreach ($product->screenshots as $shot) { - Storage::disk('public')->delete($shot->path); + $files[] = $product->thumbnail; } $product->delete(); + // Files go only after the row is gone + Storage::disk('public')->delete($files); + return redirect()->route('products.index') ->with('success', 'Product deleted successfully!'); } diff --git a/resources/views/components/product/gallery.blade.php b/resources/views/components/product/gallery.blade.php index 3a1697e..7babbe9 100644 --- a/resources/views/components/product/gallery.blade.php +++ b/resources/views/components/product/gallery.blade.php @@ -18,7 +18,7 @@ @keydown.arrow-left.window="open && prev()" {{ $attributes }}>
- @foreach($tiles as $i => $shot) diff --git a/resources/views/components/product/plans-form.blade.php b/resources/views/components/product/plans-form.blade.php index 57536a7..e6eacaa 100644 --- a/resources/views/components/product/plans-form.blade.php +++ b/resources/views/components/product/plans-form.blade.php @@ -1,7 +1,7 @@ @props(['plans' => []]) @php - $initial = collect(old('plans', $plans))->map(fn ($p) => [ + $initial = collect(old('plans_present') ? old('plans', []) : $plans)->map(fn ($p) => [ 'name' => $p['name'] ?? '', 'price' => $p['price'] ?? '', 'period' => $p['period'] ?? 'monthly', @@ -12,6 +12,7 @@
+