Ir al contenido principal

Ideas para sistema gerencial (parte 2)

 Dashboard gerencial

Perfecto. Vamos a empezar por el Dashboard Gerencial. Es la puerta de entrada a toda la información macro de la clínica.


📊 Dashboard Gerencial - Diseño y Estructura

🎯 Objetivo del Dashboard

Proveer al gerente/administrador una vista panorámica de la salud del negocio en tiempo real, permitiendo identificar tendencias, detectar problemas y tomar decisiones informadas.


🧩 Estructura de la Pantalla

text
┌─────────────────────────────────────────────────────────────────────────┐
│  📊 DASHBOARD GERENCIAL                          [Fecha: Ago 2026]     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐  │
│  │ 💰 Facturación │ │ 🏦 Deuda     │ │ 👥 Pacientes  │ │ ✅ Conversión │  │
│  │  $125.430     │ │  $45.200     │ │  142 activos │ │  78%         │  │
│  │  ↑ 12%        │ │  ↓ 5%        │ │  ↑ 15%       │ │  ↑ 3%        │  │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘  │
│                                                                         │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  📈 Evolución de Facturación (últimos 6 meses)                   │  │
│  │  ████████████████████████████████████████████████████████████  │  │
│  │  Ene  Feb  Mar  Abr  May  Jun  Jul  Ago                         │  │
│  └──────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│  ┌─────────────────────────┐  ┌─────────────────────────────────────┐  │
│  │  🏆 Tratamientos más     │  │  🏥 Productividad por odontólogo    │  │
│  │  frecuentes              │  │  Dr. García   ████████████░  87%   │  │
│  │  1. Extracción  32%      │  │  Dra. López  ██████░░░░░░  65%   │  │
│  │  2. Endodoncia  25%      │  │  Dr. Pérez   █████████░░░  78%   │  │
│  │  3. Prótesis    18%      │  └─────────────────────────────────────┘  │
│  │  4. Implantes   15%      │                                          │
│  │  5. Ortodoncia  10%      │                                          │
│  └─────────────────────────┘                                          │
│                                                                         │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  📋 Últimos movimientos importantes                               │  │
│  │  ● Propuesta N°42 aceptada (Dr. García) - $2.300               │  │
│  │  ● Pago registrado (Juan Pérez) - $1.500                       │  │
│  │  ● Nueva propuesta emitida (Dra. López) - $4.200               │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

📁 Estructura de Archivos

text
app/
├── Http/
│   └── Controllers/
│       └── DashboardController.php
├── Services/
│   └── DashboardService.php

resources/
├── js/
│   ├── Pages/
│   │   └── Dashboard/
│   │       └── Gerencial.tsx
│   └── Components/
│       └── Dashboard/
│           ├── KPICard.tsx
│           ├── FacturacionChart.tsx
│           ├── TratamientosFrecuentes.tsx
│           ├── ProductividadOdontologos.tsx
│           └── UltimosMovimientos.tsx

routes/
└── web.php

config/
└── dashboard.php

📊 Migración: Tabla de Métricas Mensuales (Cache)

php
// database/migrations/xxxx_xx_xx_create_metricas_mensuales_table.php

<?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('metricas_mensuales', function (Blueprint $table) {
            $table->id();
            $table->year('anio');
            $table->unsignedTinyInteger('mes');
            $table->decimal('facturacion_total', 12, 2)->default(0);
            $table->decimal('cobrado_total', 12, 2)->default(0);
            $table->decimal('deuda_total', 12, 2)->default(0);
            $table->unsignedInteger('propuestas_emitidas')->default(0);
            $table->unsignedInteger('propuestas_aceptadas')->default(0);
            $table->unsignedInteger('propuestas_rechazadas')->default(0);
            $table->unsignedInteger('nuevos_pacientes')->default(0);
            $table->unsignedInteger('pacientes_activos')->default(0);
            $table->unsignedInteger('tratamientos_realizados')->default(0);
            $table->unsignedInteger('pagos_registrados')->default(0);
            $table->json('tratamientos_detalle')->nullable(); // JSON con conteo por prestación
            $table->timestamps();

            $table->unique(['anio', 'mes']);
            $table->index('anio');
            $table->index('mes');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('metricas_mensuales');
    }
};

🧠 Servicio: DashboardService

php
// app/Services/DashboardService.php

<?php

namespace App\Services;

