Guía de Implementación Frontend: Distribución Equitativa y Porcentaje

Objetivo

Este documento describe cómo implementar un componente en **Next.js** que permita distribuir una cantidad total entre meses seleccionados, usando dos métodos:

  • Equitativa: Divide automáticamente entre los meses seleccionados.
  • Por porcentaje: El usuario asigna porcentajes manualmente (deben sumar 100%).

El componente realiza la distribución en el cliente y envía los datos validados al backend .NET.

Aquí tienes la guía explicativa sobre el problema clásico de distribución proporcional con redondeo y cómo resolverlo:


Guía: Distribución proporcional y el problema del redondeo

1. ¿Qué es la distribución proporcional?

Cuando tienes una cantidad total (por ejemplo, \$10,000) y deseas distribuirla entre varias partes (meses) según porcentajes, el cálculo básico es:

Ejemplo:

  • Total = $10,000
  • Enero = 8.33% → $833.33

2. El problema del redondeo

En sistemas financieros, los valores se muestran con 2 decimales.
Si calculas cada mes y redondeas individualmente, la suma puede no coincidir con el total:

833.33+833.33+=9,999.99(faltan 0.01)

Este error acumulado ocurre porque cada valor se redondea antes de sumar.


3. ¿Por qué es crítico?

  • En presupuestos y contabilidad, la suma debe ser exacta.
  • Si no corriges el error, el backend puede rechazar la operación o generar inconsistencias.

4. Solución: Algoritmo de ajuste

Pasos:

  1. Calcular todos los valores sin redondear.
  2. Redondear cada valor a 2 decimales.
  3. Calcular la diferencia entre la suma redondeada y el total.
  4. Ajustar la diferencia en uno o más elementos (normalmente el último o el mayor valor).

Ejemplo:

  • Total = $10,000
  • Suma redondeada = $9,999.99
  • Diferencia = $0.01
    → Ajustar el último valor: $833.34.

5. Beneficios

✔ Garantiza que la suma final = cantidad total.
✔ Cumple reglas contables.
✔ Evita errores en validación del backend.


¿Quieres que ahora te entregue el componente actualizado en Next.js con este algoritmo integrado, listo para usar en tu proyecto?
Este componente:

  • Calcula distribución equitativa o por porcentaje.
  • Aplica el ajuste para que la suma sea exacta.
  • Prepara el payload para enviar al backend.

¿Confirmas que lo haga?


Diseño de Interfaz / Referencias visuales:

-Vista Equitativa

– Vista Por porcentaje

Código del Componente Next.js

 

import { useState } from "react";

const meses = [
  "Enero","Febrero","Marzo","Abril","Mayo","Junio",
  "Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"
];

