{"id":8,"date":"2026-05-26T16:35:42","date_gmt":"2026-05-26T16:35:42","guid":{"rendered":"https:\/\/app.micaldo.es\/?page_id=8"},"modified":"2026-05-26T18:30:23","modified_gmt":"2026-05-26T18:30:23","slug":"kpis-ventas","status":"publish","type":"page","link":"https:\/\/app.micaldo.es\/","title":{"rendered":"KPIs Ventas"},"content":{"rendered":"\n<style data-wp-block-html=\"css\">\n<style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f4f5f7; color: #1a1a1a; font-size: 15px; }\n \n    header { background: #fff; border-bottom: 1px solid #e2e4e8; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 10; }\n    header h1 { font-size: 17px; font-weight: 600; color: #1a1a1a; display: flex; align-items: center; gap: 8px; }\n    header h1 span { color: #e05c2e; }\n    #status-txt { font-size: 12px; color: #888; }\n \n    .container { max-width: 1200px; margin: 0 auto; padding: 24px; }\n \n    .setup-card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 20px 24px; margin-bottom: 24px; }\n    .setup-card h2 { font-size: 14px; font-weight: 600; margin-bottom: 14px; color: #444; }\n    .setup-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }\n    .setup-row label { font-size: 12px; color: #666; display: block; margin-bottom: 4px; }\n    .setup-row select, .setup-row input[type=\"date\"] { font-size: 13px; border: 1px solid #ddd; border-radius: 6px; padding: 7px 10px; background: #fff; color: #1a1a1a; height: 36px; }\n    #custom-dates { display: none; gap: 10px; flex-wrap: wrap; }\n    .btn { background: #e05c2e; color: #fff; border: none; border-radius: 6px; padding: 0 18px; height: 36px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: background 0.15s; }\n    .btn:hover { background: #c44d22; }\n    .btn-outline { background: #fff; color: #444; border: 1px solid #ddd; border-radius: 6px; padding: 0 14px; height: 32px; font-size: 12px; cursor: pointer; transition: background 0.15s; }\n    .btn-outline:hover { background: #f4f5f7; }\n    .err { background: #fff0ed; color: #b94040; border: 1px solid #fbc9b8; border-radius: 6px; padding: 10px 14px; font-size: 13px; margin-top: 10px; display: none; }\n \n    .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }\n    .kpi-card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 16px 18px; }\n    .kpi-label { font-size: 12px; color: #888; margin-bottom: 6px; }\n    .kpi-value { font-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.1; }\n    .kpi-sub { font-size: 11px; color: #aaa; margin-top: 5px; }\n    .kpi-card.green .kpi-value { color: #2d7a3a; }\n    .kpi-card.red .kpi-value { color: #b94040; }\n    .kpi-card.orange .kpi-value { color: #c47a1a; }\n \n    .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }\n    .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 16px; }\n    @media (max-width: 800px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }\n \n    .card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 20px 20px; }\n    .card-title { font-size: 13px; font-weight: 600; color: #444; margin-bottom: 14px; }\n \n    .top-list { list-style: none; }\n    .top-list li { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px solid #f0f0f0; font-size: 13px; }\n    .top-list li:last-child { border-bottom: none; }\n    .top-list .nm { color: #1a1a1a; max-width: 65%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    .top-list .am { font-weight: 600; color: #1a1a1a; }\n    .rank { display: inline-block; width: 18px; color: #aaa; font-size: 11px; }\n \n    table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }\n    th { text-align: left; color: #888; font-weight: 600; font-size: 11px; padding: 0 8px 10px; border-bottom: 1px solid #f0f0f0; text-transform: uppercase; letter-spacing: 0.04em; }\n    td { padding: 8px 8px; border-bottom: 1px solid #f5f5f5; color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n    tr:last-child td { border-bottom: none; }\n \n    .badge { display: inline-block; padding: 3px 9px; border-radius: 99px; font-size: 11px; font-weight: 600; }\n    .b-warn { background: #fff4e0; color: #a06800; }\n    .b-danger { background: #fff0ed; color: #b94040; }\n    .b-success { background: #edfaf1; color: #2d7a3a; }\n \n    .loading { text-align: center; padding: 48px; color: #888; font-size: 14px; display: none; }\n    .spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #e05c2e; border-radius: 50%; animation: spin 0.7s linear infinite; margin-right: 8px; vertical-align: middle; }\n    @keyframes spin { to { transform: rotate(360deg); } }\n \n    #dashboard { display: none; }\n \n    .donut-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; font-size: 12px; color: #666; }\n    .donut-legend span { display: flex; align-items: center; gap: 5px; }\n    .dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }\n \n    .period-badge { background: #f0f0f0; border-radius: 99px; padding: 3px 12px; font-size: 12px; color: #666; }\n  <\/style>\n<\/style>\n\n<style>\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f4f5f7; color: #1a1a1a; font-size: 15px; }\nheader { background: #fff; border-bottom: 1px solid #e2e4e8; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 10; }\nheader h1 { font-size: 17px; font-weight: 600; color: #1a1a1a; display: flex; align-items: center; gap: 8px; }\nheader h1 span { color: #e05c2e; }\n#status-txt { font-size: 12px; color: #888; }\n.container { max-width: 1200px; margin: 0 auto; padding: 24px; }\n.setup-card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 20px 24px; margin-bottom: 24px; }\n.setup-card h2 { font-size: 14px; font-weight: 600; margin-bottom: 14px; color: #444; }\n.setup-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }\n.setup-row label { font-size: 12px; color: #666; display: block; margin-bottom: 4px; }\n.setup-row select, .setup-row input[type=\"date\"] { font-size: 13px; border: 1px solid #ddd; border-radius: 6px; padding: 7px 10px; background: #fff; color: #1a1a1a; height: 36px; }\n.setup-row select.wide { min-width: 220px; max-width: 320px; }\n#custom-dates { display: none; gap: 10px; flex-wrap: wrap; }\n.btn { background: #e05c2e; color: #fff; border: none; border-radius: 6px; padding: 0 18px; height: 36px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; }\n.btn:hover { background: #c44d22; }\n.btn-outline { background: #fff; color: #444; border: 1px solid #ddd; border-radius: 6px; padding: 0 14px; height: 32px; font-size: 12px; cursor: pointer; }\n.btn-outline:hover { background: #f4f5f7; }\n.btn-ghost { background: transparent; color: #888; border: 1px solid #e2e4e8; border-radius: 6px; padding: 0 12px; height: 36px; font-size: 12px; cursor: pointer; }\n.btn-ghost:hover { background: #f4f5f7; }\n.err { background: #fff0ed; color: #b94040; border: 1px solid #fbc9b8; border-radius: 6px; padding: 10px 14px; font-size: 13px; margin-top: 10px; display: none; }\n.filter-tag { display: inline-flex; align-items: center; gap: 6px; background: #fff4e0; color: #a06800; border: 1px solid #f0d080; border-radius: 99px; padding: 4px 12px; font-size: 12px; font-weight: 600; margin-top: 10px; }\n.filter-tag button { background: none; border: none; cursor: pointer; color: #a06800; font-size: 14px; line-height: 1; padding: 0; }\n.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px; }\n.kpi-card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 16px 18px; }\n.kpi-label { font-size: 12px; color: #888; margin-bottom: 6px; }\n.kpi-value { font-size: 22px; font-weight: 700; color: #1a1a1a; line-height: 1.1; }\n.kpi-sub { font-size: 11px; color: #aaa; margin-top: 5px; }\n.kpi-card.green .kpi-value { color: #2d7a3a; }\n.kpi-card.red .kpi-value { color: #b94040; }\n.kpi-card.orange .kpi-value { color: #c47a1a; }\n.kpi-card.blue .kpi-value { color: #1a6ab9; }\n.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }\n@media (max-width: 800px) { .grid-2 { grid-template-columns: 1fr; } }\n.card { background: #fff; border: 1px solid #e2e4e8; border-radius: 10px; padding: 20px; }\n.card-title { font-size: 13px; font-weight: 600; color: #444; margin-bottom: 14px; }\n.top-list { list-style: none; }\n.top-list li { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px solid #f0f0f0; font-size: 13px; gap: 8px; cursor: pointer; }\n.top-list li:last-child { border-bottom: none; }\n.top-list li:hover { background: #f9f9f9; margin: 0 -8px; padding: 7px 8px; border-radius: 6px; }\n.top-list .nm { color: #1a1a1a; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.top-list .am { font-weight: 600; color: #1a1a1a; white-space: nowrap; }\n.rank { display: inline-block; width: 18px; color: #aaa; font-size: 11px; flex-shrink: 0; }\n.bar-wrap { flex: 1; height: 6px; background: #f0f0f0; border-radius: 3px; margin: 0 8px; min-width: 40px; }\n.bar-fill { height: 6px; border-radius: 3px; background: #e05c2e; }\n.arrow { color: #ccc; font-size: 14px; flex-shrink: 0; }\ntable { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }\nth { text-align: left; color: #888; font-weight: 600; font-size: 11px; padding: 0 8px 10px; border-bottom: 1px solid #f0f0f0; text-transform: uppercase; letter-spacing: 0.04em; }\ntd { padding: 8px 8px; border-bottom: 1px solid #f5f5f5; color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\ntr:last-child td { border-bottom: none; }\n.badge { display: inline-block; padding: 3px 9px; border-radius: 99px; font-size: 11px; font-weight: 600; }\n.b-warn { background: #fff4e0; color: #a06800; }\n.b-danger { background: #fff0ed; color: #b94040; }\n.b-ok { background: #e8f5eb; color: #2d7a3a; }\n.loading { text-align: center; padding: 48px; color: #888; font-size: 14px; display: none; }\n.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #ddd; border-top-color: #e05c2e; border-radius: 50%; animation: spin 0.7s linear infinite; margin-right: 8px; vertical-align: middle; }\n@keyframes spin { to { transform: rotate(360deg); } }\n#dashboard { display: none; }\n.donut-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; font-size: 12px; color: #666; }\n.donut-legend span { display: flex; align-items: center; gap: 5px; }\n.dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }\n\n\/* MODAL *\/\n.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: flex-start; padding: 32px 16px; overflow-y: auto; }\n.modal-overlay.open { display: flex; }\n.modal { background: #fff; border-radius: 14px; width: 100%; max-width: 860px; margin: auto; box-shadow: 0 24px 60px rgba(0,0,0,0.25); }\n.modal-header { padding: 20px 24px; border-bottom: 1px solid #e2e4e8; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }\n.modal-header h2 { font-size: 16px; font-weight: 700; margin-bottom: 3px; }\n.modal-header .sub { font-size: 12px; color: #888; }\n.modal-close { background: #f0f0f0; border: none; border-radius: 8px; width: 32px; height: 32px; font-size: 18px; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }\n.modal-close:hover { background: #e0e0e0; }\n.modal-body { padding: 20px 24px 28px; }\n.m-kpis { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; margin-bottom: 20px; }\n.m-kpi { background: #f8f9fa; border-radius: 8px; padding: 12px 14px; }\n.m-kpi .lbl { font-size: 11px; color: #888; margin-bottom: 4px; }\n.m-kpi .val { font-size: 18px; font-weight: 700; }\n.m-kpi.green .val { color: #2d7a3a; }\n.m-kpi.orange .val { color: #c47a1a; }\n.m-kpi.blue .val { color: #1a6ab9; }\n.m-section { margin-top: 20px; }\n.m-section-title { font-size: 11px; font-weight: 700; color: #aaa; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 12px; }\n.m-chart { position: relative; height: 170px; margin-bottom: 4px; }\n.m-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.m-table th { text-align: left; color: #888; font-size: 11px; font-weight: 600; padding: 0 8px 8px; border-bottom: 1px solid #eee; text-transform: uppercase; letter-spacing: 0.04em; }\n.m-table td { padding: 8px; border-bottom: 1px solid #f5f5f5; }\n.m-table tr:last-child td { border-bottom: none; }\n.m-table .r { text-align: right; font-weight: 600; }\n<\/style>\n\n<header>\n  <h1>\ud83d\udcca KPIs Ventas <span>MiCaldo<\/span><\/h1>\n  <div style=\"display:flex;align-items:center;gap:12px;\">\n    <span id=\"status-txt\"><\/span>\n    <button class=\"btn-outline\" onclick=\"toggleSetup()\">\u2699 Configurar<\/button>\n  <\/div>\n<\/header>\n\n<div class=\"container\">\n  <div class=\"setup-card\" id=\"setup-card\">\n    <h2>Filtros<\/h2>\n    <div class=\"setup-row\">\n      <div>\n        <label>Periodo<\/label>\n        <select id=\"period-select\" onchange=\"onPeriodChange()\">\n          <option value=\"month\">Este mes<\/option>\n          <option value=\"quarter\">Este trimestre<\/option>\n          <option value=\"year\" selected>Este a\u00f1o<\/option>\n          <option value=\"last_year\">A\u00f1o pasado<\/option>\n          <option value=\"custom\">Personalizado<\/option>\n        <\/select>\n      <\/div>\n      <div id=\"custom-dates\" style=\"display:none;gap:10px;flex-wrap:wrap;\">\n        <div><label>Desde<\/label><input type=\"date\" id=\"date-from\"\/><\/div>\n        <div><label>Hasta<\/label><input type=\"date\" id=\"date-to\"\/><\/div>\n      <\/div>\n      <div>\n        <label>Cliente<\/label>\n        <select id=\"client-select\" class=\"wide\">\n          <option value=\"\">\u2014 Todos los clientes \u2014<\/option>\n        <\/select>\n      <\/div>\n      <button class=\"btn\" onclick=\"loadData()\">\u21bb Cargar datos<\/button>\n      <button class=\"btn-ghost\" onclick=\"resetFilters()\">\u2715 Reset<\/button>\n    <\/div>\n    <div id=\"filter-tag-wrap\"><\/div>\n    <div class=\"err\" id=\"err-box\"><\/div>\n  <\/div>\n\n  <div class=\"loading\" id=\"loading-msg\"><span class=\"spinner\"><\/span> Consultando Holded&#8230;<\/div>\n\n  <div id=\"dashboard\">\n    <div class=\"kpi-grid\">\n      <div class=\"kpi-card\"><div class=\"kpi-label\">Facturaci\u00f3n total<\/div><div class=\"kpi-value\" id=\"k-total\">\u2014<\/div><div class=\"kpi-sub\" id=\"k-total-s\">\u2014<\/div><\/div>\n      <div class=\"kpi-card green\"><div class=\"kpi-label\">Cobrado<\/div><div class=\"kpi-value\" id=\"k-paid\">\u2014<\/div><div class=\"kpi-sub\" id=\"k-paid-s\">\u2014<\/div><\/div>\n      <div class=\"kpi-card orange\"><div class=\"kpi-label\">Pendiente cobro<\/div><div class=\"kpi-value\" id=\"k-pending\">\u2014<\/div><div class=\"kpi-sub\" id=\"k-pending-s\">\u2014<\/div><\/div>\n      <div class=\"kpi-card red\"><div class=\"kpi-label\">Vencido sin pagar<\/div><div class=\"kpi-value\" id=\"k-overdue\">\u2014<\/div><div class=\"kpi-sub\" id=\"k-overdue-s\">\u2014<\/div><\/div>\n      <div class=\"kpi-card\"><div class=\"kpi-label\">Ticket medio<\/div><div class=\"kpi-value\" id=\"k-avg\">\u2014<\/div><div class=\"kpi-sub\">por factura<\/div><\/div>\n      <div class=\"kpi-card blue\"><div class=\"kpi-label\">KGs vendidos<\/div><div class=\"kpi-value\" id=\"k-kg\">\u2014<\/div><div class=\"kpi-sub\">unidades totales<\/div><\/div>\n      <div class=\"kpi-card\"><div class=\"kpi-label\">N\u00ba facturas<\/div><div class=\"kpi-value\" id=\"k-count\">\u2014<\/div><div class=\"kpi-sub\" id=\"k-count-s\">\u2014<\/div><\/div>\n    <\/div>\n\n    <div class=\"grid-2\">\n      <div class=\"card\"><div class=\"card-title\">Facturaci\u00f3n mensual<\/div><div style=\"position:relative;height:220px;\"><canvas id=\"c-monthly\"><\/canvas><\/div><\/div>\n      <div class=\"card\"><div class=\"card-title\">Estado de facturas<\/div><div class=\"donut-legend\" id=\"donut-legend\"><\/div><div style=\"position:relative;height:190px;display:flex;align-items:center;justify-content:center;\"><canvas id=\"c-status\"><\/canvas><\/div><\/div>\n    <\/div>\n\n    <div class=\"grid-2\">\n      <div class=\"card\">\n        <div class=\"card-title\">Top clientes \u2014 Facturaci\u00f3n <small style=\"color:#aaa;font-weight:400\">clic = ficha<\/small><\/div>\n        <ul class=\"top-list\" id=\"list-clients\"><\/ul>\n      <\/div>\n      <div class=\"card\">\n        <div class=\"card-title\">Top clientes \u2014 KGs <small style=\"color:#aaa;font-weight:400\">clic = ficha<\/small><\/div>\n        <ul class=\"top-list\" id=\"list-kg\"><\/ul>\n      <\/div>\n    <\/div>\n\n    <div class=\"grid-2\">\n      <div class=\"card\"><div class=\"card-title\">Top productos \/ servicios<\/div><ul class=\"top-list\" id=\"list-products\"><\/ul><\/div>\n      <div class=\"card\"><div class=\"card-title\">Pendientes de cobro<\/div>\n        <table><thead><tr><th style=\"width:40%\">Cliente<\/th><th style=\"width:28%\">Importe<\/th><th style=\"width:32%\">Estado<\/th><\/tr><\/thead><tbody id=\"t-pending\"><\/tbody><\/table>\n      <\/div>\n    <\/div>\n\n    <div class=\"grid-2\">\n      <div class=\"card\"><div class=\"card-title\">Facturaci\u00f3n acumulada<\/div><div style=\"position:relative;height:200px;\"><canvas id=\"c-cumul\"><\/canvas><\/div><\/div>\n      <div class=\"card\"><div class=\"card-title\">KGs estimados por mes<\/div><div style=\"position:relative;height:200px;\"><canvas id=\"c-kg-month\"><\/canvas><\/div><\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- MODAL FICHA CLIENTE -->\n<div class=\"modal-overlay\" id=\"modal-overlay\">\n  <div class=\"modal\" id=\"modal-box\">\n    <div class=\"modal-header\">\n      <div>\n        <h2 id=\"m-name\">\u2014<\/h2>\n        <div class=\"sub\" id=\"m-sub\">\u2014<\/div>\n      <\/div>\n      <button class=\"modal-close\" id=\"modal-close-btn\">\u2715<\/button>\n    <\/div>\n    <div class=\"modal-body\">\n      <div class=\"m-kpis\">\n        <div class=\"m-kpi\"><div class=\"lbl\">Facturaci\u00f3n<\/div><div class=\"val\" id=\"m-total\">\u2014<\/div><\/div>\n        <div class=\"m-kpi green\"><div class=\"lbl\">Cobrado<\/div><div class=\"val\" id=\"m-paid\">\u2014<\/div><\/div>\n        <div class=\"m-kpi orange\"><div class=\"lbl\">Pendiente<\/div><div class=\"val\" id=\"m-pend\">\u2014<\/div><\/div>\n        <div class=\"m-kpi blue\"><div class=\"lbl\">KGs<\/div><div class=\"val\" id=\"m-kg\">\u2014<\/div><\/div>\n      <\/div>\n      <div class=\"m-section\">\n        <div class=\"m-section-title\">Evoluci\u00f3n mensual<\/div>\n        <div class=\"m-chart\"><canvas id=\"c-m-monthly\"><\/canvas><\/div>\n      <\/div>\n      <div class=\"m-section\">\n        <div class=\"m-section-title\">Historial de facturas<\/div>\n        <table class=\"m-table\">\n          <thead><tr>\n            <th style=\"width:80px\">Fecha<\/th>\n            <th style=\"width:100px\">N\u00ba Factura<\/th>\n            <th>Productos<\/th>\n            <th style=\"width:90px;text-align:right\">Importe<\/th>\n            <th style=\"width:85px\">Estado<\/th>\n          <\/tr><\/thead>\n          <tbody id=\"m-inv-body\"><\/tbody>\n        <\/table>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js@4.4.1\/dist\/chart.umd.min.js\"><\/script>\n<script>\n(function() {\n  const WEBHOOK = 'https:\/\/n8n.linktelservices.com\/webhook\/holded-kpi';\n  let cMonthly, cStatus, cCumul, cKgMonth, cMModal;\n  let setupVisible = true;\n  let _invoicesRaw = [];\n\n  \/\/ \u2500\u2500 UTILIDADES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const fmt = n => new Intl.NumberFormat('es-ES',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);\n  const fmtS = n => Math.abs(n)>=1e6?(n\/1e6).toFixed(1)+'M \u20ac':Math.abs(n)>=1000?(n\/1000).toFixed(1)+'k \u20ac':fmt(n);\n  const fmtKg = n => n>=1000?(n\/1000).toFixed(1)+'k kg':n+' kg';\n  const fmtDate = ts => { if(!ts) return '\u2014'; const d=new Date(ts*1000); return d.toLocaleDateString('es-ES',{day:'2-digit',month:'2-digit',year:'2-digit'}); };\n\n  function showErr(msg){const b=document.getElementById('err-box');b.textContent='\u26a0 '+msg;b.style.display='block';}\n  function clearErr(){document.getElementById('err-box').style.display='none';}\n\n  \/\/ \u2500\u2500 CONTROLES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  function onPeriodChange(){\n    document.getElementById('custom-dates').style.display=\n      document.getElementById('period-select').value==='custom'?'flex':'none';\n  }\n  window.onPeriodChange = onPeriodChange;\n\n  function toggleSetup(){\n    setupVisible=!setupVisible;\n    document.getElementById('setup-card').style.display=setupVisible?'block':'none';\n  }\n  window.toggleSetup = toggleSetup;\n\n  function resetFilters(){\n    document.getElementById('period-select').value='year';\n    document.getElementById('client-select').value='';\n    document.getElementById('custom-dates').style.display='none';\n    document.getElementById('filter-tag-wrap').innerHTML='';\n  }\n  window.resetFilters = resetFilters;\n\n  function getTs(){\n    const now=new Date(),y=now.getFullYear(),m=now.getMonth();\n    const p=document.getElementById('period-select').value;\n    let from,to;\n    if(p==='month'){from=new Date(y,m,1,0,0,0);to=new Date(y,m+1,0,23,59,59);}\n    else if(p==='quarter'){const q=Math.floor(m\/3);from=new Date(y,q*3,1,0,0,0);to=new Date(y,q*3+3,0,23,59,59);}\n    else if(p==='year'){from=new Date(y,0,1,0,0,0);to=new Date(y,11,31,23,59,59);}\n    else if(p==='last_year'){from=new Date(y-1,0,1,0,0,0);to=new Date(y-1,11,31,23,59,59);}\n    else{\n      const[fy,fm,fd]=document.getElementById('date-from').value.split('-').map(Number);\n      const[ty,tm,td]=document.getElementById('date-to').value.split('-').map(Number);\n      from=new Date(fy,fm-1,fd,0,0,0);to=new Date(ty,tm-1,td,23,59,59);\n    }\n    return{startp:Math.floor(from.getTime()\/1000),endp:Math.floor(to.getTime()\/1000)};\n  }\n\n  \/\/ \u2500\u2500 CARGA DE DATOS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  async function loadData(){\n    clearErr();\n    const client=document.getElementById('client-select').value;\n    const wrap=document.getElementById('filter-tag-wrap');\n    wrap.innerHTML=client?`<div class=\"filter-tag\">\ud83d\udc64 ${client} <button id=\"rm-client-tag\">\u2715<\/button><\/div>`:'';\n    if(client) document.getElementById('rm-client-tag').addEventListener('click',()=>{\n      document.getElementById('client-select').value='';wrap.innerHTML='';loadData();\n    });\n\n    document.getElementById('loading-msg').style.display='block';\n    document.getElementById('dashboard').style.display='none';\n    const{startp,endp}=getTs();\n    const filterClient=client;\n    try{\n      const res=await fetch(WEBHOOK,{\n        method:'POST',headers:{'Content-Type':'application\/json'},\n        body:JSON.stringify({startp,endp,filterClient})\n      });\n      if(!res.ok) throw new Error('Error HTTP '+res.status);\n      const data=await res.json();\n      if(!data.kpis) throw new Error('Respuesta inesperada de n8n.');\n\n      \/\/ Guardar facturas raw\n      if(data.invoicesRaw) _invoicesRaw=data.invoicesRaw;\n\n      \/\/ Poblar selector clientes\n      if(data.allClients&&data.allClients.length>0){\n        const sel=document.getElementById('client-select');\n        const cur=sel.value;\n        sel.innerHTML='<option value=\"\">\u2014 Todos los clientes \u2014<\/option>';\n        data.allClients.forEach(c=>{\n          const o=document.createElement('option');\n          o.value=c;o.textContent=c;\n          if(c===cur)o.selected=true;\n          sel.appendChild(o);\n        });\n      }\n\n      render(data);\n      document.getElementById('status-txt').textContent='Actualizado: '+new Date().toLocaleTimeString('es-ES',{hour:'2-digit',minute:'2-digit'});\n      document.getElementById('setup-card').style.display='none';\n      setupVisible=false;\n    }catch(e){showErr(e.message);}\n    document.getElementById('loading-msg').style.display='none';\n    document.getElementById('dashboard').style.display='block';\n  }\n  window.loadData=loadData;\n\n  \/\/ \u2500\u2500 MODAL FICHA CLIENTE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  function openClientModal(clientName){\n    const invs=_invoicesRaw.filter(inv=>(inv.contactName||'')===clientName);\n    const now=Math.floor(Date.now()\/1000);\n    let total=0,paid=0,pend=0,kg=0;\n    const monthMap={};\n    const rows=[];\n\n    invs.forEach(inv=>{\n      const amount=parseFloat(inv.total)||0;\n      const isPaid=inv.status===1||(inv.paymentsTotal>0&&inv.paymentsTotal>=amount-0.01&&amount>0);\n      total+=amount;\n      if(isPaid)paid+=amount;else pend+=amount;\n\n      (inv.products||[]).forEach(l=>{\n        const u=parseFloat(l.units)||0;\n        if(u>0&&(parseFloat(l.price)||0)>0)kg+=u;\n      });\n\n      const date=inv.date||0;\n      if(date>0){\n        const d=new Date(date*1000);\n        const mk=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0');\n        monthMap[mk]=(monthMap[mk]||0)+amount;\n      }\n\n      const prods=(inv.products||[]).map(l=>(l.name||l.desc||'').split('\\n')[0].trim()).filter(Boolean);\n      const prodStr=prods.slice(0,2).join(', ')+(prods.length>2?` +${prods.length-2}`:'');\n\n      let badge='';\n      if(isPaid) badge='<span class=\"badge b-ok\">Cobrada<\/span>';\n      else if(inv.status===2) badge='<span class=\"badge b-warn\">Parcial<\/span>';\n      else badge=(inv.dueDate&&inv.dueDate<now)?'<span class=\"badge b-danger\">Vencida<\/span>':'<span class=\"badge b-warn\">Pendiente<\/span>';\n\n      rows.push({date,docNumber:inv.docNumber||(inv.draft?'Borrador':'\u2014'),prodStr,amount,badge});\n    });\n\n    rows.sort((a,b)=>b.date-a.date);\n\n    document.getElementById('m-name').textContent=clientName;\n    document.getElementById('m-sub').textContent=invs.length+' facturas en el per\u00edodo';\n    document.getElementById('m-total').textContent=fmtS(total);\n    document.getElementById('m-paid').textContent=fmtS(paid);\n    document.getElementById('m-pend').textContent=fmtS(pend);\n    document.getElementById('m-kg').textContent=fmtKg(Math.round(kg));\n\n    document.getElementById('m-inv-body').innerHTML=rows.map(r=>\n      `<tr>\n        <td>${fmtDate(r.date)}<\/td>\n        <td style=\"color:#999;font-size:12px\">${r.docNumber}<\/td>\n        <td style=\"font-size:12px;color:#666\">${r.prodStr||'\u2014'}<\/td>\n        <td class=\"r\">${fmt(r.amount)}<\/td>\n        <td>${r.badge}<\/td>\n      <\/tr>`\n    ).join('')||'<tr><td colspan=\"5\" style=\"color:#aaa;text-align:center;padding:16px\">Sin facturas<\/td><\/tr>';\n\n    const months=Object.keys(monthMap).sort();\n    const amounts=months.map(mk=>monthMap[mk]);\n    if(cMModal)cMModal.destroy();\n    cMModal=new Chart(document.getElementById('c-m-monthly'),{\n      type:'bar',\n      data:{labels:months,datasets:[{label:'\u20ac',data:amounts,backgroundColor:'#e05c2e',borderRadius:4}]},\n      options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},\n        scales:{y:{ticks:{callback:v=>fmtS(v),font:{size:11}},grid:{color:'rgba(0,0,0,0.05)'}},\n                x:{ticks:{font:{size:10}}}}}\n    });\n\n    document.getElementById('modal-overlay').classList.add('open');\n    document.body.style.overflow='hidden';\n  }\n\n  \/\/ Cerrar modal\n  document.getElementById('modal-close-btn').addEventListener('click',closeModal);\n  document.getElementById('modal-overlay').addEventListener('click',function(e){\n    if(e.target===this)closeModal();\n  });\n  document.addEventListener('keydown',function(e){if(e.key==='Escape')closeModal();});\n\n  function closeModal(){\n    document.getElementById('modal-overlay').classList.remove('open');\n    document.body.style.overflow='';\n  }\n\n  \/\/ \u2500\u2500 RENDER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  function render(data){\n    const k=data.kpis;\n    document.getElementById('k-total').textContent=fmtS(k.totalAmount);\n    document.getElementById('k-total-s').textContent=k.totalInvoices+' facturas';\n    document.getElementById('k-paid').textContent=fmtS(k.paidAmount);\n    document.getElementById('k-paid-s').textContent=k.paidPct+'% cobrado';\n    document.getElementById('k-pending').textContent=fmtS(k.pendingAmount);\n    document.getElementById('k-pending-s').textContent=k.pendingCount+' facturas';\n    document.getElementById('k-overdue').textContent=fmtS(k.overdueAmount);\n    document.getElementById('k-overdue-s').textContent=k.overdueCount+' vencidas';\n    document.getElementById('k-avg').textContent=fmtS(k.avgTicket);\n    document.getElementById('k-kg').textContent=fmtKg(k.totalKg);\n    document.getElementById('k-count').textContent=k.totalInvoices;\n    document.getElementById('k-count-s').textContent=k.paidCount+' cobradas \u00b7 '+(k.pendingCount+k.overdueCount)+' pend.';\n\n    const months=data.monthly.map(m=>m.month);\n    const amounts=data.monthly.map(m=>m.amount);\n\n    if(cMonthly)cMonthly.destroy();\n    cMonthly=new Chart(document.getElementById('c-monthly'),{\n      type:'bar',data:{labels:months,datasets:[{label:'\u20ac',data:amounts,backgroundColor:'#e05c2e',borderRadius:4}]},\n      options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},\n        scales:{y:{ticks:{callback:v=>fmtS(v),font:{size:11}},grid:{color:'rgba(0,0,0,0.05)'}},\n                x:{ticks:{font:{size:10},autoSkip:false,maxRotation:45}}}}\n    });\n\n    const sColors=['#2d7a3a','#e05c2e','#b94040'];\n    const sLabels=['Cobrado','Pendiente','Vencido'];\n    const sData=[k.paidCount,k.pendingCount,k.overdueCount];\n    document.getElementById('donut-legend').innerHTML=sLabels.map((l,i)=>\n      `<span><span class=\"dot\" style=\"background:${sColors[i]}\"><\/span>${l} (${sData[i]})<\/span>`).join('');\n    if(cStatus)cStatus.destroy();\n    cStatus=new Chart(document.getElementById('c-status'),{\n      type:'doughnut',data:{labels:sLabels,datasets:[{data:sData,backgroundColor:sColors,borderWidth:0}]},\n      options:{responsive:true,maintainAspectRatio:false,cutout:'68%',plugins:{legend:{display:false}}}\n    });\n\n    \/\/ Top clientes \u2014 con listener correcto\n    const maxC=data.topClients[0]?.amount||1;\n    const listC=document.getElementById('list-clients');\n    listC.innerHTML='';\n    data.topClients.forEach((c,i)=>{\n      const li=document.createElement('li');\n      li.innerHTML=`<span class=\"rank\">${i+1}.<\/span>\n        <span class=\"nm\" title=\"${c.name}\">${c.name}<\/span>\n        <div class=\"bar-wrap\"><div class=\"bar-fill\" style=\"width:${Math.round(c.amount\/maxC*100)}%\"><\/div><\/div>\n        <span class=\"am\">${fmtS(c.amount)}<\/span>\n        <span class=\"arrow\">\u203a<\/span>`;\n      li.addEventListener('click',()=>openClientModal(c.name));\n      listC.appendChild(li);\n    });\n    if(!data.topClients.length) listC.innerHTML='<li style=\"color:#aaa;font-size:13px\">Sin datos<\/li>';\n\n    \/\/ Top clientes KGs\n    const maxKg=(data.kgByClient||[])[0]?.kg||1;\n    const listKg=document.getElementById('list-kg');\n    listKg.innerHTML='';\n    (data.kgByClient||[]).forEach((c,i)=>{\n      const li=document.createElement('li');\n      li.innerHTML=`<span class=\"rank\">${i+1}.<\/span>\n        <span class=\"nm\" title=\"${c.name}\">${c.name}<\/span>\n        <div class=\"bar-wrap\"><div class=\"bar-fill\" style=\"width:${Math.round(c.kg\/maxKg*100)}%;background:#1a6ab9\"><\/div><\/div>\n        <span class=\"am\">${fmtKg(c.kg)}<\/span>\n        <span class=\"arrow\">\u203a<\/span>`;\n      li.addEventListener('click',()=>openClientModal(c.name));\n      listKg.appendChild(li);\n    });\n    if(!(data.kgByClient||[]).length) listKg.innerHTML='<li style=\"color:#aaa;font-size:13px\">Sin datos<\/li>';\n\n    document.getElementById('list-products').innerHTML=data.topProducts.map((p,i)=>\n      `<li style=\"cursor:default\"><span class=\"nm\"><span class=\"rank\">${i+1}.<\/span>${p.name}<\/span><span class=\"am\">${fmtS(p.amount)}<\/span><\/li>`\n    ).join('')||'<li style=\"color:#aaa;font-size:13px\">Sin datos<\/li>';\n\n    let cum=0;\n    const cumData=amounts.map(a=>{cum+=a;return cum;});\n    if(cCumul)cCumul.destroy();\n    cCumul=new Chart(document.getElementById('c-cumul'),{\n      type:'line',data:{labels:months,datasets:[{label:'Acumulado',data:cumData,borderColor:'#2d7a3a',backgroundColor:'rgba(45,122,58,0.08)',fill:true,tension:0.3,pointRadius:3}]},\n      options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},\n        scales:{y:{ticks:{callback:v=>fmtS(v),font:{size:11}},grid:{color:'rgba(0,0,0,0.05)'}},\n                x:{ticks:{font:{size:10},autoSkip:false,maxRotation:45}}}}\n    });\n\n    if(cKgMonth)cKgMonth.destroy();\n    cKgMonth=new Chart(document.getElementById('c-kg-month'),{\n      type:'bar',data:{labels:months,datasets:[{label:'KGs',data:amounts.map(a=>k.totalAmount>0?Math.round(k.totalKg*a\/k.totalAmount):0),backgroundColor:'#1a6ab9',borderRadius:4}]},\n      options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},\n        scales:{y:{ticks:{callback:v=>fmtKg(v),font:{size:11}},grid:{color:'rgba(0,0,0,0.05)'}},\n                x:{ticks:{font:{size:10},autoSkip:false,maxRotation:45}}}}\n    });\n\n    document.getElementById('t-pending').innerHTML=data.pendingInvoices.map(inv=>{\n      const badge=inv.status==='overdue'?'<span class=\"badge b-danger\">Vencida<\/span>':'<span class=\"badge b-warn\">Pendiente<\/span>';\n      return `<tr><td title=\"${inv.client}\">${inv.client}<\/td><td style=\"font-weight:600\">${fmt(inv.amount)}<\/td><td>${badge}<\/td><\/tr>`;\n    }).join('')||'<tr><td colspan=\"3\" style=\"color:#aaa;text-align:center;padding:16px\">Sin pendientes<\/td><\/tr>';\n  }\n\n})();\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udcca KPIs Ventas MiCaldo \u2699 Configurar Filtros Periodo Este mesEste trimestreEste a\u00f1oA\u00f1o pasadoPersonalizado Desde Hasta Cliente \u2014 Todos los clientes \u2014 \u21bb Cargar datos \u2715 Reset Consultando Holded&#8230; Facturaci\u00f3n total \u2014 \u2014 Cobrado \u2014 \u2014 Pendiente cobro \u2014 \u2014 Vencido sin pagar \u2014 \u2014 Ticket medio \u2014 por factura KGs vendidos \u2014 unidades totales N\u00ba [&hellip;]<\/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-8","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/pages\/8","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/comments?post=8"}],"version-history":[{"count":8,"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/pages\/8\/revisions"}],"predecessor-version":[{"id":20,"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/pages\/8\/revisions\/20"}],"wp:attachment":[{"href":"https:\/\/app.micaldo.es\/index.php\/wp-json\/wp\/v2\/media?parent=8"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}