Skip to main content

Overview

Core Projects provides comprehensive analytics through the Gerencia (Management) dashboard, offering real-time insights into sales performance, inventory status, advisor metrics, and financial projections.

Management Dashboard

The Gerencia dashboard is the central hub for business intelligence:
class GerenciaDashboardWebController extends Controller
{
    public function index(Request $request, GerenciaEstadisticasService $service)
    {
        $filtros = [
            'desde' => $request->query('desde'),
            'hasta' => $request->query('hasta'),
            'proyecto_id' => $request->query('proyecto_id'),
            'asesor_id' => $request->query('asesor_id'),
            'estado_inmueble' => $request->query('estado_inmueble'),
        ];
        
        $dashboard = $service->obtenerDashboard($filtros);
        $planPagosCI = $service->planPagosCI($filtros, $desde, $hasta);
        
        return Inertia::render('Gerencia/Dashboard/Index', array_merge($dashboard, [
            'proyectos' => Proyecto::orderBy('nombre')->get(),
            'empleados' => Empleado::comerciales()->get(),
            'estadosInmueble' => EstadoInmueble::orderBy('nombre')->get(),
            'filtros' => $filtros,
            'planPagosCI' => $planPagosCI,
        ]));
    }
}

Dashboard Components

The dashboard provides multiple analytical views:

1. Global Summary

Key Performance Indicators

Real-time metrics:
  • Total sales value (period)
  • Units sold
  • Available inventory
  • Average sale price
  • Sales velocity
public function resumenGlobal(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $ventasQuery = Venta::query()
        ->where('tipo_operacion', 'venta')
        ->whereBetween('fecha_venta', [$desde, $hasta]);
    
    if (!empty($filtros['proyecto_id'])) {
        $ventasQuery->where('id_proyecto', $filtros['proyecto_id']);
    }
    
    if (!empty($filtros['asesor_id'])) {
        $ventasQuery->where('id_empleado', $filtros['asesor_id']);
    }
    
    $ventasTotales = (float) $ventasQuery->sum('valor_total');
    $unidadesVendidas = (int) $ventasQuery->count();
    
    $estadoDisponibleId = EstadoInmueble::whereRaw('LOWER(nombre) = ?', ['disponible'])
        ->value('id_estado_inmueble');
    
    $inventarioAptos = Apartamento::where('id_estado_inmueble', $estadoDisponibleId)->count();
    $inventarioLocs = Local::where('id_estado_inmueble', $estadoDisponibleId)->count();
    
    return [
        'ventas_totales' => $ventasTotales,
        'unidades_vendidas' => $unidadesVendidas,
        'inventario_disponible' => $inventarioAptos + $inventarioLocs,
    ];
}

2. Sales by Project

Project Performance

Comparison across projects:
  • Total sales value per project
  • Units sold per project
  • Average price per project
  • Sell-through rate
public function ventasPorProyecto(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $rows = Venta::query()
        ->select(
            'id_proyecto',
            DB::raw('SUM(valor_total) as total_valor'),
            DB::raw('COUNT(*) as unidades')
        )
        ->where('tipo_operacion', 'venta')
        ->whereBetween('fecha_venta', [$desde, $hasta])
        ->groupBy('id_proyecto')
        ->get();
    
    $proyectos = Proyecto::whereIn('id_proyecto', $rows->pluck('id_proyecto'))
        ->get()
        ->keyBy('id_proyecto');
    
    return $rows->map(function ($r) use ($proyectos) {
        $proyecto = $proyectos[$r->id_proyecto] ?? null;
        
        return [
            'id_proyecto' => $r->id_proyecto,
            'nombre' => $proyecto->nombre ?? 'Desconocido',
            'total_valor' => (float) $r->total_valor,
            'unidades' => (int) $r->unidades,
        ];
    })->values()->all();
}

3. Projection vs. Reality

Goal Tracking

Compare goals against actual performance:
  • Monthly sales goals (units & value)
  • Actual sales achieved
  • Variance (over/under performance)
  • Progress percentage
