(á) áĄáááşááŻáśá¸áĄááąáá˛áˇ cloudflare account áá áşááŻáážááááŻáˇáááŻáĄááşááŤáááş áĄáąáŹááşá link áááąáá˝áŹá¸ááźáŽá¸ááąáŹáˇ áĄáá˝ááşááá° áá˝ááˇáşáááŻáˇáááŤáááşá
Cloudflare áááŻáážáááşááźáŽá¸ááąáŹáˇ account áá˝ááˇáşááŤá
(á) Cloudflare account áááŹáááş dashboard áááąáážáŹ áááşáááş menu áááą Compute - workers & pages ááᯠáááşááŤ
(á) Create Application áááŻáážáááşááźáŽá¸ ááąáŹáˇ worker page áá
áşáᯠááŻááşááŤáááşá
Start with hello world ááᯠáá˝áąá¸á፠worker name áážáŹ áááá ááąá¸áááŻááąáŹááŹáááş ááᯠááąá¸áááŻááşá፠ááźáŽá¸áááş deploy áááŻáážáááşáááŻááşááŤá
Worker page áá˛áˇ ááŹáááşáážáŹ edit code áááŻáážáááşááźáŽá¸ááąáŹáˇ áá˝ááˇáşáááŻááşááŤ
ááźáŽá¸áááş worker.js áĄáąáŹááşá ááąá¸ááŹá¸áá˛áˇ code áá˝áąááᯠselect áážááşááźáŽá¸ áĄááŻááşááźááşááŤá áĄáąáŹááşá ááąá¸ááŹá¸áá˛áˇ code áĄááŻááşááŻáśá¸ááᯠcopy áá°á¸áá°ááźáŽá¸ áĄá
áŹá¸áááŻá¸á፠ááŹáááşáážáŹ áážááá˛áˇ deploy ááᯠáážáááşáááŻááşááŤ
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/index.js
var index_default = {
async fetch(request, env) {
const url = new URL(request.url);
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
async function getActiveMonth() {
let month = await env.finance_db.prepare("SELECT * FROM months WHERE is_active = 1 ORDER BY id DESC LIMIT 1").first();
if (!month) {
const d = /* @__PURE__ */ new Date();
const mName = d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, "0");
const res = await env.finance_db.prepare("INSERT INTO months (month_name, cash_opening, kpay_opening, is_active) VALUES (?, 0, 0, 1) RETURNING *").bind(mName).first();
month = res;
}
return month;
}
__name(getActiveMonth, "getActiveMonth");
if (url.pathname === "/api/months" && request.method === "GET") {
const { results } = await env.finance_db.prepare("SELECT id, month_name, is_active FROM months ORDER BY id DESC").all();
return new Response(JSON.stringify(results), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/api/month/start" && request.method === "POST") {
const active = await getActiveMonth();
const { results } = await env.finance_db.prepare(`
SELECT wallet, type, SUM(amount) as total
FROM transactions WHERE month_id = ? GROUP BY wallet, type
`).bind(active.id).all();
let cash = active.cash_opening;
let kpay = active.kpay_opening;
results.forEach((row) => {
const val = row.type === "income" ? row.total : -row.total;
if (row.wallet === "cash") cash += val;
if (row.wallet === "kpay") kpay += val;
});
const body = await request.json();
const newMonthName = body.month_name || "New Month";
await env.finance_db.prepare("UPDATE months SET is_active = 0 WHERE id = ?").bind(active.id).run();
await env.finance_db.prepare("INSERT INTO months (month_name, cash_opening, kpay_opening, is_active) VALUES (?, ?, ?, 1)").bind(newMonthName, cash, kpay).run();
return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/") {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Finance App</title>
<script src="https://cdn.tailwindcss.com"><\/script>
<style>
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
select#monthSelector {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='white'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 1em;
padding-right: 2rem;
}
</style>
</head>
<body class="bg-gray-100 p-4 font-sans pb-20">
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">\u1000\u102D\u102F\u101A\u103A\u1015\u102D\u102F\u1004\u103A \u1004\u103D\u1031\u1005\u102C\u101B\u1004\u103A\u1038</h1>
<button onclick="startNewMonth()" class="text-xs bg-gray-800 text-white px-3 py-1.5 rounded shadow hover:bg-gray-700 transition-colors">\u101C\u101E\u1005\u103A\u1005\u1019\u101A\u103A \u{1F504}</button>
</div>
<!-- Summary Dashboard -->
<div class="bg-blue-50 p-4 rounded-lg mb-6 shadow-sm border border-blue-100 relative mt-4">
<!-- Month Selector -->
<div class="absolute -top-4 left-4">
<select id="monthSelector" onchange="changeMonth()" class="bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-full shadow-sm border-2 border-white cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300">
<option value="active">Loading...</option>
</select>
</div>
<div onclick="setTxFilter('all', 'all')" class="cursor-pointer hover:bg-blue-100 p-2 rounded -mx-2 mb-1 transition-colors mt-2">
<h2 class="text-sm text-gray-500 font-semibold mb-1">Total Balance (\u1021\u102C\u1038\u101C\u102F\u1036\u1038)</h2>
<p class="text-3xl font-bold text-blue-600" id="totalBalance">0 Ks</p>
</div>
<div class="flex justify-between border-t border-blue-200 pt-3">
<div onclick="setTxFilter('cash', 'all', null)" class="cursor-pointer hover:bg-blue-100 p-2 rounded -ml-2 transition-colors">
<p class="text-xs text-gray-500">Cash (\u101C\u1000\u103A\u1004\u1004\u103A\u1038)</p>
<p class="font-semibold text-gray-700" id="cashBalance">0 Ks</p>
</div>
<div onclick="setTxFilter('kpay', 'all', null)" class="cursor-pointer hover:bg-blue-100 p-2 rounded -mr-2 text-right transition-colors">
<p class="text-xs text-gray-500">KPay</p>
<p class="font-semibold text-gray-700" id="kpayBalance">0 Ks</p>
</div>
</div>
</div>
<!-- Add/Edit Transaction Form -->
<div id="formSection">
<div id="saveNotice" class="hidden mb-3 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700"></div>
<form id="txForm" class="space-y-4 bg-white p-1 -mx-1 rounded-lg transition-colors duration-300">
<div class="flex justify-between items-center mb-2 hidden" id="editHeader">
<span class="text-sm font-bold text-blue-600">\u270F\uFE0F \u1005\u102C\u101B\u1004\u103A\u1038\u1015\u103C\u1004\u103A\u1006\u1004\u103A\u101B\u1014\u103A</span>
</div>
<div class="grid grid-cols-2 gap-4">
<button type="button" id="btnIncome" class="w-full py-2 bg-green-100 text-green-700 font-bold rounded border border-green-300">\u101D\u1004\u103A\u1004\u103D\u1031 (+)</button>
<button type="button" id="btnExpense" class="w-full py-2 bg-red-500 text-white font-bold rounded border border-red-600 shadow-md">\u1011\u103D\u1000\u103A\u1004\u103D\u1031 (-)</button>
</div>
<input type="hidden" id="txType" value="expense">
<div>
<label class="block text-sm font-medium text-gray-700">\u1015\u1019\u102C\u100F</label>
<input type="number" id="amount" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Wallet</label>
<select id="wallet" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
<option value="cash">Cash (\u101C\u1000\u103A\u1004\u1004\u103A\u1038)</option>
<option value="kpay">KPay</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">\u101B\u1000\u103A\u1005\u103D\u1032</label>
<input type="date" id="date" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
</div>
</div>
<div id="categoryDiv">
<label class="block text-sm font-medium text-gray-700">\u1021\u1019\u103B\u102D\u102F\u1038\u1021\u1005\u102C\u1038</label>
<select id="category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
<!-- Categories will be loaded here -->
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">\u1019\u103E\u1010\u103A\u1001\u103B\u1000\u103A</label>
<input type="text" id="note" placeholder="\u1018\u102C\u1021\u1010\u103D\u1000\u103A\u101C\u1032..." class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border">
</div>
<div class="flex gap-2">
<button type="submit" id="btnSubmit" class="flex-1 bg-blue-600 text-white font-bold py-3 rounded-md shadow hover:bg-blue-700 transition-colors">\u1005\u102C\u101B\u1004\u103A\u1038\u101E\u103D\u1004\u103A\u1038\u1019\u100A\u103A</button>
<button type="button" id="btnCancelEdit" onclick="cancelEdit()" class="hidden w-1/3 bg-gray-400 text-white font-bold py-3 rounded-md shadow hover:bg-gray-500 transition-colors">\u1019\u101C\u102F\u1015\u103A\u1010\u1031\u102C\u1037\u1015\u102B</button>
</div>
</form>
</div>
<div id="oldMonthWarning" class="hidden mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-800 text-sm text-center">
\u26A0\uFE0F \u101A\u1001\u1004\u103A\u101C\u101F\u1031\u102C\u1004\u103A\u1038 \u1005\u102C\u101B\u1004\u103A\u1038\u1019\u103B\u102C\u1038\u1000\u102D\u102F \u1000\u103C\u100A\u1037\u103A\u101B\u103E\u102F\u1014\u1031\u1015\u102B\u101E\u100A\u103A\u104B \u1021\u101E\u1005\u103A\u101E\u103D\u1004\u103A\u1038\u101B\u1014\u103A Current Month \u1000\u102D\u102F \u1015\u103C\u1014\u103A\u101B\u103D\u1031\u1038\u1015\u1031\u1038\u1015\u102B\u104B
</div>
<!-- Category Breakdown -->
<div class="mt-8 pt-6 border-t border-gray-200">
<h3 class="text-lg font-bold text-gray-800 mb-4">\u1021\u1019\u103B\u102D\u102F\u1038\u1021\u1005\u102C\u1038\u1021\u101C\u102D\u102F\u1000\u103A \u1021\u101E\u102F\u1036\u1038\u1005\u101B\u102D\u1010\u103A</h3>
<div id="categoryBreakdown" class="space-y-2">
<p class="text-gray-500 text-sm">Loading...</p>
</div>
</div>
<!-- Recent Transactions Section -->
<div class="mt-8 pt-6 border-t border-gray-200" id="recentSection">
<div class="flex justify-between items-center gap-3 mb-3">
<h3 class="text-lg font-bold text-gray-800">\u1005\u102C\u101B\u1004\u103A\u1038\u1019\u103E\u1010\u103A\u1010\u1019\u103A\u1038\u1019\u103B\u102C\u1038</h3>
<div id="recentFilterTotal" class="text-sm font-bold text-blue-700 bg-blue-50 border border-blue-200 px-3 py-1.5 rounded-full">Total: 0 Ks</div>
</div>
<!-- Filters -->
<div class="flex overflow-x-auto pb-2 mb-4 gap-2 hide-scrollbar">
<button id="btn-flt-all" onclick="setTxFilter('all', 'all', 'btn-flt-all')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-blue-600 text-white shadow-sm transition-all ring-2 ring-blue-300">All</button>
<button id="btn-flt-allin" onclick="setTxFilter('all', 'income', 'btn-flt-allin')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all">All in</button>
<button id="btn-flt-allout" onclick="setTxFilter('all', 'expense', 'btn-flt-allout')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all">All out</button>
<button id="btn-flt-cashin" onclick="setTxFilter('cash', 'income', 'btn-flt-cashin')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all">Cash in</button>
<button id="btn-flt-kpayin" onclick="setTxFilter('kpay', 'income', 'btn-flt-kpayin')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all">Kpay in</button>
<button id="btn-flt-cashout" onclick="setTxFilter('cash', 'expense', 'btn-flt-cashout')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all">Cash out</button>
<button id="btn-flt-kpayout" onclick="setTxFilter('kpay', 'expense', 'btn-flt-kpayout')" class="whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all">Kpay out</button>
</div>
<div id="recentTransactions" class="space-y-2">
<p class="text-gray-500 text-sm">Loading...</p>
</div>
</div>
</div>
<script>
document.getElementById('date').valueAsDate = new Date();
let activeWalletFlt = 'all';
let activeTypeFlt = 'all';
let editingTxId = null;
let selectedMonthId = 'active';
window.loadedTxs = {};
const btnIncome = document.getElementById('btnIncome');
const btnExpense = document.getElementById('btnExpense');
const txType = document.getElementById('txType');
const categoryDiv = document.getElementById('categoryDiv');
const monthSelector = document.getElementById('monthSelector');
btnIncome.onclick = () => {
txType.value = 'income';
btnIncome.className = "w-full py-2 bg-green-500 text-white font-bold rounded border border-green-600 shadow-md";
btnExpense.className = "w-full py-2 bg-red-100 text-red-700 font-bold rounded border border-red-300";
categoryDiv.style.display = 'none';
};
btnExpense.onclick = () => {
txType.value = 'expense';
btnExpense.className = "w-full py-2 bg-red-500 text-white font-bold rounded border border-red-600 shadow-md";
btnIncome.className = "w-full py-2 bg-green-100 text-green-700 font-bold rounded border border-green-300";
categoryDiv.style.display = 'block';
};
fetch('/api/categories').then(res => res.json()).then(data => {
const select = document.getElementById('category');
data.forEach(cat => {
select.innerHTML += \`<option value="\${cat.id}">\${cat.name}</option>\`;
});
});
function loadMonths() {
fetch('/api/months').then(res => res.json()).then(data => {
monthSelector.innerHTML = '';
data.forEach(m => {
const label = m.is_active ? m.month_name + ' (Current)' : m.month_name;
const val = m.is_active ? 'active' : m.id;
monthSelector.innerHTML += \`<option value="\${val}" \${m.is_active && selectedMonthId==='active' ? 'selected' : ''}>\${label}</option>\`;
});
});
}
loadMonths();
function changeMonth() {
selectedMonthId = monthSelector.value;
if (selectedMonthId === 'active') {
document.getElementById('formSection').classList.remove('hidden');
document.getElementById('oldMonthWarning').classList.add('hidden');
monthSelector.classList.replace('bg-gray-600', 'bg-blue-600');
} else {
document.getElementById('formSection').classList.add('hidden');
document.getElementById('oldMonthWarning').classList.remove('hidden');
monthSelector.classList.replace('bg-blue-600', 'bg-gray-600');
cancelEdit();
}
loadDashboard();
}
function toggleCat(id) {
const el = document.getElementById('cat-tx-' + id);
const icon = document.getElementById('cat-icon-' + id);
if (el.classList.contains('hidden')) {
el.classList.remove('hidden');
icon.style.transform = 'rotate(180deg)';
} else {
el.classList.add('hidden');
icon.style.transform = 'rotate(0deg)';
}
}
function handleEdit(id) {
if (selectedMonthId !== 'active') return alert('\u101C\u101F\u1031\u102C\u1004\u103A\u1038\u1005\u102C\u101B\u1004\u103A\u1038\u1019\u103B\u102C\u1038\u1000\u102D\u102F \u1015\u103C\u1004\u103A\u1006\u1004\u103A\u104D\u1019\u101B\u1015\u102B\u104B');
const tx = window.loadedTxs[id];
if (!tx) return;
editingTxId = id;
document.getElementById('amount').value = tx.amount;
document.getElementById('wallet').value = tx.wallet;
document.getElementById('date').value = tx.date;
document.getElementById('note').value = tx.note || '';
if (tx.type === 'income') {
btnIncome.click();
} else {
btnExpense.click();
document.getElementById('category').value = tx.category_id;
}
document.getElementById('btnSubmit').innerText = '\u1015\u103C\u1004\u103A\u1006\u1004\u103A\u1019\u100A\u103A';
document.getElementById('btnSubmit').classList.replace('bg-blue-600', 'bg-yellow-500');
document.getElementById('btnSubmit').classList.replace('hover:bg-blue-700', 'hover:bg-yellow-600');
document.getElementById('btnCancelEdit').classList.remove('hidden');
document.getElementById('editHeader').classList.remove('hidden');
document.getElementById('txForm').classList.add('bg-yellow-50', 'ring-2', 'ring-yellow-200');
window.scrollTo({top: 0, behavior: 'smooth'});
}
function cancelEdit() {
editingTxId = null;
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
document.getElementById('date').valueAsDate = new Date();
document.getElementById('btnSubmit').innerText = '\u1005\u102C\u101B\u1004\u103A\u1038\u101E\u103D\u1004\u103A\u1038\u1019\u100A\u103A';
document.getElementById('btnSubmit').classList.replace('bg-yellow-500', 'bg-blue-600');
document.getElementById('btnSubmit').classList.replace('hover:bg-yellow-600', 'hover:bg-blue-700');
document.getElementById('btnCancelEdit').classList.add('hidden');
document.getElementById('editHeader').classList.add('hidden');
document.getElementById('txForm').classList.remove('bg-yellow-50', 'ring-2', 'ring-yellow-200');
}
async function handleDelete(id) {
if (selectedMonthId !== 'active') return alert('\u101C\u101F\u1031\u102C\u1004\u103A\u1038\u1005\u102C\u101B\u1004\u103A\u1038\u1019\u103B\u102C\u1038\u1000\u102D\u102F \u1016\u103B\u1000\u103A\u104D\u1019\u101B\u1015\u102B\u104B');
if (!confirm('\u1012\u102E\u1005\u102C\u101B\u1004\u103A\u1038\u1000\u102D\u102F \u1016\u103B\u1000\u103A\u1019\u103E\u102C \u101E\u1031\u1001\u103B\u102C\u1015\u103C\u102E\u101C\u102C\u1038?')) return;
await fetch('/api/transactions/' + id, { method: 'DELETE' });
if (editingTxId === id) cancelEdit();
loadDashboard();
}
async function startNewMonth() {
const mName = prompt("\u101C\u1021\u101E\u1005\u103A\u1021\u1010\u103D\u1000\u103A \u1014\u102C\u1019\u100A\u103A\u1015\u1031\u1038\u1015\u102B (\u1025\u1015\u1019\u102C: May 2024)");
if(!mName) return;
if(!confirm(\`\u101A\u1001\u102F\u101C\u1000\u103A\u1000\u103B\u1014\u103A\u1004\u103D\u1031\u1019\u103B\u102C\u1038\u1000\u102D\u102F "\${mName}" \u1021\u1010\u103D\u1000\u103A Opening Balance \u1021\u1016\u103C\u1005\u103A\u1015\u103C\u1031\u102C\u1004\u103A\u1038\u1015\u103C\u102E\u1038 \u101C\u101E\u1005\u103A \u1005\u1010\u1004\u103A\u1019\u103E\u102C \u101E\u1031\u1001\u103B\u102C\u1015\u103C\u102E\u101C\u102C\u1038?\`)) return;
await fetch('/api/month/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ month_name: mName })
});
alert('\u101C\u101E\u1005\u103A \u1005\u1010\u1004\u103A\u1015\u102B\u1015\u103C\u102E!');
selectedMonthId = 'active';
loadMonths();
loadDashboard();
}
function setTxFilter(wallet, type, btnId) {
activeWalletFlt = wallet;
activeTypeFlt = type;
if (wallet === 'all' && type === 'all' && !btnId) {
btnId = 'btn-flt-all';
}
const styles = {
'btn-flt-all': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-white text-gray-700 border border-gray-300 shadow-sm transition-all",
'btn-flt-allin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all",
'btn-flt-allout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all",
'btn-flt-cashin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all",
'btn-flt-kpayin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-50 text-green-700 border border-green-200 shadow-sm transition-all",
'btn-flt-cashout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all",
'btn-flt-kpayout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-50 text-red-700 border border-red-200 shadow-sm transition-all"
};
const activeStyles = {
'btn-flt-all': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-blue-600 text-white shadow-sm transition-all ring-2 ring-blue-300",
'btn-flt-allin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-500 text-white shadow-sm transition-all ring-2 ring-green-300",
'btn-flt-allout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-500 text-white shadow-sm transition-all ring-2 ring-red-300",
'btn-flt-cashin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-500 text-white shadow-sm transition-all ring-2 ring-green-300",
'btn-flt-kpayin': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-green-500 text-white shadow-sm transition-all ring-2 ring-green-300",
'btn-flt-cashout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-500 text-white shadow-sm transition-all ring-2 ring-red-300",
'btn-flt-kpayout': "whitespace-nowrap px-4 py-1.5 text-xs font-bold rounded-full bg-red-500 text-white shadow-sm transition-all ring-2 ring-red-300"
};
Object.keys(styles).forEach(id => {
const el = document.getElementById(id);
if(el) el.className = (btnId === id) ? activeStyles[id] : styles[id];
});
loadRecentTransactions();
}
function loadRecentTransactions() {
let apiUrl = \`/api/transactions/recent?month=\${selectedMonthId}&\`;
if (activeWalletFlt !== 'all') apiUrl += 'wallet=' + activeWalletFlt + '&';
if (activeTypeFlt !== 'all') apiUrl += 'type=' + activeTypeFlt;
fetch(apiUrl).then(res => res.json()).then(data => {
const div = document.getElementById('recentTransactions');
const totalEl = document.getElementById('recentFilterTotal');
const total = data.reduce((sum, tx) => sum + Number(tx.amount || 0), 0);
if (activeTypeFlt === 'all') {
totalEl.classList.add('hidden');
} else {
totalEl.classList.remove('hidden');
const totalLabel = activeTypeFlt === 'expense' ? 'Total Out' : 'Total In';
const totalColor = activeTypeFlt === 'expense'
? 'text-red-700 bg-red-50 border border-red-200'
: 'text-green-700 bg-green-50 border border-green-200';
totalEl.className = 'text-sm font-bold px-3 py-1.5 rounded-full ' + totalColor;
totalEl.innerText = totalLabel + ': ' + total.toLocaleString() + ' Ks';
}
if (data.length === 0) {
div.innerHTML = '<p class="text-gray-500 text-sm text-center py-4 border border-dashed rounded bg-gray-50">\u1019\u103E\u1010\u103A\u1010\u1019\u103A\u1038 \u1019\u101B\u103E\u102D\u101E\u1031\u1038\u1015\u102B</p>';
return;
}
div.innerHTML = data.map(tx => {
window.loadedTxs[tx.id] = tx;
const isInc = tx.type === 'income';
const amountColor = isInc ? 'text-green-600' : 'text-red-600';
const sign = isInc ? '+' : '-';
const catName = isInc ? '\u101D\u1004\u103A\u1004\u103D\u1031' : tx.category_name;
const controls = selectedMonthId === 'active' ? \`
<div class="flex items-center gap-1.5 border-l pl-2 border-gray-200">
<button onclick="handleEdit(\${tx.id})" class="text-blue-500 font-bold p-1 hover:bg-blue-50 rounded">\u270F\uFE0F</button>
<button onclick="handleDelete(\${tx.id})" class="text-red-500 font-bold p-1 hover:bg-red-50 rounded">\u{1F5D1}\uFE0F</button>
</div>
\` : '';
return \`
<div class="p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:border-blue-200 transition-colors">
<div class="flex justify-between items-start mb-1">
<span class="font-bold text-sm text-gray-800">\${catName}</span>
<span class="font-bold \${amountColor}">\${sign}\${tx.amount.toLocaleString()} Ks</span>
</div>
<div class="flex justify-between items-center text-xs text-gray-500 mt-2">
<div class="flex items-center gap-2">
<span class="bg-gray-100 px-2 py-0.5 rounded text-[10px] font-bold text-gray-600 uppercase border border-gray-200">\${tx.wallet}</span>
<span>\${tx.date}</span>
</div>
<div class="flex items-center gap-3">
<span class="italic max-w-[80px] truncate" title="\${tx.note || ''}">\${tx.note || ''}</span>
\${controls}
</div>
</div>
</div>
\`}).join('');
});
}
function loadDashboard() {
fetch(\`/api/summary?month=\${selectedMonthId}\`).then(res => res.json()).then(data => {
document.getElementById('totalBalance').innerText = data.total.toLocaleString() + " Ks";
document.getElementById('cashBalance').innerText = data.cash.toLocaleString() + " Ks";
document.getElementById('kpayBalance').innerText = data.kpay.toLocaleString() + " Ks";
});
fetch(\`/api/expenses/category?month=\${selectedMonthId}\`).then(res => res.json()).then(data => {
const div = document.getElementById('categoryBreakdown');
div.innerHTML = data.map(item => {
let shortName = item.name
.replace('\u1044\u104B \u1000\u101C\u1031\u1038\u1000\u103B\u1031\u102C\u1004\u103A\u1038\u1014\u1032\u1037\u1019\u102F\u1014\u1037\u103A', '\u1044\u104B \u1000\u103B\u1031\u102C\u1004\u103A\u1038')
.replace('\u1046\u104B \u101C\u102F\u1015\u103A\u1004\u1014\u103A\u1038\u101E\u102F\u1036\u1038 \u1005\u102D\u102F\u1000\u103A\u1004\u103D\u1031', '\u1046\u104B \u101C\u102F\u1015\u103A\u1004\u1014\u103A\u1038')
.replace('\u1042\u104B \u1005\u102C\u1038\u1005\u101B\u102D\u1010\u103A', '\u1042\u104B \u1005\u102C\u1038\u101E\u1031\u102C\u1000\u103A')
.replace('\u1043\u104B \u1021\u101D\u1010\u103A\u1021\u1005\u102C\u1038', '\u1043\u104B \u1021\u101D\u1010\u103A');
// If NO transactions
if (item.transactions.length === 0) {
return \`
<div class="mb-2 bg-white rounded border border-gray-100 overflow-hidden shadow-sm opacity-60">
<div class="flex justify-between items-center p-3">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-500 leading-tight truncate">\${shortName}</span>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="font-medium text-gray-400">0 Ks</span>
</div>
</div>
</div>
\`;
}
// If HAS transactions
const txHtml = item.transactions.map(tx => {
window.loadedTxs[tx.id] = tx;
return \`
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-0 hover:bg-gray-50 -mx-4 px-4 transition-colors">
<div class="flex flex-col">
<span class="text-xs text-gray-600 font-medium">\${tx.date} <span class="text-gray-400 font-normal ml-1 italic">\${tx.note ? '- '+tx.note : ''}</span></span>
<span class="text-[10px] text-gray-400 uppercase mt-0.5">\${tx.wallet}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-xs font-semibold text-gray-700">\${tx.amount.toLocaleString()} Ks</span>
</div>
</div>
\`}).join('');
return \`
<div class="mb-2 bg-gray-50 rounded border border-gray-200 overflow-hidden shadow-sm">
<div onclick="toggleCat(\${item.id})" class="flex justify-between items-center p-3 cursor-pointer hover:bg-gray-100 transition-colors gap-2">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="text-sm font-bold text-gray-700 leading-tight truncate">\${shortName}</span>
<span class="text-[10px] bg-white border border-gray-200 text-gray-600 px-1.5 py-0.5 rounded-full whitespace-nowrap">\${item.transactions.length}</span>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="font-bold text-red-600">\${item.total.toLocaleString()} Ks</span>
<svg id="cat-icon-\${item.id}" class="w-4 h-4 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</div>
</div>
<div id="cat-tx-\${item.id}" class="hidden bg-white px-4 py-1 border-t border-gray-100">
\${txHtml}
</div>
</div>
\`}).join('');
});
loadRecentTransactions();
}
loadDashboard();
function showSaveNotice(message, kind = 'success') {
const notice = document.getElementById('saveNotice');
notice.className = kind === 'success'
? 'mb-3 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700'
: 'mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700';
notice.textContent = message;
notice.classList.remove('hidden');
clearTimeout(window.saveNoticeTimer);
window.saveNoticeTimer = setTimeout(() => {
notice.classList.add('hidden');
}, 2500);
}
document.getElementById('txForm').onsubmit = async (e) => {
e.preventDefault();
const payload = {
type: txType.value,
amount: parseFloat(document.getElementById('amount').value),
wallet: document.getElementById('wallet').value,
date: document.getElementById('date').value,
category_id: txType.value === 'expense' ? parseInt(document.getElementById('category').value) : null,
note: document.getElementById('note').value
};
try {
if (editingTxId) {
await fetch('/api/transactions/' + editingTxId, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
cancelEdit();
showSaveNotice('\u1005\u102C\u101B\u1004\u103A\u1038\u1000\u102D\u102F \u1021\u1031\u102C\u1004\u103A\u1019\u103C\u1004\u103A\u1005\u103D\u102C \u1015\u103C\u1004\u103A\u1006\u1004\u103A\u1015\u103C\u102E\u1038\u1015\u102B\u1015\u103C\u102E \u2705');
} else {
await fetch('/api/transactions', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
showSaveNotice('\u1005\u102C\u101B\u1004\u103A\u1038\u101E\u103D\u1004\u103A\u1038\u1015\u103C\u102E\u1038\u1015\u102B\u1015\u103C\u102E \u2705');
}
loadDashboard();
} catch (err) {
showSaveNotice('\u1010\u1005\u103A\u1001\u102F\u1001\u102F\u1019\u103E\u102C\u1038\u101E\u103D\u102C\u1038\u1015\u102B\u1010\u101A\u103A\u104B \u1011\u1015\u103A\u1005\u1019\u103A\u1038\u1000\u103C\u100A\u1037\u103A\u1015\u102B\u104B', 'error');
}
};
<\/script>
</body>
</html>`;
return new Response(html, {
headers: { "Content-Type": "text/html;charset=UTF-8" }
});
}
if (url.pathname === "/api/categories" && request.method === "GET") {
const { results } = await env.finance_db.prepare("SELECT * FROM categories").all();
return new Response(JSON.stringify(results), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/api/transactions" && request.method === "POST") {
const activeMonth = await getActiveMonth();
const body = await request.json();
const stmt = env.finance_db.prepare(
"INSERT INTO transactions (month_id, date, type, wallet, amount, category_id, note) VALUES (?, ?, ?, ?, ?, ?, ?)"
).bind(activeMonth.id, body.date, body.type, body.wallet, body.amount, body.category_id, body.note);
await stmt.run();
return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname.startsWith("/api/transactions/") && request.method === "PUT") {
const id = url.pathname.split("/").pop();
const body = await request.json();
const stmt = env.finance_db.prepare(
"UPDATE transactions SET date = ?, type = ?, wallet = ?, amount = ?, category_id = ?, note = ? WHERE id = ?"
).bind(body.date, body.type, body.wallet, body.amount, body.category_id, body.note, id);
await stmt.run();
return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname.startsWith("/api/transactions/") && request.method === "DELETE") {
const id = url.pathname.split("/").pop();
await env.finance_db.prepare("DELETE FROM transactions WHERE id = ?").bind(id).run();
return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/api/summary" && request.method === "GET") {
const mQuery = url.searchParams.get("month");
const targetMonth = mQuery === "active" ? await getActiveMonth() : await env.finance_db.prepare("SELECT * FROM months WHERE id = ?").bind(mQuery).first() || await getActiveMonth();
const { results } = await env.finance_db.prepare(`
SELECT wallet, type, SUM(amount) as total
FROM transactions WHERE month_id = ? GROUP BY wallet, type
`).bind(targetMonth.id).all();
let cash = targetMonth.cash_opening;
let kpay = targetMonth.kpay_opening;
results.forEach((row) => {
const val = row.type === "income" ? row.total : -row.total;
if (row.wallet === "cash") cash += val;
if (row.wallet === "kpay") kpay += val;
});
return new Response(JSON.stringify({
month_name: targetMonth.month_name,
total: cash + kpay,
cash,
kpay
}), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/api/expenses/category" && request.method === "GET") {
const mQuery = url.searchParams.get("month");
const targetMonth = mQuery === "active" ? await getActiveMonth() : await env.finance_db.prepare("SELECT * FROM months WHERE id = ?").bind(mQuery).first() || await getActiveMonth();
const { results } = await env.finance_db.prepare(`
SELECT
c.id,
c.name,
COALESCE(SUM(t.amount), 0) as total,
json_group_array(
CASE WHEN t.id IS NOT NULL THEN
json_object('id', t.id, 'date', t.date, 'amount', t.amount, 'note', t.note, 'wallet', t.wallet, 'type', t.type, 'category_id', t.category_id)
ELSE NULL END
) as transactions_json
FROM categories c
LEFT JOIN transactions t ON c.id = t.category_id AND t.type = 'expense' AND t.month_id = ?
GROUP BY c.id ORDER BY c.id ASC
`).bind(targetMonth.id).all();
const formatted = results.map((r) => {
let rawTxs = [];
try {
rawTxs = JSON.parse(r.transactions_json);
} catch (e) {
}
let validTxs = rawTxs.filter((tx) => tx !== null);
return {
id: r.id,
name: r.name,
total: r.total,
transactions: validTxs.sort((a, b) => new Date(b.date) - new Date(a.date))
};
});
return new Response(JSON.stringify(formatted), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
if (url.pathname === "/api/transactions/recent" && request.method === "GET") {
const mQuery = url.searchParams.get("month");
const targetMonth = mQuery === "active" ? await getActiveMonth() : await env.finance_db.prepare("SELECT * FROM months WHERE id = ?").bind(mQuery).first() || await getActiveMonth();
const walletFilter = url.searchParams.get("wallet");
const typeFilter = url.searchParams.get("type");
let query = `
SELECT t.*, c.name as category_name
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.month_id = ?
`;
let params = [targetMonth.id];
if (walletFilter && walletFilter !== "all") {
query += ` AND t.wallet = ? `;
params.push(walletFilter);
}
if (typeFilter && typeFilter !== "all") {
query += ` AND t.type = ? `;
params.push(typeFilter);
}
query += ` ORDER BY t.date DESC, t.id DESC LIMIT 200`;
const { results } = await env.finance_db.prepare(query).bind(...params).all();
return new Response(JSON.stringify(results), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
}
return new Response("Not found", { status: 404 });
}
};
export {
index_default as default
};
//# sourceMappingURL=index.js.map
finance-db áááŻáˇ áĄáááĄááť ááąá¸ááąá¸áááŤáááş