{"id":30,"date":"2025-08-26T14:33:17","date_gmt":"2025-08-26T12:33:17","guid":{"rendered":"http:\/\/192.168.100.133\/?page_id=30"},"modified":"2025-09-23T14:12:56","modified_gmt":"2025-09-23T12:12:56","slug":"tracker","status":"publish","type":"page","link":"http:\/\/www.nesh.co.za\/index.php\/tracker\/","title":{"rendered":"Mayor Income"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" \/>\n  <title>Atlas Earth Badge Tracker<\/title>\n  <style>\n    :root {\n      --blue: #3498db;\n      --green: #2ecc71;\n      --orange: #f39c12;\n      --red: #e74c3c;\n      --purple: #9b59b6;\n      --muted: #ecf0f1;\n      --card-width: 720px;\n      --header-bg: #fefefe;\n      --panel-bg: #f7f9fc;\n    }\n    body {\n      font-family: Arial, sans-serif;\n      background: #f7f7f7;\n      margin: 0;\n      padding: 20px;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n    }\n    h1 {\n      margin: 0 0 14px 0;\n      font-size: 1.5rem;\n      text-align: center;\n      color: var(--blue);\n      background: var(--header-bg);\n      padding: 10px;\n      border-radius: 6px;\n    }\n    .card {\n      width: 100%;\n      max-width: var(--card-width);\n      background: #fff;\n      border-radius: 10px;\n      padding: 14px;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n      box-sizing: border-box;\n      margin-bottom: 14px;\n    }\n    .input {\n      width: 100%;\n      padding: 10px 12px;\n      border-radius: 8px;\n      border: 1px solid #ccc;\n      font-size: 1rem;\n      box-sizing: border-box;\n      margin-bottom: 8px;\n    }\n    .controls {\n      display: flex;\n      gap: 8px;\n      flex-wrap: wrap;\n      justify-content: center;\n      margin-top: 8px;\n    }\n    .button {\n      padding: 10px 14px;\n      border: none;\n      border-radius: 8px;\n      cursor: pointer;\n      font-size: 0.95rem;\n      color: white;\n      min-width: 110px;\n    }\n    .button-add { background: var(--blue); }\n    .button-report { background: var(--green); }\n    .button-monthly { background: var(--purple); }\n    .button-weekly { background: #1abc9c; }\n    .button-backup { background: var(--orange); }\n    .button-restore { background: var(--red); }\n    #message {\n      margin-top: 8px;\n      color: var(--green);\n      font-weight: 500;\n      text-align: center;\n    }\n    #reportContainer, #chartContainer {\n      width: 100%;\n      max-width: var(--card-width);\n      margin-top: 8px;\n    }\n    .user-card {\n      background: #fff;\n      border-radius: 8px;\n      padding: 12px;\n      margin-bottom: 12px;\n      box-shadow: 0 1px 6px rgba(0,0,0,0.05);\n    }\n    .user-header {\n      display: flex;\n      justify-content: space-between;\n      gap: 10px;\n      margin-bottom: 8px;\n    }\n    .username-purple {\n      color: var(--purple);\n      font-weight: 700;\n      font-size: 1rem;\n    }\n    .btn-small {\n      padding: 6px 8px;\n      border-radius: 6px;\n      border: none;\n      cursor: pointer;\n      font-size: 0.85rem;\n      color: #fff;\n      min-width: 90px;\n      text-align: center;\n    }\n    .btn-delete { background: var(--red); }\n    .btn-delete-month { background: var(--orange); }\n    .delete-week-btn { background: var(--red); }\n    .month-summary {\n      background: #d9eefc;\n      border-radius: 8px;\n      padding: 10px;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      gap: 10px;\n      margin-bottom: 6px;\n    }\n    .month-info {\n      font-weight: 600;\n      color: var(--blue);\n    }\n    .accordion-btn {\n      width: 100%;\n      text-align: left;\n      background: var(--muted);\n      border: none;\n      padding: 10px;\n      border-radius: 8px;\n      cursor: pointer;\n      margin-bottom: 6px;\n      font-size: 0.95rem;\n      color: var(--blue);\n    }\n    .accordion-btn.active { background: #cfe4fb; }\n    .panel {\n      display: none;\n      background: var(--panel-bg);\n      border-left: 3px solid var(--blue);\n      padding: 8px 10px;\n      border-radius: 6px;\n      margin-bottom: 8px;\n    }\n    .week-row {\n      padding: 6px 0;\n      border-bottom: 1px solid #eee;\n    }\n    .week-row:last-child { border-bottom: none; }\n\n    \/* Summary panel inside chart container *\/\n    .chart-summary {\n      margin-top: 12px;\n      padding: 10px 12px;\n      background: #fff;\n      border-radius: 8px;\n      border: 1px solid #eee;\n      box-shadow: 0 1px 6px rgba(0,0,0,0.04);\n      font-size: 14px;\n      color: #333;\n    }\n  <\/style>\n<\/head>\n<body>\n\n  <h1>Enter Badge Income for a Town<\/h1>\n\n  <div class=\"card\">\n    <input id=\"town\" class=\"input\" placeholder=\"Town\" \/>\n    <input id=\"badgeIncome\" class=\"input\" type=\"number\" placeholder=\"Badge Income\" \/>\n    <input id=\"date\" class=\"input\" type=\"date\" \/>\n    <div class=\"controls\">\n      <button id=\"addEntry\" class=\"button button-add\">Add Entry<\/button>\n    <\/div>\n    <div id=\"message\" aria-live=\"polite\"><\/div>\n  <\/div>\n\n  <div class=\"card\">\n    <div class=\"controls\">\n      <button id=\"createReport\" class=\"button button-report\">Report<\/button>\n      <button id=\"monthlyChartBtn\" class=\"button button-monthly\">Monthly Chart<\/button>\n      <button id=\"weeklyChartBtn\" class=\"button button-weekly\">Weekly Chart<\/button>\n      <button id=\"backupData\" class=\"button button-backup\">Backup<\/button>\n      <button id=\"restoreData\" class=\"button button-restore\">Restore<\/button>\n    <\/div>\n  <\/div>\n\n  <div id=\"reportContainer\" aria-live=\"polite\"><\/div>\n\n  <div id=\"chartContainer\" class=\"card\" style=\"display:none; height:400px;\">\n    <canvas id=\"chartCanvas\" style=\"width:100%; height:100%\"><\/canvas>\n    <!-- summary will be appended here inside the same container -->\n  <\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-plugin-annotation@1.4.0\"><\/script>\n<script>\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n\n  const townInput = document.getElementById(\"town\");\n  const incomeInput = document.getElementById(\"badgeIncome\");\n  const dateInput = document.getElementById(\"date\");\n  const addEntryBtn = document.getElementById(\"addEntry\");\n  const createReportBtn = document.getElementById(\"createReport\");\n  const monthlyChartBtn = document.getElementById(\"monthlyChartBtn\");\n  const weeklyChartBtn = document.getElementById(\"weeklyChartBtn\");\n  const backupBtn = document.getElementById(\"backupData\");\n  const restoreBtn = document.getElementById(\"restoreData\");\n\n  const reportContainer = document.getElementById(\"reportContainer\");\n  const chartContainer = document.getElementById(\"chartContainer\");\n  let chartCanvas = document.getElementById(\"chartCanvas\");\n  const messageDiv = document.getElementById(\"message\");\n\n  let database = [];\n  let chartInstance = null;\n  let lastTown = \"\";\n  let currentView = \"report\";\n\n  const todayStr = new Date().toISOString().split(\"T\")[0];\n  dateInput.value = todayStr;\n\n  \/\/ Utility functions\n  function loadDatabase() {\n    try {\n      database = JSON.parse(localStorage.getItem(\"badgeDatabase\") || \"[]\");\n    } catch (e) {\n      database = [];\n    }\n  }\n\n  function saveDatabase() {\n    localStorage.setItem(\"badgeDatabase\", JSON.stringify(database));\n  }\n\n  function formatLocalDate(dateInput) {\n    \/\/ Accept either Date or YYYY-MM-DD string\n    const d = (dateInput instanceof Date) ? dateInput : new Date(dateInput);\n    const tzOff = d.getTimezoneOffset() * 60000;\n    return new Date(d.getTime() - tzOff).toISOString().split(\"T\")[0];\n  }\n\n  function monthKeyFromDate(dateStr) {\n    return dateStr.slice(0, 7);\n  }\n\n  function getWeekStart(dateStr) {\n    const d = new Date(dateStr);\n    const day = d.getDay();\n    const diff = (day === 0 ? -6 : 1 - day); \/\/ Monday\n    const ws = new Date(d);\n    ws.setDate(ws.getDate() + diff);\n    ws.setHours(0,0,0,0);\n    return ws;\n  }\n\n  \/\/ Add Entry\n  addEntryBtn.addEventListener(\"click\", () => {\n    let town = townInput.value.trim().toUpperCase();\n    const income = parseInt(incomeInput.value, 10);\n    const dateVal = dateInput.value;\n\n    if (!town || isNaN(income) || !dateVal) {\n      alert(\"Enter valid data\");\n      return;\n    }\n\n    loadDatabase();\n    database.push({ town, income, date: dateVal });\n    saveDatabase();\n\n    lastTown = town;\n    townInput.value = lastTown;\n    dateInput.value = todayStr;\n    incomeInput.value = \"\";\n\n    messageDiv.textContent = `Added ${town} on ${dateVal} (${income})`;\n    setTimeout(() => messageDiv.textContent = \"\", 2500);\n\n    if (currentView === \"report\") generateReport();\n    if (currentView === \"monthly\") generateMonthlyChart();\n    if (currentView === \"weekly\") generateWeeklyChart();\n  });\n\n  \/\/ Delete helpers\n  function deleteTown(town) {\n    if (!confirm(\"Delete all data for \" + town + \"?\")) return;\n    loadDatabase();\n    database = database.filter(e => e.town !== town);\n    saveDatabase();\n    generateReport();\n  }\n\n  function deleteMonth(town, month) {\n    if (!confirm(\"Delete \" + month + \" for \" + town + \"?\")) return;\n    loadDatabase();\n    database = database.filter(e => !(e.town === town && e.date.startsWith(month)));\n    saveDatabase();\n    generateReport();\n  }\n\n  function deleteWeek(town, weekKey) {\n    if (!confirm(\"Delete week \" + weekKey + \" for \" + town + \"?\")) return;\n    loadDatabase();\n    database = database.filter(e => !(e.town === town && getWeekStart(e.date).getTime() === getWeekStart(weekKey).getTime()));\n    saveDatabase();\n    generateReport();\n  }\n\n  function deleteDailyEntry(entry) {\n    if (!confirm(`Delete entry for ${entry.town} on ${entry.date} (${entry.income})?`)) return;\n    loadDatabase();\n    database = database.filter(e => !(e.town === entry.town && e.date === entry.date && e.income === entry.income));\n    saveDatabase();\n    generateReport();\n  }\n\n  \/\/ Report rendering (kept as original behaviour)\n  function generateReport() {\n    currentView = \"report\";\n    loadDatabase();\n    reportContainer.style.display = \"block\";\n    chartContainer.style.display = \"none\";\n    reportContainer.innerHTML = \"\";\n\n    if (!database.length) {\n      reportContainer.textContent = \"No data\";\n      return;\n    }\n\n    const grouped = {};\n    database.forEach(e => {\n      if (!grouped[e.town]) grouped[e.town] = [];\n      grouped[e.town].push(e);\n    });\n\n    Object.keys(grouped).sort().forEach(town => {\n      const entries = grouped[town].sort((a,b) => a.date.localeCompare(b.date));\n      const card = document.createElement(\"div\");\n      card.className = \"user-card\";\n\n      const hdr = document.createElement(\"div\");\n      hdr.className = \"user-header\";\n      hdr.innerHTML = `<div class=\"username-purple\">${town}<\/div>`;\n\n      const delBtn = document.createElement(\"button\");\n      delBtn.className = \"btn-small btn-delete\";\n      delBtn.textContent = \"Delete Town\";\n      delBtn.addEventListener(\"click\", () => deleteTown(town));\n      hdr.appendChild(delBtn);\n      card.appendChild(hdr);\n\n      const months = {};\n      entries.forEach(e => {\n        const mk = monthKeyFromDate(e.date);\n        if (!months[mk]) months[mk] = [];\n        months[mk].push(e);\n      });\n\n      const monthKeys = Object.keys(months).sort().reverse(); \/\/ Latest first\n\n      \/\/ Container for older months\n      const olderMonthsContainer = document.createElement(\"div\");\n      olderMonthsContainer.style.display = \"none\"; \/\/ hidden by default\n\n      monthKeys.forEach((mk, index) => {\n        const monthEntries = months[mk];\n        const total = monthEntries.reduce((s,e) => s+e.income, 0);\n\n        const bar = document.createElement(\"div\");\n        bar.className = \"month-summary\";\n        bar.innerHTML = `<div class=\"month-info\"><b>${mk}<\/b><br>Total Badge Income: ${total}<\/div>`;\n        const delM = document.createElement(\"button\");\n        delM.className = \"btn-small btn-delete-month\";\n        delM.textContent = \"Delete Month\";\n        delM.addEventListener(\"click\", () => deleteMonth(town,mk));\n        bar.appendChild(delM);\n\n        \/\/ Weekly accordion for this month\n        const acc = document.createElement(\"button\");\n        acc.className = \"accordion-btn\";\n        acc.textContent = \"Show Weekly Data\";\n\n        const panel = document.createElement(\"div\");\n        panel.className = \"panel\";\n        panel.style.display = \"none\";\n        acc.textContent = \"Show Weekly Data\";\n\n        \/\/ Weekly grouping\n        const weeks = {};\n        monthEntries.forEach(e => {\n          const ws = getWeekStart(e.date);\n          const we = new Date(ws);\n          we.setDate(we.getDate()+6);\n          const wk = formatLocalDate(we);\n          if (!weeks[wk]) weeks[wk] = [];\n          weeks[wk].push(e);\n        });\n\n        Object.keys(weeks).sort().forEach(wk => {\n          const weekEntries = weeks[wk];\n          const totalW = weekEntries.reduce((s,e)=>s+e.income,0);\n\n          const weekDiv = document.createElement(\"div\");\n          weekDiv.className = \"week-row\";\n\n          const weekHeader = document.createElement(\"div\");\n          weekHeader.style.display=\"flex\";\n          weekHeader.style.justifyContent=\"space-between\";\n          weekHeader.style.alignItems=\"center\";\n          weekHeader.textContent = `${wk} \u2192 Total Badge Income = ${totalW}`;\n\n          const delW = document.createElement(\"button\");\n          delW.className=\"btn-small delete-week-btn\";\n          delW.textContent=\"Delete Week\";\n          delW.addEventListener(\"click\", ()=>deleteWeek(town,wk));\n          weekHeader.appendChild(delW);\n          weekDiv.appendChild(weekHeader);\n\n          const dayPanel = document.createElement(\"div\");\n          dayPanel.style.paddingLeft=\"10px\";\n          weekEntries.forEach(e=>{\n            const dayRow=document.createElement(\"div\");\n            dayRow.style.display=\"flex\";\n            dayRow.style.justifyContent=\"space-between\";\n            dayRow.style.alignItems=\"center\";\n            dayRow.style.fontSize=\"0.85rem\";\n\n            const dayText=document.createElement(\"span\");\n            dayText.textContent=`${e.date} \u2192 ${e.income}`;\n            dayRow.appendChild(dayText);\n\n            const delDayBtn=document.createElement(\"button\");\n            delDayBtn.className=\"btn-small btn-delete\";\n            delDayBtn.textContent=\"Delete\";\n            delDayBtn.addEventListener(\"click\",()=>deleteDailyEntry(e));\n            dayRow.appendChild(delDayBtn);\n\n            dayPanel.appendChild(dayRow);\n          });\n          weekDiv.appendChild(dayPanel);\n          panel.appendChild(weekDiv);\n        });\n\n        acc.addEventListener(\"click\", ()=>{\n          const open = panel.style.display === \"block\";\n          panel.style.display = open ? \"none\" : \"block\";\n          acc.classList.toggle(\"active\", !open);\n          acc.textContent = open ? \"Show Weekly Data\" : \"Hide Weekly Data\";\n        });\n\n        if(index === 0){\n          card.appendChild(bar);\n          card.appendChild(panel);\n          card.appendChild(acc);\n        } else {\n          olderMonthsContainer.appendChild(bar);\n          olderMonthsContainer.appendChild(panel);\n          olderMonthsContainer.appendChild(acc);\n        }\n      });\n\n      \/\/ Add toggle for older months\n      if(monthKeys.length > 1){\n        const toggleBtn = document.createElement(\"button\");\n        toggleBtn.className = \"accordion-btn\";\n        toggleBtn.textContent = \"Show Older Months\";\n        toggleBtn.addEventListener(\"click\", ()=>{\n          const showing = olderMonthsContainer.style.display === \"block\";\n          olderMonthsContainer.style.display = showing ? \"none\" : \"block\";\n          toggleBtn.textContent = showing ? \"Show Older Months\" : \"Hide Older Months\";\n        });\n        card.appendChild(toggleBtn);\n        card.appendChild(olderMonthsContainer);\n      }\n\n      reportContainer.appendChild(card);\n    });\n  }\n\n  createReportBtn.addEventListener(\"click\", generateReport);\n\n  \/\/ Charts utilities\n  function resetChartCanvas(){\n    if(chartInstance) {\n      try { chartInstance.destroy(); } catch(e){ \/* ignore *\/ }\n      chartInstance = null;\n    }\n    \/\/ replace canvas element inside chartContainer\n    const old = chartContainer.querySelector('#chartCanvas');\n    const newCanvas = document.createElement('canvas');\n    newCanvas.id = 'chartCanvas';\n    newCanvas.style.width = '100%';\n    newCanvas.style.height = '100%';\n    if(old && old.parentNode) old.parentNode.replaceChild(newCanvas, old);\n    chartCanvas = newCanvas;\n\n    \/\/ remove old summary if present\n    const oldSummary = chartContainer.querySelector('.chart-summary');\n    if (oldSummary) oldSummary.remove();\n  }\n\n  \/\/ Helper to create a summary panel inside chart container\n  function appendSummaryHtml(html) {\n    let summary = chartContainer.querySelector('.chart-summary');\n    if (!summary) {\n      summary = document.createElement('div');\n      summary.className = 'chart-summary';\n      chartContainer.appendChild(summary);\n    }\n    summary.innerHTML = html;\n  }\n\n  \/\/ Generate Month chart with averages injected into legend labels (and dashed annotation lines)\n  function generateMonthlyChart(){\n    currentView=\"monthly\";\n    loadDatabase();\n    if(!database.length){ chartContainer.innerHTML=\"No data\"; return; }\n\n    chartContainer.style.display=\"block\";\n    reportContainer.style.display=\"none\";\n    resetChartCanvas();\n\n    const towns=[...new Set(database.map(e=>e.town))].sort();\n    const monthKeys=[...new Set(database.map(e=>e.date.slice(0,7)))].sort().slice(-3);\n    const datasets=[];\n\n    \/\/ compute values and averages\n    const avgValues = {};\n    towns.forEach((t,i)=>{\n      const vals = monthKeys.map(m=>{\n        const entries = database.filter(e=>e.town===t && e.date.startsWith(m));\n        return entries.reduce((s,e)=>s+e.income,0);\n      });\n      datasets.push({\n        label: t,\n        data: vals,\n        backgroundColor: `hsl(${(i*60)%360} 70% 50%)`.replace(\/\\s\/g,',') \/\/ keep readable\n      });\n      const avg = vals.reduce((a,b)=>a+b,0)\/(vals.length||1);\n      avgValues[t] = Number.isFinite(avg) ? avg.toFixed(1) : \"0.0\";\n    });\n\n    \/\/ annotations for averages (simple horizontal dashed lines)\n    const annotations = {};\n    towns.forEach((t,i)=>{\n      const avg = parseFloat(avgValues[t]);\n      if (!isNaN(avg)) {\n        annotations[`avg_${i}`] = {\n          type: 'line',\n          yMin: avg,\n          yMax: avg,\n          borderColor: `hsl(${(i*60)%360} 70% 35%)`.replace(\/\\s\/g,','),\n          borderWidth: 2,\n          borderDash: [6,6]\n          \/\/ no label here (user requested averages in legend instead)\n        };\n      }\n    });\n\n    \/\/ Create chart - ensure legend generateLabels injects average into text\n    chartInstance = new Chart(chartCanvas.getContext('2d'), {\n      type: 'bar',\n      data: {\n        labels: monthKeys,\n        datasets\n      },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        plugins: {\n          title: { display: true, text: 'Monthly Badge Income (Latest 3 Months)' },\n          annotation: { annotations },\n          legend: {\n            labels: {\n              \/\/ Use Chart.defaults.plugins.legend.labels.generateLabels to get original objects then modify .text\n              generateLabels: (chart) => {\n                const original = Chart.defaults.plugins?.legend?.labels?.generateLabels?.(chart) || [];\n                \/\/ map dataset legend entries to append average\n                return original.map(item => {\n                  if (typeof item.datasetIndex === 'number') {\n                    const ds = chart.data.datasets[item.datasetIndex];\n                    const town = ds?.label;\n                    if (town && avgValues[town] !== undefined) {\n                      \/\/ append avg to the text\n                      return Object.assign({}, item, { text: `${town} (Avg: ${avgValues[town]})` });\n                    }\n                  }\n                  return item;\n                });\n              }\n            }\n          }\n        },\n        scales: {\n          y: { beginAtZero: true, ticks: { stepSize: 20 } }\n        }\n      }\n    });\n\n    \/\/ Build summary HTML and append inside the same chart card\n    const monthTotals = monthKeys.map(m => database.filter(e => e.date.startsWith(m)).reduce((s,e)=>s+e.income,0));\n    const html = `<strong>Monthly Summary<\/strong><div style=\"margin-top:8px;\">${monthKeys.map((m,i)=>`<div>${m}: Total Badge Income = ${monthTotals[i]}<\/div>`).join('')}<\/div>`;\n    appendSummaryHtml(html);\n  }\n\n  \/\/ Weekly chart (similar to monthly)\n  function generateWeeklyChart(){\n    currentView=\"weekly\";\n    loadDatabase();\n    if(!database.length){ chartContainer.innerHTML=\"No data\"; return; }\n\n    chartContainer.style.display=\"block\";\n    reportContainer.style.display=\"none\";\n    resetChartCanvas();\n\n    const towns=[...new Set(database.map(e=>e.town))].sort();\n    const weeks = {};\n    database.forEach(e=>{\n      const ws = getWeekStart(e.date);\n      const we = new Date(ws);\n      we.setDate(we.getDate()+6);\n      const key = formatLocalDate(we);\n      if(!weeks[key]) weeks[key] = [];\n      weeks[key].push(e);\n    });\n\n    const weekKeys = Object.keys(weeks).sort().slice(-4);\n    const datasets = [];\n    const avgValues = {};\n\n    towns.forEach((t,i)=>{\n      const vals = weekKeys.map(wk=>{\n        const entries = (weeks[wk] || []).filter(e => e.town === t);\n        return entries.reduce((s,e)=>s+e.income,0);\n      });\n      datasets.push({\n        label: t,\n        data: vals,\n        backgroundColor: `hsl(${(i*60)%360} 70% 50%)`.replace(\/\\s\/g,',')\n      });\n      const avg = vals.reduce((a,b)=>a+b,0)\/(vals.length||1);\n      avgValues[t] = Number.isFinite(avg) ? avg.toFixed(1) : \"0.0\";\n    });\n\n    \/\/ annotations for averages\n    const annotations = {};\n    towns.forEach((t,i)=>{\n      const avg = parseFloat(avgValues[t]);\n      if (!isNaN(avg)) {\n        annotations[`avgw_${i}`] = {\n          type: 'line',\n          yMin: avg,\n          yMax: avg,\n          borderColor: `hsl(${(i*60)%360} 70% 35%)`.replace(\/\\s\/g,','),\n          borderWidth: 2,\n          borderDash: [6,6]\n        };\n      }\n    });\n\n    chartInstance = new Chart(chartCanvas.getContext('2d'), {\n      type: 'bar',\n      data: { labels: weekKeys, datasets },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        plugins: {\n          title: { display: true, text: 'Weekly Badge Income (Latest 4 Weeks)' },\n          annotation: { annotations },\n          legend: {\n            labels: {\n              generateLabels: (chart) => {\n                const original = Chart.defaults.plugins?.legend?.labels?.generateLabels?.(chart) || [];\n                return original.map(item => {\n                  if (typeof item.datasetIndex === 'number') {\n                    const ds = chart.data.datasets[item.datasetIndex];\n                    const town = ds?.label;\n                    if (town && avgValues[town] !== undefined) {\n                      return Object.assign({}, item, { text: `${town} (Avg: ${avgValues[town]})` });\n                    }\n                  }\n                  return item;\n                });\n              }\n            }\n          }\n        },\n        scales: {\n          y: { beginAtZero: true, ticks: { stepSize: 20 } }\n        }\n      }\n    });\n\n    const weekTotals = weekKeys.map(wk => (weeks[wk] || []).reduce((s,e)=>s+e.income,0));\n    const html = `<strong>Weekly Summary<\/strong><div style=\"margin-top:8px;\">${weekKeys.map((wk,i)=>`<div>${wk}: Total Badge Income = ${weekTotals[i]}<\/div>`).join('')}<\/div>`;\n    appendSummaryHtml(html);\n  }\n\n  monthlyChartBtn.addEventListener(\"click\", generateMonthlyChart);\n  weeklyChartBtn.addEventListener(\"click\", generateWeeklyChart);\n\n  \/\/ Backup \/ Restore\n  backupBtn.addEventListener(\"click\",()=>{\n    loadDatabase();\n    const blob=new Blob([JSON.stringify(database,null,2)],{type:\"application\/json\"});\n    const a=document.createElement(\"a\");\n    a.href=URL.createObjectURL(blob);\n    a.download=\"badgeDatabase_backup.json\";\n    a.click();\n    URL.revokeObjectURL(a.href);\n    messageDiv.textContent=\"Backup downloaded\";\n    setTimeout(()=>messageDiv.textContent=\"\",2000);\n  });\n\n  restoreBtn.addEventListener(\"click\",()=>{\n    const input=document.createElement(\"input\");\n    input.type=\"file\";\n    input.accept=\"application\/json\";\n    input.addEventListener(\"change\",e=>{\n      const file=e.target.files[0];\n      const reader=new FileReader();\n      reader.onload=ev=>{\n        try{\n          database=JSON.parse(ev.target.result);\n          saveDatabase();\n          generateReport();\n          messageDiv.textContent=\"Data restored\";\n          setTimeout(()=>messageDiv.textContent=\"\",2000);\n        }catch(err){\n          alert(\"Invalid file\");\n        }\n      };\n      reader.readAsText(file);\n    });\n    input.click();\n  });\n\n  \/\/ initial load\n  loadDatabase();\n  generateReport();\n  if(lastTown) townInput.value=lastTown;\n\n});\n<\/script>\n\n<\/body>\n<\/html>\n\n\n\n\n<p class=\"has-text-align-center\">.<\/p>\n\n\n\n<p>.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Atlas Earth Badge Tracker Enter Badge Income for a Town Add Entry Report Monthly Chart Weekly Chart Backup Restore . .<\/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-30","page","type-page","status-publish"],"_links":{"self":[{"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/pages\/30","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/comments?post=30"}],"version-history":[{"count":18,"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/pages\/30\/revisions"}],"predecessor-version":[{"id":91,"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/pages\/30\/revisions\/91"}],"wp:attachment":[{"href":"http:\/\/www.nesh.co.za\/index.php\/wp-json\/wp\/v2\/media?parent=30"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}