use App\Models\Cuenta;
use App\Models\Paciente;
use App\Models\TratamientoPropuesta;
use App\Models\Pago;
use App\Models\MetricaMensual;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class DashboardService
{
    /**
     * Obtener los KPIs principales.
     */
    public function getKPIs(?int $consultorioId = null): array
    {
        // Facturación total (propuestas aceptadas)
        $facturacionTotal = TratamientoPropuesta::where('estado', 'ACEPTADA')
            ->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
            ->sum('total');

        // Deuda total
        $deudaTotal = Cuenta::when($consultorioId, function ($q) use ($consultorioId) {
            return $q->whereHas('paciente', fn($q2) => $q2->where('consultorio_id', $consultorioId));
        })->sum('saldo');

        // Pacientes activos (con propuestas en el último mes)
        $pacientesActivos = Paciente::whereHas('propuestas', function ($q) {
            $q->where('emitida_at', '>=', now()->subMonth());
        })->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
          ->count();

        // Tasa de conversión
        $emitidas = TratamientoPropuesta::where('estado', '!=', 'BORRADOR')
            ->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
            ->count();

        $aceptadas = TratamientoPropuesta::where('estado', 'ACEPTADA')
            ->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
            ->count();

        $tasaConversion = $emitidas > 0 ? round(($aceptadas / $emitidas) * 100, 1) : 0;

        return [
            'facturacion_total' => $facturacionTotal,
            'facturacion_vs_mes_anterior' => $this->calcularVariacionFacturacion($consultorioId),
            'deuda_total' => $deudaTotal,
            'deuda_vs_mes_anterior' => $this->calcularVariacionDeuda($consultorioId),
            'pacientes_activos' => $pacientesActivos,
            'pacientes_vs_mes_anterior' => $this->calcularVariacionPacientes($consultorioId),
            'tasa_conversion' => $tasaConversion,
            'tasa_conversion_vs_mes_anterior' => $this->calcularVariacionConversion($consultorioId),
            'periodo' => now()->format('F Y'),
        ];
    }

    /**
     * Obtener evolución de facturación (últimos N meses).
     */
    public function getEvolucionFacturacion(int $meses = 6, ?int $consultorioId = null): Collection
    {
        $fechas = collect();

        for ($i = $meses - 1; $i >= 0; $i--) {
            $mes = now()->subMonths($i);
            $fechas->push([
                'anio' => $mes->year,
                'mes' => $mes->month,
                'label' => $mes->format('M Y'),
            ]);
        }

        return $fechas->map(function ($fecha) use ($consultorioId) {
            $total = TratamientoPropuesta::where('estado', 'ACEPTADA')
                ->whereYear('emitida_at', $fecha['anio'])
                ->whereMonth('emitida_at', $fecha['mes'])
                ->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
                ->sum('total');

            return [
                'label' => $fecha['label'],
                'facturacion' => $total,
            ];
        });
    }

    /**
     * Obtener tratamientos más frecuentes.
     */
    public function getTratamientosFrecuentes(int $limit = 5, ?int $consultorioId = null): Collection
    {
        return DB::table('tratamiento_propuesta_items as tpi')
            ->join('prestaciones as p', 'tpi.prestacion_id', '=', 'p.id')
            ->join('tratamiento_propuestas as tp', 'tpi.tratamiento_propuesta_id', '=', 'tp.id')
            ->where('tp.estado', 'ACEPTADA')
            ->when($consultorioId, fn($q) => $q->where('tp.consultorio_id', $consultorioId))
            ->select(
                'p.id as prestacion_id',
                'p.nombre as tratamiento',
                DB::raw('COUNT(*) as cantidad'),
                DB::raw('SUM(tpi.importe) as total_ingresado'),
                DB::raw('ROUND((COUNT(*) * 100.0 / (SELECT COUNT(*) FROM tratamiento_propuesta_items WHERE tratamiento_propuesta_id IN (SELECT id FROM tratamiento_propuestas WHERE estado = "ACEPTADA" ' . ($consultorioId ? 'AND consultorio_id = ' . $consultorioId : '') . '))), 1) as porcentaje')
            )
            ->groupBy('p.id', 'p.nombre')
            ->orderBy('cantidad', 'desc')
            ->limit($limit)
            ->get();
    }

    /**
     * Obtener productividad por odontólogo.
     */
    public function getProductividadOdontologos(?int $consultorioId = null): Collection
    {
        return DB::table('tratamiento_propuestas as tp')
            ->join('users as u', 'tp.creado_por', '=', 'u.id')
            ->where('tp.estado', 'ACEPTADA')
            ->when($consultorioId, fn($q) => $q->where('tp.consultorio_id', $consultorioId))
            ->select(
                'u.id as odontologo_id',
                'u.name as odontologo',
                DB::raw('COUNT(*) as propuestas_aceptadas'),
                DB::raw('SUM(tp.total) as total_facturado')
            )
            ->groupBy('u.id', 'u.name')
            ->orderBy('total_facturado', 'desc')
            ->get()
            ->map(function ($item) {
                $maxTotal = $this->getMaxFacturadoOdontologo();
                $item->porcentaje = $maxTotal > 0 ? round(($item->total_facturado / $maxTotal) * 100) : 0;
                return $item;
            });
    }

    /**
     * Obtener últimos movimientos relevantes.
     */
    public function getUltimosMovimientos(int $limit = 10, ?int $consultorioId = null): Collection
    {
        // Combinar eventos de propuestas y pagos
        $propuestas = TratamientoPropuesta::with('paciente', 'creador')
            ->whereIn('estado', ['ACEPTADA', 'EMITIDA', 'RECHAZADA'])
            ->when($consultorioId, fn($q) => $q->where('consultorio_id', $consultorioId))
            ->latest('updated_at')
            ->limit($limit)
            ->get()
            ->map(fn($p) => [
                'fecha' => $p->updated_at,
                'tipo' => 'propuesta_' . strtolower($p->estado->value),
                'descripcion' => "Propuesta #{$p->numero} " . strtolower($p->estado->value),
                'monto' => $p->total,
                'paciente' => $p->paciente->full_name,
                'odontologo' => $p->creador?->name,
            ]);

        $pagos = Pago::with('paciente', 'usuario')
            ->where('anulado', false)
            ->when($consultorioId, fn($q) => $q->whereHas('paciente', fn($q2) => $q2->where('consultorio_id', $consultorioId)))
            ->latest('created_at')
            ->limit($limit)
            ->get()
            ->map(fn($p) => [
                'fecha' => $p->created_at,
                'tipo' => 'pago',
                'descripcion' => 'Pago registrado',
                'monto' => $p->monto,
                'paciente' => $p->paciente->full_name,
                'odontologo' => $p->usuario?->name,
            ]);

        return $propuestas->concat($pagos)
            ->sortByDesc('fecha')
            ->take($limit)
            ->values();
    }

    // Métodos auxiliares para cálculos de variación
    private function calcularVariacionFacturacion(?int $consultorioId): float
    {
        // ... implementación
        return 12.5; // placeholder
    }

    private function calcularVariacionDeuda(?int $consultorioId): float
    {
        // ... implementación
        return -5.2; // placeholder
    }

    private function calcularVariacionPacientes(?int $consultorioId): float
    {
        // ... implementación
        return 15.0; // placeholder
    }

    private function calcularVariacionConversion(?int $consultorioId): float
    {
        // ... implementación
        return 3.0; // placeholder
    }

    private function getMaxFacturadoOdontologo(): float
    {
        return DB::table('tratamiento_propuestas')
            ->where('estado', 'ACEPTADA')
            ->select('creado_por', DB::raw('SUM(total) as total'))
            ->groupBy('creado_por')
            ->orderBy('total', 'desc')
            ->value('total') ?? 0;
    }
}

