Docs: Creado documento RECOMENDACIONES, modificado README
This commit is contained in:
@@ -610,363 +610,6 @@ Dashboard mejorado con:
|
||||
- Reportes consolidados
|
||||
```
|
||||
|
||||
## 🔧 Mejoras Técnicas Recomendadas
|
||||
|
||||
### Optimización de Rendimiento
|
||||
|
||||
#### 1. Implementar Caché Estratégico
|
||||
```php
|
||||
// ProductosController.php
|
||||
public function index(Request $request)
|
||||
{
|
||||
$cacheKey = 'products.' . md5(serialize($request->all()));
|
||||
|
||||
$products = Cache::remember($cacheKey, 300, function () use ($request) {
|
||||
return Product::query()
|
||||
->when($request->search, fn($q, $term) =>
|
||||
$q->where('name', 'like', "%{$term}%")
|
||||
)
|
||||
->paginate(10);
|
||||
});
|
||||
}
|
||||
|
||||
// Limpiar caché al modificar productos
|
||||
Cache::forget('products.*');
|
||||
```
|
||||
|
||||
#### 2. Eager Loading para Relaciones
|
||||
```php
|
||||
// Evitar N+1 queries
|
||||
$sales = Sale::with(['client', 'details.product'])
|
||||
->latest()
|
||||
->paginate(15);
|
||||
|
||||
// En lugar de:
|
||||
$sales = Sale::all(); // Genera queries adicionales
|
||||
```
|
||||
|
||||
#### 3. Índices de Base de Datos
|
||||
```php
|
||||
// En migraciones
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->index('sku');
|
||||
$table->index('type');
|
||||
$table->index(['stock_quantity', 'min_stock_alert']);
|
||||
});
|
||||
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->index('created_at');
|
||||
$table->index(['client_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Paginación con Cursor
|
||||
```php
|
||||
// Para datasets grandes
|
||||
$products = Product::latest()->cursorPaginate(20);
|
||||
```
|
||||
|
||||
### Refactorización de Código
|
||||
|
||||
#### 1. Services para Lógica de Negocio
|
||||
```php
|
||||
// app/Services/SaleService.php
|
||||
class SaleService
|
||||
{
|
||||
public function processSale(array $data): Sale
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$sale = Sale::create([...]);
|
||||
|
||||
foreach ($data['items'] as $item) {
|
||||
$this->addItem($sale, $item);
|
||||
}
|
||||
|
||||
return $sale;
|
||||
});
|
||||
}
|
||||
|
||||
private function addItem(Sale $sale, array $item): void
|
||||
{
|
||||
$product = Product::findOrFail($item['product_id']);
|
||||
|
||||
if ($product->stock_quantity < $item['quantity']) {
|
||||
throw new InsufficientStockException();
|
||||
}
|
||||
|
||||
SaleDetail::create([...]);
|
||||
$product->decrement('stock_quantity', $item['quantity']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Form Request Validation
|
||||
```php
|
||||
// app/Http/Requests/StoreSaleRequest.php
|
||||
class StoreSaleRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => 'nullable|exists:clients,id',
|
||||
'payment_method' => 'required|string',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'items.required' => 'Debe agregar al menos un producto.',
|
||||
'items.*.quantity.min' => 'La cantidad debe ser mayor a 0.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// En el controlador
|
||||
public function store(StoreSaleRequest $request)
|
||||
{
|
||||
// $request ya está validado
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Repository Pattern
|
||||
```php
|
||||
// app/Repositories/ProductRepository.php
|
||||
interface ProductRepositoryInterface
|
||||
{
|
||||
public function search(string $term): Collection;
|
||||
public function lowStock(): Collection;
|
||||
public function byType(string $type): Collection;
|
||||
}
|
||||
|
||||
class ProductRepository implements ProductRepositoryInterface
|
||||
{
|
||||
public function search(string $term): Collection
|
||||
{
|
||||
return Product::where('name', 'like', "%{$term}%")
|
||||
->orWhere('sku', 'like', "%{$term}%")
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar en AppServiceProvider
|
||||
$this->app->bind(
|
||||
ProductRepositoryInterface::class,
|
||||
ProductRepository::class
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. Policies para Autorización
|
||||
```php
|
||||
// app/Policies/ProductPolicy.php
|
||||
class ProductPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return in_array($user->role, ['admin', 'employee']);
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
|
||||
public function update(User $user, Product $product): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
|
||||
public function delete(User $user, Product $product): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
// En controlador
|
||||
$this->authorize('create', Product::class);
|
||||
```
|
||||
|
||||
### Mejoras de Seguridad
|
||||
|
||||
#### 1. Rate Limiting Específico
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::post('/sales', [SaleController::class, 'store'])
|
||||
->middleware(['auth', 'throttle:sales']);
|
||||
|
||||
// app/Providers/RouteServiceProvider.php
|
||||
RateLimiter::for('sales', function (Request $request) {
|
||||
return Limit::perMinute(10)->by($request->user()?->id);
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. Sanitización de Inputs
|
||||
```php
|
||||
// En Form Request
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
$this->merge([
|
||||
'name' => strip_tags($this->name),
|
||||
'description' => Purifier::clean($this->description),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Protección CSRF Explícita
|
||||
```php
|
||||
// Verificar en AJAX
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Validación de Imágenes
|
||||
```php
|
||||
'image' => [
|
||||
'nullable',
|
||||
'image',
|
||||
'mimes:jpeg,png,jpg,webp',
|
||||
'max:2048', // 2MB
|
||||
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000'
|
||||
]
|
||||
```
|
||||
|
||||
### Mejoras de UI/UX
|
||||
|
||||
#### 1. Loading States
|
||||
```blade
|
||||
<button type="submit" id="submit-btn" class="...">
|
||||
<span class="loading hidden">
|
||||
<svg class="animate-spin h-5 w-5" ...></svg>
|
||||
</span>
|
||||
<span class="text">Guardar</span>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
$('#form').on('submit', function() {
|
||||
$('#submit-btn .loading').removeClass('hidden');
|
||||
$('#submit-btn .text').addClass('hidden');
|
||||
$('#submit-btn').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 2. Confirmaciones Elegantes
|
||||
```javascript
|
||||
// Usar SweetAlert2
|
||||
Swal.fire({
|
||||
title: '¿Eliminar producto?',
|
||||
text: "Esta acción no se puede revertir",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ccff00',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Ejecutar eliminación
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. Toast Notifications
|
||||
```javascript
|
||||
// Mensajes no intrusivos
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
timer: 3000
|
||||
});
|
||||
|
||||
Toast.fire({
|
||||
icon: 'success',
|
||||
title: 'Producto guardado'
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Validación en Tiempo Real
|
||||
```javascript
|
||||
$('input[name="sku"]').on('blur', function() {
|
||||
const sku = $(this).val();
|
||||
|
||||
$.get(`/api/products/check-sku/${sku}`, function(data) {
|
||||
if (data.exists) {
|
||||
// Mostrar error
|
||||
$(this).addClass('border-red-500');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
#### 1. Tests de Feature
|
||||
```php
|
||||
// tests/Feature/ProductTest.php
|
||||
public function test_admin_can_create_product()
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post('/productos', [
|
||||
'name' => 'Test Product',
|
||||
'price' => 100,
|
||||
'stock_quantity' => 10,
|
||||
'min_stock_alert' => 5,
|
||||
'type' => 'accessory'
|
||||
])
|
||||
->assertRedirect('/productos')
|
||||
->assertSessionHas('success');
|
||||
|
||||
$this->assertDatabaseHas('products', [
|
||||
'name' => 'Test Product'
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_create_product()
|
||||
{
|
||||
$response = $this->post('/productos', []);
|
||||
$response->assertRedirect('/login');
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Tests Unitarios
|
||||
```php
|
||||
// tests/Unit/ProductTest.php
|
||||
public function test_product_has_low_stock()
|
||||
{
|
||||
$product = Product::factory()->create([
|
||||
'stock_quantity' => 3,
|
||||
'min_stock_alert' => 5
|
||||
]);
|
||||
|
||||
$this->assertTrue($product->hasLowStock());
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Browser Tests (Dusk)
|
||||
```php
|
||||
// tests/Browser/PosTest.php
|
||||
public function test_can_complete_sale()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->loginAs(User::find(1))
|
||||
->visit('/sales/create')
|
||||
->select('items[0][product_id]', 1)
|
||||
->type('items[0][quantity]', 2)
|
||||
->press('Confirmar Venta')
|
||||
->assertPathIs('/sales/1');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 🐛 Issues Conocidos y Soluciones
|
||||
|
||||
### 1. Select2 no se inicializa en filas dinámicas
|
||||
@@ -1096,7 +739,7 @@ Este proyecto está bajo la Licencia MIT. Ver archivo `LICENSE` para detalles.
|
||||
|
||||
---
|
||||
|
||||
**Versión**: 2.0.0
|
||||
**Versión**: 0.2.0
|
||||
**Estado**: En desarrollo activo 🚧
|
||||
**Última actualización**: Enero 2026
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
## 🔧 Mejoras Técnicas Recomendadas
|
||||
|
||||
### Optimización de Rendimiento
|
||||
|
||||
#### 1. Implementar Caché Estratégico
|
||||
```php
|
||||
// ProductosController.php
|
||||
public function index(Request $request)
|
||||
{
|
||||
$cacheKey = 'products.' . md5(serialize($request->all()));
|
||||
|
||||
$products = Cache::remember($cacheKey, 300, function () use ($request) {
|
||||
return Product::query()
|
||||
->when($request->search, fn($q, $term) =>
|
||||
$q->where('name', 'like', "%{$term}%")
|
||||
)
|
||||
->paginate(10);
|
||||
});
|
||||
}
|
||||
|
||||
// Limpiar caché al modificar productos
|
||||
Cache::forget('products.*');
|
||||
```
|
||||
|
||||
#### 2. Eager Loading para Relaciones
|
||||
```php
|
||||
// Evitar N+1 queries
|
||||
$sales = Sale::with(['client', 'details.product'])
|
||||
->latest()
|
||||
->paginate(15);
|
||||
|
||||
// En lugar de:
|
||||
$sales = Sale::all(); // Genera queries adicionales
|
||||
```
|
||||
|
||||
#### 3. Índices de Base de Datos
|
||||
```php
|
||||
// En migraciones
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->index('sku');
|
||||
$table->index('type');
|
||||
$table->index(['stock_quantity', 'min_stock_alert']);
|
||||
});
|
||||
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->index('created_at');
|
||||
$table->index(['client_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Paginación con Cursor
|
||||
```php
|
||||
// Para datasets grandes
|
||||
$products = Product::latest()->cursorPaginate(20);
|
||||
```
|
||||
|
||||
### Refactorización de Código
|
||||
|
||||
#### 1. Services para Lógica de Negocio
|
||||
```php
|
||||
// app/Services/SaleService.php
|
||||
class SaleService
|
||||
{
|
||||
public function processSale(array $data): Sale
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$sale = Sale::create([...]);
|
||||
|
||||
foreach ($data['items'] as $item) {
|
||||
$this->addItem($sale, $item);
|
||||
}
|
||||
|
||||
return $sale;
|
||||
});
|
||||
}
|
||||
|
||||
private function addItem(Sale $sale, array $item): void
|
||||
{
|
||||
$product = Product::findOrFail($item['product_id']);
|
||||
|
||||
if ($product->stock_quantity < $item['quantity']) {
|
||||
throw new InsufficientStockException();
|
||||
}
|
||||
|
||||
SaleDetail::create([...]);
|
||||
$product->decrement('stock_quantity', $item['quantity']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Form Request Validation
|
||||
```php
|
||||
// app/Http/Requests/StoreSaleRequest.php
|
||||
class StoreSaleRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => 'nullable|exists:clients,id',
|
||||
'payment_method' => 'required|string',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'items.required' => 'Debe agregar al menos un producto.',
|
||||
'items.*.quantity.min' => 'La cantidad debe ser mayor a 0.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// En el controlador
|
||||
public function store(StoreSaleRequest $request)
|
||||
{
|
||||
// $request ya está validado
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Repository Pattern
|
||||
```php
|
||||
// app/Repositories/ProductRepository.php
|
||||
interface ProductRepositoryInterface
|
||||
{
|
||||
public function search(string $term): Collection;
|
||||
public function lowStock(): Collection;
|
||||
public function byType(string $type): Collection;
|
||||
}
|
||||
|
||||
class ProductRepository implements ProductRepositoryInterface
|
||||
{
|
||||
public function search(string $term): Collection
|
||||
{
|
||||
return Product::where('name', 'like', "%{$term}%")
|
||||
->orWhere('sku', 'like', "%{$term}%")
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar en AppServiceProvider
|
||||
$this->app->bind(
|
||||
ProductRepositoryInterface::class,
|
||||
ProductRepository::class
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. Policies para Autorización
|
||||
```php
|
||||
// app/Policies/ProductPolicy.php
|
||||
class ProductPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return in_array($user->role, ['admin', 'employee']);
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
|
||||
public function update(User $user, Product $product): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
|
||||
public function delete(User $user, Product $product): bool
|
||||
{
|
||||
return $user->role === 'admin';
|
||||
}
|
||||
}
|
||||
|
||||
// En controlador
|
||||
$this->authorize('create', Product::class);
|
||||
```
|
||||
|
||||
### Mejoras de Seguridad
|
||||
|
||||
#### 1. Rate Limiting Específico
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::post('/sales', [SaleController::class, 'store'])
|
||||
->middleware(['auth', 'throttle:sales']);
|
||||
|
||||
// app/Providers/RouteServiceProvider.php
|
||||
RateLimiter::for('sales', function (Request $request) {
|
||||
return Limit::perMinute(10)->by($request->user()?->id);
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. Sanitización de Inputs
|
||||
```php
|
||||
// En Form Request
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
$this->merge([
|
||||
'name' => strip_tags($this->name),
|
||||
'description' => Purifier::clean($this->description),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Protección CSRF Explícita
|
||||
```php
|
||||
// Verificar en AJAX
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Validación de Imágenes
|
||||
```php
|
||||
'image' => [
|
||||
'nullable',
|
||||
'image',
|
||||
'mimes:jpeg,png,jpg,webp',
|
||||
'max:2048', // 2MB
|
||||
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000'
|
||||
]
|
||||
```
|
||||
|
||||
### Mejoras de UI/UX
|
||||
|
||||
#### 1. Loading States
|
||||
```blade
|
||||
<button type="submit" id="submit-btn" class="...">
|
||||
<span class="loading hidden">
|
||||
<svg class="animate-spin h-5 w-5" ...></svg>
|
||||
</span>
|
||||
<span class="text">Guardar</span>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
$('#form').on('submit', function() {
|
||||
$('#submit-btn .loading').removeClass('hidden');
|
||||
$('#submit-btn .text').addClass('hidden');
|
||||
$('#submit-btn').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 2. Confirmaciones Elegantes
|
||||
```javascript
|
||||
// Usar SweetAlert2
|
||||
Swal.fire({
|
||||
title: '¿Eliminar producto?',
|
||||
text: "Esta acción no se puede revertir",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ccff00',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Ejecutar eliminación
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. Toast Notifications
|
||||
```javascript
|
||||
// Mensajes no intrusivos
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
timer: 3000
|
||||
});
|
||||
|
||||
Toast.fire({
|
||||
icon: 'success',
|
||||
title: 'Producto guardado'
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Validación en Tiempo Real
|
||||
```javascript
|
||||
$('input[name="sku"]').on('blur', function() {
|
||||
const sku = $(this).val();
|
||||
|
||||
$.get(`/api/products/check-sku/${sku}`, function(data) {
|
||||
if (data.exists) {
|
||||
// Mostrar error
|
||||
$(this).addClass('border-red-500');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
#### 1. Tests de Feature
|
||||
```php
|
||||
// tests/Feature/ProductTest.php
|
||||
public function test_admin_can_create_product()
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post('/productos', [
|
||||
'name' => 'Test Product',
|
||||
'price' => 100,
|
||||
'stock_quantity' => 10,
|
||||
'min_stock_alert' => 5,
|
||||
'type' => 'accessory'
|
||||
])
|
||||
->assertRedirect('/productos')
|
||||
->assertSessionHas('success');
|
||||
|
||||
$this->assertDatabaseHas('products', [
|
||||
'name' => 'Test Product'
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_guest_cannot_create_product()
|
||||
{
|
||||
$response = $this->post('/productos', []);
|
||||
$response->assertRedirect('/login');
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Tests Unitarios
|
||||
```php
|
||||
// tests/Unit/ProductTest.php
|
||||
public function test_product_has_low_stock()
|
||||
{
|
||||
$product = Product::factory()->create([
|
||||
'stock_quantity' => 3,
|
||||
'min_stock_alert' => 5
|
||||
]);
|
||||
|
||||
$this->assertTrue($product->hasLowStock());
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Browser Tests (Dusk)
|
||||
```php
|
||||
// tests/Browser/PosTest.php
|
||||
public function test_can_complete_sale()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->loginAs(User::find(1))
|
||||
->visit('/sales/create')
|
||||
->select('items[0][product_id]', 1)
|
||||
->type('items[0][quantity]', 2)
|
||||
->press('Confirmar Venta')
|
||||
->assertPathIs('/sales/1');
|
||||
});
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user