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 () {