🎯 Controlador del Dashboard

php
// app/Http/Controllers/DashboardController.php

<?php

namespace App\Http\Controllers;

use App\Services\DashboardService;
use Inertia\Inertia;

class DashboardController extends Controller
{
    public function __construct(
        private DashboardService $dashboardService
    ) {
    }

    public function gerencial()
    {
        // Obtener el consultorio de la sesión
        $consultorioId = session()->get(config('constant.CONSULTORIO_SESSION_KEY'));

        $data = [
            'kpis' => $this->dashboardService->getKPIs($consultorioId),
            'evolucion_facturacion' => $this->dashboardService->getEvolucionFacturacion(6, $consultorioId),
            'tratamientos_frecuentes' => $this->dashboardService->getTratamientosFrecuentes(5, $consultorioId),
            'productividad_odontologos' => $this->dashboardService->getProductividadOdontologos($consultorioId),
            'ultimos_movimientos' => $this->dashboardService->getUltimosMovimientos(10, $consultorioId),
        ];

        return Inertia::render('Dashboard/Gerencial', $data);
    }
}

📊 Componente KPICard (React)

tsx
// resources/js/Components/Dashboard/KPICard.tsx

import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowUp, ArrowDown } from 'lucide-react';
import { formatCurrency } from '@/lib/utils';

