Back to Tech

Cloudflare account သုံးပြီးတော့ web app တစ်ခု လုပ်ကြည့်မယ်

(၁) အရင်ဆုံးအနေနဲ့ 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
depoly နှိပ်ပြီးရင် worker page ကို ပြန်ထွက်ပါ
ဘယ်ဘက် menu ကနေ stroage & databases အောက်က D1 SQL database ကို ဝင်ပါ ညာဘက်က Create database ကိုနှိပ်ပါ
Name ကို finance-db လို့ အတိအကျ ရေးပေးရပါမယ်
Back to Tech