{"id":543,"date":"2025-11-16T14:37:21","date_gmt":"2025-11-16T14:37:21","guid":{"rendered":"https:\/\/bodafranyjose.com\/?page_id=543"},"modified":"2026-04-13T17:05:50","modified_gmt":"2026-04-13T17:05:50","slug":"gastos","status":"publish","type":"page","link":"https:\/\/bodafranyjose.com\/index.php\/gastos\/","title":{"rendered":"Gastos"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"543\" class=\"elementor elementor-543\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-308fee6 e-flex e-con-boxed e-con e-parent\" data-id=\"308fee6\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ceb7807 elementor-widget elementor-widget-html\" data-id=\"ceb7807\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<style>\n  .gastos-container {\n    background: #fff;\n    border-radius: 22px;\n    box-shadow: 0 6px 24px rgba(0,0,0,0.07);\n    padding: 22px 18px;\n    max-width: 950px;\n    margin: 0 auto;\n    font-family: 'Poppins', sans-serif;\n    color: #333;\n  }\n\n  .gastos-head {\n    text-align: center;\n    margin-bottom: 14px;\n  }\n\n  .gastos-title {\n    color: #b67b91;\n    margin: 0 0 5px;\n    font-size: 1.72rem;\n    font-weight: 700;\n    line-height: 1.15;\n  }\n\n  .gastos-subtitle {\n    color: #666;\n    margin: 0;\n    font-size: 0.9rem;\n    font-weight: 600;\n    line-height: 1.28;\n  }\n\n  .resumen-dashboard {\n    display: grid;\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    gap: 9px;\n    margin-bottom: 10px;\n  }\n\n  .resumen-card {\n    background: #faf6f8;\n    border: 1px solid transparent;\n    border-radius: 13px;\n    text-align: center;\n    padding: 8px 7px;\n    box-shadow: 0 2px 9px rgba(0,0,0,0.04);\n    cursor: pointer;\n    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;\n  }\n\n  .resumen-card:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 5px 14px rgba(0,0,0,0.07);\n  }\n\n  .resumen-card.active {\n    background: #fff;\n    border-color: #b67b91;\n    box-shadow: 0 5px 16px rgba(182,123,145,0.16);\n  }\n\n  .resumen-card h3 {\n    font-size: 0.76rem;\n    color: #b67b91;\n    margin: 0 0 4px;\n    font-weight: 700;\n    line-height: 1.1;\n  }\n\n  .resumen-card span {\n    font-size: 1.08rem;\n    font-weight: 800;\n    color: #333;\n    line-height: 1.1;\n  }\n\n  .resumen-card.total { background: #f8e6ec; }\n  .resumen-card.pagado { background: #e3f7e6; }\n  .resumen-card.restante { background: #fff3d8; }\n\n  .resumen-card.active.total,\n  .resumen-card.active.pagado,\n  .resumen-card.active.restante {\n    background: #fff;\n  }\n\n  .progress-block {\n    margin: 0 0 12px;\n  }\n\n  .progress-label {\n    display: flex;\n    justify-content: space-between;\n    gap: 10px;\n    color: #666;\n    font-size: 0.76rem;\n    font-weight: 700;\n    margin-bottom: 5px;\n  }\n\n  .progress-bar-container {\n    width: 100%;\n    height: 7px;\n    background: #f0eaed;\n    border-radius: 999px;\n    overflow: hidden;\n  }\n\n  .progress-bar {\n    height: 100%;\n    background: linear-gradient(90deg, #b67b91, #a56b82);\n    width: 0%;\n    border-radius: 999px;\n    transition: width 0.4s ease-in-out;\n  }\n\n  .add-gasto-panel {\n    margin-bottom: 10px;\n  }\n\n  .add-toggle-btn {\n    display: block;\n    width: 100%;\n    min-height: 42px;\n    background: #b67b91;\n    color: #fff;\n    border: none;\n    border-radius: 10px;\n    padding: 10px 14px;\n    cursor: pointer;\n    font-family: 'Poppins', sans-serif;\n    font-weight: 800;\n    font-size: 0.92rem;\n    transition: background 0.25s ease;\n  }\n\n  .add-toggle-btn:hover {\n    background: #a56b82;\n  }\n\n  .add-gasto {\n    display: none;\n    grid-template-columns: 1.2fr 0.7fr 0.7fr auto;\n    gap: 9px;\n    align-items: center;\n    margin-top: 9px;\n    background: #faf6f8;\n    border: 1px solid #f0e6ea;\n    border-radius: 16px;\n    padding: 12px;\n  }\n\n  .add-gasto.active {\n    display: grid;\n  }\n\n  .add-gasto input {\n    width: 100%;\n    min-height: 41px;\n    padding: 9px 10px;\n    border: 1px solid #e2d4da;\n    border-radius: 10px;\n    font-size: 16px;\n    box-sizing: border-box;\n    background: #fff;\n    font-family: 'Poppins', sans-serif;\n  }\n\n  .gastos-toolbar {\n    display: grid;\n    grid-template-columns: 1fr auto auto;\n    gap: 9px;\n    align-items: center;\n    margin-bottom: 12px;\n  }\n\n  .buscar-wrap {\n  position: relative;\n}\n\n.buscar-wrap::before,\n.buscar-wrap::after {\n  content: none;\n  display: none;\n}\n\n.buscar-gasto,\n.ordenar-gasto {\n  width: 100%;\n  min-height: 41px;\n  padding: 9px 11px;\n  border-radius: 10px;\n  border: 1px solid #e2d4da;\n  font-size: 16px;\n  box-sizing: border-box;\n  background: #fff;\n  font-family: 'Poppins', sans-serif;\n  font-weight: 400;\n}\n\n.buscar-gasto {\n  padding-left: 11px;\n}\n\n.buscar-gasto::placeholder {\n  color: #888;\n  font-weight: 300;\n}\n\n.buscar-gasto:focus,\n.ordenar-gasto:focus,\n.add-gasto input:focus,\n.ficha-input:focus {\n  outline: 2px solid #f2e1e8;\n  border-color: #b67b91;\n}\n\n\n  .add-btn,\n  .btn-close,\n  .export-btn,\n  .btn-danger {\n    border-radius: 10px;\n    cursor: pointer;\n    font-family: 'Poppins', sans-serif;\n    font-weight: 700;\n    font-size: 0.86rem;\n    transition: 0.25s ease;\n    white-space: nowrap;\n    min-height: 41px;\n  }\n\n  .add-btn,\n  .btn-close {\n    background: #b67b91;\n    color: white;\n    border: none;\n    padding: 9px 13px;\n  }\n\n  .add-btn:hover,\n  .btn-close:hover {\n    background: #a56b82;\n  }\n\n  .export-btn {\n    background: #fff;\n    color: #b67b91;\n    border: 1px solid #e5c9d4;\n    padding: 8px 12px;\n  }\n\n  .export-btn:hover {\n    background: #f9f0f4;\n    border-color: #b67b91;\n  }\n\n  .btn-danger {\n    background: #fff;\n    color: #a5447d;\n    border: 1px solid #f0c8d6;\n    padding: 8px 12px;\n  }\n\n  .btn-danger:hover {\n    background: #fcefee;\n    color: #8f3568;\n  }\n\n  .gastos-lista {\n    display: grid;\n    gap: 7px;\n  }\n\n  .gasto-row {\n    width: 100%;\n    display: grid;\n    grid-template-columns: 1fr auto;\n    gap: 10px;\n    align-items: center;\n    border: 1px solid #f0e6ea;\n    border-radius: 14px;\n    background: #fff;\n    padding: 9px 10px;\n    box-shadow: 0 2px 10px rgba(0,0,0,0.035);\n    cursor: pointer;\n    text-align: left;\n    font-family: 'Poppins', sans-serif;\n    color: #333;\n    transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;\n  }\n\n  .gasto-row:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 4px 14px rgba(0,0,0,0.055);\n  }\n\n  .gasto-row.pagado-completo {\n    border-color: #d9efd9;\n    background: linear-gradient(90deg, #f7fcf8, #fff);\n  }\n\n  .gasto-info {\n    min-width: 0;\n  }\n\n  .gasto-concepto {\n    display: block;\n    font-weight: 800;\n    color: #444;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    line-height: 1.18;\n    font-size: 0.95rem;\n  }\n\n  .gasto-linea {\n    display: block;\n    margin-top: 3px;\n    color: #777;\n    font-size: 0.76rem;\n    font-weight: 600;\n    line-height: 1.15;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .gasto-side {\n    display: flex;\n    align-items: center;\n    gap: 7px;\n  }\n\n  .badge {\n    display: inline-flex;\n    align-items: center;\n    background: #fff;\n    color: #b67b91;\n    border: 1px solid #f0e6ea;\n    border-radius: 999px;\n    padding: 3px 8px;\n    font-size: 0.68rem;\n    font-weight: 800;\n    line-height: 1.1;\n    white-space: nowrap;\n  }\n\n  .badge.ok {\n    color: #3c8d56;\n    border-color: #d9efd9;\n  }\n\n  .badge.warn {\n    color: #a5447d;\n    border-color: #f0c8d6;\n  }\n\n  .row-chevron {\n    color: #b67b91;\n    font-size: 1.1rem;\n    font-weight: 800;\n    line-height: 1;\n  }\n\n  .sin-resultados {\n    text-align: center;\n    color: #b67b91;\n    margin: 14px 0;\n    background: #faf6f8;\n    border-radius: 14px;\n    padding: 16px;\n    font-weight: 700;\n  }\n\n  .gasto-modal {\n    display: none;\n    position: fixed;\n    z-index: 99999;\n    inset: 0;\n    background: rgba(45, 35, 40, 0.38);\n    padding: 18px;\n    box-sizing: border-box;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .gasto-modal.active {\n    display: flex;\n  }\n\n  .gasto-modal-card {\n    width: min(560px, 100%);\n    max-height: calc(100vh - 36px);\n    overflow-y: auto;\n    background: #fff;\n    border-radius: 18px;\n    box-shadow: 0 18px 45px rgba(0,0,0,0.22);\n    border: 1px solid #f0e6ea;\n  }\n\n  .ficha-head {\n    display: grid;\n    grid-template-columns: 1fr 40px;\n    gap: 10px;\n    align-items: center;\n    padding: 14px 14px 12px;\n    background: #faf6f8;\n    border-bottom: 1px solid #f0e6ea;\n    border-radius: 18px 18px 0 0;\n  }\n\n  .ficha-title {\n    margin: 0;\n    color: #b67b91;\n    font-size: 1.08rem;\n    font-weight: 800;\n    line-height: 1.2;\n  }\n\n  .ficha-subtitle {\n    margin: 3px 0 0;\n    color: #666;\n    font-size: 0.8rem;\n    font-weight: 600;\n  }\n\n  .ficha-x {\n    width: 40px;\n    height: 40px;\n    border: 1px solid #f0e6ea;\n    border-radius: 10px;\n    background: #fff;\n    color: #b67b91;\n    cursor: pointer;\n    font-weight: 800;\n    font-size: 1.08rem;\n    line-height: 1;\n    padding: 0;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    appearance: none;\n  }\n\n  .ficha-body {\n    padding: 14px;\n  }\n\n  .ficha-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 10px;\n  }\n\n  .ficha-field {\n    min-width: 0;\n  }\n\n  .ficha-field.full {\n    grid-column: 1 \/ -1;\n  }\n\n  .ficha-field label {\n    display: block;\n    color: #b67b91;\n    font-weight: 800;\n    font-size: 0.82rem;\n    margin-bottom: 5px;\n  }\n\n  .ficha-input {\n    width: 100%;\n    min-height: 42px;\n    padding: 9px 10px;\n    border-radius: 10px;\n    border: 1px solid #e2d4da;\n    font-size: 16px;\n    box-sizing: border-box;\n    background: #fff;\n    font-family: 'Poppins', sans-serif;\n  }\n\n  .ficha-total-line {\n    margin-top: 12px;\n    padding: 10px;\n    border: 1px solid #f0e6ea;\n    border-radius: 12px;\n    background: #faf6f8;\n    color: #444;\n    font-size: 0.86rem;\n    font-weight: 700;\n    line-height: 1.3;\n  }\n\n  .ficha-total-line.ok {\n    color: #3c8d56;\n    border-color: #d9efd9;\n    background: #f7fcf8;\n  }\n\n  .ficha-total-line.warn {\n    color: #a5447d;\n    border-color: #f0c8d6;\n    background: #fff8fb;\n  }\n\n  .ficha-actions {\n    display: grid;\n    grid-template-columns: 1fr;\n    gap: 9px;\n    margin-top: 14px;\n  }\n\n  .ficha-danger-zone {\n    margin-top: 13px;\n    padding-top: 13px;\n    border-top: 1px solid #f4e5eb;\n  }\n\n  .ficha-danger-zone .btn-danger {\n    width: 100%;\n  }\n\n  @media (max-width: 768px) {\n    .gastos-container {\n      padding: 16px 12px;\n      border-radius: 18px;\n    }\n\n    .gastos-head {\n      margin-bottom: 10px;\n    }\n\n    .gastos-title {\n      font-size: 1.36rem;\n      margin-bottom: 4px;\n    }\n\n    .gastos-subtitle {\n      font-size: 0.78rem;\n      line-height: 1.25;\n      max-width: 340px;\n      margin: 0 auto;\n    }\n\n    .resumen-dashboard {\n      grid-template-columns: repeat(3, minmax(0, 1fr));\n      gap: 6px;\n      margin-bottom: 8px;\n    }\n\n    .resumen-card {\n      padding: 7px 4px;\n      border-radius: 11px;\n      min-height: 48px;\n    }\n\n    .resumen-card h3 {\n      font-size: 0.56rem;\n      margin-bottom: 3px;\n    }\n\n    .resumen-card span {\n      font-size: 0.82rem;\n    }\n\n    .progress-block {\n      margin-bottom: 10px;\n    }\n\n    .progress-label {\n      font-size: 0.7rem;\n      margin-bottom: 4px;\n    }\n\n    .progress-bar-container {\n      height: 6px;\n    }\n\n    .add-gasto {\n      grid-template-columns: 1fr;\n      gap: 8px;\n      padding: 11px;\n    }\n\n    .gastos-toolbar {\n      grid-template-columns: 1fr 1fr;\n      gap: 8px;\n      margin-bottom: 10px;\n    }\n\n    .buscar-wrap {\n      grid-column: 1 \/ -1;\n    }\n\n    .export-btn,\n    .ordenar-gasto,\n    .add-btn,\n    .add-toggle-btn {\n      width: 100%;\n    }\n\n    .gastos-lista {\n      gap: 6px;\n    }\n\n    .gasto-row {\n      grid-template-columns: 1fr auto;\n      gap: 8px;\n      padding: 8px 9px;\n      border-radius: 13px;\n      min-height: 48px;\n    }\n\n    .gasto-concepto {\n      font-size: 0.9rem;\n    }\n\n    .gasto-linea {\n      font-size: 0.7rem;\n      margin-top: 2px;\n    }\n\n    .badge {\n      font-size: 0.6rem;\n      padding: 3px 6px;\n    }\n\n    .gasto-side {\n      gap: 5px;\n    }\n\n    .row-chevron {\n      font-size: 1rem;\n    }\n\n    .gasto-modal {\n      padding: 10px;\n      align-items: flex-end;\n    }\n\n    .gasto-modal-card {\n      max-height: calc(100vh - 20px);\n      border-radius: 18px;\n    }\n\n    .ficha-head {\n      padding: 13px 13px 11px;\n    }\n\n    .ficha-title {\n      font-size: 1rem;\n    }\n\n    .ficha-body {\n      padding: 12px;\n    }\n\n    .ficha-grid {\n      grid-template-columns: 1fr 1fr;\n      gap: 9px;\n    }\n\n    .ficha-field.full {\n      grid-column: 1 \/ -1;\n    }\n\n    .btn-close,\n    .btn-danger {\n      width: 100%;\n    }\n  }\n\n  @media (max-width: 380px) {\n    .resumen-card h3 {\n      font-size: 0.52rem;\n    }\n\n    .resumen-card span {\n      font-size: 0.76rem;\n    }\n\n    .gastos-toolbar {\n      grid-template-columns: 1fr;\n    }\n\n    .badge {\n      display: none;\n    }\n\n    .ficha-grid {\n      grid-template-columns: 1fr;\n    }\n  }\n<\/style>\n\n<div class=\"gastos-container\">\n  <div class=\"gastos-head\">\n    <h2 class=\"gastos-title\">\ud83d\udcb0 Control de Gastos<\/h2>\n    <p class=\"gastos-subtitle\">Controla presupuesto, pagos realizados y cantidades pendientes.<\/p>\n  <\/div>\n\n  <div class=\"resumen-dashboard\">\n    <div class=\"resumen-card total active\" onclick=\"filtrarGastos('todos')\">\n      <h3>Total<\/h3>\n      <span id=\"totalPrecio\">0\u20ac<\/span>\n    <\/div>\n    <div class=\"resumen-card pagado\" onclick=\"filtrarGastos('pagados')\">\n      <h3>Pagado<\/h3>\n      <span id=\"totalPagado\">0\u20ac<\/span>\n    <\/div>\n    <div class=\"resumen-card restante\" onclick=\"filtrarGastos('pendientes')\">\n      <h3>Restante<\/h3>\n      <span id=\"totalRestante\">0\u20ac<\/span>\n    <\/div>\n  <\/div>\n\n  <div class=\"progress-block\">\n    <div class=\"progress-label\">\n      <span>Presupuesto pagado<\/span>\n      <span id=\"progressText\">0%<\/span>\n    <\/div>\n    <div class=\"progress-bar-container\">\n      <div class=\"progress-bar\" id=\"progressBar\"><\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"add-gasto-panel\">\n    <button class=\"add-toggle-btn\" onclick=\"toggleFormularioGasto()\">\u2795 A\u00f1adir gasto<\/button>\n\n    <div class=\"add-gasto\" id=\"addGastoForm\">\n      <input type=\"text\" id=\"nuevoGasto\" placeholder=\"Concepto del gasto...\">\n      <input type=\"number\" id=\"nuevoPrecio\" placeholder=\"Precio total (\u20ac)\" min=\"0\" step=\"0.01\">\n      <input type=\"number\" id=\"nuevoPagado\" placeholder=\"Pagado (\u20ac)\" min=\"0\" step=\"0.01\">\n      <button class=\"add-btn\" onclick=\"agregarGasto()\">Guardar gasto<\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"gastos-toolbar\">\n    <div class=\"buscar-wrap\">\n      <input type=\"text\" id=\"buscarGasto\" class=\"buscar-gasto\" placeholder=\"Buscar gasto...\" oninput=\"actualizarBusqueda(this.value)\">\n    <\/div>\n\n    <select id=\"ordenGastos\" class=\"ordenar-gasto\" onchange=\"actualizarOrden(this.value)\">\n      <option value=\"concepto\">Concepto<\/option>\n      <option value=\"restante\">Mayor restante<\/option>\n      <option value=\"precio\">Mayor importe<\/option>\n      <option value=\"pagado\">Mayor pagado<\/option>\n    <\/select>\n\n    <button class=\"export-btn\" onclick=\"exportarGastos()\">\ud83d\udce4 Exportar<\/button>\n  <\/div>\n\n  <div id=\"listaGastos\" class=\"gastos-lista\"><\/div>\n<\/div>\n\n<div class=\"gasto-modal\" id=\"gastoModal\" onclick=\"cerrarFichaDesdeFondo(event)\">\n  <div class=\"gasto-modal-card\" role=\"dialog\" aria-modal=\"true\">\n    <div class=\"ficha-head\">\n      <div>\n        <h3 class=\"ficha-title\" id=\"fichaTitulo\">Editar gasto<\/h3>\n        <p class=\"ficha-subtitle\">Los cambios se guardan autom\u00e1ticamente.<\/p>\n      <\/div>\n      <button class=\"ficha-x\" onclick=\"cerrarFicha()\">\u2715<\/button>\n    <\/div>\n\n    <div class=\"ficha-body\">\n      <div class=\"ficha-grid\">\n        <div class=\"ficha-field full\">\n          <label>Concepto<\/label>\n          <input type=\"text\" id=\"fichaConcepto\" class=\"ficha-input\" oninput=\"actualizarTextoFicha('concepto', this.value)\">\n        <\/div>\n\n        <div class=\"ficha-field\">\n          <label>Precio (\u20ac)<\/label>\n          <input type=\"number\" id=\"fichaPrecio\" class=\"ficha-input\" min=\"0\" step=\"0.01\" oninput=\"actualizarNumeroFicha('precio', this.value)\">\n        <\/div>\n\n        <div class=\"ficha-field\">\n          <label>Pagado (\u20ac)<\/label>\n          <input type=\"number\" id=\"fichaPagado\" class=\"ficha-input\" min=\"0\" step=\"0.01\" oninput=\"actualizarNumeroFicha('pagado', this.value)\">\n        <\/div>\n      <\/div>\n\n      <div class=\"ficha-total-line\" id=\"fichaBalance\">Pendiente 0\u20ac<\/div>\n\n      <div class=\"ficha-actions\">\n        <button class=\"btn-close\" onclick=\"cerrarFicha()\">Cerrar ficha<\/button>\n      <\/div>\n\n      <div class=\"ficha-danger-zone\">\n        <button class=\"btn-danger\" onclick=\"eliminarGastoEditando()\">Eliminar gasto<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\nconst JSON_URL = 'https:\/\/bodafranyjose.com\/wp-content\/uploads\/gastos\/gastos.json';\nconst GUARDAR_URL = 'https:\/\/bodafranyjose.com\/wp-content\/uploads\/gastos\/guardar_gastos.php?key=bodafest2025KEY';\n\nlet gastos = [];\nlet busquedaActiva = '';\nlet filtroActivo = 'todos';\nlet ordenActivo = 'concepto';\nlet guardadoTimer = null;\nlet gastoEditando = null;\n\nfunction escaparHTML(valor) {\n  return String(valor || \"\")\n    .replace(\/&\/g, \"&amp;\")\n    .replace(\/<\/g, \"&lt;\")\n    .replace(\/>\/g, \"&gt;\")\n    .replace(\/\"\/g, \"&quot;\")\n    .replace(\/'\/g, \"&#039;\");\n}\n\nfunction normalizarTexto(valor) {\n  return String(valor || \"\")\n    .normalize(\"NFD\")\n    .replace(\/[\\u0300-\\u036f]\/g, \"\")\n    .trim()\n    .toLowerCase();\n}\n\nfunction limpiarCSV(valor) {\n  return String(valor || \"\")\n    .replace(\/\"\/g, '\"\"')\n    .replace(\/\\n\/g, \" \")\n    .replace(\/\\r\/g, \" \");\n}\n\nfunction formatearEuros(valor) {\n  return (Number(valor) || 0).toLocaleString(\"es-ES\", {\n    minimumFractionDigits: 0,\n    maximumFractionDigits: 2\n  }) + \"\u20ac\";\n}\n\nfunction restanteGasto(gasto) {\n  return (Number(gasto.precio) || 0) - (Number(gasto.pagado) || 0);\n}\n\nfunction estaPagado(gasto) {\n  const precio = Number(gasto.precio) || 0;\n  const pagado = Number(gasto.pagado) || 0;\n  return precio > 0 && pagado >= precio;\n}\n\nfunction programarGuardado() {\n  if (guardadoTimer) {\n    clearTimeout(guardadoTimer);\n  }\n\n  guardadoTimer = setTimeout(function () {\n    guardarCambios(true);\n  }, 650);\n}\n\nasync function cargarDatos() {\n  try {\n    const res = await fetch(JSON_URL + '?nocache=' + Date.now());\n    gastos = await res.json();\n\n    if (!Array.isArray(gastos)) {\n      gastos = [];\n    }\n\n    gastos = gastos.map(function (gasto) {\n      return Object.assign({}, gasto, {\n        concepto: gasto.concepto || \"\",\n        precio: Number(gasto.precio) || 0,\n        pagado: Number(gasto.pagado) || 0\n      });\n    });\n  } catch (e) {\n    console.error(\"Error cargando gastos:\", e);\n    gastos = [];\n  }\n\n  renderGastos();\n}\n\nfunction ordenarLista(lista) {\n  return lista.slice().sort(function (a, b) {\n    if (ordenActivo === 'restante') {\n      return restanteGasto(b) - restanteGasto(a);\n    }\n\n    if (ordenActivo === 'precio') {\n      return (Number(b.precio) || 0) - (Number(a.precio) || 0);\n    }\n\n    if (ordenActivo === 'pagado') {\n      return (Number(b.pagado) || 0) - (Number(a.pagado) || 0);\n    }\n\n    return String(a.concepto || \"\").localeCompare(String(b.concepto || \"\"), \"es\");\n  });\n}\n\nfunction renderGastos() {\n  const contenedor = document.getElementById(\"listaGastos\");\n  contenedor.innerHTML = \"\";\n\n  const termino = normalizarTexto(busquedaActiva);\n\n  let lista = gastos.map(function (g, idx) {\n    return Object.assign({}, g, { index: idx });\n  }).filter(function (g) {\n    if (filtroActivo === 'pagados' && !estaPagado(g)) return false;\n    if (filtroActivo === 'pendientes' && estaPagado(g)) return false;\n\n    if (!termino) return true;\n\n    return normalizarTexto(g.concepto).includes(termino);\n  });\n\n  lista = ordenarLista(lista);\n  actualizarTotales();\n\n  if (lista.length === 0) {\n    contenedor.innerHTML = '<p class=\"sin-resultados\">Sin gastos en este filtro.<\/p>';\n    return;\n  }\n\n  lista.forEach(function (g) {\n    const i = g.index;\n    const precio = Number(g.precio) || 0;\n    const pagado = Number(g.pagado) || 0;\n    const concepto = g.concepto || \"\";\n    const restante = precio - pagado;\n    const pagadoCompleto = estaPagado(g);\n\n    const estadoBadge = pagadoCompleto\n      ? '<span class=\"badge ok\">Pagado<\/span>'\n      : `<span class=\"badge warn\">${formatearEuros(restante)}<\/span>`;\n\n    const item = document.createElement(\"button\");\n    item.type = \"button\";\n    item.className = \"gasto-row \" + (pagadoCompleto ? \"pagado-completo\" : \"pendiente\");\n    item.onclick = function () {\n      abrirFicha(i);\n    };\n\n    item.innerHTML = `\n      <div class=\"gasto-info\">\n        <span class=\"gasto-concepto\">${escaparHTML(concepto)}<\/span>\n        <span class=\"gasto-linea\">${formatearEuros(pagado)} pagado de ${formatearEuros(precio)}<\/span>\n      <\/div>\n\n      <div class=\"gasto-side\">\n        ${estadoBadge}\n        <span class=\"row-chevron\">\u203a<\/span>\n      <\/div>\n    `;\n\n    contenedor.appendChild(item);\n  });\n}\n\nfunction abrirFicha(i) {\n  if (!gastos[i]) {\n    return;\n  }\n\n  gastoEditando = i;\n\n  const gasto = gastos[i];\n  const modal = document.getElementById(\"gastoModal\");\n  const titulo = document.getElementById(\"fichaTitulo\");\n  const concepto = document.getElementById(\"fichaConcepto\");\n  const precio = document.getElementById(\"fichaPrecio\");\n  const pagado = document.getElementById(\"fichaPagado\");\n\n  titulo.textContent = gasto.concepto || \"Editar gasto\";\n  concepto.value = gasto.concepto || \"\";\n  precio.value = Number(gasto.precio) || 0;\n  pagado.value = Number(gasto.pagado) || 0;\n\n  actualizarBalanceFicha();\n\n  if (modal) {\n    modal.classList.add(\"active\");\n    document.body.style.overflow = \"hidden\";\n  }\n}\n\nfunction cerrarFicha() {\n  const modal = document.getElementById(\"gastoModal\");\n\n  if (modal) {\n    modal.classList.remove(\"active\");\n  }\n\n  document.body.style.overflow = \"\";\n  gastoEditando = null;\n  renderGastos();\n}\n\nfunction cerrarFichaDesdeFondo(event) {\n  if (event.target && event.target.id === \"gastoModal\") {\n    cerrarFicha();\n  }\n}\n\nfunction actualizarBalanceFicha() {\n  const balance = document.getElementById(\"fichaBalance\");\n\n  if (gastoEditando === null || !gastos[gastoEditando] || !balance) {\n    return;\n  }\n\n  const gasto = gastos[gastoEditando];\n  const precio = Number(gasto.precio) || 0;\n  const pagado = Number(gasto.pagado) || 0;\n  const restante = precio - pagado;\n\n  balance.classList.remove(\"ok\", \"warn\");\n\n  if (estaPagado(gasto)) {\n    balance.textContent = \"Pagado completo \u00b7 \" + formatearEuros(pagado) + \" de \" + formatearEuros(precio);\n    balance.classList.add(\"ok\");\n  } else {\n    balance.textContent = \"Pendiente \" + formatearEuros(restante) + \" \u00b7 \" + formatearEuros(pagado) + \" pagado de \" + formatearEuros(precio);\n    balance.classList.add(\"warn\");\n  }\n}\n\nfunction actualizarTextoFicha(campo, valor) {\n  if (gastoEditando === null || !gastos[gastoEditando]) {\n    return;\n  }\n\n  gastos[gastoEditando][campo] = valor;\n\n  if (campo === \"concepto\") {\n    const titulo = document.getElementById(\"fichaTitulo\");\n    if (titulo) {\n      titulo.textContent = valor || \"Editar gasto\";\n    }\n  }\n\n  actualizarTotales();\n  actualizarBalanceFicha();\n  programarGuardado();\n}\n\nfunction actualizarNumeroFicha(campo, valor) {\n  if (gastoEditando === null || !gastos[gastoEditando]) {\n    return;\n  }\n\n  gastos[gastoEditando][campo] = parseFloat(valor) || 0;\n  actualizarTotales();\n  actualizarBalanceFicha();\n  programarGuardado();\n}\n\nasync function eliminarGastoEditando() {\n  if (gastoEditando === null || !gastos[gastoEditando]) {\n    return;\n  }\n\n  await eliminarGasto(gastoEditando);\n  cerrarFicha();\n}\n\nfunction toggleFormularioGasto() {\n  const form = document.getElementById(\"addGastoForm\");\n\n  if (!form) return;\n\n  form.classList.toggle(\"active\");\n\n  if (form.classList.contains(\"active\")) {\n    const input = document.getElementById(\"nuevoGasto\");\n\n    if (input) {\n      setTimeout(function () {\n        input.focus();\n      }, 100);\n    }\n  }\n}\n\nasync function agregarGasto() {\n  const conceptoInput = document.getElementById(\"nuevoGasto\");\n  const precioInput = document.getElementById(\"nuevoPrecio\");\n  const pagadoInput = document.getElementById(\"nuevoPagado\");\n\n  const concepto = conceptoInput.value.trim();\n  const precio = parseFloat(precioInput.value) || 0;\n  const pagado = parseFloat(pagadoInput.value) || 0;\n\n  if (!concepto) {\n    alert(\"\u26a0\ufe0f Escribe el concepto del gasto antes de a\u00f1adir.\");\n    return;\n  }\n\n  gastos.push({\n    concepto,\n    precio,\n    pagado\n  });\n\n  conceptoInput.value = \"\";\n  precioInput.value = \"\";\n  pagadoInput.value = \"\";\n\n  const form = document.getElementById(\"addGastoForm\");\n\n  if (form) {\n    form.classList.remove(\"active\");\n  }\n\n  renderGastos();\n  await guardarCambios(true);\n}\n\nfunction actualizarBusqueda(valor) {\n  busquedaActiva = valor;\n  renderGastos();\n}\n\nfunction actualizarOrden(valor) {\n  ordenActivo = valor;\n  renderGastos();\n}\n\nfunction filtrarGastos(tipo) {\n  filtroActivo = tipo;\n\n  document.querySelectorAll('.resumen-card').forEach(function (card) {\n    card.classList.remove('active');\n  });\n\n  let selector = '.resumen-card.total';\n\n  if (tipo === 'pagados') {\n    selector = '.resumen-card.pagado';\n  } else if (tipo === 'pendientes') {\n    selector = '.resumen-card.restante';\n  }\n\n  const card = document.querySelector(selector);\n\n  if (card) {\n    card.classList.add('active');\n  }\n\n  renderGastos();\n}\n\nfunction actualizarTotales() {\n  let total = 0;\n  let pagado = 0;\n\n  gastos.forEach(function (g) {\n    total += Number(g.precio) || 0;\n    pagado += Number(g.pagado) || 0;\n  });\n\n  const restante = total - pagado;\n  const porcentaje = total > 0 ? Math.min((pagado \/ total) * 100, 100) : 0;\n\n  document.getElementById(\"totalPrecio\").textContent = formatearEuros(total);\n  document.getElementById(\"totalPagado\").textContent = formatearEuros(pagado);\n  document.getElementById(\"totalRestante\").textContent = formatearEuros(restante);\n  document.getElementById(\"progressBar\").style.width = porcentaje + \"%\";\n\n  const progressText = document.getElementById(\"progressText\");\n  if (progressText) {\n    progressText.textContent = Math.round(porcentaje) + \"%\";\n  }\n}\n\nasync function eliminarGasto(i) {\n  if (!gastos[i]) {\n    return;\n  }\n\n  const concepto = gastos[i].concepto || \"este gasto\";\n\n  if (confirm(`\u00bfEliminar \"${concepto}\"?`)) {\n    gastos.splice(i, 1);\n    renderGastos();\n    await guardarCambios(true);\n  }\n}\n\nasync function guardarCambios(silencioso = false) {\n  try {\n    const res = await fetch(GUARDAR_URL, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify(gastos)\n    });\n\n    if (!res.ok) {\n      throw new Error(\"Error HTTP \" + res.status);\n    }\n\n    actualizarTotales();\n\n    if (!silencioso) {\n      alert(\"\u2705 Gastos guardados correctamente\");\n    }\n  } catch (e) {\n    console.error(\"\u274c Error al guardar gastos:\", e);\n\n    if (!silencioso) {\n      alert(\"\u274c No se pudieron guardar los gastos\");\n    }\n  }\n}\n\nfunction exportarGastos() {\n  const filas = [];\n\n  filas.push([\n    \"Concepto\",\n    \"Precio\",\n    \"Pagado\",\n    \"Restante\",\n    \"Estado\"\n  ]);\n\n  const listaOrdenada = gastos\n    .slice()\n    .sort(function (a, b) {\n      return String(a.concepto || \"\").localeCompare(String(b.concepto || \"\"), \"es\");\n    });\n\n  listaOrdenada.forEach(function (g) {\n    const precio = Number(g.precio) || 0;\n    const pagado = Number(g.pagado) || 0;\n    const restante = precio - pagado;\n\n    filas.push([\n      g.concepto || \"\",\n      precio,\n      pagado,\n      restante,\n      estaPagado(g) ? \"Pagado\" : \"Pendiente\"\n    ]);\n  });\n\n  const csv = filas\n    .map(function (fila) {\n      return fila.map(function (celda) {\n        return `\"${limpiarCSV(celda)}\"`;\n      }).join(\";\");\n    })\n    .join(\"\\n\");\n\n  const blob = new Blob([\"\\uFEFF\" + csv], {\n    type: \"text\/csv;charset=utf-8;\"\n  });\n\n  const url = URL.createObjectURL(blob);\n  const enlace = document.createElement(\"a\");\n\n  enlace.href = url;\n  enlace.download = \"gastos-boda-fran-y-jose.csv\";\n  enlace.click();\n\n  URL.revokeObjectURL(url);\n}\n\ncargarDatos();\n\nwindow.guardarCambios = guardarCambios;\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>\ud83d\udcb0 Control de Gastos Controla presupuesto, pagos realizados y cantidades pendientes. Total 0\u20ac Pagado 0\u20ac Restante 0\u20ac Presupuesto pagado 0% \u2795 A\u00f1adir gasto Guardar gasto ConceptoMayor restanteMayor importeMayor pagado \ud83d\udce4 Exportar Editar gasto Los cambios se guardan autom\u00e1ticamente. \u2715 Concepto Precio (\u20ac) Pagado (\u20ac) Pendiente 0\u20ac Cerrar ficha Eliminar gasto<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-543","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Gastos -<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Gastos -\" \/>\n<meta property=\"og:description\" content=\"\ud83d\udcb0 Control de Gastos Controla presupuesto, pagos realizados y cantidades pendientes. Total 0\u20ac Pagado 0\u20ac Restante 0\u20ac Presupuesto pagado 0% \u2795 A\u00f1adir gasto Guardar gasto ConceptoMayor restanteMayor importeMayor pagado \ud83d\udce4 Exportar Editar gasto Los cambios se guardan autom\u00e1ticamente. \u2715 Concepto Precio (\u20ac) Pagado (\u20ac) Pendiente 0\u20ac Cerrar ficha Eliminar gasto\" \/>\n<meta property=\"og:url\" content=\"https:\/\/bodafranyjose.com\/index.php\/gastos\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-13T17:05:50+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"13 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/gastos\\\/\",\"url\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/gastos\\\/\",\"name\":\"Gastos -\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/#website\"},\"datePublished\":\"2025-11-16T14:37:21+00:00\",\"dateModified\":\"2026-04-13T17:05:50+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/gastos\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/gastos\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/gastos\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Portada\",\"item\":\"https:\\\/\\\/bodafranyjose.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Gastos\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/#website\",\"url\":\"https:\\\/\\\/bodafranyjose.com\\\/\",\"name\":\"\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/bodafranyjose.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Gastos -","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"es_ES","og_type":"article","og_title":"Gastos -","og_description":"\ud83d\udcb0 Control de Gastos Controla presupuesto, pagos realizados y cantidades pendientes. Total 0\u20ac Pagado 0\u20ac Restante 0\u20ac Presupuesto pagado 0% \u2795 A\u00f1adir gasto Guardar gasto ConceptoMayor restanteMayor importeMayor pagado \ud83d\udce4 Exportar Editar gasto Los cambios se guardan autom\u00e1ticamente. \u2715 Concepto Precio (\u20ac) Pagado (\u20ac) Pendiente 0\u20ac Cerrar ficha Eliminar gasto","og_url":"https:\/\/bodafranyjose.com\/index.php\/gastos\/","article_modified_time":"2026-04-13T17:05:50+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"13 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/bodafranyjose.com\/index.php\/gastos\/","url":"https:\/\/bodafranyjose.com\/index.php\/gastos\/","name":"Gastos -","isPartOf":{"@id":"https:\/\/bodafranyjose.com\/#website"},"datePublished":"2025-11-16T14:37:21+00:00","dateModified":"2026-04-13T17:05:50+00:00","breadcrumb":{"@id":"https:\/\/bodafranyjose.com\/index.php\/gastos\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/bodafranyjose.com\/index.php\/gastos\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/bodafranyjose.com\/index.php\/gastos\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Portada","item":"https:\/\/bodafranyjose.com\/"},{"@type":"ListItem","position":2,"name":"Gastos"}]},{"@type":"WebSite","@id":"https:\/\/bodafranyjose.com\/#website","url":"https:\/\/bodafranyjose.com\/","name":"","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/bodafranyjose.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/543","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/comments?post=543"}],"version-history":[{"count":115,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/543\/revisions"}],"predecessor-version":[{"id":997,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/543\/revisions\/997"}],"wp:attachment":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/media?parent=543"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}