feat: agregar soporte para subcategorías, incluyendo controladores, solicitudes y migraciones

This commit is contained in:
Juan Felipe Zapata Moreno 2026-02-25 12:31:51 -06:00
parent 1c21602b7e
commit 37f91d84f2
13 changed files with 235 additions and 1 deletions

View File

@ -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
]);

View File

@ -0,0 +1,55 @@
<?php namespace App\Http\Controllers\App;
use App\Http\Controllers\Controller;
use App\Http\Requests\App\SubcategoryStoreRequest;
use App\Http\Requests\App\SubcategoryUpdateRequest;
use App\Models\Category;
use App\Models\Subcategory;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class SubcategoryController extends Controller
{
public function index(Category $category)
{
$subcategorias = $category->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();
}
}

View File

@ -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();

View File

@ -21,6 +21,7 @@ public function rules(): array
return [
'name' => ['required', 'string', 'max:100'],
'description' => ['nullable', 'string', 'max:225'],
'is_active' => ['nullable', 'boolean'],
];
}

View File

@ -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.',
];
}
}

View File

@ -0,0 +1,32 @@
<?php namespace App\Http\Requests\App;
use Illuminate\Foundation\Http\FormRequest;
class SubcategoryStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['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.',
];
}
}

View File

@ -0,0 +1,31 @@
<?php namespace App\Http\Requests\App;
use Illuminate\Foundation\Http\FormRequest;
class SubcategoryUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['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.',
];
}
}

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,30 @@
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Subcategory extends Model
{
use SoftDeletes;
protected $fillable = [
'category_id',
'name',
'description',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
public function category()
{
return $this->belongsTo(Category::class);
}
public function inventories()
{
return $this->hasMany(Inventory::class);
}
}

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('subcategories', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('inventories', function (Blueprint $table) {
$table->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');
});
}
};

View File

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