public function proyeccionVsRealMensual(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $ano = (int) $desde->year;
    $mes = (int) $desde->month;
    
    // Get goals from Meta model
    $metas = Meta::where('ano', $ano)
        ->where('mes', $mes)
        ->when(!empty($filtros['proyecto_id']), 
            fn($q) => $q->where('id_proyecto', $filtros['proyecto_id'])
        )
        ->get()
        ->keyBy('id_proyecto');
    
    // Get actual sales
    $ventas = Venta::query()
        ->select(
            'id_proyecto',
            DB::raw('COUNT(*) as real_unidades'),
            DB::raw('SUM(valor_total) as real_valor')
        )
        ->where('tipo_operacion', 'venta')
        ->whereYear('fecha_venta', $ano)
        ->whereMonth('fecha_venta', $mes)
        ->groupBy('id_proyecto')
        ->get();
    
    // Combine and compare
    $resultado = [];
    foreach ($proyectos as $proyecto) {
        $meta = $metas[$proyecto->id_proyecto] ?? null;
        $venta = $ventas->firstWhere('id_proyecto', $proyecto->id_proyecto);
        
        $resultado[] = [
            'proyecto' => $proyecto->nombre,
            'meta_unidades' => (int) ($meta->meta_unidades ?? 0),
            'meta_valor' => (float) ($meta->meta_valor ?? 0),
            'real_unidades' => (int) ($venta->real_unidades ?? 0),
            'real_valor' => (float) ($venta->real_valor ?? 0),
        ];
    }
    
    return $resultado;
}

4. Sales Velocity

Time-to-Sale Metrics

Analyze sales speed:
  • Average days from project start to sale
  • Days to sell by project
  • Velocity trends over time
  • Fastest/slowest moving units
public function velocidadVentasPorProyecto(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $ventasGrouped = Venta::where('tipo_operacion', 'venta')
        ->whereBetween('fecha_venta', [$desde, $hasta])
        ->get()
        ->groupBy('id_proyecto');
    
    $resultado = [];
    
    foreach ($ventasGrouped as $idProyecto => $ventasProyecto) {
        $proyecto = Proyecto::find($idProyecto);
        if (!$proyecto || !$proyecto->fecha_inicio) continue;
        
        $inicio = Carbon::parse($proyecto->fecha_inicio);
        
        $dias = $ventasProyecto->map(function ($v) use ($inicio) {
            if (!$v->fecha_venta) return null;
            return $inicio->diffInDays(Carbon::parse($v->fecha_venta));
        })->filter(fn($x) => $x !== null);
        
        $promedio = $dias->count() ? round($dias->avg(), 1) : null;
        
        $resultado[] = [
            'proyecto' => $proyecto->nombre,
            'dias_promedio_venta' => $promedio,
        ];
    }
    
    return $resultado;
}

5. Separation Effectiveness

Conversion Metrics

Track separation outcomes:
  • Total separations by advisor
  • Separations converted to sales
  • Expired/cancelled separations
  • Conversion rate
public function separacionesYEfectividad(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $separaciones = Venta::where('tipo_operacion', 'separacion')
        ->whereBetween('fecha_venta', [$desde, $hasta])
        ->get()
        ->groupBy('id_empleado');
    
    $hoy = Carbon::today();
    $resultado = [];
    
    foreach ($separaciones as $idEmpleado => $seps) {
        $empleado = Empleado::find($idEmpleado);
        
        $total = $seps->count();
        $caducadas = $seps->filter(function ($v) use ($hoy) {
            return $v->fecha_limite_separacion
                ? Carbon::parse($v->fecha_limite_separacion)->lt($hoy)
                : false;
        })->count();
        
        $ejecutadas = $total - $caducadas;
        
        $resultado[] = [
            'empleado' => $empleado->nombre . ' ' . $empleado->apellido,
            'total_separaciones' => $total,
            'separaciones_caducadas' => $caducadas,
            'separaciones_ejecutadas' => $ejecutadas,
            'tasa_efectividad' => $total > 0 ? round(($ejecutadas / $total) * 100, 1) : 0,
        ];
    }
    
    return $resultado;
}