export default function ModalDistribucionConAjuste() {
  const [cantidadTotal, setCantidadTotal] = useState<number>(10000);
  const [metodo, setMetodo] = useState<"equitativa" | "porcentaje">("equitativa");
  const [mesesSeleccionados, setMesesSeleccionados] = useState<string[]>([...meses]);
  const [porcentajes, setPorcentajes] = useState<{[key:string]: number}>({});
  const [loading, setLoading] = useState(false);

  const toggleMes = (mes: string) => {
    setMesesSeleccionados(prev =>
      prev.includes(mes) ? prev.filter(m => m !== mes) : [...prev, mes]
    );
  };

  const handlePorcentajeChange = (mes: string, value: number) => {
    setPorcentajes(prev => ({ ...prev, [mes]: value }));
  };

  const sumaPorcentajes = Object.values(porcentajes).reduce((a,b)=>a+b,0);

  /** Algoritmo para calcular distribución con ajuste */
  const calcularDistribucion = () => {
    let valores = mesesSeleccionados.map(mes => {
      const porcentaje = metodo === "equitativa"
        ? (100 / mesesSeleccionados.length)
        : (porcentajes[mes] || 0);
      return (cantidadTotal * porcentaje) / 100;
    });

    // Redondear a 2 decimales
    let valoresRedondeados = valores.map(v => parseFloat(v.toFixed(2)));

    // Ajustar diferencia
    const sumaRedondeada = valoresRedondeados.reduce((a,b)=>a+b,0);
    const diferencia = parseFloat((cantidadTotal - sumaRedondeada).toFixed(2));

    if (diferencia !== 0 && valoresRedondeados.length > 0) {
      valoresRedondeados[valoresRedondeados.length - 1] += diferencia;
    }

    return valoresRedondeados;
  };

  const valoresFinales = calcularDistribucion();
  const isDistribuirDisabled = metodo === "porcentaje" && sumaPorcentajes !== 100;

  const handleSubmit = async () => {
    const distribucion = mesesSeleccionados.map((mes, index) => ({
      mes,
      porcentaje: metodo === "equitativa" ? (100 / mesesSeleccionados.length) : (porcentajes[mes] || 0),
      valor: valoresFinales[index]
    }));

    const payload = {
      cantidadTotal,
      metodo,
      distribucion
    };

    try {
      setLoading(true);
      const response = await fetch("/api/distribuir", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload)
      });
      if (!response.ok) throw new Error("Error al enviar datos");
      alert("Distribución enviada correctamente");
    } catch (error) {
      console.error(error);
      alert("Hubo un problema al enviar la distribución");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="p-6 bg-white rounded shadow-lg w-[650px]">
      <h2 className="text-xl font-bold mb-4">Asignar distribución</h2>

      {/* Cantidad total */}
      <div className="mb-4">
        <label className="block mb-1">Cantidad total:</label>
        <input
          type="number"
          className="border p-2 w-full"
          value={cantidadTotal}
          onChange={(e) => setCantidadTotal(Number(e.target.value))}
        />
      </div>

      {/* Selector de método */}
      <div className="mb-4 flex gap-6">
        <label className="flex items-center gap-2">
          <input
            type="radio"
            checked={metodo === "equitativa"}
            onChange={() => setMetodo("equitativa")}
          /> Equitativa
        </label>
        <label className="flex items-center gap-2">
          <input
            type="radio"
            checked={metodo === "porcentaje"}
            onChange={() => setMetodo("porcentaje")}
          /> Por porcentaje
        </label>
      </div>

      {/* Selección de meses */}
      <div className="grid grid-cols-3 gap-2 mb-4">
        {meses.map(mes => (
          <label key={mes} className="flex items-center gap-2">
            <input
              type="checkbox"
              checked={mesesSeleccionados.includes(mes)}
              onChange={() => toggleMes(mes)}
            />
            {mes}
          </label>
        ))}
      </div>

      {/* Tabla */}
      <table className="w-full border">
        <thead>
          <tr className="bg-gray-100">
            <th className="p-2 border">Mes</th>
            <th className="p-2 border">Porcentaje</th>
            <th className="p-2 border">Valor calculado</th>
          </tr>
        </thead>
        <tbody>
          {mesesSeleccionados.map((mes, index) => (
            <tr key={mes}>
              <td className="border p-2">{mes}</td>
              <td className="border p-2">
                {metodo === "porcentaje" ? (
                  <input
                    type="number"
                    className="border p-1 w-20"
                    value={porcentajes[mes] || ""}
                    onChange={(e) => handlePorcentajeChange(mes, Number(e.target.value))}
                  /> %
                ) : (
                  `${(100 / mesesSeleccionados.length).toFixed(2)}%`
                )}
              </td>
              <td className="border p-2">{valoresFinales[index].toFixed(2)}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Validación */}
      {metodo === "porcentaje" && (
        <p className={`mt-2 text-sm ${sumaPorcentajes === 100 ? "text-green-600" : "text-red-600"}`}>
          Suma porcentajes: {sumaPorcentajes}%
        </p>
      )}

      {/* Botón */}
      <button
        className="mt-4 bg-green-600 text-white px-4 py-2 rounded disabled:bg-gray-400"
        disabled={isDistribuirDisabled || loading}
        onClick={handleSubmit}
      >
        {loading ? "Enviando..." : "Distribuir"}
      </button>
    </div>
  );
}

 

✅ Consideraciones

  • Validar que la suma de porcentajes sea exactamente 100% antes de enviar.
  • En modo Equitativa, los porcentajes y valores se calculan automáticamente.
  • El componente debe ser responsivo y compatible con Tailwind CSS.
  • Reemplazar "/api/distribuir" por la URL real del backend .NET.