interface Props {
    title: string;
    value: number | string;
    prefix?: string;
    suffix?: string;
    variation?: number;
    icon?: React.ReactNode;
}

export default function KPICard({ title, value, prefix, suffix, variation, icon }: Props) {
    const isPositive = variation && variation > 0;
    const isNegative = variation && variation < 0;

    const formattedValue = typeof value === 'number' && prefix === '$'
        ? formatCurrency(value)
        : typeof value === 'number'
            ? value.toLocaleString('es-AR')
            : value;

    return (
        <Card>
            <CardHeader className="flex flex-row items-center justify-between pb-2">
                <CardTitle className="text-sm font-medium text-muted-foreground">
                    {title}
                </CardTitle>
                {icon && <div className="text-muted-foreground">{icon}</div>}
            </CardHeader>
            <CardContent>
                <div className="text-2xl font-bold">
                    {prefix}{formattedValue}{suffix}
                </div>
                {variation !== undefined && (
                    <div className={`flex items-center text-xs ${isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-muted-foreground'}`}>
                        {isPositive && <ArrowUp className="h-3 w-3 mr-1" />}
                        {isNegative && <ArrowDown className="h-3 w-3 mr-1" />}
                        <span>{Math.abs(variation)}%</span>
                        <span className="text-muted-foreground ml-1">vs mes anterior</span>
                    </div>
                )}
            </CardContent>
        </Card>
    );
}

📊 Pantalla Gerencial (React)

tsx
// resources/js/Pages/Dashboard/Gerencial.tsx

import React from 'react';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import KPICard from '@/Components/Dashboard/KPICard';
import { DollarSign, Users, FileText, TrendingUp } from 'lucide-react';
import { formatCurrency } from '@/lib/utils';

interface Props {
    kpis: {
        facturacion_total: number;
        facturacion_vs_mes_anterior: number;
        deuda_total: number;
        deuda_vs_mes_anterior: number;
        pacientes_activos: number;
        pacientes_vs_mes_anterior: number;
        tasa_conversion: number;
        tasa_conversion_vs_mes_anterior: number;
        periodo: string;
    };
    evolucion_facturacion: { label: string; facturacion: number }[];
    tratamientos_frecuentes: { tratamiento: string; cantidad: number; porcentaje: number; total_ingresado: number }[];
    productividad_odontologos: { odontologo: string; propuestas_aceptadas: number; total_facturado: number; porcentaje: number }[];
    ultimos_movimientos: { fecha: string; tipo: string; descripcion: string; monto: number; paciente: string; odontologo?: string }[];
}

