Initial commit: Horux Strategy Platform
- Laravel 11 backend with API REST - React 18 + TypeScript + Vite frontend - Multi-parser architecture for accounting systems (CONTPAQi, Aspel, SAP) - 27+ financial metrics calculation - PDF report generation with Browsershot - Complete documentation (10 documents) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
62
backend/app/Http/Controllers/Admin/GiroController.php
Normal file
62
backend/app/Http/Controllers/Admin/GiroController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Giro;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GiroController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->json(Giro::all());
|
||||
}
|
||||
|
||||
public function activos(): JsonResponse
|
||||
{
|
||||
return response()->json(Giro::where('activo', true)->get());
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'required|string|max:255|unique:giros,nombre',
|
||||
'activo' => 'boolean',
|
||||
]);
|
||||
|
||||
$giro = Giro::create($validated);
|
||||
|
||||
return response()->json($giro, 201);
|
||||
}
|
||||
|
||||
public function show(Giro $giro): JsonResponse
|
||||
{
|
||||
return response()->json($giro);
|
||||
}
|
||||
|
||||
public function update(Request $request, Giro $giro): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'string|max:255|unique:giros,nombre,' . $giro->id,
|
||||
'activo' => 'boolean',
|
||||
]);
|
||||
|
||||
$giro->update($validated);
|
||||
|
||||
return response()->json($giro);
|
||||
}
|
||||
|
||||
public function destroy(Giro $giro): JsonResponse
|
||||
{
|
||||
if ($giro->clientes()->exists()) {
|
||||
return response()->json([
|
||||
'message' => 'No se puede eliminar un giro con clientes asociados'
|
||||
], 422);
|
||||
}
|
||||
|
||||
$giro->delete();
|
||||
return response()->json(['message' => 'Giro eliminado']);
|
||||
}
|
||||
}
|
||||
71
backend/app/Http/Controllers/Admin/ReglaMapeeoController.php
Normal file
71
backend/app/Http/Controllers/Admin/ReglaMapeeoController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ReglaMapeo;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReglaMapeeoController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = ReglaMapeo::with(['reporteContable', 'categoriaContable']);
|
||||
|
||||
if ($request->has('sistema_origen')) {
|
||||
$query->where('sistema_origen', $request->sistema_origen);
|
||||
}
|
||||
|
||||
return response()->json($query->orderBy('prioridad', 'desc')->get());
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'sistema_origen' => 'required|string|max:50',
|
||||
'cuenta_padre_codigo' => 'nullable|string|max:20',
|
||||
'rango_inicio' => 'nullable|string|max:20',
|
||||
'rango_fin' => 'nullable|string|max:20',
|
||||
'patron_regex' => 'nullable|string|max:255',
|
||||
'reporte_contable_id' => 'required|exists:reportes_contables,id',
|
||||
'categoria_contable_id' => 'required|exists:categorias_contables,id',
|
||||
'prioridad' => 'integer',
|
||||
'activo' => 'boolean',
|
||||
]);
|
||||
|
||||
$regla = ReglaMapeo::create($validated);
|
||||
|
||||
return response()->json($regla->load(['reporteContable', 'categoriaContable']), 201);
|
||||
}
|
||||
|
||||
public function show(ReglaMapeo $reglaMapeo): JsonResponse
|
||||
{
|
||||
return response()->json($reglaMapeo->load(['reporteContable', 'categoriaContable']));
|
||||
}
|
||||
|
||||
public function update(Request $request, ReglaMapeo $reglaMapeo): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'sistema_origen' => 'string|max:50',
|
||||
'cuenta_padre_codigo' => 'nullable|string|max:20',
|
||||
'rango_inicio' => 'nullable|string|max:20',
|
||||
'rango_fin' => 'nullable|string|max:20',
|
||||
'patron_regex' => 'nullable|string|max:255',
|
||||
'reporte_contable_id' => 'exists:reportes_contables,id',
|
||||
'categoria_contable_id' => 'exists:categorias_contables,id',
|
||||
'prioridad' => 'integer',
|
||||
'activo' => 'boolean',
|
||||
]);
|
||||
|
||||
$reglaMapeo->update($validated);
|
||||
|
||||
return response()->json($reglaMapeo->load(['reporteContable', 'categoriaContable']));
|
||||
}
|
||||
|
||||
public function destroy(ReglaMapeo $reglaMapeo): JsonResponse
|
||||
{
|
||||
$reglaMapeo->delete();
|
||||
return response()->json(['message' => 'Regla de mapeo eliminada']);
|
||||
}
|
||||
}
|
||||
87
backend/app/Http/Controllers/Admin/UmbralController.php
Normal file
87
backend/app/Http/Controllers/Admin/UmbralController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Umbral;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UmbralController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Umbral::with('giro');
|
||||
|
||||
if ($request->has('giro_id')) {
|
||||
$query->where('giro_id', $request->giro_id);
|
||||
}
|
||||
|
||||
return response()->json($query->get());
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'metrica' => 'required|string|max:100',
|
||||
'muy_positivo' => 'nullable|numeric',
|
||||
'positivo' => 'nullable|numeric',
|
||||
'neutral' => 'nullable|numeric',
|
||||
'negativo' => 'nullable|numeric',
|
||||
'muy_negativo' => 'nullable|numeric',
|
||||
'giro_id' => 'nullable|exists:giros,id',
|
||||
]);
|
||||
|
||||
$umbral = Umbral::create($validated);
|
||||
|
||||
return response()->json($umbral->load('giro'), 201);
|
||||
}
|
||||
|
||||
public function show(Umbral $umbral): JsonResponse
|
||||
{
|
||||
return response()->json($umbral->load('giro'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Umbral $umbral): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'metrica' => 'string|max:100',
|
||||
'muy_positivo' => 'nullable|numeric',
|
||||
'positivo' => 'nullable|numeric',
|
||||
'neutral' => 'nullable|numeric',
|
||||
'negativo' => 'nullable|numeric',
|
||||
'muy_negativo' => 'nullable|numeric',
|
||||
'giro_id' => 'nullable|exists:giros,id',
|
||||
]);
|
||||
|
||||
$umbral->update($validated);
|
||||
|
||||
return response()->json($umbral->load('giro'));
|
||||
}
|
||||
|
||||
public function destroy(Umbral $umbral): JsonResponse
|
||||
{
|
||||
$umbral->delete();
|
||||
return response()->json(['message' => 'Umbral eliminado']);
|
||||
}
|
||||
|
||||
public function porMetrica(string $metrica, ?int $giroId = null): JsonResponse
|
||||
{
|
||||
// Buscar primero umbral específico del giro, luego el genérico
|
||||
$umbral = Umbral::where('metrica', $metrica)
|
||||
->where('giro_id', $giroId)
|
||||
->first();
|
||||
|
||||
if (!$umbral) {
|
||||
$umbral = Umbral::where('metrica', $metrica)
|
||||
->whereNull('giro_id')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$umbral) {
|
||||
return response()->json(['message' => 'Umbral no encontrado'], 404);
|
||||
}
|
||||
|
||||
return response()->json($umbral);
|
||||
}
|
||||
}
|
||||
71
backend/app/Http/Controllers/Admin/UsuarioController.php
Normal file
71
backend/app/Http/Controllers/Admin/UsuarioController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UsuarioController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$usuarios = User::with('cliente')->get();
|
||||
return response()->json($usuarios);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
'role' => 'required|in:admin,analista,cliente,empleado',
|
||||
'cliente_id' => 'nullable|exists:clientes,id',
|
||||
]);
|
||||
|
||||
$usuario = User::create([
|
||||
'nombre' => $validated['nombre'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'role' => $validated['role'],
|
||||
'cliente_id' => $validated['cliente_id'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($usuario->load('cliente'), 201);
|
||||
}
|
||||
|
||||
public function show(User $usuario): JsonResponse
|
||||
{
|
||||
return response()->json($usuario->load('cliente'));
|
||||
}
|
||||
|
||||
public function update(Request $request, User $usuario): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'string|max:255',
|
||||
'email' => 'email|unique:users,email,' . $usuario->id,
|
||||
'password' => 'nullable|string|min:8',
|
||||
'role' => 'in:admin,analista,cliente,empleado',
|
||||
'cliente_id' => 'nullable|exists:clientes,id',
|
||||
]);
|
||||
|
||||
if (isset($validated['password'])) {
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
} else {
|
||||
unset($validated['password']);
|
||||
}
|
||||
|
||||
$usuario->update($validated);
|
||||
|
||||
return response()->json($usuario->load('cliente'));
|
||||
}
|
||||
|
||||
public function destroy(User $usuario): JsonResponse
|
||||
{
|
||||
$usuario->delete();
|
||||
return response()->json(['message' => 'Usuario eliminado']);
|
||||
}
|
||||
}
|
||||
74
backend/app/Http/Controllers/AuthController.php
Normal file
74
backend/app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['Las credenciales proporcionadas son incorrectas.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'user' => $user->load('cliente'),
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json(['message' => 'Sesión cerrada exitosamente']);
|
||||
}
|
||||
|
||||
public function user(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json($request->user()->load('cliente'));
|
||||
}
|
||||
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
// Solo admin puede registrar usuarios
|
||||
if (!$request->user()->isAdmin()) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
'role' => 'required|in:admin,analista,cliente,empleado',
|
||||
'cliente_id' => 'nullable|exists:clientes,id',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'nombre' => $validated['nombre'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'role' => $validated['role'],
|
||||
'cliente_id' => $validated['cliente_id'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($user, 201);
|
||||
}
|
||||
}
|
||||
143
backend/app/Http/Controllers/BalanzaController.php
Normal file
143
backend/app/Http/Controllers/BalanzaController.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Balanza;
|
||||
use App\Models\Cliente;
|
||||
use App\Services\Parsers\DetectorFormato;
|
||||
use App\Services\ClasificadorCuentas;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class BalanzaController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private DetectorFormato $detector,
|
||||
private ClasificadorCuentas $clasificador,
|
||||
) {}
|
||||
|
||||
public function index(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$balanzas = $cliente->balanzas()
|
||||
->orderByDesc('periodo_fin')
|
||||
->get();
|
||||
|
||||
return response()->json($balanzas);
|
||||
}
|
||||
|
||||
public function store(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'archivo' => 'required|file|mimes:pdf,xlsx,xls,csv|max:10240',
|
||||
'periodo_inicio' => 'required|date',
|
||||
'periodo_fin' => 'required|date|after_or_equal:periodo_inicio',
|
||||
]);
|
||||
|
||||
$file = $request->file('archivo');
|
||||
$path = $file->store('balanzas/' . $cliente->id, 'local');
|
||||
|
||||
$balanza = Balanza::create([
|
||||
'cliente_id' => $cliente->id,
|
||||
'periodo_inicio' => $request->periodo_inicio,
|
||||
'periodo_fin' => $request->periodo_fin,
|
||||
'archivo_original' => $path,
|
||||
'sistema_origen' => 'pendiente',
|
||||
'status' => 'pendiente',
|
||||
]);
|
||||
|
||||
// Procesar archivo en background o inmediatamente
|
||||
try {
|
||||
$this->procesarBalanza($balanza, $path);
|
||||
} catch (\Exception $e) {
|
||||
$balanza->update([
|
||||
'status' => 'error',
|
||||
'error_mensaje' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json($balanza, 201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Balanza $balanza): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($balanza->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
return response()->json($balanza->load(['cuentas', 'cliente']));
|
||||
}
|
||||
|
||||
public function cuentas(Request $request, Balanza $balanza): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($balanza->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$cuentas = $balanza->cuentas()
|
||||
->with(['categoriaContable', 'reporteContable', 'cuentaPadre'])
|
||||
->orderBy('codigo')
|
||||
->get();
|
||||
|
||||
return response()->json($cuentas);
|
||||
}
|
||||
|
||||
public function updateExclusiones(Request $request, Balanza $balanza): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($balanza->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'exclusiones' => 'required|array',
|
||||
'exclusiones.*' => 'integer|exists:cuentas,id',
|
||||
]);
|
||||
|
||||
// Marcar todas como incluidas primero
|
||||
$balanza->cuentas()->update(['excluida' => false]);
|
||||
|
||||
// Marcar las seleccionadas como excluidas
|
||||
$balanza->cuentas()
|
||||
->whereIn('id', $request->exclusiones)
|
||||
->update(['excluida' => true]);
|
||||
|
||||
// Recalcular saldos de cuentas padre
|
||||
$cuentasPadre = $balanza->cuentasPadre()->get();
|
||||
foreach ($cuentasPadre as $cuentaPadre) {
|
||||
$cuentaPadre->recalcularSaldoDesdeHijos();
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Exclusiones actualizadas']);
|
||||
}
|
||||
|
||||
private function procesarBalanza(Balanza $balanza, string $path): void
|
||||
{
|
||||
$balanza->update(['status' => 'procesando']);
|
||||
|
||||
$fullPath = Storage::disk('local')->path($path);
|
||||
|
||||
// Detectar sistema origen
|
||||
$resultado = $this->detector->detectar($fullPath);
|
||||
$balanza->update(['sistema_origen' => $resultado['sistema']]);
|
||||
|
||||
// Parsear y guardar cuentas
|
||||
$cuentas = $resultado['parser']->parsear($fullPath);
|
||||
|
||||
foreach ($cuentas as $cuentaData) {
|
||||
$balanza->cuentas()->create($cuentaData);
|
||||
}
|
||||
|
||||
// Clasificar cuentas
|
||||
$this->clasificador->clasificar($balanza);
|
||||
|
||||
$balanza->update(['status' => 'completado']);
|
||||
}
|
||||
}
|
||||
99
backend/app/Http/Controllers/ClienteController.php
Normal file
99
backend/app/Http/Controllers/ClienteController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Cliente;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ClienteController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->isAdmin() || $user->isAnalista()) {
|
||||
$clientes = Cliente::with('giro')->get();
|
||||
} else {
|
||||
$clientes = Cliente::where('id', $user->cliente_id)->with('giro')->get();
|
||||
}
|
||||
|
||||
return response()->json($clientes);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nombre_empresa' => 'required|string|max:255',
|
||||
'giro_id' => 'required|exists:giros,id',
|
||||
'moneda' => 'string|max:3',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$cliente = new Cliente([
|
||||
'nombre_empresa' => $validated['nombre_empresa'],
|
||||
'giro_id' => $validated['giro_id'],
|
||||
'moneda' => $validated['moneda'] ?? 'MXN',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
$path = $request->file('logo')->store('logos', 'public');
|
||||
$cliente->logo = $path;
|
||||
}
|
||||
|
||||
$cliente->save();
|
||||
|
||||
return response()->json($cliente->load('giro'), 201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
return response()->json($cliente->load(['giro', 'balanzas', 'reportes']));
|
||||
}
|
||||
|
||||
public function update(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'nombre_empresa' => 'string|max:255',
|
||||
'giro_id' => 'exists:giros,id',
|
||||
'moneda' => 'string|max:3',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
'configuracion' => 'nullable|array',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
if ($cliente->logo) {
|
||||
Storage::disk('public')->delete($cliente->logo);
|
||||
}
|
||||
$validated['logo'] = $request->file('logo')->store('logos', 'public');
|
||||
}
|
||||
|
||||
$cliente->update($validated);
|
||||
|
||||
return response()->json($cliente->load('giro'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->isAdmin()) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
if ($cliente->logo) {
|
||||
Storage::disk('public')->delete($cliente->logo);
|
||||
}
|
||||
|
||||
$cliente->delete();
|
||||
|
||||
return response()->json(['message' => 'Cliente eliminado']);
|
||||
}
|
||||
}
|
||||
12
backend/app/Http/Controllers/Controller.php
Normal file
12
backend/app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
65
backend/app/Http/Controllers/CuentaController.php
Normal file
65
backend/app/Http/Controllers/CuentaController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Cuenta;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CuentaController extends Controller
|
||||
{
|
||||
public function updateClasificacion(Request $request, Cuenta $cuenta): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cuenta->balanza->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'reporte_contable_id' => 'required|exists:reportes_contables,id',
|
||||
'categoria_contable_id' => 'required|exists:categorias_contables,id',
|
||||
'requiere_revision' => 'boolean',
|
||||
'nota_revision' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$cuenta->update([
|
||||
'reporte_contable_id' => $validated['reporte_contable_id'],
|
||||
'categoria_contable_id' => $validated['categoria_contable_id'],
|
||||
'requiere_revision' => $validated['requiere_revision'] ?? false,
|
||||
'nota_revision' => $validated['nota_revision'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($cuenta->load(['categoriaContable', 'reporteContable']));
|
||||
}
|
||||
|
||||
public function toggleExclusion(Request $request, Cuenta $cuenta): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cuenta->balanza->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$cuenta->update(['excluida' => !$cuenta->excluida]);
|
||||
|
||||
// Recalcular cuenta padre si existe
|
||||
if ($cuenta->cuentaPadre) {
|
||||
$cuenta->cuentaPadre->recalcularSaldoDesdeHijos();
|
||||
}
|
||||
|
||||
return response()->json($cuenta);
|
||||
}
|
||||
|
||||
public function anomalias(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$query = Cuenta::where('requiere_revision', true)
|
||||
->with(['balanza.cliente', 'categoriaContable', 'reporteContable']);
|
||||
|
||||
if (!$user->isAdmin() && !$user->isAnalista()) {
|
||||
$query->whereHas('balanza', function ($q) use ($user) {
|
||||
$q->where('cliente_id', $user->cliente_id);
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json($query->get());
|
||||
}
|
||||
}
|
||||
162
backend/app/Http/Controllers/ReporteController.php
Normal file
162
backend/app/Http/Controllers/ReporteController.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Cliente;
|
||||
use App\Models\Reporte;
|
||||
use App\Services\CalculadorMetricas;
|
||||
use App\Services\GeneradorPdf;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class ReporteController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private CalculadorMetricas $calculador,
|
||||
private GeneradorPdf $generadorPdf,
|
||||
) {}
|
||||
|
||||
public function index(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$reportes = $cliente->reportes()
|
||||
->orderByDesc('periodo_fin')
|
||||
->get();
|
||||
|
||||
return response()->json($reportes);
|
||||
}
|
||||
|
||||
public function store(Request $request, Cliente $cliente): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($cliente->id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'nombre' => 'required|string|max:255',
|
||||
'balanza_ids' => 'required|array|min:1',
|
||||
'balanza_ids.*' => 'exists:balanzas,id',
|
||||
]);
|
||||
|
||||
// Verificar que las balanzas pertenecen al cliente
|
||||
$balanzasValidas = $cliente->balanzas()
|
||||
->whereIn('id', $validated['balanza_ids'])
|
||||
->where('status', 'completado')
|
||||
->get();
|
||||
|
||||
if ($balanzasValidas->count() !== count($validated['balanza_ids'])) {
|
||||
return response()->json([
|
||||
'message' => 'Algunas balanzas no son válidas o no están procesadas'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Determinar tipo de periodo
|
||||
$periodoTipo = $this->determinarTipoPeriodo($balanzasValidas);
|
||||
|
||||
$reporte = Reporte::create([
|
||||
'cliente_id' => $cliente->id,
|
||||
'nombre' => $validated['nombre'],
|
||||
'periodo_tipo' => $periodoTipo,
|
||||
'periodo_inicio' => $balanzasValidas->min('periodo_inicio'),
|
||||
'periodo_fin' => $balanzasValidas->max('periodo_fin'),
|
||||
'status' => 'procesando',
|
||||
]);
|
||||
|
||||
$reporte->balanzas()->attach($validated['balanza_ids']);
|
||||
|
||||
// Calcular métricas
|
||||
try {
|
||||
$dataCalculada = $this->calculador->calcular($reporte);
|
||||
$reporte->update([
|
||||
'data_calculada' => $dataCalculada,
|
||||
'fecha_generacion' => now(),
|
||||
'status' => 'completado',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$reporte->update(['status' => 'error']);
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json($reporte, 201);
|
||||
}
|
||||
|
||||
public function show(Request $request, Reporte $reporte): JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($reporte->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
return response()->json($reporte->load(['cliente', 'balanzas']));
|
||||
}
|
||||
|
||||
public function pdf(Request $request, Reporte $reporte): BinaryFileResponse|JsonResponse
|
||||
{
|
||||
if (!$request->user()->canAccessCliente($reporte->cliente_id)) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
// Verificar permisos de empleado si aplica
|
||||
$user = $request->user();
|
||||
if ($user->isEmpleado()) {
|
||||
$permiso = $user->permisosEmpleado()
|
||||
->where('cliente_id', $reporte->cliente_id)
|
||||
->first();
|
||||
|
||||
if (!$permiso || !$permiso->tienePermiso('exportar_pdf')) {
|
||||
return response()->json(['message' => 'No tiene permiso para exportar PDF'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$reporte->pdf_path || !file_exists(storage_path('app/' . $reporte->pdf_path))) {
|
||||
// Generar PDF
|
||||
$pdfPath = $this->generadorPdf->generar($reporte);
|
||||
$reporte->update(['pdf_path' => $pdfPath]);
|
||||
}
|
||||
|
||||
return response()->download(
|
||||
storage_path('app/' . $reporte->pdf_path),
|
||||
$reporte->nombre . '.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Reporte $reporte): JsonResponse
|
||||
{
|
||||
if (!$request->user()->isAdmin() && !$request->user()->isAnalista()) {
|
||||
return response()->json(['message' => 'No autorizado'], 403);
|
||||
}
|
||||
|
||||
$reporte->delete();
|
||||
|
||||
return response()->json(['message' => 'Reporte eliminado']);
|
||||
}
|
||||
|
||||
private function determinarTipoPeriodo($balanzas): string
|
||||
{
|
||||
if ($balanzas->count() < 2) {
|
||||
$balanza = $balanzas->first();
|
||||
$dias = $balanza->periodo_inicio->diffInDays($balanza->periodo_fin);
|
||||
|
||||
if ($dias <= 35) return 'mensual';
|
||||
if ($dias <= 100) return 'trimestral';
|
||||
return 'anual';
|
||||
}
|
||||
|
||||
// Calcular diferencia promedio entre periodos
|
||||
$diferencias = [];
|
||||
$sorted = $balanzas->sortBy('periodo_inicio')->values();
|
||||
|
||||
for ($i = 1; $i < $sorted->count(); $i++) {
|
||||
$diferencias[] = $sorted[$i - 1]->periodo_fin->diffInDays($sorted[$i]->periodo_inicio);
|
||||
}
|
||||
|
||||
$promedio = array_sum($diferencias) / count($diferencias);
|
||||
|
||||
if ($promedio <= 35) return 'mensual';
|
||||
if ($promedio <= 100) return 'trimestral';
|
||||
return 'anual';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user