6. Inventory by Project

Detailed Inventory

Unit-level inventory tracking:
  • All units by project and tower
  • Current state (available, sold, separated)
  • Base and current prices
  • Assigned advisor (if sold)
  • Sale date
public function inventarioPorProyecto(array $filtros): array
{
    $proyectos = Proyecto::with([
        'torres.apartamentos.estadoInmueble',
        'torres.apartamentos.tipoApartamento',
        'torres.apartamentos.ventas',
        'torres.locales.estadoInmueble',
        'torres.locales.ventas',
    ])->get();
    
    $result = [];
    
    foreach ($proyectos as $proyecto) {
        $items = [];
        
        foreach ($proyecto->torres as $torre) {
            foreach ($torre->apartamentos as $apto) {
                $ultimaVenta = $apto->ventas->first();
                $asesor = $ultimaVenta?->empleado;
                
                $items[] = [
                    'tipo' => 'Apartamento',
                    'etiqueta' => 'Apto ' . $apto->numero,
                    'precio_base' => (float)($apto->tipoApartamento->valor_estimado ?? 0),
                    'precio_vigente' => (float)($apto->valor_final ?? $apto->valor_total ?? 0),
                    'estado' => $apto->estadoInmueble->nombre,
                    'asesor' => $asesor ? ($asesor->nombre . ' ' . $asesor->apellido) : null,
                    'fecha_operacion' => $ultimaVenta?->fecha_venta,
                ];
            }
        }
        
        $result[] = [
            'proyecto' => $proyecto->nombre,
            'inmuebles' => $items,
        ];
    }
    
    return $result;
}

7. Sales by Advisor and Project

Advisor Performance

Individual advisor metrics:
  • Sales per advisor
  • Separations created
  • Conversion rates
  • Performance by project
public function ventasPorAsesorProyecto(array $filtros, Carbon $desde, Carbon $hasta): array
{
    $rows = Venta::query()
        ->select(
            'id_proyecto',
            'id_empleado',
            DB::raw("SUM(CASE WHEN tipo_operacion = 'venta' THEN 1 ELSE 0 END) as ventas"),
            DB::raw("SUM(CASE WHEN tipo_operacion = 'separacion' THEN 1 ELSE 0 END) as separaciones"),
            DB::raw("SUM(CASE WHEN tipo_operacion = 'separacion' 
                AND fecha_limite_separacion >= CURRENT_DATE THEN 1 ELSE 0 END) as separaciones_ejecutadas"),
            DB::raw("SUM(CASE WHEN tipo_operacion = 'separacion' 
                AND fecha_limite_separacion < CURRENT_DATE THEN 1 ELSE 0 END) as separaciones_caducadas")
        )
        ->whereBetween('fecha_venta', [$desde, $hasta])
        ->groupBy('id_proyecto', 'id_empleado')
        ->get();
    
    return $rows->map(function ($r) use ($proyectos, $empleados) {
        $proyecto = $proyectos[$r->id_proyecto] ?? null;
        $empleado = $empleados[$r->id_empleado] ?? null;
        
        return [
            'proyecto' => $proyecto->nombre ?? 'Desconocido',
            'empleado' => $empleado ? ($empleado->nombre . ' ' . $empleado->apellido) : 'Sin asignar',
            'ventas' => (int) $r->ventas,
            'separaciones' => (int) $r->separaciones,
            'separaciones_ejecutadas' => (int) $r->separaciones_ejecutadas,
            'separaciones_caducadas' => (int) $r->separaciones_caducadas,
        ];
    })->values()->all();
}

Additional Analytics

Inventory State Distribution

Inventory Breakdown

Visual distribution of inventory:
  • Available units
  • Sold units
  • Separated units
  • Blocked/frozen units
  • By project comparison