export default function Gerencial({
    kpis,
    evolucion_facturacion,
    tratamientos_frecuentes,
    productividad_odontologos,
    ultimos_movimientos,
}: Props) {
    return (
        <DashboardLayout>
            <div className="container mx-auto py-6 space-y-6">
                {/* Header */}
                <div className="flex justify-between items-center">
                    <div>
                        <h1 className="text-2xl font-bold">Dashboard Gerencial</h1>
                        <p className="text-muted-foreground">{kpis.periodo}</p>
                    </div>
                    <div className="flex gap-2">
                        <select className="border rounded px-3 py-1 text-sm dark:bg-gray-800">
                            <option>Último mes</option>
                            <option>Último trimestre</option>
                            <option>Último año</option>
                        </select>
                    </div>
                </div>

                {/* KPIs */}
                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
                    <KPICard
                        title="Facturación"
                        value={kpis.facturacion_total}
                        prefix="$"
                        variation={kpis.facturacion_vs_mes_anterior}
                        icon={<DollarSign className="h-4 w-4" />}
                    />
                    <KPICard
                        title="Deuda"
                        value={kpis.deuda_total}
                        prefix="$"
                        variation={kpis.deuda_vs_mes_anterior}
                        icon={<FileText className="h-4 w-4" />}
                    />
                    <KPICard
                        title="Pacientes Activos"
                        value={kpis.pacientes_activos}
                        variation={kpis.pacientes_vs_mes_anterior}
                        icon={<Users className="h-4 w-4" />}
                    />
                    <KPICard
                        title="Tasa de Conversión"
                        value={kpis.tasa_conversion}
                        suffix="%"
                        variation={kpis.tasa_conversion_vs_mes_anterior}
                        icon={<TrendingUp className="h-4 w-4" />}
                    />
                </div>

                {/* Gráfico de Evolución */}
                <Card>
                    <CardHeader>
                        <CardTitle>Evolución de Facturación</CardTitle>
                    </CardHeader>
                    <CardContent>
                        <div className="h-64 flex items-end justify-between gap-2">
                            {evolucion_facturacion.map((item) => {
                                const max = Math.max(...evolucion_facturacion.map(i => i.facturacion));
                                const height = max > 0 ? (item.facturacion / max) * 100 : 0;
                                return (
                                    <div key={item.label} className="flex flex-col items-center flex-1">
                                        <div
                                            className="w-full bg-primary rounded-t transition-all duration-300"
                                            style={{ height: `${height}%` }}
                                        />
                                        <span className="text-xs text-muted-foreground mt-2">{item.label}</span>
                                    </div>
                                );
                            })}
                        </div>
                    </CardContent>
                </Card>

                {/* Dos columnas: Tratamientos + Productividad */}
                <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
                    {/* Tratamientos frecuentes */}
                    <Card>
                        <CardHeader>
                            <CardTitle>Tratamientos más frecuentes</CardTitle>
                        </CardHeader>
                        <CardContent>
                            <div className="space-y-3">
                                {tratamientos_frecuentes.map((item) => (
                                    <div key={item.tratamiento}>
                                        <div className="flex justify-between text-sm">
                                            <span>{item.tratamiento}</span>
                                            <span className="font-medium">{item.porcentaje}%</span>
                                        </div>
                                        <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
                                            <div
                                                className="bg-primary rounded-full h-2"
                                                style={{ width: `${item.porcentaje}%` }}
                                            />
                                        </div>
                                    </div>
                                ))}
                            </div>
                        </CardContent>
                    </Card>

                    {/* Productividad odontólogos */}
                    <Card>
                        <CardHeader>
                            <CardTitle>Productividad por odontólogo</CardTitle>
                        </CardHeader>
                        <CardContent>
                            <div className="space-y-3">
                                {productividad_odontologos.map((item) => (
                                    <div key={item.odontologo}>
                                        <div className="flex justify-between text-sm">
                                            <span>{item.odontologo}</span>
                                            <span className="font-medium">{item.porcentaje}%</span>
                                        </div>
                                        <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
                                            <div
                                                className="bg-green-500 rounded-full h-2"
                                                style={{ width: `${item.porcentaje}%` }}
                                            />
                                        </div>
                                    </div>
                                ))}
                            </div>
                        </CardContent>
                    </Card>
                </div>

                {/* Últimos movimientos */}
                <Card>
                    <CardHeader>
                        <CardTitle>Últimos movimientos importantes</CardTitle>
                    </CardHeader>
                    <CardContent>
                        <div className="space-y-2">
                            {ultimos_movimientos.map((movimiento, index) => (
                                <div key={index} className="flex justify-between items-center py-2 border-b last:border-0">
                                    <div>
                                        <p className="text-sm font-medium">{movimiento.descripcion}</p>
                                        <p className="text-xs text-muted-foreground">
                                            {movimiento.paciente}{movimiento.odontologo || 'Sistema'}
                                        </p>
                                    </div>
                                    <div className="text-right">
                                        <span className={`text-sm font-medium ${movimiento.monto > 0 ? 'text-green-600' : ''}`}>
                                            {formatCurrency(movimiento.monto)}
                                        </span>
                                        <p className="text-xs text-muted-foreground">
                                            {new Date(movimiento.fecha).toLocaleString()}
                                        </p>
                                    </div>
                                </div>
                            ))}
                        </div>
                    </CardContent>
                </Card>
            </div>
        </DashboardLayout>
    );
}

🛣️ Rutas

php
// routes/web.php

use App\Http\Controllers\DashboardController;

Route::middleware(['web'])->group(function () {
    Route::get('/dashboard/gerencial', [DashboardController::class, 'gerencial'])
        ->name('dashboard.gerencial');
});

🚀 Comando para Calcular Métricas Mensuales

