filled('caja') || $request->filled('box_number'); $packages = Package::query(); if ($shouldLoadTags) { $packages->with([ 'tags:id,folio,tag_number,package_id,status_id,vehicle_id,module_id', 'tags.status:id,code,name', 'tags.vehicle:id,placa,niv', 'tags.module:id,name', 'user:id,name,username' ]); } else { $packages->with('user:id,name,username'); } $packages->withCount('tags')->orderBy('id', 'ASC'); if ($request->filled('lote') || $request->filled('lot')) { $loteValue = $request->input('lote') ?? $request->input('lot'); $packages->where('lot', 'LIKE', '%' . trim($loteValue) . '%'); } if ($request->filled('caja') || $request->filled('box_number')) { $cajaValue = $request->input('caja') ?? $request->input('box_number'); $packages->where('box_number', 'LIKE', '%' . trim($cajaValue) . '%'); } $paginatedPackages = $packages->paginate(config('app.pagination')); // Validación si no hay resultados if ($paginatedPackages->isEmpty()) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontraron paquetes con los criterios de búsqueda proporcionados.', 'filters_applied' => [ 'lote' => $request->input('lote') ?? $request->input('lot'), 'caja' => $request->input('caja') ?? $request->input('box_number'), ] ]); } // Si hay filtro de caja, incluir estadísticas de tags if ($request->filled('caja') || $request->filled('box_number')) { $paginatedPackages->getCollection()->transform(function ($package) { return [ 'id' => $package->id, 'lot' => $package->lot, 'box_number' => $package->box_number, 'starting_page' => $package->starting_page, 'ending_page' => $package->ending_page, 'created_by' => $package->user ? [ 'id' => $package->user->id, 'name' => $package->user->name, 'username' => $package->user->username, ] : null, 'estadisticas' => [ 'total' => $package->tags->count(), 'available' => $package->tags->filter(function($tag) { return $tag->status && $tag->status->code === 'available'; })->count(), 'assigned' => $package->tags->filter(function($tag) { return $tag->status && $tag->status->code === 'assigned'; })->count(), 'cancelled' => $package->tags->filter(function($tag) { return $tag->status && $tag->status->code === 'cancelled'; })->count(), ], 'tags' => $package->tags->map(function ($tag) { return [ 'id' => $tag->id, 'folio' => $tag->folio, 'tag_number' => $tag->tag_number, 'status' => $tag->status ? [ 'id' => $tag->status->id, 'code' => $tag->status->code, 'name' => $tag->status->name, ] : null, 'vehicle' => $tag->vehicle ? [ 'id' => $tag->vehicle->id, 'placa' => $tag->vehicle->placa, 'niv' => $tag->vehicle->niv, ] : null, 'module' => $tag->module ? [ 'id' => $tag->module->id, 'name' => $tag->module->name, ] : null, ]; }), ]; }); } return ApiResponse::OK->response([ 'Paquetes' => $paginatedPackages, ]); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al obtener los paquetes', 'error' => $e->getMessage(), ]); } } public function store(PackageStoreRequest $request) { try { DB::beginTransaction(); // Verificar folios en el mismo lote antes de crear $packageIds = Package::where('lot', $request->lot)->pluck('id'); $conflicting = Tag::whereIn('package_id', $packageIds) ->whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page]) ->pluck('folio'); if ($conflicting->isNotEmpty()) { DB::rollBack(); return ApiResponse::UNPROCESSABLE_CONTENT->response([ 'starting_page' => [ 'Los folios ' . $conflicting->join(', ') . ' ya están registrados en el lote "' . $request->lot . '".', ], ]); } $form = $request->validated(); $form['user_id'] = Auth::id(); $package = Package::create($form); $statusAvailable = CatalogTagStatus::where('code', 'available')->firstOrFail(); for ($page = $request->starting_page; $page <= $request->ending_page; $page++) { Tag::create([ 'folio' => $page, 'tag_number' => null, 'package_id' => $package->id, 'status_id' => $statusAvailable->id, ]); } DB::commit(); return ApiResponse::CREATED->response([ 'message' => 'Paquete registrado exitosamente con sus tags', 'package' => $package->load('tags'), 'tags_created' => $package->tags()->count(), ]); } catch (\Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al crear el paquete', 'error' => $e->getMessage(), ]); } } public function show($id) { try { $package = Package::with(['tags'])->findOrFail($id); return ApiResponse::OK->response([ 'package' => $package, ]); } catch (ModelNotFoundException $e) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Paquete no encontrado.', ]); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al obtener el paquete', 'error' => $e->getMessage(), ]); } } public function update(PackageUpdateRequest $request, $id) { try { $package = Package::with('tags')->findOrFail($id); $validated = $request->validated(); // Validar si el paquete tiene tags asignados $hasTags = $package->tags()->count() > 0; // Si tiene tags, validar que no se cambien los rangos de páginas if ($hasTags) { if (isset($validated['starting_page']) && $validated['starting_page'] != $package->starting_page) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se puede cambiar el rango inicial porque el paquete ya tiene tags asignados.', 'current_starting_page' => $package->starting_page, 'requested_starting_page' => $validated['starting_page'], 'tags_count' => $package->tags()->count(), ]); } if (isset($validated['ending_page']) && $validated['ending_page'] != $package->ending_page) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se puede cambiar el rango final porque el paquete ya tiene tags asignados.', 'current_ending_page' => $package->ending_page, 'requested_ending_page' => $validated['ending_page'], 'tags_count' => $package->tags()->count(), ]); } } // Validar que la combinación de lote + caja sea única if (isset($validated['lot']) || isset($validated['box_number'])) { $newLot = $validated['lot'] ?? $package->lot; $newBoxNumber = $validated['box_number'] ?? $package->box_number; $existingPackage = Package::where('lot', $newLot) ->where('box_number', $newBoxNumber) ->where('id', '!=', $package->id) ->first(); if ($existingPackage) { return ApiResponse::BAD_REQUEST->response([ 'message' => "Ya existe otro paquete con el lote '{$newLot}' y caja '{$newBoxNumber}'.", 'existing_package_id' => $existingPackage->id, 'current_lot' => $package->lot, 'current_box_number' => $package->box_number, 'requested_lot' => $newLot, 'requested_box_number' => $newBoxNumber, ]); } } DB::beginTransaction(); // Guardar valores anteriores para el log $changes = []; foreach ($validated as $key => $value) { if ($package->$key != $value) { $changes[$key] = [ 'old' => $package->$key, 'new' => $value, ]; } } // Si no hay cambios if (empty($changes)) { return ApiResponse::OK->response([ 'message' => 'No se realizaron cambios en el paquete.', 'package' => [ 'id' => $package->id, 'lot' => $package->lot, 'box_number' => $package->box_number, 'starting_page' => $package->starting_page, 'ending_page' => $package->ending_page, ], ]); } $package->update($validated); DB::commit(); return ApiResponse::OK->response([ 'message' => 'Paquete actualizado exitosamente', 'package' => [ 'id' => $package->id, 'lot' => $package->lot, 'box_number' => $package->box_number, 'starting_page' => $package->starting_page, 'ending_page' => $package->ending_page, 'updated_at' => $package->updated_at->format('Y-m-d H:i:s'), ], 'changes' => $changes, ]); } catch (QueryException $e) { DB::rollBack(); if ($e->getCode() == 23000 && str_contains($e->getMessage(), 'packages_lot_box_unique')) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Ya existe un paquete con esa combinación de lote y caja.', 'error' => $e->getMessage(), ]); } return ApiResponse::NOT_FOUND->response([ 'message' => 'Error de base de datos al actualizar el paquete', 'error' => $e->getMessage(), ]); } catch (\Exception $e) { DB::rollBack(); return ApiResponse::NOT_FOUND->response([ 'message' => 'Error al actualizar el paquete', 'error' => $e->getMessage(), ]); } } public function destroy($id) { try { DB::beginTransaction(); $package = Package::findOrFail($id); if ($package->tags()->count() > 0) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se puede eliminar el paquete porque tiene tags asociados.', 'tags_count' => $package->tags()->count(), ]); } $package->delete(); DB::commit(); return ApiResponse::OK->response([ 'message' => 'Paquete eliminado exitosamente.', ]); } catch (\Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al eliminar el paquete.', 'error' => $e->getMessage(), ]); } } /** * Obtener tags de una caja específica con paginación */ public function getBoxTags(Request $request) { try { // Si no se envían parámetros, obtener el primer paquete if (!$request->has('lot') && !$request->has('box_number')) { $package = Package::with('user')->orderBy('id', 'DESC')->first(); if (!$package) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontraron paquetes en el sistema.', ]); } } else { // Validar parámetros si se proporcionan $validated = $request->validate([ 'lot' => 'required|string', 'box_number' => 'required|string|max:255', ]); $lot = $validated['lot']; $boxNumber = $validated['box_number']; // Buscar el paquete $package = Package::with('user')->where('lot', $lot) ->where('box_number', $boxNumber) ->first(); if (!$package) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontró un paquete con el lote y caja especificados.', 'lot' => $lot, 'box_number' => $boxNumber, ]); } } // Obtener tags con paginación $tags = Tag::with(['status:id,code,name', 'vehicle:id,placa,niv', 'module:id,name', 'cancellationLogs.cancellationReason']) ->where('package_id', $package->id) ->orderBy('id', 'ASC'); // Filtro adicional por status si se proporciona if ($request->filled('status')) { $tags->whereHas('status', function ($q) use ($request) { $q->where('code', $request->input('status')); }); } if($request->filled('module_id')) { $tags->where('module_id', $request->input('module_id')); } $paginatedTags = $tags->paginate($request->input('per_page', 25)); // Transformar tags $paginatedTags->getCollection()->transform(function ($tag) { return [ 'id' => $tag->id, 'folio' => $tag->folio, 'tag_number' => $tag->tag_number, 'status' => [ 'id' => $tag->status->id, 'code' => $tag->status->code, 'name' => $tag->status->name, ], 'vehicle' => $tag->vehicle ? [ 'id' => $tag->vehicle->id, 'placa' => $tag->vehicle->placa, 'niv' => $tag->vehicle->niv, ] : null, 'module' => $tag->module ? [ 'id' => $tag->module->id, 'name' => $tag->module->name, ] : null, 'cancellation_reason' => in_array($tag->status?->code, ['cancelled', 'damaged']) && $tag->cancellationLogs->first() ? [ 'reason' => $tag->cancellationLogs->first()->cancellationReason?->name, 'observations' => $tag->cancellationLogs->first()->cancellation_observations, 'cancelled_at' => $tag->cancellationLogs->first()->cancellation_at, ] : null, ]; }); return ApiResponse::OK->response([ 'package' => [ 'id' => $package->id, 'lot' => $package->lot, 'box_number' => $package->box_number, 'starting_page' => $package->starting_page, 'ending_page' => $package->ending_page, 'created_by' => $package->user ? [ 'id' => $package->user->id, 'name' => $package->user->name, 'username' => $package->user->username, ] : null, ], 'tags' => $paginatedTags, ]); } catch (\Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al obtener los tags de la caja.', 'error' => $e->getMessage(), ]); } } }