buckets() ->orderedBySortOrder() ->get() ->map(function ($bucket) { return [ 'id' => $bucket->id, 'name' => $bucket->name, 'priority' => $bucket->priority, 'sort_order' => $bucket->sort_order, 'allocation_type' => $bucket->allocation_type, 'allocation_value' => $bucket->allocation_value, 'allocation_type_label' => $bucket->getAllocationTypeLabel(), 'formatted_allocation_value' => $bucket->getFormattedAllocationValue(), 'current_balance' => $bucket->getCurrentBalance(), 'has_available_space' => $bucket->hasAvailableSpace(), 'available_space' => $bucket->getAvailableSpace(), ]; }); return response()->json([ 'buckets' => $buckets ]); } public function store(Request $request, Scenario $scenario): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', 'allocation_type' => 'required|in:' . implode(',', [ Bucket::TYPE_FIXED_LIMIT, Bucket::TYPE_PERCENTAGE, Bucket::TYPE_UNLIMITED, ]), 'allocation_value' => 'nullable|numeric', 'priority' => 'nullable|integer|min:1', ]); try { $createBucketAction = new CreateBucketAction(); $bucket = $createBucketAction->execute( $scenario, $validated['name'], $validated['allocation_type'], $validated['allocation_value'], $validated['priority'] ?? null ); return response()->json([ 'bucket' => $this->formatBucketResponse($bucket), 'message' => 'Bucket created successfully.' ], 201); } catch (InvalidArgumentException $e) { return response()->json([ 'message' => 'Validation failed.', 'errors' => ['allocation_value' => [$e->getMessage()]] ], 422); } } public function update(Request $request, Bucket $bucket): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', 'allocation_type' => 'required|in:' . implode(',', [ Bucket::TYPE_FIXED_LIMIT, Bucket::TYPE_PERCENTAGE, Bucket::TYPE_UNLIMITED, ]), 'allocation_value' => 'nullable|numeric', 'priority' => 'nullable|integer|min:1', ]); // Validate allocation_value based on allocation_type $allocationValueRules = Bucket::allocationValueRules($validated['allocation_type']); $request->validate([ 'allocation_value' => $allocationValueRules, ]); // Set allocation_value to null for unlimited buckets if ($validated['allocation_type'] === Bucket::TYPE_UNLIMITED) { $validated['allocation_value'] = null; } // Handle priority change if needed if (isset($validated['priority']) && $validated['priority'] !== $bucket->priority) { $this->updateBucketPriority($bucket, $validated['priority']); $validated['sort_order'] = $validated['priority']; } $bucket->update($validated); return response()->json([ 'bucket' => $this->formatBucketResponse($bucket), 'message' => 'Bucket updated successfully.' ]); } /** * Remove the specified bucket. */ public function destroy(Bucket $bucket): JsonResponse { $scenarioId = $bucket->scenario_id; $deletedPriority = $bucket->priority; $bucket->delete(); // Shift remaining priorities down to fill the gap $this->shiftPrioritiesDown($scenarioId, $deletedPriority); return response()->json([ 'message' => 'Bucket deleted successfully.' ]); } /** * Update bucket priorities (for drag-and-drop reordering). */ public function updatePriorities(Request $request, Scenario $scenario): JsonResponse { $validated = $request->validate([ 'bucket_priorities' => 'required|array', 'bucket_priorities.*.id' => 'required|exists:buckets,id', 'bucket_priorities.*.priority' => 'required|integer|min:1', ]); foreach ($validated['bucket_priorities'] as $bucketData) { $bucket = Bucket::find($bucketData['id']); if ($bucket && $bucket->scenario_id === $scenario->id) { $bucket->update([ 'priority' => $bucketData['priority'], 'sort_order' => $bucketData['priority'], ]); } } return response()->json([ 'message' => 'Bucket priorities updated successfully.' ]); } /** * Format bucket data for JSON response. */ private function formatBucketResponse(Bucket $bucket): array { return [ 'id' => $bucket->id, 'name' => $bucket->name, 'priority' => $bucket->priority, 'sort_order' => $bucket->sort_order, 'allocation_type' => $bucket->allocation_type, 'allocation_value' => $bucket->allocation_value, 'allocation_type_label' => $bucket->getAllocationTypeLabel(), 'formatted_allocation_value' => $bucket->getFormattedAllocationValue(), 'current_balance' => $bucket->getCurrentBalance(), 'has_available_space' => $bucket->hasAvailableSpace(), 'available_space' => $bucket->getAvailableSpace(), ]; } /** * Shift priorities down to fill gap after deletion. */ private function shiftPrioritiesDown(int $scenarioId, int $deletedPriority): void { Bucket::query() ->where('scenario_id', $scenarioId) ->where('priority', '>', $deletedPriority) ->decrement('priority'); } /** * Update a bucket's priority and adjust other buckets accordingly. */ private function updateBucketPriority(Bucket $bucket, int $newPriority): void { $oldPriority = $bucket->priority; $scenario = $bucket->scenario; if ($newPriority === $oldPriority) { return; } // Use database transaction to handle constraint conflicts DB::transaction(function () use ($bucket, $scenario, $oldPriority, $newPriority) { // Temporarily set the moving bucket to a high priority to avoid conflicts $tempPriority = $scenario->buckets()->max('priority') + 100; $bucket->update(['priority' => $tempPriority]); if ($newPriority < $oldPriority) { // Moving up - shift others down $scenario->buckets() ->where('id', '!=', $bucket->id) ->whereBetween('priority', [$newPriority, $oldPriority - 1]) ->increment('priority'); } else { // Moving down - shift others up $scenario->buckets() ->where('id', '!=', $bucket->id) ->whereBetween('priority', [$oldPriority + 1, $newPriority]) ->decrement('priority'); } // Finally, set the bucket to its new priority $bucket->update(['priority' => $newPriority]); }); } }