public function estadoInventario()
{
    return Proyecto::with([
        'torres.apartamentos.estadoInmueble',
        'torres.locales.estadoInmueble',
    ])
    ->get()
    ->map(function ($p) {
        $estados = [
            'Disponible' => 0,
            'Vendido' => 0,
            'Separado' => 0,
            'Bloqueado' => 0,
            'Congelado' => 0,
        ];
        
        foreach ($p->torres as $torre) {
            foreach ($torre->apartamentos as $a) {
                $nombre = $a->estadoInmueble?->nombre;
                if ($nombre && array_key_exists($nombre, $estados)) {
                    $estados[$nombre]++;
                }
            }
            
            foreach ($torre->locales as $l) {
                $nombre = $l->estadoInmueble?->nombre;
                if ($nombre && array_key_exists($nombre, $estados)) {
                    $estados[$nombre]++;
                }
            }
        }
        
        return [
            'proyecto' => $p->nombre,
            'estados' => $estados,
        ];
    });
}

Advisor Rankings

Top Performers

Leaderboard of sales advisors:
  • Total sales value
  • Number of units sold
  • Average sale value
  • Ranking by performance
public function rankingAsesores()
{
    return DB::table('ventas')
        ->join('empleados', 'empleados.id_empleado', '=', 'ventas.id_empleado')
        ->where('ventas.tipo_operacion', 'venta')
        ->select(
            'empleados.id_empleado',
            DB::raw("CONCAT(empleados.nombre, ' ', empleados.apellido) as asesor"),
            DB::raw("SUM(ventas.valor_total) as total_ventas")
        )
        ->groupBy('empleados.id_empleado', 'empleados.nombre', 'empleados.apellido')
        ->orderByDesc('total_ventas')
        ->get();
}

Monthly Absorption

Sales Trends

Time series analysis:
  • Units sold per month
  • By project
  • Trend identification
  • Seasonal patterns
public function absorcionMensual()
{
    return DB::table('ventas')
        ->join('proyectos', 'proyectos.id_proyecto', '=', 'ventas.id_proyecto')
        ->where('ventas.tipo_operacion', 'venta')
        ->select(
            'proyectos.nombre as proyecto',
            DB::raw("TO_CHAR(fecha_venta, 'YYYY-MM') as mes"),
            DB::raw("COUNT(*) as unidades")
        )
        ->groupBy('proyectos.nombre', DB::raw("TO_CHAR(fecha_venta, 'YYYY-MM')"))
        ->orderBy('mes')
        ->get();
}

Filtering & Date Ranges

All reports support flexible filtering:

Date Range

Select custom start and end dates

By Project

Filter to specific project

By Advisor

View specific advisor’s performance

By Property State

Filter inventory by state

Data Exports

Reports can be exported for external analysis:

Payment Plan Export

public function exportPlanPagosCI(Request $request, GerenciaEstadisticasService $service)
{
    $filtros = [
        'desde' => $request->query('desde'),
        'hasta' => $request->query('hasta'),
        'proyecto_id' => $request->query('proyecto_id'),
        'asesor_id' => $request->query('asesor_id'),
    ];
    
    [$desde, $hasta] = $service->rangoFechas($filtros);
    $plan = $service->planPagosCI($filtros, $desde, $hasta);
    
    return Excel::download(
        new PlanPagosCIExport($plan['encabezados'], $plan['filas'], $plan['totales']),
        'plan_pagos_cuota_inicial.xlsx'
    );
}

Excel Export Features

  • Formatted spreadsheets
  • Multiple sheets (summary, details)
  • Formulas and totals
  • Date formatting
  • Currency formatting

Access Control

Dashboard access is role-based:
// Only commercial roles can access dashboard
$cargo = Cargo::whereIn('nombre', [
    'Directora Comercial', 
    'Asesora Comercial'
])->get();

$empleados = Empleado::whereIn('id_cargo', $cargo->pluck('id_cargo'))
    ->select('id_empleado', 'nombre', 'apellido')
    ->get();

Best Practices

Management should review key metrics weekly to identify trends and issues early.
Configure monthly sales goals based on historical performance and market conditions.
Track separation-to-sale conversion rates to improve sales processes.
Use Excel exports for custom analysis and presentations to stakeholders.
Regularly compare projects to identify best practices and areas for improvement.

Build docs developers (and LLMs) love