From 37f91d84f2ecac12918e1425e2f4e1d68ba3d23b Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Wed, 25 Feb 2026 12:31:51 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20soporte=20para=20subcategor?= =?UTF-8?q?=C3=ADas,=20incluyendo=20controladores,=20solicitudes=20y=20mig?= =?UTF-8?q?raciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/App/CategoryController.php | 5 +- .../Controllers/App/SubcategoryController.php | 55 +++++++++++++++++++ .../Controllers/App/WarehouseController.php | 7 +++ .../Requests/App/CategoryStoreRequest.php | 1 + .../Requests/App/CategoryUpdateRequest.php | 2 + .../Requests/App/SubcategoryStoreRequest.php | 32 +++++++++++ .../Requests/App/SubcategoryUpdateRequest.php | 31 +++++++++++ app/Models/Category.php | 8 +++ app/Models/Inventory.php | 6 ++ app/Models/Subcategory.php | 30 ++++++++++ ...2_25_100000_create_subcategories_table.php | 26 +++++++++ ...dd_subcategory_id_to_inventories_table.php | 23 ++++++++ routes/api.php | 10 ++++ 13 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/App/SubcategoryController.php create mode 100644 app/Http/Requests/App/SubcategoryStoreRequest.php create mode 100644 app/Http/Requests/App/SubcategoryUpdateRequest.php create mode 100644 app/Models/Subcategory.php create mode 100644 database/migrations/2026_02_25_100000_create_subcategories_table.php create mode 100644 database/migrations/2026_02_25_100001_add_subcategory_id_to_inventories_table.php diff --git a/app/Http/Controllers/App/CategoryController.php b/app/Http/Controllers/App/CategoryController.php index db66dc7..31f897d 100644 --- a/app/Http/Controllers/App/CategoryController.php +++ b/app/Http/Controllers/App/CategoryController.php @@ -11,7 +11,8 @@ class CategoryController extends Controller { public function index() { - $categorias = Category::where('is_active', true) + $categorias = Category::with(['subcategories' => fn ($q) => $q->where('is_active', true)->orderBy('name')]) + ->where('is_active', true) ->orderBy('name') ->paginate(config('app.pagination')); @@ -22,6 +23,8 @@ public function index() public function show(Category $categoria) { + $categoria->load(['subcategories' => fn ($q) => $q->where('is_active', true)->orderBy('name')]); + return ApiResponse::OK->response([ 'model' => $categoria ]); diff --git a/app/Http/Controllers/App/SubcategoryController.php b/app/Http/Controllers/App/SubcategoryController.php new file mode 100644 index 0000000..f72b7ed --- /dev/null +++ b/app/Http/Controllers/App/SubcategoryController.php @@ -0,0 +1,55 @@ +subcategories() + ->where('is_active', true) + ->orderBy('name') + ->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'subcategories' => $subcategorias, + ]); + } + + public function show(Category $category, Subcategory $subcategory) + { + return ApiResponse::OK->response([ + 'model' => $subcategory, + ]); + } + + public function store(SubcategoryStoreRequest $request, Category $category) + { + $subcategoria = $category->subcategories()->create($request->validated()); + + return ApiResponse::OK->response([ + 'model' => $subcategoria, + ]); + } + + public function update(SubcategoryUpdateRequest $request, Category $category, Subcategory $subcategory) + { + $subcategory->update($request->validated()); + + return ApiResponse::OK->response([ + 'model' => $subcategory->fresh(), + ]); + } + + public function destroy(Category $category, Subcategory $subcategory) + { + $subcategory->delete(); + + return ApiResponse::OK->response(); + } +} diff --git a/app/Http/Controllers/App/WarehouseController.php b/app/Http/Controllers/App/WarehouseController.php index b62409d..045d1b6 100644 --- a/app/Http/Controllers/App/WarehouseController.php +++ b/app/Http/Controllers/App/WarehouseController.php @@ -112,6 +112,13 @@ public function destroy(int $id) ]); } + // Verificar si es el almacén principal + if ($warehouse->is_main) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'No se puede eliminar el almacén principal' + ]); + } + // Verificar si tiene stock $hasStock = $warehouse->inventories()->wherePivot('stock', '>', 0)->exists(); diff --git a/app/Http/Requests/App/CategoryStoreRequest.php b/app/Http/Requests/App/CategoryStoreRequest.php index 3e6cf5f..59a4a0d 100644 --- a/app/Http/Requests/App/CategoryStoreRequest.php +++ b/app/Http/Requests/App/CategoryStoreRequest.php @@ -21,6 +21,7 @@ public function rules(): array return [ 'name' => ['required', 'string', 'max:100'], 'description' => ['nullable', 'string', 'max:225'], + 'is_active' => ['nullable', 'boolean'], ]; } diff --git a/app/Http/Requests/App/CategoryUpdateRequest.php b/app/Http/Requests/App/CategoryUpdateRequest.php index fca682d..c7e2679 100644 --- a/app/Http/Requests/App/CategoryUpdateRequest.php +++ b/app/Http/Requests/App/CategoryUpdateRequest.php @@ -21,6 +21,7 @@ public function rules(): array return [ 'name' => ['nullable', 'string', 'max:100'], 'description' => ['nullable', 'string', 'max:225'], + 'is_active' => ['nullable', 'boolean'], ]; } @@ -31,6 +32,7 @@ public function messages(): array 'name.max' => 'El nombre no debe exceder los 100 caracteres.', 'description.string' => 'La descripción debe ser una cadena de texto.', 'description.max' => 'La descripción no debe exceder los 225 caracteres.', + 'is_active.boolean' => 'El campo activo debe ser verdadero o falso.', ]; } } diff --git a/app/Http/Requests/App/SubcategoryStoreRequest.php b/app/Http/Requests/App/SubcategoryStoreRequest.php new file mode 100644 index 0000000..1891542 --- /dev/null +++ b/app/Http/Requests/App/SubcategoryStoreRequest.php @@ -0,0 +1,32 @@ + ['required', 'string', 'max:100'], + 'description' => ['nullable', 'string', 'max:255'], + 'is_active' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'El nombre es obligatorio.', + 'name.string' => 'El nombre debe ser una cadena de texto.', + 'name.max' => 'El nombre no debe exceder los 100 caracteres.', + 'description.string' => 'La descripción debe ser una cadena de texto.', + 'description.max' => 'La descripción no debe exceder los 255 caracteres.', + 'is_active.boolean' => 'El campo activo debe ser verdadero o falso.', + ]; + } +} diff --git a/app/Http/Requests/App/SubcategoryUpdateRequest.php b/app/Http/Requests/App/SubcategoryUpdateRequest.php new file mode 100644 index 0000000..9d8d8fb --- /dev/null +++ b/app/Http/Requests/App/SubcategoryUpdateRequest.php @@ -0,0 +1,31 @@ + ['nullable', 'string', 'max:100'], + 'description' => ['nullable', 'string', 'max:255'], + 'is_active' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'name.string' => 'El nombre debe ser una cadena de texto.', + 'name.max' => 'El nombre no debe exceder los 100 caracteres.', + 'description.string' => 'La descripción debe ser una cadena de texto.', + 'description.max' => 'La descripción no debe exceder los 255 caracteres.', + 'is_active.boolean' => 'El campo activo debe ser verdadero o falso.', + ]; + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php index f06409e..3d0d25d 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; /** * Descripción @@ -15,6 +16,8 @@ */ class Category extends Model { + use SoftDeletes; + protected $fillable = [ 'name', 'description', @@ -25,6 +28,11 @@ class Category extends Model 'is_active' => 'boolean', ]; + public function subcategories() + { + return $this->hasMany(Subcategory::class); + } + public function inventories() { return $this->hasMany(Inventory::class); diff --git a/app/Models/Inventory.php b/app/Models/Inventory.php index 80cedc9..6f3bc2f 100644 --- a/app/Models/Inventory.php +++ b/app/Models/Inventory.php @@ -22,6 +22,7 @@ class Inventory extends Model { protected $fillable = [ 'category_id', + 'subcategory_id', 'unit_of_measure_id', 'name', 'key_sat', @@ -76,6 +77,11 @@ public function category() return $this->belongsTo(Category::class); } + public function subcategory() + { + return $this->belongsTo(Subcategory::class); + } + public function unitOfMeasure() { return $this->belongsTo(UnitOfMeasurement::class); diff --git a/app/Models/Subcategory.php b/app/Models/Subcategory.php new file mode 100644 index 0000000..5b807c2 --- /dev/null +++ b/app/Models/Subcategory.php @@ -0,0 +1,30 @@ + 'boolean', + ]; + + public function category() + { + return $this->belongsTo(Category::class); + } + + public function inventories() + { + return $this->hasMany(Inventory::class); + } +} diff --git a/database/migrations/2026_02_25_100000_create_subcategories_table.php b/database/migrations/2026_02_25_100000_create_subcategories_table.php new file mode 100644 index 0000000..2944a38 --- /dev/null +++ b/database/migrations/2026_02_25_100000_create_subcategories_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('subcategories'); + } +}; diff --git a/database/migrations/2026_02_25_100001_add_subcategory_id_to_inventories_table.php b/database/migrations/2026_02_25_100001_add_subcategory_id_to_inventories_table.php new file mode 100644 index 0000000..d6179dd --- /dev/null +++ b/database/migrations/2026_02_25_100001_add_subcategory_id_to_inventories_table.php @@ -0,0 +1,23 @@ +foreignId('subcategory_id')->nullable()->after('category_id')->constrained()->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('inventories', function (Blueprint $table) { + $table->dropForeign(['subcategory_id']); + $table->dropColumn('subcategory_id'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 70ed53d..c2cfe41 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\App\BundleController; use App\Http\Controllers\App\CashRegisterController; use App\Http\Controllers\App\CategoryController; +use App\Http\Controllers\App\SubcategoryController; use App\Http\Controllers\App\ClientController; use App\Http\Controllers\App\ClientTierController; use App\Http\Controllers\App\ExcelController; @@ -85,6 +86,15 @@ //CATEGORIAS Route::resource('categorias', CategoryController::class); + // SUBCATEGORIAS + Route::prefix('categorias/{category}/subcategorias')->group(function () { + Route::get('/', [SubcategoryController::class, 'index']); + Route::post('/', [SubcategoryController::class, 'store']); + Route::get('/{subcategory}', [SubcategoryController::class, 'show']); + Route::put('/{subcategory}', [SubcategoryController::class, 'update']); + Route::delete('/{subcategory}', [SubcategoryController::class, 'destroy']); + }); + //BUNDLES/KITS Route::resource('bundles', BundleController::class); Route::prefix('bundles')->group(function () {