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:
2026-01-31 22:24:00 -06:00
commit 4c3dc94ff2
107 changed files with 10701 additions and 0 deletions

View 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';
}
}