const SUPABASE_URL = "[Link]
co";
const SUPABASE_KEY = "sb_publishable_p5mwf4IaC-7v2uxdWbNjhA_9pNcfeEu";
const supabaseClient = [Link](SUPABASE_URL, SUPABASE_KEY);
// ─── Chart colour palette (mapped to category color names) ─────────────────
const CHART_PALETTE = {
pink: "rgba(244, 114, 182, 0.85)",
blue: "rgba(96, 165, 250, 0.85)",
lavender: "rgba(167, 139, 250, 0.85)",
yellow: "rgba(251, 191, 36, 0.85)",
mint: "rgba(52, 211, 153, 0.85)",
sky: "rgba(56, 189, 248, 0.85)",
gray: "rgba(156, 163, 175, 0.85)"
};
const MONTH_NAMES = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
// ─── App state ──────────────────────────────────────────────────────────────
let state = {
currentUser: null,
expenses: [],
categories: [],
settings: {
currency: "PHP",
budgets: { weekly: 0, monthly: 0, yearly: 0 }
};
// Chart instance store (destroy before re-render)
const charts = { today: null, month: null, year: null };
// ─── Boot ───────────────────────────────────────────────────────────────────
[Link]("DOMContentLoaded", async () => {
setupAuthTabs();
setupAuthForms();
setupNavigation();
setupExpenseForm();
setupSettings();
setupLogout();
setupChartTabs();
setupAllocTabs();
setupUserCategories();
setTodayDate();
const { data } = await [Link]();
if ([Link]?.user) {
await handleAuthenticatedUser([Link]);
} else {
showAuth();
[Link](async (_event, session) => {
if (session?.user) {
await handleAuthenticatedUser([Link]);
} else {
[Link] = null;
showAuth();
}
});
});
async function handleAuthenticatedUser(user) {
[Link] = {
id: [Link],
name: user.user_metadata?.name || user.user_metadata?.full_name || [Link]?.split("@")[0] ||
"User",
email: [Link]
};
await ensureProfileAndDefaults();
await loadUserData();
showApp();
// ─── AUTH ────────────────────────────────────────────────────────────────────
function setupAuthTabs() {
[Link]("loginTabBtn").addEventListener("click", () => switchAuthTab("login"));
[Link]("registerTabBtn").addEventListener("click", () => switchAuthTab("register"));
function switchAuthTab(tab) {
const loginForm = [Link]("loginForm");
const registerForm = [Link]("registerForm");
const loginTabBtn = [Link]("loginTabBtn");
const registerTabBtn= [Link]("registerTabBtn");
const authMessage = [Link]("authMessage");
[Link] = "";
if (tab === "login") {
[Link]("hidden");
[Link]("hidden");
[Link]("active");
[Link]("active");
} else {
[Link]("hidden");
[Link]("hidden");
[Link]("active");
[Link]("active");
function setupAuthForms() {
[Link]("loginForm").addEventListener("submit", handleLogin);
[Link]("registerForm").addEventListener("submit", handleRegister);
async function handleRegister(event) {
[Link]();
const name = [Link]("registerName").[Link]();
const email = [Link]("registerEmail").[Link]().toLowerCase();
const password = [Link]("registerPassword").value;
const confirmPassword = [Link]("registerConfirmPassword").value;
const authMessage = [Link]("authMessage");
[Link] = "#dc2626";
if (!name || !email || !password || !confirmPassword) { [Link] = "Please complete
all fields."; return; }
if ([Link] < 6) { [Link] = "Password must be at least 6 characters.";
return; }
if (password !== confirmPassword) { [Link] = "Passwords do not match."; return; }
const { error } = await [Link]({ email, password, options: { data: { name } } });
if (error) { [Link] = [Link]; return; }
[Link] = "#16a34a";
[Link] = "Account created! You can now sign in.";
[Link]("registerForm").reset();
switchAuthTab("login");
async function handleLogin(event) {
[Link]();
const email = [Link]("loginEmail").[Link]().toLowerCase();
const password = [Link]("loginPassword").value;
const authMessage = [Link]("authMessage");
[Link] = "#dc2626";
const { error } = await [Link]({ email, password });
if (error) [Link] = [Link];
function setupLogout() {
[Link]("logoutBtn").addEventListener("click", async () => {
await [Link]();
});
}
// ─── VISIBILITY ─────────────────────────────────────────────────────────────
function showAuth() {
[Link]("authScreen").[Link]("hidden");
[Link]("appShell").[Link]("hidden");
function showApp() {
[Link]("authScreen").[Link]("hidden");
[Link]("appShell").[Link]("hidden");
[Link]("welcomeText").textContent = `Welcome, ${[Link]}!`;
renderAll();
// ─── DATABASE ────────────────────────────────────────────────────────────────
async function ensureProfileAndDefaults() {
// Upsert profile
await [Link]("profiles").upsert({
id: [Link],
name: [Link],
email: [Link]
});
// Ensure user_settings row exists
const settingsRes = await supabaseClient
.from("user_settings")
.select("*")
.eq("user_id", [Link])
.maybeSingle();
if (![Link]) {
await [Link]("user_settings").insert({
user_id: [Link],
currency: "PHP",
weekly_budget: 0,
monthly_budget: 0,
yearly_budget: 0
});
// NOTE: Categories are now global — no per-user category seeding needed.
async function loadUserData() {
// Fetch global categories (user_id IS NULL) + user's own categories
// RLS policy handles this: select is allowed for (user_id is null OR user_id = [Link]())
const categoriesResponse = await supabaseClient
.from("categories")
.select("*")
.order("created_at", { ascending: true });
const expensesResponse = await supabaseClient
.from("expenses")
.select("*")
.eq("user_id", [Link])
.order("expense_date", { ascending: false });
const settingsResponse = await supabaseClient
.from("user_settings")
.select("*")
.eq("user_id", [Link])
.maybeSingle();
if ([Link]) { [Link]([Link]);
alert([Link]); }
if ([Link]) { [Link]([Link]);
alert([Link]); }
if ([Link]) { [Link]([Link]);
alert([Link]); }
[Link] = [Link] || [];
[Link] = ([Link] || []).map((expense) => {
const category = [Link]((cat) => [Link] === expense.category_id);
return {
id: [Link],
amount: Number([Link]),
description: [Link],
date: expense.expense_date,
categoryId: expense.category_id,
categoryName: category?.name || "Unknown",
categoryIcon: category?.icon || "📦",
categoryColor: category?.color || "gray"
};
});
const settings = [Link] || null;
[Link] = {
currency: settings?.currency || "PHP",
budgets: {
weekly: Number(settings?.weekly_budget || 0),
monthly: Number(settings?.monthly_budget || 0),
yearly: Number(settings?.yearly_budget || 0)
};
// ─── NAVIGATION ──────────────────────────────────────────────────────────────
function setupNavigation() {
[Link](".nav-item").forEach((item) => {
[Link]("click", () => showPage([Link]));
});
function showPage(pageId) {
[Link](".page").forEach((page) => {
[Link]("active", [Link] === pageId);
});
[Link](".nav-item").forEach((item) => {
[Link]("active", [Link] === pageId);
});
// ─── EXPENSE FORM ────────────────────────────────────────────────────────────
function setupExpenseForm() {
[Link]("expenseForm").addEventListener("submit", handleAddExpense);
[Link]("cancelExpenseBtn").addEventListener("click", () => setTodayDate());
}
async function handleAddExpense(event) {
[Link]();
const amount = parseFloat([Link]("amount").value);
const categoryId = [Link]("categorySelect").value;
const description = [Link]("description").[Link]();
const expenseDate = [Link]("expenseDate").value;
if (!amount || amount <= 0) { alert("Please enter a valid amount."); return; }
if (!categoryId) { alert("Please select a category."); return; }
if (!description) { alert("Please enter a description."); return; }
const { error } = await [Link]("expenses").insert({
user_id: [Link],
user_email: [Link],
amount,
description,
expense_date: expenseDate,
category_id: categoryId
});
if (error) { [Link]("Add expense error:", error); alert([Link]); return; }
[Link]();
setTodayDate();
await loadUserData();
renderAll();
showPage("all-expenses");
}
async function deleteExpense(id) {
if () return;
const { error } = await supabaseClient
.from("expenses").delete()
.eq("id", id)
.eq("user_id", [Link]);
if (error) { [Link](error); alert([Link]); return; }
await loadUserData();
renderAll();
// ─── SETTINGS ────────────────────────────────────────────────────────────────
function setupSettings() {
const budgetForm = [Link]("budgetForm");
if (budgetForm) [Link]("submit", handleSaveBudgetSettings);
setupBudgetCascade();
// ── Budget cascade: yearly → monthly+weekly | monthly → weekly | weekly → nothing
function setupBudgetCascade() {
const yearlyEl = [Link]("yearlyBudgetInput");
const monthlyEl = [Link]("monthlyBudgetInput");
const weeklyEl = [Link]("weeklyBudgetInput");
if (!yearlyEl || !monthlyEl || !weeklyEl) return;
// YEARLY typed → cascade down to monthly & weekly
[Link]("input", () => {
const yearly = parseFloat([Link]) || 0;
if (yearly > 0) {
const monthly = +(yearly / 12).toFixed(2);
const weekly = +(yearly / 52).toFixed(2);
[Link] = monthly;
[Link] = weekly;
setAutoField("monthly", true, `Auto: ₱${[Link]()} ÷ 12 months`);
setAutoField("weekly", true, `Auto: ₱${[Link]()} ÷ 52 weeks`);
setHint("yearlyHint", `→ Monthly: ${formatCurrency(monthly)} · Weekly: $
{formatCurrency(weekly)}`);
} else {
// Yearly cleared — reset cascaded fields
[Link] = 0;
[Link] = 0;
setAutoField("monthly", false, "");
setAutoField("weekly", false, "");
setHint("yearlyHint", "");
});
// MONTHLY typed → cascade down to weekly only (yearly stays untouched)
[Link]("input", () => {
// Only cascade if yearly is not driving things
const yearly = parseFloat([Link]) || 0;
if (yearly > 0) return; // yearly is master, skip
const monthly = parseFloat([Link]) || 0;
if (monthly > 0) {
const weekly = +(monthly / 4.3333).toFixed(2);
[Link] = weekly;
setAutoField("weekly", true, `Auto: ₱${[Link]()} ÷ 4.33 weeks`);
setHint("monthlyHint", `→ Weekly: ${formatCurrency(weekly)}`);
} else {
[Link] = 0;
setAutoField("weekly", false, "");
setHint("monthlyHint", "");
});
// WEEKLY typed → no cascade; clear its own auto state
[Link]("input", () => {
const yearly = parseFloat([Link]) || 0;
const monthly = parseFloat([Link]) || 0;
// If neither yearly nor monthly is driving weekly, remove auto badge
if (!yearly && !monthly) {
setAutoField("weekly", false, "");
setHint("weeklyHint", "Monthly & Yearly remain unchanged.");
});
// Helper: toggle auto-badge + input styling
function setAutoField(period, isAuto, hintText) {
const badge = [Link](`${period}AutoBadge`);
const field = [Link](`${period}BudgetField`);
const input = [Link](`${period}BudgetInput`);
const hint = [Link](`${period}Hint`);
if (badge) [Link]("hidden", !isAuto);
if (field) [Link]("is-auto", isAuto);
if (input) [Link]("input-auto", isAuto);
if (hint) [Link] = hintText || "";
function setHint(id, text) {
const el = [Link](id);
if (el) [Link] = text;
async function handleSaveBudgetSettings(event) {
[Link]();
const currency = [Link]("currencyInput").[Link]().toUpperCase();
const weekly = parseFloat([Link]("weeklyBudgetInput").value || "0");
const monthly = parseFloat([Link]("monthlyBudgetInput").value || "0");
const yearly = parseFloat([Link]("yearlyBudgetInput").value || "0");
if (!currency || [Link] !== 3) {
showBudgetMsg("Currency must be exactly 3 letters, e.g. PHP.", "error");
return;
if (weekly < 0 || monthly < 0 || yearly < 0) {
showBudgetMsg("Budgets cannot be negative.", "error");
return;
const { error } = await [Link]("user_settings").upsert({
user_id: [Link],
currency,
weekly_budget: weekly,
monthly_budget: monthly,
yearly_budget: yearly
});
if (error) {
[Link]("Save settings error:", error);
showBudgetMsg([Link], "error");
return;
await loadUserData();
renderAll();
showBudgetMsg("✅ Budget settings saved successfully!", "success");
// Inline save message (replaces browser alert)
function showBudgetMsg(text, type) {
const el = [Link]("budgetSaveMsg");
if (!el) return;
[Link] = text;
[Link] = `budget-save-msg ${type}`;
[Link]("hidden");
setTimeout(() => [Link]("hidden"), 4000);
// ─── CHART TABS ──────────────────────────────────────────────────────────────
function setupChartTabs() {
[Link](".chart-tab-btn").forEach((btn) => {
[Link]("click", () => {
const view = [Link];
[Link](".chart-tab-btn").forEach((b) => [Link]("active"));
[Link](".chart-view").forEach((v) => [Link]("active"));
[Link]("active");
const panelId = `chartView${[Link](0).toUpperCase() + [Link](1)}`;
[Link](panelId)?.[Link]("active");
});
});
// ─── RENDER ALL ──────────────────────────────────────────────────────────────
function renderAll() {
renderCategorySelect();
renderSettingsCategories();
renderUserCategories();
renderDashboard();
renderExpensesList();
fillSettingsForm();
renderBudgetOverview();
renderCategoryBudgetAllocation();
renderCharts();
}
// ─── CATEGORY SELECT ─────────────────────────────────────────────────────────
function renderCategorySelect() {
const select = [Link]("categorySelect");
[Link] = `<option value="">Select category</option>`;
[Link]((category) => {
const option = [Link]("option");
[Link] = [Link];
[Link] = `${[Link]} ${[Link]}`;
[Link](option);
});
// ─── SETTINGS CATEGORIES — admin global (read-only) ─────────────────────────
function renderSettingsCategories() {
const container = [Link]("settingsCategoryGrid");
[Link] = "";
// Only global/admin categories (user_id is null)
const adminCats = [Link]((c) => !c.user_id);
if (![Link]) {
[Link] = `<div class="empty-state"><p>No default categories found.</p></div>`;
return;
[Link]((category) => {
// locked field is now dynamic — admin can toggle it in Table Editor
const lockBadge = [Link]
? `<span class="lock-badge">🔒 Locked</span>`
: `<span class="unlock-badge">🔓 Unlocked</span>`;
const item = [Link]("div");
[Link] = `settings-category-item${![Link] ? " default-cat-unlocked" : ""}`;
[Link] = `
<div class="settings-left">
<div class="icon-circle ${[Link] || "gray"}">${[Link]}</div>
<div class="user-cat-info">
<span>${escapeHtml([Link])}</span>
<small class="cat-admin-note">Admin default · visible to all users</small>
</div>
</div>
${lockBadge}
`;
[Link](item);
});
// ─── USER CUSTOM CATEGORIES ──────────────────────────────────────────────────
function setupUserCategories() {
const form = [Link]("addUserCategoryForm");
if (form) [Link]("submit", handleAddUserCategory);
function renderUserCategories() {
const container = [Link]("userCategoryGrid");
const countBadge = [Link]("userCatCount");
if (!container) return;
// Only the current user's own categories
const userCats = [Link](
(c) => c.user_id && c.user_id === [Link]
);
if (countBadge) [Link] = `${[Link]} custom`;
[Link] = "";
if (![Link]) {
[Link] = `<div class="empty-state"><p>No custom categories yet. Add one
above!</p></div>`;
return;
[Link]((category) => {
const isUsed = [Link]((e) => [Link] === [Link]);
const adminLocked = [Link] === true; // admin toggled this in Table Editor
const cantDelete = isUsed || adminLocked;
// Decide status badge/note
let statusNote = "";
if (adminLocked) {
statusNote = `<span class="cat-admin-lock-badge">🔒 Admin Locked</span>`;
} else if (isUsed) {
statusNote = `<small class="cat-in-use-note">In use — cannot delete</small>`;
}
// Delete button disabled reason
let disabledAttr = "";
let disabledStyle = "";
let disabledTitle = "";
if (adminLocked) {
disabledAttr = "disabled";
disabledStyle = "opacity:0.35;cursor:not-allowed;";
disabledTitle = `title="Locked by admin — contact your admin to unlock"`;
} else if (isUsed) {
disabledAttr = "disabled";
disabledStyle = "opacity:0.35;cursor:not-allowed;";
disabledTitle = `title="This category is linked to an expense"`;
const item = [Link]("div");
[Link] = `settings-category-item${adminLocked ? " cat-item-locked" : ""}`;
[Link] = `
<div class="settings-left">
<div class="icon-circle ${[Link] || "gray"}">${[Link]}</div>
<div class="user-cat-info">
<span>${escapeHtml([Link])}</span>
${statusNote}
</div>
</div>
<button
class="delete-btn user-cat-delete-btn"
type="button"
${disabledAttr}
${disabledTitle}
style="${disabledStyle}"
>🗑 Delete</button>
`;
if (!cantDelete) {
[Link](".user-cat-delete-btn")
.addEventListener("click", () => handleDeleteUserCategory([Link], [Link]));
[Link](item);
});
async function handleAddUserCategory(event) {
[Link]();
const name = [Link]("newCategoryName").[Link]();
const icon = [Link]("newCategoryIcon").[Link]() || "🏷";
const color = [Link]("newCategoryColor").value || "gray";
if (!name) {
showUserCatMsg("Please enter a category name.", "error");
return;
// Check for duplicate name among all categories visible to this user
const duplicate = [Link](
(c) => [Link]() === [Link]()
);
if (duplicate) {
showUserCatMsg(`A category named "${name}" already exists.`, "error");
return;
const { error } = await [Link]("categories").insert({
user_id: [Link],
user_email: [Link], // stored so admin can see who owns it
name,
icon,
color,
locked: false
});
if (error) {
[Link]("Add user category error:", error);
// Friendly duplicate error
const msg = [Link]?.toLowerCase() || "";
if ([Link]("duplicate") || [Link]("unique")) {
showUserCatMsg(`A category named "${name}" already exists.`, "error");
} else {
showUserCatMsg([Link], "error");
return;
[Link]("addUserCategoryForm").reset();
await loadUserData();
renderAll();
showUserCatMsg(`✅ Category "${name}" added successfully!`, "success");
async function handleDeleteUserCategory(id, name) {
if () return;
const { error } = await supabaseClient
.from("categories")
.delete()
.eq("id", id)
.eq("user_id", [Link]); // safety: only own rows
if (error) {
[Link]("Delete user category error:", error);
showUserCatMsg([Link], "error");
return;
await loadUserData();
renderAll();
showUserCatMsg(`🗑 Category "${name}" deleted.`, "success");
function showUserCatMsg(text, type) {
const el = [Link]("userCatMsg");
if (!el) return;
[Link] = text;
[Link] = `user-cat-msg ${type}`;
[Link]("hidden");
setTimeout(() => [Link]("hidden"), 4000);
// ─── DASHBOARD ───────────────────────────────────────────────────────────────
function renderDashboard() {
const total = sumExpenses([Link]);
const monthTotal = sumExpenses(getCurrentMonthExpenses());
[Link]("dashboardTotal").textContent = formatCurrency(total);
[Link]("dashboardMonthTotal").textContent = formatCurrency(monthTotal);
[Link]("dashboardCount").textContent = [Link];
const container = [Link]("recentExpensesContainer");
[Link] = "";
if (![Link]) {
[Link] = `<div class="empty-state"><p>No expenses yet</p></div>`;
return;
[Link](0, 5).forEach((expense) => {
const item = [Link]("div");
[Link] = "simple-list-item";
[Link] = `
<div class="simple-list-left">
<div class="icon-circle ${[Link] || "gray"}">${[Link]}</div>
<div>
<strong>${escapeHtml([Link])}</strong>
<p>${escapeHtml([Link])} • ${formatDisplayDate([Link])}</p>
</div>
</div>
<strong>${formatCurrency([Link])}</strong>
`;
[Link](item);
});
// ─── EXPENSES LIST ───────────────────────────────────────────────────────────
function renderExpensesList() {
const expensesList = [Link]("expensesList");
[Link] = "";
[Link]("allExpenseCount").textContent =
`${[Link]} transaction${[Link] === 1 ? "" : "s"}`;
[Link]("allExpenseTotal").textContent =
formatCurrency(sumExpenses([Link]));
if (![Link]) {
[Link] = `<div class="empty-state"><p>No expenses yet</p></div>`;
return;
[Link]((expense) => {
const item = [Link]("div");
[Link] = "expense-item";
[Link] = `
<div class="expense-left">
<div class="icon-circle ${[Link] || "gray"}">${[Link]}</div>
<div class="expense-details">
<h4>${escapeHtml([Link])}</h4>
<p>${escapeHtml([Link])} • ${formatDisplayDate([Link])}</p>
<small class="user-badge">${escapeHtml([Link])}</small>
</div>
</div>
<div class="expense-right">
<div class="expense-amount">${formatCurrency([Link])}</div>
<button class="delete-btn" type="button">Delete</button>
</div>
`;
[Link](".delete-btn").addEventListener("click", () => deleteExpense([Link]));
[Link](item);
});
// ─── SETTINGS FORM ───────────────────────────────────────────────────────────
function fillSettingsForm() {
const c = [Link]("currencyInput");
const w = [Link]("weeklyBudgetInput");
const m = [Link]("monthlyBudgetInput");
const y = [Link]("yearlyBudgetInput");
if (!c) return;
const currency = [Link] || "PHP";
const weekly = Number([Link] || 0);
const monthly = Number([Link] || 0);
const yearly = Number([Link] || 0);
[Link] = currency;
[Link] = weekly;
[Link] = monthly;
[Link] = yearly;
// Restore cascade visual state based on loaded values
const yearlyDrivesAll = yearly > 0 && [Link](monthly - yearly / 12) < 0.05 && [Link](weekly -
yearly / 52) < 0.05;
const monthlyDrivesWeek = !yearlyDrivesAll && monthly > 0 && [Link](weekly - monthly / 4.3333)
< 0.05;
if (yearlyDrivesAll) {
setAutoField("monthly", true, `Auto: ${formatCurrency(yearly)} ÷ 12 months`);
setAutoField("weekly", true, `Auto: ${formatCurrency(yearly)} ÷ 52 weeks`);
setHint("yearlyHint", `→ Monthly: ${formatCurrency(monthly)} · Weekly: $
{formatCurrency(weekly)}`);
} else if (monthlyDrivesWeek) {
setAutoField("monthly", false, "");
setAutoField("weekly", true, `Auto: ${formatCurrency(monthly)} ÷ 4.33 weeks`);
setHint("monthlyHint", `→ Weekly: ${formatCurrency(weekly)}`);
} else {
setAutoField("monthly", false, "");
setAutoField("weekly", false, "");
setHint("yearlyHint", "");
setHint("monthlyHint", "");
setHint("weeklyHint", "");
// ─── BUDGET OVERVIEW ─────────────────────────────────────────────────────────
function renderBudgetOverview() {
const weekSpent = getCurrentWeekTotal();
const monthSpent = sumExpenses(getCurrentMonthExpenses());
const yearSpent = getCurrentYearTotal();
const wb = Number([Link] || 0);
const mb = Number([Link] || 0);
const yb = Number([Link] || 0);
const set = (id, val) => { const el = [Link](id); if (el) [Link] = val; };
set("weeklyBudgetStatus", `${formatCurrency(weekSpent)} / ${formatCurrency(wb)}`);
set("monthlyBudgetStatus", `${formatCurrency(monthSpent)} / ${formatCurrency(mb)}`);
set("yearlyBudgetStatus", `${formatCurrency(yearSpent)} / ${formatCurrency(yb)}`);
set("weeklyBudgetHint", buildBudgetHint(weekSpent, wb));
set("monthlyBudgetHint", buildBudgetHint(monthSpent, mb));
set("yearlyBudgetHint", buildBudgetHint(yearSpent, yb));
//
════════════════════════════════════════════════════════════
═══════════════
// CHARTS
//
════════════════════════════════════════════════════════════
═══════════════
function renderCharts() {
renderTodayChart();
renderMonthChart();
renderYearChart();
/* ── Shared: destroy old instance ── */
function destroyChart(key) {
if (charts[key]) { charts[key].destroy(); charts[key] = null; }
/* ── Shared: show/hide empty state ── */
function setChartEmpty(canvasId, emptyId, isEmpty) {
const canvas = [Link](canvasId);
const empty = [Link](emptyId);
if (!canvas || !empty) return;
[Link] = isEmpty ? "none" : "block";
[Link]("hidden", !isEmpty);
/* ── TODAY: doughnut chart by category ── */
function renderTodayChart() {
destroyChart("today");
const today = new Date().toISOString().split("T")[0];
const todayExpenses = [Link]((e) => [Link] === today);
if (![Link]) {
setChartEmpty("todayChart", "todayEmpty", true);
[Link]("todayLegend").innerHTML = "";
return;
}
setChartEmpty("todayChart", "todayEmpty", false);
// Aggregate by category
const catMap = {};
[Link]((e) => {
if (!catMap[[Link]]) {
catMap[[Link]] = { total: 0, color: [Link], icon: [Link] };
catMap[[Link]].total += [Link];
});
const labels = [Link](catMap);
const data = [Link]((l) => catMap[l].total);
const bgColors = [Link]((l) => CHART_PALETTE[catMap[l].color] || CHART_PALETTE.gray);
const totalToday = [Link]((a, b) => a + b, 0);
const ctx = [Link]("todayChart");
[Link] = new Chart(ctx, {
type: "doughnut",
data: {
labels,
datasets: [{
data,
backgroundColor: bgColors,
borderWidth: 3,
borderColor: "#ffffff",
hoverOffset: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
cutout: "65%",
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => ` ${formatCurrency([Link])} (${(([Link] / totalToday) * 100).toFixed(1)}%)`
});
// Custom legend
const legend = [Link]("todayLegend");
[Link] = [Link]((label, i) => `
<div class="legend-item">
<span class="legend-dot" style="background:${bgColors[i]}"></span>
<span class="legend-label">${escapeHtml(label)}</span>
<span class="legend-value">${formatCurrency(data[i])}</span>
</div>
`).join("");
/* ── MONTH: bar chart — daily totals for current month ── */
function renderMonthChart() {
destroyChart("month");
const now = new Date();
const year = [Link]();
const month = [Link]();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const dailyTotals = Array(daysInMonth).fill(0);
[Link]((e) => {
const d = new Date(`${[Link]}T00:00:00`);
if ([Link]() === year && [Link]() === month) {
dailyTotals[[Link]() - 1] += [Link];
});
const hasData = [Link]((v) => v > 0);
if (!hasData) { setChartEmpty("monthChart", "monthEmpty", true); return; }
setChartEmpty("monthChart", "monthEmpty", false);
const labels = [Link]({ length: daysInMonth }, (_, i) => String(i + 1));
const ctx = [Link]("monthChart");
[Link] = new Chart(ctx, {
type: "bar",
data: {
labels,
datasets: [{
label: "Daily Spending",
data: dailyTotals,
backgroundColor: "rgba(37, 99, 235, 0.75)",
borderRadius: 6,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: (items) => `Day ${items[0].label}`,
label: (ctx) => ` ${formatCurrency([Link].y)}`
},
scales: {
x: {
grid: { display: false },
ticks: { font: { size: 11 } }
},
y: {
grid: { color: "rgba(0,0,0,0.05)" },
ticks: {
callback: (v) => formatCurrency(v),
font: { size: 11 }
}
}
});
/* ── YEAR: bar chart — monthly totals for current year ── */
function renderYearChart() {
destroyChart("year");
const year = new Date().getFullYear();
const monthlyTotals = Array(12).fill(0);
[Link]((e) => {
const d = new Date(`${[Link]}T00:00:00`);
if ([Link]() === year) monthlyTotals[[Link]()] += [Link];
});
const hasData = [Link]((v) => v > 0);
if (!hasData) { setChartEmpty("yearChart", "yearEmpty", true); return; }
setChartEmpty("yearChart", "yearEmpty", false);
const ctx = [Link]("yearChart");
[Link] = new Chart(ctx, {
type: "bar",
data: {
labels: MONTH_NAMES,
datasets: [{
label: "Monthly Spending",
data: monthlyTotals,
backgroundColor: [Link]((v) =>
v === [Link](...monthlyTotals)
? "rgba(37, 99, 235, 0.85)"
: "rgba(37, 99, 235, 0.4)"
),
borderRadius: 8,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => ` ${formatCurrency([Link].y)}`
},
scales: {
x: { grid: { display: false }, ticks: { font: { size: 12 } } },
y: {
grid: { color: "rgba(0,0,0,0.05)" },
ticks: { callback: (v) => formatCurrency(v), font: { size: 11 } }
});
}
// ─── CATEGORY BUDGET ALLOCATION (NEW) ────────────────────────────────────────
function setupAllocTabs() {
[Link](".alloc-tab").forEach((btn) => {
[Link]("click", () => {
[Link](".alloc-tab").forEach((b) => [Link]("active"));
[Link]("active");
renderCategoryBudgetAllocation();
});
});
function renderCategoryBudgetAllocation() {
const container = [Link]("categoryAllocationList");
if (!container) return;
const numCats = [Link];
if (!numCats) {
[Link] = `<p class="alloc-empty">No categories found.</p>`;
return;
// Determine active period tab
const activeTab = [Link](".[Link]");
const period = activeTab?.[Link] || "monthly";
const budgetMap = {
weekly: [Link],
monthly: [Link],
yearly: [Link]
};
const budget = Number(budgetMap[period] || 0);
if (!budget || budget <= 0) {
[Link] = `<p class="alloc-empty">Set a ${period} budget in Settings → Budget Settings
to see per-category allocation.</p>`;
return;
const perCategory = budget / numCats;
const pct = (100 / numCats).toFixed(1);
// Also compute how much each category has actually been spent this period
const spentMap = {};
[Link]((cat) => { spentMap[[Link]] = 0; });
const now = new Date();
[Link]((e) => {
const d = new Date(`${[Link]}T00:00:00`);
let inPeriod = false;
if (period === "weekly") {
const diff = [Link]() === 0 ? 6 : [Link]() - 1;
const startOfWeek = new Date(now);
[Link](0, 0, 0, 0);
[Link]([Link]() - diff);
const endOfWeek = new Date(startOfWeek);
[Link]([Link]() + 7);
inPeriod = d >= startOfWeek && d < endOfWeek;
} else if (period === "monthly") {
inPeriod = [Link]() === [Link]() && [Link]() === [Link]();
} else if (period === "yearly") {
inPeriod = [Link]() === [Link]();
if (inPeriod && [Link]([Link])) {
spentMap[[Link]] += [Link];
});
const periodLabel = { weekly: "this week", monthly: "this month", yearly: "this year" }[period];
[Link] = [Link]((cat) => {
const spent = spentMap[[Link]] || 0;
const remaining = perCategory - spent;
const usedPct = [Link]((spent / perCategory) * 100, 100);
const isOver = spent > perCategory;
const barColor = isOver
? "#ef4444"
: usedPct > 80
? "#f59e0b"
: "var(--primary)";
const hintText = isOver
? `Over by ${formatCurrency([Link](remaining))}`
: `${formatCurrency(remaining)} left`;
return `
<div class="alloc-item">
<div class="alloc-left">
<div class="icon-circle ${[Link] || "gray"}">${[Link]}</div>
<div class="alloc-meta">
<span class="alloc-name">${escapeHtml([Link])}</span>
<span class="alloc-hint ${isOver ? "over" : ""}">${hintText} ${periodLabel}</span>
</div>
</div>
<div class="alloc-right">
<div class="alloc-bar-track">
<div class="alloc-bar-fill" style="width:${[Link](1)}%; background:${barColor};"></div>
</div>
<div class="alloc-numbers">
<span class="alloc-spent">${formatCurrency(spent)}</span>
<span class="alloc-sep">/</span>
<span class="alloc-cap">${formatCurrency(perCategory)}</span>
<span class="alloc-pct-badge">${pct}%</span>
</div>
</div>
</div>
`;
}).join("");
}
// ─── HELPERS ─────────────────────────────────────────────────────────────────
function setTodayDate() {
[Link]("expenseDate").value = new Date().toISOString().split("T")[0];
function sumExpenses(expenses) {
return [Link]((sum, e) => sum + Number([Link] || 0), 0);
function getCurrentMonthExpenses() {
const now = new Date();
return [Link]((e) => {
const d = new Date(`${[Link]}T00:00:00`);
return [Link]() === [Link]() && [Link]() === [Link]();
});
function getCurrentWeekTotal() {
const now = new Date();
const diff = [Link]() === 0 ? 6 : [Link]() - 1;
const startOfWeek = new Date(now);
[Link](0, 0, 0, 0);
[Link]([Link]() - diff);
const endOfWeek = new Date(startOfWeek);
[Link]([Link]() + 7);
return [Link]((sum, e) => {
const d = new Date(`${[Link]}T00:00:00`);
return (d >= startOfWeek && d < endOfWeek) ? sum + Number([Link] || 0) : sum;
}, 0);
function getCurrentYearTotal() {
const year = new Date().getFullYear();
return [Link]((sum, e) => {
return new Date(`${[Link]}T00:00:00`).getFullYear() === year
? sum + Number([Link] || 0) : sum;
}, 0);
function buildBudgetHint(spent, budget) {
if (!budget || budget <= 0) return "No budget set";
const rem = budget - spent;
return rem >= 0
? `Remaining: ${formatCurrency(rem)}`
: `Over budget by ${formatCurrency([Link](rem))}`;
function formatCurrency(amount) {
const currency = [Link]?.currency || "PHP";
try {
return new [Link]("en-PH", { style: "currency", currency }).format(amount || 0);
} catch {
return `${currency} ${Number(amount || 0).toFixed(2)}`;
function formatDisplayDate(dateString) {
return new Date(`${dateString}T00:00:00`).toLocaleDateString("en-PH", {
year: "numeric", month: "short", day: "numeric"
});
function escapeHtml(text) {
const div = [Link]("div");
[Link] = text;
return [Link];