php
// app/Console/Commands/CalcularMetricasMensuales.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\DashboardService;
use App\Models\MetricaMensual;

class CalcularMetricasMensuales extends Command
{
    protected $signature = 'dashboard:calcular-metricas {--mes=} {--anio=}';
    protected $description = 'Calcula las métricas mensuales para el dashboard';

    public function handle(DashboardService $service)
    {
        $mes = $this->option('mes') ?? now()->month;
        $anio = $this->option('anio') ?? now()->year;

        // ... lógica para calcular y guardar en MetricaMensual

        $this->info("Métricas calculadas para {$mes}/{$anio}");
    }
}

Resumen de Implementación

ComponenteEstado
Migración metricas_mensualesPendiente
DashboardServicePendiente
DashboardControllerPendiente
KPICard (React)Pendiente
Pantalla Gerencial.tsxPendiente
Gráficos y visualizacionesPendiente
Ruta /dashboard/gerencialPendiente
Comando de cálculoPendiente

¿Por dónde querés empezar? ¿Migración, Service, Controlador, o componente React?

Comentarios

Entradas populares de este blog

Crear proyecto de frontend con Vite, React, Tailwind, Typescript y JSX

 ¿Cómo se crea un proyecto exclusivo de frontend con Vite, React, Tailwind, Typescript y JSX? ¡Absolutamente! Entiendo que quieres la guía completa para pasar de cero a tener un proyecto de frontend moderno, vacío y listo para ser llenado con tu código. A continuación, te proporciono un archivo de guía detallada que incluye todos los comandos necesarios para inicializar el proyecto, configurar Tailwind CSS, y los pasos clave para empezar a "poblarlo". Voy a generar un documento Markdown con la guía paso a paso, incluyendo los comandos de terminal y las modificaciones esenciales a los archivos de configuración. Guía: Creación de un Proyecto Frontend Puro (Vite, React, TypeScript, Tailwind) Este documento detalla el proceso para inicializar un proyecto de frontend vacío y optimizado, utilizando Vite como empaquetador, React como librería, TypeScript para tipado y Tailwind CSS para estilizado. 1. Inicialización del Proyecto con Vite Vite es la herramienta más rápida para emp...

Tokens V2

Tokens: Programme to discover tokens, where there are not. Now available at  https://puszcza.gnu.org.ua/projects/tokens/ This is Version 2, for  version 1, go here . Synopsis: use TokensV2; sub printFile; my @FORMAT = ( ['<Message Date=".*?" Time=".*?" DateTime=".*?" SessionID=".*?"><From>(?:<User FriendlyName=".*?"/>)+</From><To>(?:<User FriendlyName=".*?"/>)+</To><Text(?: Style=".*?")?>.*?</Text></Message>',   sub {     my $fh = $_[1];     my ($d, $t, $f, $s, $T) = $_[0] =~ m|<Message Date="(.*?)" Time="(.*?)" DateTime=".*?" SessionID=".*?">(<From>(?:<User FriendlyName=".*?"/>)+</From>)<To>(?:<User FriendlyName=".*?"/>)+</To><Text(?: Style="(.*?)")?>(.*?)</Text></Message>|;     my $F = join '<br />', ...

Perl Net::LDAP::SimpleServer

Adaptaciones sobre el módulo LDAP Server para Windows (Strawberry Perl) Lista de adaptaciones (continúa más abajo): - Relajación de condiciones de bind:     - Cuenta principal (principal account)     - Validación de contraseñas Ubicación del archivo: %Strawberry_Perl%\site\lib\net\ldap\SimpleServer\ProtocolHandler.pm CPAN: http://search.cpan.org/~russoz/Net-LDAP-SimpleServer-0.0.17/lib/Net/LDAP/SimpleServer.pm Código: package Net::LDAP::SimpleServer::ProtocolHandler; use strict; use warnings; # ABSTRACT: LDAP protocol handler used with Net::LDAP::SimpleServer our $VERSION = '0.0.17';    # VERSION use Net::LDAP::Server; use base 'Net::LDAP::Server'; use fields qw(store root_dn root_pw allow_anon); use Carp; use Net::LDAP::LDIF; use Net::LDAP::Util qw{canonical_dn}; use Net::LDAP::FilterMatch; use Net::LDAP::Constant (     qw/LDAP_SUCCESS LDAP_AUTH_UNKNOWN LDAP_INVALID_CREDENTIALS/,   ...