GCash Management System
Powered by Gervic Programs
Managing a high-volume GCash business requires more than just a notebook and a pen. In this post, I’m unveiling a custom project designed to completely transform your financial operations.
If you are looking to move away from manual tracking and need a system that offers professional auditing, accounting, and security, you’re in the right place. We are powering this application with a Google Sheets backend and Google Apps Script, all wrapped in a modern UI for a premium experience.
Executive Summary
This system is a comprehensive, web-based financial management suite designed specifically for GCash, remittance, and lending businesses. It replaces manual logbooks with a digital ledger that automates calculations, enforces financial balancing, and generates professional accounting reports in real-time.
1. Core Features (What the System Does)
Centralized Transaction Processing:
Records day-to-day operations: Cash In, Cash Out, Load, and Bill Payments. It handles internal fund movements like Replenishments and Wallet-to-Wallet transfers, while also recording owner Investments and Withdrawals.
Smart Credit & Loan Management:
Tracks "Load/Cash In on account" via Customer Credits, automatically flagging overdue accounts (3+ days) with penalties. Includes distinct tracking for cash loans with support for partial payments.
Wallet Inventory System:
Tracks balances for specific GCash accounts individually. You will always know exactly how much funds are in which phone or account at any moment.
Automated Accounting:
Generates a Double-Entry General Journal and real-time Financial Statements (Income Statement and Balance Sheet). It also calculates Daily Profit Sharing based on individual capital contributions relative to the transaction date.
2. Key Benefits (Why It Helps You)
A. Financial Integrity & Accuracy: Our "Force Balance" logic ensures digital records match physical cash to the centavo. Zero-Sum Accounting prevents "floating" money.
B. Operational Efficiency: Auto-Calculation eliminates manual errors in service fees. Instant Search allows you to find any transaction by Name or Ref No without flipping through paper logs.
C. Loss Prevention & Debt Recovery: The dashboard highlights overdue credits in VIBRANT PINK. Penalty automation prevents revenue leakage from late payers.
D. Security & Control: Role-Based Access limits staff to specific wallets while Admins control sensitive settings. A full Audit Trail logs every action with a user timestamp.
System Setup & Implementation
Step 1, 2 & 3: Database Initialization
Open sheets.new and create 8 tabs: Users, Settings, Wallets, Capital, System, Transactions, Journal, ProfitShare. Enter the following headers in Row 1:
| Sheet Name | Column Headers (Row 1) |
|---|---|
| Users | Username | Password | Role | Full Name | Profile Image |
| Settings | Key | Value |
| Wallets | Wallet Name | Owner | Status | Notes |
| Capital | Owner Name | Capital Amount | Share % | Status |
| System | Metric | Value (GCash Balance, Cash on Hand, AR-Borrowings, AR-Customers) |
| Transactions | Date | ID | Type | Customer | Source | Ref No | Amount | Fee | Total | Paid | Change | User |
| Journal | Date | Txn ID | Account Name | Debit | Credit | Description |
| ProfitShare | Date | Owner | Capital Balance | Share % | Daily Net Income | Profit Allocation |
Step 4 & 5: Scripts & Deployment
Navigate to Extensions > Apps Script. Create four files (Code.gs, index.html, css.html, js.html) and paste the corresponding codes below:
1. Code.gs
var SHEET_ID = 'YOUR_SHEET_ITEM_ID';
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('GCASH SYSTEM')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
// ==========================================
// --- DATA FETCHING & DASHBOARD ---
// ==========================================
function getSystemBalance() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('System');
// Default values if sheet is broken/missing
var def = { gcash: "₱0.00", cash: "₱0.00", ar_borrow: "₱0.00", ar_cust: "₱0.00" };
if (!sheet) return def;
var lastRow = sheet.getLastRow();
if (lastRow < 2) return def;
// Fetch values from System Sheet (Ledger Balances)
var data = sheet.getRange(2, 2, Math.min(lastRow, 5), 1).getValues();
// --- FIX: CALCULATE TRUE CREDITS (INCLUDING PENALTIES) ---
// Instead of reading the static ledger value for Credits (ar_cust),
// we calculate the live value including time-based penalties.
var creditList = getOutstandingCredits();
var totalCreditsWithPenalty = 0;
if (creditList && creditList.length > 0) {
creditList.forEach(function(item) {
totalCreditsWithPenalty += item.remainingBalance;
});
}
return {
gcash: parseFloat(data[0] ? data[0][0] : 0).toLocaleString('en-PH', {style: 'currency', currency: 'PHP'}),
cash: parseFloat(data[1] ? data[1][0] : 0).toLocaleString('en-PH', {style: 'currency', currency: 'PHP'}),
ar_borrow: parseFloat(data[2] ? data[2][0] : 0).toLocaleString('en-PH', {style: 'currency', currency: 'PHP'}),
// OVERRIDE: Use the calculated total that includes penalties
ar_cust: totalCreditsWithPenalty.toLocaleString('en-PH', {style: 'currency', currency: 'PHP'})
};
}
function getCustomerList() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var data = [];
var sheet = ss.getSheetByName('Transactions');
if (sheet && sheet.getLastRow() > 1) {
var raw = sheet.getRange(2, 4, sheet.getLastRow() - 1, 1).getValues();
raw.forEach(function(r) { if(r[0]) data.push(r[0]); });
}
var capSheet = ss.getSheetByName('Capital');
if (capSheet && capSheet.getLastRow() > 1) {
var capData = capSheet.getRange(2, 1, capSheet.getLastRow() - 1, 1).getValues();
capData.forEach(function(r) { if(r[0]) data.push(r[0]); });
}
var unique = {};
data.forEach(function(i) { unique[i] = true; });
return Object.keys(unique).sort();
}
// ==========================================
// --- USER MANAGEMENT (PROFILE IMAGE FIX) ---
// ==========================================
function loginUser(username, password) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] == username && data[i][1] == password) {
// RECONSTRUCT IMAGE FROM CHUNKS (Col E onwards)
var fullImage = "";
// Loop from index 4 (Column E) to the end of the row
for (var c = 4; c < data[i].length; c++) {
fullImage += data[i][c];
}
return {
status: 'success',
role: data[i][2],
name: data[i][3],
// If string is empty, return null
image: (fullImage && fullImage.length > 50) ? fullImage : null
};
}
}
return { status: 'error', message: 'Invalid credentials' };
}
function saveUserProfileImage(username, base64String) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] == username) {
// 1. Clear old image data (From Col E to end of row)
var lastCol = sheet.getLastColumn();
if (lastCol > 4) {
sheet.getRange(i + 1, 5, 1, lastCol - 4).clearContent();
}
// 2. CHUNK THE STRING (50k chars per cell)
var CHUNK_SIZE = 50000;
var chunks = [];
for (var k = 0; k < base64String.length; k += CHUNK_SIZE) {
chunks.push(base64String.substring(k, k + CHUNK_SIZE));
}
// 3. SAVE CHUNKS (Starting at Column 5 / Column E)
if (chunks.length > 0) {
sheet.getRange(i + 1, 5, 1, chunks.length).setValues([chunks]);
}
return { status: 'success' };
}
}
return { status: 'error', message: 'User not found' };
}
// ==========================================
// --- WALLET MANAGEMENT (ROLE BASED) ---
// ==========================================
// Helper: Get a map of Admin Usernames { 'admin1': true }
function getAdminUserMap(ss) {
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
var map = {};
for (var i = 1; i < data.length; i++) {
if (data[i][2] === 'Admin') {
map[data[i][0]] = true;
}
}
// Keep 'System' as legacy fallback
map['System'] = true;
return map;
}
// 1. Fetch Wallets for Transaction Dropdowns (Source/Target)
function getUserWallets(username) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var wSheet = ss.getSheetByName('Wallets');
if (!wSheet) return [];
var adminMap = getAdminUserMap(ss);
var isRequesterAdmin = adminMap[username];
var data = wSheet.getDataRange().getValues();
var allowedWallets = [];
for (var i = 1; i < data.length; i++) {
var walletName = data[i][0];
var owner = data[i][1];
var status = data[i][2];
if (status === 'Active') {
// --- STRICTER LOGIC ---
// 1. If User is Admin: Show ALL Active Wallets.
// 2. If User is Staff: Show ONLY wallets owned by them.
// (We removed the logic that automatically shared Admin wallets)
if (isRequesterAdmin || owner === username) {
allowedWallets.push(walletName);
}
}
}
return allowedWallets;
}
// 2. Fetch Wallets for "My Wallets" Dashboard (With Balances)
function getUserDetailedWallets(username) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var adminMap = getAdminUserMap(ss);
var isRequesterAdmin = adminMap[username];
var wSheet = ss.getSheetByName('Wallets');
var userWallets = {};
if (wSheet) {
var wData = wSheet.getDataRange().getValues();
for (var i = 1; i < wData.length; i++) {
var wName = wData[i][0];
var wOwner = wData[i][1];
var wStatus = wData[i][2];
if (isRequesterAdmin || wOwner === username) {
userWallets[wName] = {
status: wStatus,
balance: 0,
owner: wOwner,
isShared: !!adminMap[wOwner]
};
}
}
}
// Calculate Balances
var tSheet = ss.getSheetByName('Transactions');
if (tSheet) {
var tData = tSheet.getDataRange().getValues();
for (var i = 1; i < tData.length; i++) {
var row = tData[i];
var type = String(row[2]);
var customer = String(row[3]);
var source = String(row[4]);
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
var settlement = String(row[9]);
// Logic for Source Wallet
if (userWallets[source]) {
if (type === 'Cash In' || type === 'Load') userWallets[source].balance -= amount;
else if (type === 'Transfer') userWallets[source].balance -= (amount + fee);
else if (type === 'Cash Out') userWallets[source].balance += total;
else if (type === 'Add GCash') userWallets[source].balance += amount;
else if (type === 'Import Wallet') userWallets[source].balance += amount;
// --- THIS IS THE CRITICAL FIX ---
// If Customer Pays Debt via GCash, the "Source" column holds the receiving wallet.
// We MUST ADD the total to this wallet.
else if ((type === 'Pay Credit' || type === 'Pay Borrowing') && settlement === 'GCash') {
userWallets[source].balance += total;
}
// --------------------------------
}
// Logic for Destination Wallet (Transfers)
if (type === 'Transfer' && userWallets[customer]) {
userWallets[customer].balance += amount;
}
}
}
var result = [];
for (var name in userWallets) {
result.push({
name: name,
status: userWallets[name].status,
balance: userWallets[name].balance,
owner: userWallets[name].owner,
isShared: userWallets[name].isShared
});
}
result.sort(function(a, b) {
if (a.isShared && !b.isShared) return -1;
if (!a.isShared && b.isShared) return 1;
return a.name.localeCompare(b.name);
});
return result;
}
// 3. Public Wallet Breakdown (Login Screen) - Shows ALL Active Wallets
function getWalletBreakdown() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var txnSheet = ss.getSheetByName('Transactions');
var walletSheet = ss.getSheetByName('Wallets');
if (!txnSheet) return [];
var activeWallets = {};
// 1. Initialize Active Wallets
if (walletSheet) {
var wData = walletSheet.getDataRange().getValues();
for (var i = 1; i < wData.length; i++) {
if (wData[i][2] === 'Active') {
activeWallets[wData[i][0]] = 0;
}
}
}
// 2. Calculate Balances
var data = txnSheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var row = data[i];
var type = String(row[2]);
var customer = String(row[3]);
var source = String(row[4]);
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
var settlement = String(row[9]);
if (activeWallets.hasOwnProperty(source)) {
if (['Cash In', 'Load'].includes(type)) activeWallets[source] -= amount;
else if (type === 'Transfer') activeWallets[source] -= (amount + fee);
else if (type === 'Cash Out') activeWallets[source] += total;
else if (type === 'Add GCash') activeWallets[source] += amount;
else if (type === 'Import Wallet') activeWallets[source] += amount;
// --- ADD THIS LOGIC HERE TOO ---
else if ((type === 'Pay Credit' || type === 'Pay Borrowing') && settlement === 'GCash') {
activeWallets[source] += total;
}
// -------------------------------
}
if (type === 'Transfer' && activeWallets.hasOwnProperty(customer)) {
activeWallets[customer] += amount;
}
}
// 3. Convert to Array
var result = [];
for (var key in activeWallets) {
result.push({ name: key, balance: activeWallets[key] });
}
result.sort(function(a, b) { return a.name.localeCompare(b.name); });
return result;
}
// 4. Request New Wallet
function requestNewWallet(form) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Wallets');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0].toLowerCase() === form.walletName.toLowerCase()) {
return { status: 'error', message: 'Wallet name already exists.' };
}
}
sheet.appendRow([form.walletName, form.username, 'Pending', 'Created via App']);
return { status: 'success', message: 'Wallet requested. Wait for Admin approval.' };
}
// 5. Get Wallet History (Ledger)
function getWalletHistory(walletName) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Transactions');
var data = sheet.getDataRange().getValues();
var history = [];
var targetRaw = String(walletName);
var targetClean = targetRaw.trim().toLowerCase();
var rowsScanned = 0;
var lastSourcesSeen = [];
if (data.length > 1) {
for (var i = data.length - 1; i >= 1; i--) {
var row = data[i];
rowsScanned++;
var type = String(row[2] || "");
var customerRaw = String(row[3] || "");
var sourceRaw = String(row[4] || "");
var sourceClean = sourceRaw.trim().toLowerCase();
var customerClean = customerRaw.trim().toLowerCase();
if (rowsScanned <= 5) lastSourcesSeen.push("[" + sourceRaw + "]");
var isSource = (sourceClean.indexOf(targetClean) > -1);
var isTarget = (type === 'Transfer' && customerClean.indexOf(targetClean) > -1);
if (isSource || isTarget) {
var dateStr = "--";
try {
if (row[0]) {
// --- CHANGED HERE: Date Only (e.g. "Jul 11, 2025") ---
dateStr = Utilities.formatDate(new Date(row[0]), "Asia/Manila", "MMM dd, yyyy");
}
} catch(e) {
dateStr = "Invalid Date";
}
var effect = 0;
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
if (type === 'Cash In' || type === 'Load') effect = -amount;
else if (type === 'Transfer' && isSource) effect = -(amount + fee);
else if (type === 'Transfer' && isTarget) effect = amount;
else if (type === 'Cash Out') effect = total;
else if (type === 'Add GCash') effect = amount;
history.push({
date: dateStr,
id: row[1],
type: type,
customer: (type === 'Transfer' && isSource) ? "To: " + customerRaw : customerRaw,
amount: Math.abs(effect),
fee: fee,
total: effect,
isCredit: (effect > 0)
});
if (history.length >= 50) break;
}
}
}
if (history.length === 0) {
return [{
debug: true,
msg: "Searching for: '" + targetRaw + "'",
scanned: rowsScanned,
recent: lastSourcesSeen.join(", ")
}];
}
return history;
}
// 6. Get ALL Active Wallets (For Transfer Destinations)
function getAllActiveWallets() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Wallets');
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
var wallets = [];
for (var i = 1; i < data.length; i++) {
// Return all wallets that are 'Active', regardless of owner
if (data[i][2] === 'Active') {
wallets.push(data[i][0]);
}
}
return wallets.sort();
}
// ==========================================
// --- ADMIN APPROVALS ---
// ==========================================
function getPendingWalletRequests(username) {
if (!isAdmin(username)) return { status: 'error', message: 'Unauthorized' };
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Wallets');
if (!sheet) return { status: 'success', data: [] };
var data = sheet.getDataRange().getValues();
var pending = [];
for (var i = 1; i < data.length; i++) {
if (data[i][2] === 'Pending') {
pending.push({
walletName: data[i][0],
owner: data[i][1],
notes: data[i][3]
});
}
}
return { status: 'success', data: pending };
}
function processWalletRequest(adminUser, walletName, action) {
if (!isAdmin(adminUser)) return { status: 'error', message: 'Unauthorized' };
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Wallets');
var data = sheet.getDataRange().getValues();
var found = false;
for (var i = 1; i < data.length; i++) {
if (data[i][0] === walletName) {
if (action === 'approve') {
sheet.getRange(i + 1, 3).setValue('Active');
} else if (action === 'reject') {
sheet.deleteRow(i + 1);
}
found = true;
break;
}
}
if (found) return { status: 'success' };
return { status: 'error', message: 'Wallet not found' };
}
// ==========================================
// --- BUSINESS LOGIC: WITHDRAWAL LIMITS ---
// ==========================================
function getOwnerLimit(ownerName) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var capSheet = ss.getSheetByName('Capital');
if (!capSheet) return { found: false, limit: 0, msg: "Capital tab missing" };
var capData = capSheet.getDataRange().getValues();
var ownerCapital = 0, ownerShare = 0, found = false;
for (var i = 1; i < capData.length; i++) {
if (capData[i][0] === ownerName && capData[i][3] === 'Active') {
ownerCapital = Number(capData[i][1]);
ownerShare = Number(capData[i][2]);
found = true;
break;
}
}
if (!found) return { found: false, limit: 0, msg: "Owner not found" };
var txnSheet = ss.getSheetByName('Transactions');
var totalFees = 0;
if (txnSheet && txnSheet.getLastRow() > 1) {
var feeData = txnSheet.getRange(2, 8, txnSheet.getLastRow() - 1, 1).getValues();
feeData.forEach(function(row) { totalFees += Number(row[0] || 0); });
}
return { found: true, limit: ownerCapital + (totalFees * ownerShare) };
}
// ==========================================
// --- VIEW: TRANSACTION LISTS ---
// ==========================================
function getTransactionsByType(type, limit) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Transactions');
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
var results = [];
// Clean the requested type for comparison (e.g. "add gcash")
var targetType = String(type).trim().toLowerCase();
// Loop backwards (Newest first)
for (var i = data.length - 1; i >= 1; i--) {
var row = data[i];
var rowType = String(row[2] || "").trim().toLowerCase();
if (rowType === targetType) {
var dateStr = "--";
var rawDate = null;
var rawVal = row[0];
// 1. DATE PARSING (With Philippines Timezone)
if (rawVal) {
try {
// Format: "Jul 11, 2025" (Date Only)
dateStr = Utilities.formatDate(new Date(rawVal), "Asia/Manila", "MMM dd, yyyy");
rawDate = new Date(rawVal).getTime();
} catch (e) {
dateStr = "Invalid Date";
}
}
if (!rawDate) rawDate = new Date().getTime();
// 2. SMART DISPLAY NAME (Customer/Source Column)
var displayStr = row[3]; // Default: Customer Name
// For "Add GCash", show: "Fund Source → Target Wallet"
if (targetType === 'add gcash') {
var fundSource = row[3] || "Source";
var targetWallet = row[4] || "Wallet";
displayStr = fundSource + " → " + targetWallet;
}
// For "Transfer", show: "Source → Target"
else if (targetType === 'transfer') {
var sourceW = row[4] || "Wallet A";
var targetW = row[3] || "Wallet B";
displayStr = sourceW + " → " + targetW;
}
results.push({
date: dateStr,
rawDate: rawDate,
id: row[1],
customer: displayStr, // We inject the smarter name here
source: row[4],
ref: row[5],
amount: Number(row[6]),
fee: Number(row[7]),
total: Number(row[8]),
type: row[2], // Return original casing
settlement: row[9],
amountPaid: row[10]
});
if (limit > 0 && results.length >= limit) break;
}
}
return results;
}
// ==========================================
// --- VIEW: HISTORY & AUDIT ---
// ==========================================
function getHistoryForID(originalId) {
try {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Transactions');
if (!sheet) return [{debug: true, msg: "Transactions tab not found"}];
var data = sheet.getDataRange().getValues();
if (data.length < 2) return [{debug: true, msg: "Database is empty."}];
var headers = data[0];
var colId = -1, colType = -1, colRef = -1, colAmount = -1, colFee = -1, colTotal = -1, colDate = 0;
for (var h = 0; h < headers.length; h++) {
var header = String(headers[h]).toLowerCase().trim();
if (colId === -1 && (header === "transaction id" || header === "id" || header === "txn id")) colId = h;
else if (colType === -1 && (header === "type" || header === "transaction type")) colType = h;
else if (colRef === -1 && (header === "ref no" || header === "reference" || header === "ref")) colRef = h;
else if (colAmount === -1 && (header === "amount" || header === "principal")) colAmount = h;
else if (colTotal === -1 && (header === "total" || header === "total due")) colTotal = h;
else if (colFee === -1 && (header === "fee" || header === "charge")) colFee = h;
else if (colDate === 0 && (header === "date")) colDate = h;
}
if (colId === -1) colId = 1;
if (colType === -1) colType = 2;
if (colRef === -1) colRef = 5;
if (colAmount === -1) colAmount = 6;
if (colFee === -1) colFee = 7;
if (colTotal === -1) colTotal = 8;
var history = [];
var targetId = String(originalId).trim();
var originalFound = false;
var originalTimestamp = 0;
var originalFee = 0;
for (var i = 1; i < data.length; i++) {
var row = data[i];
var rowId = String(row[colId] || "").trim();
var rowType = String(row[colType] || "").trim();
var rowRef = String(row[colRef] || "").trim();
var rowPrincipal = Number(row[colAmount]) || 0;
var rowFee = Number(row[colFee]) || 0;
var rowTotal = Number(row[colTotal]) || 0;
var rawDate = row[colDate];
var txnTime = new Date().getTime();
if (rawDate instanceof Date) txnTime = rawDate.getTime();
else if (typeof rawDate === 'string' && rawDate !== "") {
var parsed = new Date(rawDate);
if (!isNaN(parsed.getTime())) txnTime = parsed.getTime();
}
if (rowId === targetId) {
originalFound = true;
originalTimestamp = txnTime;
originalFee = rowFee;
history.push({
timestamp: txnTime,
priority: 1,
activity: "Principal Amount",
amount: rowPrincipal,
type: 'debit'
});
if (rowFee > 0) {
history.push({
timestamp: txnTime,
priority: 2,
activity: "Service Charge",
amount: rowFee,
type: 'debit'
});
}
}
if ((rowType === 'Pay Credit' || rowType === 'Pay Borrowing') && rowRef === targetId) {
history.push({
timestamp: txnTime,
priority: 3,
activity: "Payment",
amount: rowTotal,
type: 'credit'
});
}
}
if (!originalFound && history.length === 0) {
return [{
debug: true,
msg: "Transaction ID not found.",
target: targetId,
colIdName: "Index " + colId,
seen: "N/A"
}];
}
if (originalFound && originalFee > 0) {
var now = new Date().getTime();
var penaltyThreshold = originalTimestamp + (3 * 24 * 60 * 60 * 1000);
if (now > penaltyThreshold) {
history.push({
timestamp: penaltyThreshold + 1000,
priority: 4,
activity: "Late Penalty",
amount: originalFee,
type: 'debit'
});
}
}
history.sort(function(a, b) {
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
return a.priority - b.priority;
});
var calcBal = 0;
return history.map(function(h) {
if (h.type === 'debit') calcBal += h.amount; else calcBal -= h.amount;
var d = new Date(h.timestamp);
var dateStr = Utilities.formatDate(d, Session.getScriptTimeZone(), "MMM dd, yyyy");
return {
dateStr: dateStr,
activity: h.activity,
amount: h.amount,
type: h.type,
balance: calcBal
};
});
} catch (e) {
return [{ debug: true, msg: "SCRIPT ERROR: " + e.toString() }];
}
}
// ==========================================
// --- CREDIT & BORROWING LISTS ---
// ==========================================
function getOutstandingCredits() {
return getOutstandingDebts('Cash In', 'Load', 'Pay Credit');
}
function getOutstandingBorrowings() {
return getOutstandingDebts('Borrow', null, 'Pay Borrowing');
}
function getOutstandingDebts(type1, type2, payType) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Transactions');
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
var debts = {};
var paymentsMap = {};
for (var i = 1; i < data.length; i++) {
var row = data[i];
var id = row[1];
var type = row[2];
var settlement = row[9];
var refNo = row[5];
var total = Number(row[8]);
var isCredit = ((type === type1 || type === type2) && settlement === 'On Account');
var isBorrow = (type === 'Borrow' && type1 === 'Borrow');
if (isCredit || isBorrow) {
debts[id] = {
date: row[0],
id: id,
customer: row[3],
type: type,
originalFee: Number(row[7]),
originalTotal: total
};
}
if (type === payType) {
if (!paymentsMap[refNo]) paymentsMap[refNo] = 0;
paymentsMap[refNo] += total;
}
}
var result = [];
var today = new Date();
for (var key in debts) {
var txn = debts[key];
var txnDate = new Date(txn.date);
var penalty = 0;
var isOverdue = false;
var diffDays = 0;
if (txn.type !== 'Borrow') {
var diffTime = Math.abs(today - txnDate);
diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
isOverdue = diffDays > 3;
penalty = isOverdue ? txn.originalFee : 0;
}
var grossAmount = txn.originalTotal + penalty;
var totalPaid = paymentsMap[txn.id] || 0;
var remainingBalance = grossAmount - totalPaid;
if (remainingBalance > 0.01) {
result.push({
id: txn.id,
date: Utilities.formatDate(txnDate, Session.getScriptTimeZone(), "MMM dd, yyyy"),
customer: txn.customer,
type: txn.type,
// NEW: This is the AR amount (505.00)
originalTotal: txn.originalTotal,
// This is what they owe NOW with penalty (510.00)
grossAmount: grossAmount,
paidSoFar: totalPaid,
remainingBalance: remainingBalance,
daysOpen: diffDays,
isOverdue: isOverdue,
penalty: penalty
});
}
}
result.sort(function(a, b) {
if (a.id < b.id) return 1;
if (a.id > b.id) return -1;
return 0;
});
return result;
}
// ==========================================
// --- SAVING & UPDATING ---
// ==========================================
function saveTransaction(form) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Transactions');
// 1. Generate ID
var prefix = "TXN";
if (form.type === 'Cash In') prefix = "CI";
else if (form.type === 'Cash Out') prefix = "CO";
else if (form.type === 'Load') prefix = "LD";
else if (form.type === 'Invest') prefix = "IV";
else if (form.type === 'Borrow') prefix = "BR";
else if (form.type === 'Withdraw') prefix = "WD";
else if (form.type === 'Add GCash') prefix = "RP";
else if (form.type === 'Transfer') prefix = "TF";
else if (form.type === 'Pay Credit') prefix = "PY";
else if (form.type === 'Pay Borrowing') prefix = "PB";
var dateSequence = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyyMMdd-HHmmss");
var id = prefix + "-" + dateSequence;
// 2. Save to Transactions Sheet
sheet.appendRow([
form.date, id, form.type, form.customer, form.source,
form.refNo, form.amount, form.fee, form.total,
form.settlement, form.amountPaid, form.tip, form.change,
form.processedBy, new Date()
]);
// Formatting
var lastRow = sheet.getLastRow();
var pesoFormat = '₱ #,##0.00';
sheet.getRange(lastRow, 7, 1, 3).setNumberFormat(pesoFormat);
sheet.getRange(lastRow, 11, 1, 3).setNumberFormat(pesoFormat);
// 3. Update Balances
updateBalancesV2(form.type, form.amount, form.fee, form.total, form.settlement);
if (form.type === 'Withdraw') {
var check = getOwnerLimit(form.customer);
if (check.found) updateOwnerCapital(form.customer, form.amount, check.limit);
}
else if (form.type === 'Invest') {
increaseOwnerCapital(form.customer, form.amount);
}
// --- CRITICAL FIX: Add the generated ID to the form object ---
form.id = id;
// 4. Generate Journal Entries
generateJournalEntries(form);
return { status: 'success', id: id };
}
function updateOwnerCapital(ownerName, withdrawAmount, maxLimit) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Capital');
var data = sheet.getDataRange().getValues();
var amount = Number(withdrawAmount);
for (var i = 1; i < data.length; i++) {
if (data[i][0] === ownerName) {
var currentCapital = Number(data[i][1]);
var newCapital = currentCapital - amount;
if (newCapital < 0) newCapital = 0;
sheet.getRange(i + 1, 2).setValue(newCapital);
if (Math.abs(amount - maxLimit) < 1.00) {
sheet.getRange(i + 1, 4).setValue("Inactive");
}
break;
}
}
}
function increaseOwnerCapital(ownerName, amount) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Capital');
var data = sheet.getDataRange().getValues();
var val = Number(amount);
var found = false;
for (var i = 1; i < data.length; i++) {
if (data[i][0] === ownerName) {
var currentCap = Number(data[i][1]);
sheet.getRange(i + 1, 2).setValue(currentCap + val);
if(data[i][3] !== 'Active') sheet.getRange(i+1, 4).setValue("Active");
found = true;
break;
}
}
if (!found) sheet.appendRow([ownerName, val, 0, "Active"]);
}
function updateBalancesV2(type, amount, fee, total, settlement) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('System');
var gcashCell = sheet.getRange(2, 2);
var cashCell = sheet.getRange(3, 2);
var arBorrowCell = sheet.getRange(4, 2);
var arCustCell = sheet.getRange(5, 2);
var currentGCash = Number(gcashCell.getValue());
var currentCash = Number(cashCell.getValue());
var currentARBorrow = Number(arBorrowCell.getValue());
var currentARCust = Number(arCustCell.getValue());
var valPrincipal = Number(amount);
var valFee = Number(fee);
var valTotal = Number(total);
if (type === 'Cash In' || type === 'Load') {
currentGCash -= valPrincipal;
if (settlement === 'On Account') currentARCust += valTotal;
else currentCash += valTotal;
}
else if (type === 'Cash Out') {
currentGCash += valTotal;
currentCash -= valPrincipal;
}
else if (type === 'Invest') {
currentCash += valPrincipal;
}
else if (type === 'Borrow') {
currentCash -= valPrincipal;
currentARBorrow += valPrincipal;
}
else if (type === 'Withdraw') {
currentCash -= valPrincipal;
}
else if (type === 'Add GCash') {
currentGCash += valPrincipal;
currentCash -= (valPrincipal + valFee);
}
else if (type === 'Transfer') {
currentGCash -= valFee;
}
else if (type === 'Pay Credit') {
// --- UPDATED ---
if (settlement === 'GCash') currentGCash += valTotal;
else currentCash += valTotal;
currentARCust -= valPrincipal;
}
else if (type === 'Pay Borrowing') {
// --- UPDATED ---
if (settlement === 'GCash') currentGCash += valTotal;
else currentCash += valTotal;
currentARBorrow -= valPrincipal;
}
gcashCell.setValue(currentGCash).setNumberFormat('₱ #,##0.00');
cashCell.setValue(currentCash).setNumberFormat('₱ #,##0.00');
arBorrowCell.setValue(currentARBorrow).setNumberFormat('₱ #,##0.00');
arCustCell.setValue(currentARCust).setNumberFormat('₱ #,##0.00');
}
// ==========================================
// --- USER MANAGEMENT ---
// ==========================================
function registerUser(form) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0].toString().toLowerCase() === form.username.toLowerCase()) {
return { status: 'error', message: 'Username already taken' };
}
}
sheet.appendRow([form.username, form.password, 'Staff', form.name]);
return { status: 'success' };
}
function changePassword(username, newPassword) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === username) {
sheet.getRange(i + 1, 2).setValue(newPassword);
return { status: 'success' };
}
}
return { status: 'error', message: 'User not found' };
}
function isAdmin(username) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] == username) {
return data[i][2] === 'Admin';
}
}
return false;
}
// ==========================================
// --- ADMIN: EDIT & DELETE & RECALCULATE ---
// ==========================================
function deleteTransaction(id, username) {
var ss = SpreadsheetApp.openById(SHEET_ID);
if (!isAdmin(username)) return { status: 'error', message: 'Unauthorized. Admins only.' };
var sheet = ss.getSheetByName('Transactions');
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
for (var i = 1; i < data.length; i++) {
if (data[i][1] === id) {
rowIndex = i + 1;
var row = data[i];
reverseBalances(row[2], Number(row[6]), Number(row[7]), Number(row[8]), row[9]);
sheet.deleteRow(rowIndex);
return { status: 'success' };
}
}
// NEW: Remove from Journal
generateJournalEntries({ id: id, action: 'delete' });
return { status: 'error', message: 'Transaction ID not found.' };
}
function editTransaction(form) {
var ss = SpreadsheetApp.openById(SHEET_ID);
if (!isAdmin(form.username)) return { status: 'error', message: 'Unauthorized. Admins only.' };
var sheet = ss.getSheetByName('Transactions');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][1] === form.id) {
var oldRow = data[i];
reverseBalances(oldRow[2], Number(oldRow[6]), Number(oldRow[7]), Number(oldRow[8]), oldRow[9]);
updateBalancesV2(form.type, form.amount, form.fee, form.total, form.settlement);
var range = sheet.getRange(i + 1, 1, 1, 14);
range.setValues([[
form.date, form.id, form.type, form.customer, form.source,
form.refNo, form.amount, form.fee, form.total,
form.settlement, form.amountPaid, form.tip, form.change,
form.processedBy
]]);
sheet.getRange(i + 1, 7, 1, 3).setNumberFormat('₱ #,##0.00');
return { status: 'success', id: form.id };
}
}
// NEW: Update Journal (Deletes old, adds new)
generateJournalEntries(form);
return { status: 'error', message: 'ID not found for editing' };
}
function reverseBalances(type, amount, fee, total, settlement) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('System');
var gcashCell = sheet.getRange(2, 2);
var cashCell = sheet.getRange(3, 2);
var arBorrowCell = sheet.getRange(4, 2);
var arCustCell = sheet.getRange(5, 2);
var currentGCash = Number(gcashCell.getValue());
var currentCash = Number(cashCell.getValue());
var currentARBorrow = Number(arBorrowCell.getValue());
var currentARCust = Number(arCustCell.getValue());
if (type === 'Cash In' || type === 'Load') {
currentGCash += amount;
if (settlement === 'On Account') currentARCust -= total;
else currentCash -= total;
}
else if (type === 'Cash Out') {
currentGCash -= total;
currentCash += amount;
}
else if (type === 'Invest') {
currentCash -= amount;
}
else if (type === 'Borrow') {
currentCash += amount;
currentARBorrow -= amount;
}
else if (type === 'Withdraw') {
currentCash += amount;
}
else if (type === 'Add GCash') {
currentGCash -= amount;
currentCash += (amount + fee);
}
else if (type === 'Transfer') {
currentGCash += fee;
}
else if (type === 'Pay Credit') {
// --- UPDATED ---
if (settlement === 'GCash') currentGCash -= total;
else currentCash -= total;
currentARCust += amount;
}
else if (type === 'Pay Borrowing') {
// --- UPDATED ---
if (settlement === 'GCash') currentGCash -= total;
else currentCash -= total;
currentARBorrow += amount;
}
gcashCell.setValue(currentGCash);
cashCell.setValue(currentCash);
arBorrowCell.setValue(currentARBorrow);
arCustCell.setValue(currentARCust);
}
function recalculateAllBalances() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var txnSheet = ss.getSheetByName('Transactions');
var sysSheet = ss.getSheetByName('System');
var capSheet = ss.getSheetByName('Capital');
var journalSheet = ss.getSheetByName('Journal');
if (!txnSheet || !sysSheet || !capSheet) return { status: 'error', message: 'Missing Sheets' };
var data = txnSheet.getDataRange().getValues();
var calcGCash = 0, calcCash = 0, calcARBorrow = 0, calcARCust = 0;
var ownerCapitals = {};
var allJournalEntries = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
var date = row[0];
var id = row[1];
var type = String(row[2]);
var customer = String(row[3]);
var source = String(row[4]);
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
var settlement = String(row[9]);
// 1. SYSTEM BALANCES
if (type === 'Cash In' || type === 'Load') {
calcGCash -= amount;
if (settlement === 'On Account') calcARCust += total; else calcCash += total;
}
else if (type === 'Cash Out') { calcGCash += total; calcCash -= amount; }
else if (type === 'Invest') { calcCash += amount; if (!ownerCapitals[customer]) ownerCapitals[customer] = 0; ownerCapitals[customer] += amount; }
else if (type === 'Borrow') { calcCash -= amount; calcARBorrow += amount; }
else if (type === 'Withdraw') { calcCash -= amount; if (!ownerCapitals[customer]) ownerCapitals[customer] = 0; ownerCapitals[customer] -= amount; }
else if (type === 'Add GCash') { calcGCash += amount; calcCash -= (amount + fee); }
else if (type === 'Import Wallet') { calcGCash += amount; }
else if (type === 'Transfer') { calcGCash -= fee; }
// --- UPDATED SECTIONS ---
else if (type === 'Pay Credit') {
if (settlement === 'GCash') calcGCash += total;
else calcCash += total;
calcARCust -= amount;
}
else if (type === 'Pay Borrowing') {
if (settlement === 'GCash') calcGCash += total;
else calcCash += total;
calcARBorrow -= amount;
}
// 2. JOURNAL REBUILD
if (journalSheet) {
var desc = type + " - " + customer;
if (['Cash In', 'Load'].includes(type)) {
if (settlement === 'On Account') {
allJournalEntries.push([date, id, 'A/R - Customers', total, 0, desc]);
allJournalEntries.push([date, id, 'GCash Wallet', 0, amount, desc]);
allJournalEntries.push([date, id, 'Service Revenue', 0, fee, desc]);
} else {
allJournalEntries.push([date, id, 'Cash on Hand', total, 0, desc]);
allJournalEntries.push([date, id, 'GCash Wallet', 0, amount, desc]);
allJournalEntries.push([date, id, 'Service Revenue', 0, fee, desc]);
}
}
else if (type === 'Cash Out') {
allJournalEntries.push([date, id, 'GCash Wallet', total, 0, desc]);
allJournalEntries.push([date, id, 'Cash on Hand', 0, amount, desc]);
allJournalEntries.push([date, id, 'Service Revenue', 0, fee, desc]);
}
else if (type === 'Add GCash') {
allJournalEntries.push([date, id, 'GCash Wallet', amount, 0, desc]);
allJournalEntries.push([date, id, 'Expenses (Txn Costs)', fee, 0, desc]);
allJournalEntries.push([date, id, 'Cash on Hand', 0, total, desc]);
}
else if (type === 'Transfer') {
allJournalEntries.push([date, id, 'GCash Wallet', amount, 0, "Transfer To: " + customer]);
if (fee > 0) allJournalEntries.push([date, id, 'Expenses (Txn Costs)', fee, 0, desc]);
allJournalEntries.push([date, id, 'GCash Wallet', 0, amount + fee, "Transfer From: " + source]);
}
// --- UPDATED JOURNAL FOR GCASH PAYMENTS ---
else if (type === 'Pay Credit' || type === 'Pay Borrowing') {
var arAccount = (type === 'Pay Borrowing') ? 'A/R - Loans' : 'A/R - Customers';
var penalty = total - amount;
var assetAccount = (settlement === 'GCash') ? 'GCash Wallet' : 'Cash on Hand';
allJournalEntries.push([date, id, assetAccount, total, 0, desc]);
allJournalEntries.push([date, id, arAccount, 0, amount, desc]);
if (penalty > 0.01) allJournalEntries.push([date, id, 'Other Income (Penalties)', 0, penalty, desc]);
}
else if (type === 'Borrow') {
allJournalEntries.push([date, id, 'A/R - Loans', amount, 0, desc]);
allJournalEntries.push([date, id, 'Cash on Hand', 0, amount, desc]);
}
else if (['Withdraw', 'Invest'].includes(type)) {
var isInvest = (type === 'Invest');
allJournalEntries.push([date, id, isInvest?'Cash on Hand':'Capital', amount, 0, desc]);
allJournalEntries.push([date, id, isInvest?'Capital':'Cash on Hand', 0, amount, desc]);
}
}
}
// SAVE SYSTEM
sysSheet.getRange(2, 2).setValue(calcGCash).setNumberFormat('₱ #,##0.00');
sysSheet.getRange(3, 2).setValue(calcCash).setNumberFormat('₱ #,##0.00');
sysSheet.getRange(4, 2).setValue(calcARBorrow).setNumberFormat('₱ #,##0.00');
sysSheet.getRange(5, 2).setValue(calcARCust).setNumberFormat('₱ #,##0.00');
// SAVE CAPITAL
var capData = capSheet.getDataRange().getValues();
var handledOwners = {};
for (var c = 1; c < capData.length; c++) {
var ownerName = capData[c][0];
var computedBal = ownerCapitals[ownerName] || 0;
capSheet.getRange(c + 1, 2).setValue(computedBal).setNumberFormat('₱ #,##0.00');
capSheet.getRange(c + 1, 4).setValue(computedBal > 0.01 ? "Active" : "Inactive");
handledOwners[ownerName] = true;
}
for (var owner in ownerCapitals) {
if (!handledOwners[owner] && Math.abs(ownerCapitals[owner]) > 0.01) {
capSheet.appendRow([owner, ownerCapitals[owner], 0, "Active"]);
capSheet.getRange(capSheet.getLastRow(), 2).setNumberFormat('₱ #,##0.00');
}
}
// SAVE JOURNAL
if (journalSheet && allJournalEntries.length > 0) {
if (journalSheet.getLastRow() > 1) {
journalSheet.getRange(2, 1, journalSheet.getLastRow() - 1, 6).clearContent();
}
journalSheet.getRange(2, 1, allJournalEntries.length, 6).setValues(allJournalEntries);
journalSheet.getRange(2, 4, allJournalEntries.length, 2).setNumberFormat('₱ #,##0.00');
}
generateProfitShareLogs();
return {
status: 'success',
message: 'System, Journal & Profit Shares Recalculated',
newBalances: { gcash: calcGCash, cash: calcCash, ar_borrow: calcARBorrow, ar_cust: calcARCust }
};
}
// ==========================================
// --- ACCOUNTING & REPORTS MODULE ---
// ==========================================
// Updated to accept date parameter
function getAccountingData(selectedDateStr) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var txnSheet = ss.getSheetByName('Transactions');
var sysSheet = ss.getSheetByName('System');
var capSheet = ss.getSheetByName('Capital');
var walletSheet = ss.getSheetByName('Wallets');
if (!txnSheet || !sysSheet || !capSheet) return { error: "Required sheets are missing." };
var data = txnSheet.getDataRange().getValues();
// --- DATE SETUP ---
var targetDate;
if (selectedDateStr) {
targetDate = new Date(selectedDateStr);
} else {
targetDate = new Date();
}
var startOfDay = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime();
var endOfDay = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate() + 1).getTime();
var currentYear = targetDate.getFullYear();
var stats = {
dailyIncome: 0, cashSales: 0, creditSales: 0,
cashFlowIn: 0, cashFlowOut: 0, gcashFlowIn: 0, gcashFlowOut: 0
};
var sci = { revenue: { serviceFees: 0, otherIncome: 0 }, expenses: { transactionCosts: 0 } };
var walletBalances = {};
if (walletSheet) {
var wData = walletSheet.getDataRange().getValues();
for (var w = 1; w < wData.length; w++) {
if (wData[w][2] === 'Active') walletBalances[wData[w][0]] = 0;
}
}
// --- NEW: FETCH DAILY SHARES FROM LOGS ---
var dailyShareBreakdown = [];
var psSheet = ss.getSheetByName('ProfitShare');
if (psSheet && selectedDateStr) {
var psData = psSheet.getDataRange().getValues();
for (var p = 1; p < psData.length; p++) {
var rowDate = psData[p][0];
if (rowDate instanceof Date) {
var rowDateStr = Utilities.formatDate(rowDate, Session.getScriptTimeZone(), "yyyy-MM-dd");
if (rowDateStr === selectedDateStr) {
dailyShareBreakdown.push({
owner: psData[p][1],
share: Number(psData[p][5])
});
}
}
}
}
// --- PROCESS TRANSACTIONS ---
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (!row[0]) continue;
var rDate = new Date(row[0]);
var rTime = rDate.getTime();
var type = String(row[2]);
var customer = String(row[3]);
var source = String(row[4]);
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
var settlement = String(row[9]);
var isSelectedDay = (rTime >= startOfDay && rTime < endOfDay);
var isYearToDate = (rDate.getFullYear() === currentYear && rTime < endOfDay);
var isPastOrPresent = (rTime < endOfDay);
// 1. REVENUE (SCI - YTD)
if (isYearToDate) {
if (['Cash In', 'Cash Out', 'Load'].includes(type)) sci.revenue.serviceFees += fee;
if (type === 'Pay Credit' || type === 'Pay Borrowing') {
var penalty = total - amount;
if (penalty > 0.01) sci.revenue.otherIncome += penalty;
}
if (['Transfer', 'Add GCash'].includes(type)) sci.expenses.transactionCosts += fee;
}
// 2. DAILY STATS
if (isSelectedDay) {
if (['Cash In', 'Cash Out', 'Load'].includes(type)) stats.dailyIncome += fee;
if (type === 'Pay Credit' || type === 'Pay Borrowing') {
var penalty = total - amount;
if (penalty > 0.01) stats.dailyIncome += penalty;
}
if (type === 'Cash In' || type === 'Load') {
if (settlement === 'On Account') stats.creditSales += total; else stats.cashSales += total;
}
if ((['Cash In', 'Load'].includes(type) && settlement !== 'On Account') || ['Pay Credit', 'Pay Borrowing'].includes(type) || type === 'Invest') {
stats.cashFlowIn += total;
}
if (['Cash Out', 'Withdraw', 'Borrow'].includes(type)) stats.cashFlowOut += amount;
if (type === 'Add GCash') stats.cashFlowOut += (amount + fee);
if (['Cash Out', 'Add GCash'].includes(type)) stats.gcashFlowIn += (type==='Cash Out'?total:amount);
if (['Cash In', 'Load'].includes(type)) stats.gcashFlowOut += amount;
if (type === 'Transfer') stats.gcashFlowOut += (amount + fee);
}
// 3. BALANCE SHEET
if (isPastOrPresent) {
if (walletBalances.hasOwnProperty(source)) {
if (['Cash In', 'Load'].includes(type)) walletBalances[source] -= amount;
else if (type === 'Transfer') walletBalances[source] -= (amount + fee);
else if (type === 'Cash Out') walletBalances[source] += total;
else if (type === 'Add GCash') walletBalances[source] += amount;
}
if (type === 'Transfer' && walletBalances.hasOwnProperty(customer)) {
walletBalances[customer] += amount;
}
}
}
var sysData = sysSheet.getRange(2, 2, 4, 1).getValues();
var walletBreakdown = [];
var calculatedGCashTotal = 0;
for (var wName in walletBalances) {
if (Math.abs(walletBalances[wName]) > 0.001) {
walletBreakdown.push({ name: wName, amount: walletBalances[wName] });
calculatedGCashTotal += walletBalances[wName];
}
}
var assets = {
gcashWallet: calculatedGCashTotal,
cashOnHand: Number(sysData[1][0]) || 0,
receivablesBorrow: Number(sysData[2][0]) || 0,
receivablesCust: Number(sysData[3][0]) || 0,
wallets: walletBreakdown
};
var totalAssets = assets.cashOnHand + assets.gcashWallet + assets.receivablesCust + assets.receivablesBorrow;
// --- EQUITY CORRECTION ---
var capData = capSheet.getDataRange().getValues();
var ownerList = [];
var totalPrincipal = 0;
for (var i = 1; i < capData.length; i++) {
var ownerName = capData[i][0];
// *** FIX: Ignore 'Retained Earnings' from the Owner List ***
// This ensures it doesn't show up as "Cap: Retained Earnings"
if (ownerName === 'Retained Earnings') continue;
if (capData[i][3] === 'Active' || Number(capData[i][1]) > 0) {
ownerList.push({ name: ownerName, amount: Number(capData[i][1]) });
totalPrincipal += Number(capData[i][1]);
}
}
// By not adding the "Retained Earnings" row to totalPrincipal,
// the subtraction (Total Assets - Principal) effectively pushes
// that 13k amount into the Calculated Retained Earnings result.
var retainedEarnings = totalAssets - totalPrincipal;
return {
today: stats, sci: sci,
dailyShares: dailyShareBreakdown,
bs: { assets: totalAssets, details: assets, equity: { total: totalAssets, principal: totalPrincipal, retained: retainedEarnings, owners: ownerList } }
};
}
// ==========================================
// --- TRIAL BALANCE MODULE ---
// ==========================================
function getTrialBalanceReport() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var txnSheet = ss.getSheetByName('Transactions');
var capSheet = ss.getSheetByName('Capital');
if (!txnSheet) return { error: "Transaction sheet missing" };
var data = txnSheet.getDataRange().getValues();
// 1. INITIALIZE ACCOUNTS (0.00)
var accounts = {
'Cash on Hand': 0,
'GCash Wallet': 0,
'A/R - Customers': 0,
'A/R - Loans': 0,
'Capital': 0,
'Service Revenue': 0,
'Other Income (Penalties)': 0,
'Expenses (Txn Costs)': 0
};
// 2. CALCULATE CAPITAL (Credit Balance)
// We fetch this directly from Capital sheet initial entries + Invest transactions
// Actually, let's calculate purely from transactions + initial capital to be safe.
var capData = capSheet.getDataRange().getValues();
for (var i = 1; i < capData.length; i++) {
// Initial Capital injection that might not be in transactions
// (Assuming Capital Sheet Col B is the running balance, we actually need the initial inputs)
// For simplicity in this system, let's treat the 'System' sheet start point as 0 and rely on 'Invest' transactions,
// BUT usually, Capital sheet has manual entries. Let's use the Capital Sheet total as the baseline Credit.
accounts['Capital'] += Number(capData[i][1]);
// Note: This is a hybrid approach. Ideally, Capital entries should be in Transactions as 'Invest'.
// If your 'Invest' transactions update the Capital sheet, we might double count if we sum both.
// DECISION: In your system, 'Invest' updates Capital Sheet. So let's just use Capital Sheet total.
}
// 3. PROCESS TRANSACTIONS (The Double Entry Logic)
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (!row[0]) continue;
var type = String(row[2]);
var amount = Number(row[6]) || 0; // Principal
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0; // Total
var settlement = String(row[9]);
// --- MAPPING LOGIC ---
if (type === 'Cash In' || type === 'Load') {
if (settlement === 'On Account') {
// Debit A/R, Credit GCash, Credit Revenue
accounts['A/R - Customers'] += total;
accounts['GCash Wallet'] -= amount;
accounts['Service Revenue'] += fee;
} else {
// Debit Cash, Credit GCash, Credit Revenue
accounts['Cash on Hand'] += total;
accounts['GCash Wallet'] -= amount;
accounts['Service Revenue'] += fee;
}
}
else if (type === 'Cash Out') {
// Debit GCash (Total), Credit Cash (Principal), Credit Revenue (Fee)
// *Correction based on your business logic:
// Customer sends GCash (Principal + Fee). You give Cash (Principal). Fee is income.
accounts['GCash Wallet'] += total;
accounts['Cash on Hand'] -= amount;
accounts['Service Revenue'] += fee;
}
else if (type === 'Add GCash') {
// Debit GCash (Amount), Debit Expense (Fee), Credit Cash (Total)
accounts['GCash Wallet'] += amount;
accounts['Expenses (Txn Costs)'] += fee;
accounts['Cash on Hand'] -= (amount + fee);
}
else if (type === 'Transfer') {
// Debit Expense (Fee), Credit GCash (Fee)
// Principal moves wallet-to-wallet (Net 0 for GCash total)
accounts['Expenses (Txn Costs)'] += fee;
accounts['GCash Wallet'] -= fee;
}
else if (type === 'Withdraw') {
// Debit Capital, Credit Cash
accounts['Capital'] -= amount;
accounts['Cash on Hand'] -= amount;
}
else if (type === 'Invest') {
// Debit Cash, Credit Capital
// NOTE: Since we pre-loaded Capital from sheet, avoid double counting if sheet updates dynamically.
// However, for Trial Balance flow, we show the movement.
// For this specific report, let's assume Capital Sheet is the Source of Truth and ignore 'Invest' here
// OR handle it if Capital Sheet is static.
// SAFE BET: Use Transaction movement for Cash, but don't add to Capital Account if pre-loaded.
accounts['Cash on Hand'] += amount;
// We already summed Capital from sheet, so we assume that sheet is updated.
}
else if (type === 'Borrow') {
// Debit A/R Loans, Credit Cash
accounts['A/R - Loans'] += amount;
accounts['Cash on Hand'] -= amount;
}
else if (type === 'Pay Credit') {
// Debit Cash (Total Paid), Credit A/R (Principal), Credit Other Income (Excess)
var principalPaid = amount;
var penalty = total - amount; // Cash Basis penalty
accounts['Cash on Hand'] += total;
accounts['A/R - Customers'] -= principalPaid;
if (penalty > 0) accounts['Other Income (Penalties)'] += penalty;
}
else if (type === 'Pay Borrowing') {
// Debit Cash, Credit A/R Loans, Credit Other Income
var principalPaid = amount;
var penalty = total - amount;
accounts['Cash on Hand'] += total;
accounts['A/R - Loans'] -= principalPaid;
if (penalty > 0) accounts['Other Income (Penalties)'] += penalty;
}
}
// 4. FORMAT FOR FRONTEND (Separating Debits and Credits)
var trialBalance = [];
var grandDebit = 0;
var grandCredit = 0;
// Helper to push row
function addRow(name, val, isDebitNormal) {
if (Math.abs(val) < 0.01) return; // Hide zero accounts
// Determine if it falls on Debit or Credit side based on value sign and normal balance
// Assets/Expenses = Debit Normal (+)
// Liab/Equity/Revenue = Credit Normal (-) in math terms, but we display absolute.
var dr = 0;
var cr = 0;
if (isDebitNormal) {
if (val >= 0) dr = val; else cr = Math.abs(val);
} else {
if (val >= 0) cr = val; else dr = Math.abs(val); // Capital/Revenue are usually positive numbers in our object
}
trialBalance.push({ name: name, dr: dr, cr: cr });
grandDebit += dr;
grandCredit += cr;
}
addRow('Cash on Hand', accounts['Cash on Hand'], true);
addRow('GCash Wallet', accounts['GCash Wallet'], true);
addRow('A/R - Customers', accounts['A/R - Customers'], true);
addRow('A/R - Loans', accounts['A/R - Loans'], true);
addRow('Capital', accounts['Capital'], false);
addRow('Service Revenue', accounts['Service Revenue'], false);
addRow('Other Income (Penalties)', accounts['Other Income (Penalties)'], false);
addRow('Expenses (Txn Costs)', accounts['Expenses (Txn Costs)'], true);
return {
rows: trialBalance,
total: { dr: grandDebit, cr: grandCredit },
balanced: Math.abs(grandDebit - grandCredit) < 1.00 // Allow small floating point margin
};
}
// ==========================================
// --- JOURNAL ENTRY ENGINE (DOUBLE ENTRY) ---
// ==========================================
function generateJournalEntries(txn) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Journal');
if (!sheet) return;
var data = sheet.getDataRange().getValues();
for (var i = data.length - 1; i >= 1; i--) {
if (data[i][1] === txn.id) sheet.deleteRow(i + 1);
}
if (txn.action === 'delete') return;
var entries = [];
var date = txn.date;
var id = txn.id;
var desc = txn.type + " - " + txn.customer;
var amount = Number(txn.amount) || 0;
var fee = Number(txn.fee) || 0;
var total = Number(txn.total) || 0;
// A. CASH IN / LOAD
if (txn.type === 'Cash In' || txn.type === 'Load') {
if (txn.settlement === 'On Account') {
entries.push([date, id, 'A/R - Customers', total, 0, desc]);
entries.push([date, id, 'GCash Wallet', 0, amount, desc]);
entries.push([date, id, 'Service Revenue', 0, fee, desc]);
} else {
entries.push([date, id, 'Cash on Hand', total, 0, desc]);
entries.push([date, id, 'GCash Wallet', 0, amount, desc]);
entries.push([date, id, 'Service Revenue', 0, fee, desc]);
}
}
// B. CASH OUT
else if (txn.type === 'Cash Out') {
entries.push([date, id, 'GCash Wallet', total, 0, desc]);
entries.push([date, id, 'Cash on Hand', 0, amount, desc]);
entries.push([date, id, 'Service Revenue', 0, fee, desc]);
}
// C. ADD GCASH
else if (txn.type === 'Add GCash') {
entries.push([date, id, 'GCash Wallet', amount, 0, desc]);
entries.push([date, id, 'Expenses (Txn Costs)', fee, 0, desc]);
entries.push([date, id, 'Cash on Hand', 0, total, desc]);
}
// D. TRANSFER
else if (txn.type === 'Transfer') {
entries.push([date, id, 'GCash Wallet', amount, 0, "Transfer To: " + txn.customer]);
if (fee > 0) entries.push([date, id, 'Expenses (Txn Costs)', fee, 0, desc]);
entries.push([date, id, 'GCash Wallet', 0, amount + fee, "Transfer From: " + txn.source]);
}
// E. PAY CREDIT / PAY BORROWING
else if (txn.type === 'Pay Credit' || txn.type === 'Pay Borrowing') {
var arAccount = (txn.type === 'Pay Borrowing') ? 'A/R - Loans' : 'A/R - Customers';
var cashReceived = total;
var principalSettled = amount;
var penaltyIncome = total - amount;
// --- UPDATED LOGIC ---
var assetAccount = (txn.settlement === 'GCash') ? 'GCash Wallet' : 'Cash on Hand';
// Dr. Asset
entries.push([date, id, assetAccount, cashReceived, 0, desc]);
// ---------------------
// Cr. AR
entries.push([date, id, arAccount, 0, principalSettled, desc]);
// Cr. Income
if (penaltyIncome > 0.01) {
entries.push([date, id, 'Other Income (Penalties)', 0, penaltyIncome, desc]);
}
}
// F. BORROW
else if (txn.type === 'Borrow') {
entries.push([date, id, 'A/R - Loans', amount, 0, desc]);
entries.push([date, id, 'Cash on Hand', 0, amount, desc]);
}
// G. WITHDRAW / INVEST
else if (txn.type === 'Withdraw') {
entries.push([date, id, 'Capital', amount, 0, desc]);
entries.push([date, id, 'Cash on Hand', 0, amount, desc]);
}
else if (txn.type === 'Invest') {
entries.push([date, id, 'Cash on Hand', amount, 0, desc]);
entries.push([date, id, 'Capital', 0, amount, desc]);
}
if (entries.length > 0) {
var lastRow = sheet.getLastRow();
sheet.getRange(lastRow + 1, 1, entries.length, 6).setValues(entries);
sheet.getRange(lastRow + 1, 4, entries.length, 2).setNumberFormat('₱ #,##0.00');
}
}
// Helper: Get Journal Data for Frontend
function getJournalLogs() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Journal');
if(!sheet || sheet.getLastRow() < 2) return [];
var data = sheet.getDataRange().getValues();
var logs = [];
// Loop backwards to show newest first
for (var i = data.length - 1; i >= 1; i--) {
var row = data[i];
// 1. SAFE DATE PARSING
var dateVal = row[0];
var dateStr = "";
if (dateVal instanceof Date) {
// Format: "Jul 16, 2025"
dateStr = Utilities.formatDate(dateVal, "Asia/Manila", "MMM dd, yyyy");
} else {
// Fallback if cell is text
dateStr = String(dateVal);
}
logs.push({
date: dateStr,
id: row[1],
account: row[2],
debit: Number(row[3]),
credit: Number(row[4]),
desc: row[5]
});
if (logs.length >= 100) break;
}
return logs;
}
// ==========================================
// --- PROFIT SHARING ENGINE (TIME-WEIGHTED) ---
// ==========================================
function generateProfitShareLogs() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var txnSheet = ss.getSheetByName('Transactions');
var psSheet = ss.getSheetByName('ProfitShare');
if (!txnSheet || !psSheet) return;
var data = txnSheet.getDataRange().getValues();
// 1. ORGANIZE DATA BY DATE
// Map: "YYYY-MM-DD" -> { income: 0, capitalMoves: [{owner, amount}] }
var dailyMap = {};
var minDate = new Date().getTime(); // Track start of time
var maxDate = new Date().getTime();
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (!row[0]) continue;
var rDate = new Date(row[0]);
// Normalize to Midnight
var dateKey = rDate.getFullYear() + "-" +
String(rDate.getMonth() + 1).padStart(2, '0') + "-" +
String(rDate.getDate()).padStart(2, '0');
var time = rDate.getTime();
if (time < minDate) minDate = time;
if (!dailyMap[dateKey]) dailyMap[dateKey] = { income: 0, moves: [] };
var type = String(row[2]);
var customer = String(row[3]);
var amount = Number(row[6]) || 0;
var fee = Number(row[7]) || 0;
var total = Number(row[8]) || 0;
// A. CALCULATE DAILY NET INCOME (Cash Basis Logic)
// Revenue
if (['Cash In', 'Cash Out', 'Load'].includes(type)) {
dailyMap[dateKey].income += fee;
}
else if (type === 'Pay Credit' || type === 'Pay Borrowing') {
// Logic: Check Fee column first (explicit), else calculate
// We use the same robust logic we added to submitTransaction
var penalty = 0;
if (fee > 0) penalty = fee; // If explicitly saved
else penalty = total - amount; // Calculated
if (penalty > 0.01) dailyMap[dateKey].income += penalty;
}
// Expenses
if (['Transfer', 'Add GCash'].includes(type)) {
dailyMap[dateKey].income -= fee;
}
// B. TRACK CAPITAL MOVES
if (type === 'Invest') {
dailyMap[dateKey].moves.push({ owner: customer, val: amount });
}
else if (type === 'Withdraw') {
dailyMap[dateKey].moves.push({ owner: customer, val: -amount });
}
}
// 2. SIMULATE HISTORY DAY-BY-DAY
var logs = [];
var currentCapital = {}; // { 'Owner A': 5000, 'Owner B': 10000 }
var grandTotalCapital = 0;
// Create date iterator
var startDate = new Date(minDate);
var endDate = new Date(); // Today
// Iterate from First Transaction -> Today
for (var d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
var dKey = d.getFullYear() + "-" +
String(d.getMonth() + 1).padStart(2, '0') + "-" +
String(d.getDate()).padStart(2, '0');
var dayData = dailyMap[dKey] || { income: 0, moves: [] };
// 2.1 Apply Capital Changes FIRST (Before profit distribution)
// Logic: If I invest today, I get a share of today's profit.
if (dayData.moves.length > 0) {
dayData.moves.forEach(function(m) {
if (!currentCapital[m.owner]) currentCapital[m.owner] = 0;
currentCapital[m.owner] += m.val;
grandTotalCapital += m.val;
});
}
// 2.2 Distribute Profit (If there is income and capital)
if (dayData.income !== 0 && grandTotalCapital > 0) {
for (var owner in currentCapital) {
var cap = currentCapital[owner];
if (cap > 0) { // Only share if positive balance
var ratio = cap / grandTotalCapital;
var share = dayData.income * ratio;
// Store Log: [Date, Owner, CapBal, %, Income, Share]
logs.push([
dKey,
owner,
cap,
ratio,
dayData.income,
share
]);
}
}
}
}
// 3. WRITE TO SHEET
// Clear old
if (psSheet.getLastRow() > 1) {
psSheet.getRange(2, 1, psSheet.getLastRow() - 1, 6).clearContent();
}
if (logs.length > 0) {
psSheet.getRange(2, 1, logs.length, 6).setValues(logs);
// Formats
psSheet.getRange(2, 3, logs.length, 1).setNumberFormat('₱ #,##0.00'); // Cap
psSheet.getRange(2, 4, logs.length, 1).setNumberFormat('0.00%'); // %
psSheet.getRange(2, 5, logs.length, 2).setNumberFormat('₱ #,##0.00'); // Income/Share
}
}
// Helper: Fetch Summary for Frontend
function getProfitShareSummary() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('ProfitShare');
if (!sheet || sheet.getLastRow() < 2) return [];
var data = sheet.getDataRange().getValues();
var summary = {};
// Aggregation
for (var i = 1; i < data.length; i++) {
var owner = data[i][1];
var capital = Number(data[i][2]); // This is daily balance, we want Current Capital
var share = Number(data[i][5]);
if (!summary[owner]) summary[owner] = { totalShare: 0, currentCap: 0 };
summary[owner].totalShare += share;
// We overwrite currentCap constantly so the last entry (latest date) wins
summary[owner].currentCap = capital;
}
var result = [];
for (var k in summary) {
// Calculate ROI
var roi = 0;
if (summary[k].currentCap > 0) {
roi = (summary[k].totalShare / summary[k].currentCap);
}
result.push({
owner: k,
capital: summary[k].currentCap,
share: summary[k].totalShare,
roi: roi
});
}
return result;
}
// ==========================================
// --- COMPANY SETTINGS (WITH THEME COLORS) ---
// ==========================================
function getCompanySettings() {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Settings');
// Default Colors (The original ones)
var defaults = {
name: "2DLR GV",
sub: "GCASH LTD",
status: "System Online",
address: "City, Philippines",
email: "admin@example.com",
contact: "0900-000-0000",
logo: "https://lh3.googleusercontent.com/d/1Dltghw9r1Dg98lLsut5DpBqDGXa0URRc",
// Default Theme Colors
col_maroon: "#800000", // Primary
col_gold: "#facc15", // Accent
col_green: "#2ecc71", // Success
col_red: "#e74c3c", // Danger
col_blue: "#3498db" // Info
};
if (!sheet) return defaults;
var data = sheet.getDataRange().getValues();
var settings = {};
for (var i = 1; i < data.length; i++) {
var key = data[i][0];
if (key === 'CompanyLogo') {
var fullString = "";
for (var c = 1; c < data[i].length; c++) fullString += data[i][c];
settings['CompanyLogo'] = fullString;
} else {
settings[key] = data[i][1];
}
}
return {
name: settings['CompanyName'] || defaults.name,
sub: settings['CompanySub'] || defaults.sub,
status: settings['SystemStatus'] || defaults.status,
address: settings['CompanyAddress'] || defaults.address,
email: settings['CompanyEmail'] || defaults.email,
contact: settings['CompanyContact'] || defaults.contact,
logo: (settings['CompanyLogo'] && settings['CompanyLogo'].length > 50) ? settings['CompanyLogo'] : defaults.logo,
// Return Colors
colors: {
maroon: settings['ThemeMaroon'] || defaults.col_maroon,
gold: settings['ThemeGold'] || defaults.col_gold,
green: settings['ThemeGreen'] || defaults.col_green,
red: settings['ThemeRed'] || defaults.col_red,
blue: settings['ThemeBlue'] || defaults.col_blue
}
};
}
function saveCompanyLogo(base64String) {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Settings');
if (!sheet) {
sheet = ss.insertSheet('Settings');
sheet.appendRow(['Key', 'Value']);
}
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
// 1. Find the row index for 'CompanyLogo'
for (var i = 1; i < data.length; i++) {
if (data[i][0] === 'CompanyLogo') {
rowIndex = i + 1; // 1-based index
break;
}
}
// 2. Clear the old row if it exists, or create a new one
if (rowIndex !== -1) {
// Clear content from Column B to the end of the row
var lastCol = sheet.getLastColumn();
if (lastCol > 1) {
sheet.getRange(rowIndex, 2, 1, lastCol - 1).clearContent();
}
} else {
// Append a new row
sheet.appendRow(['CompanyLogo']);
rowIndex = sheet.getLastRow();
}
// 3. CHUNKING LOGIC: Split string into 50k char chunks
var CHUNK_SIZE = 50000;
var chunks = [];
for (var k = 0; k < base64String.length; k += CHUNK_SIZE) {
chunks.push(base64String.substring(k, k + CHUNK_SIZE));
}
// 4. Save chunks across columns (B, C, D...)
// getRange(row, col, numRows, numCols)
sheet.getRange(rowIndex, 2, 1, chunks.length).setValues([chunks]);
return { status: 'success' };
}
function saveCompanySettings(form) {
try {
var ss = SpreadsheetApp.openById(SHEET_ID);
var sheet = ss.getSheetByName('Settings');
if (!sheet) {
sheet = ss.insertSheet('Settings');
sheet.appendRow(['Key', 'Value']);
}
var data = sheet.getDataRange().getValues();
// Prepare updates
var updates = {
'CompanyName': form.name,
'CompanySub': form.sub,
'SystemStatus': form.status,
'CompanyAddress': form.address,
'CompanyEmail': form.email,
'CompanyContact': form.contact
};
// Safely add colors only if they were sent
if (form.colors) {
updates['ThemeMaroon'] = form.colors.maroon;
updates['ThemeGold'] = form.colors.gold;
updates['ThemeGreen'] = form.colors.green;
updates['ThemeRed'] = form.colors.red;
updates['ThemeBlue'] = form.colors.blue;
}
// 1. Update existing rows
for (var i = 1; i < data.length; i++) {
var key = String(data[i][0]).trim();
if (updates.hasOwnProperty(key)) {
sheet.getRange(i + 1, 2).setValue(updates[key]);
delete updates[key];
}
}
// 2. Append new rows
for (var key in updates) {
sheet.appendRow([key, updates[key]]);
}
return { status: 'success' };
} catch (e) {
// Return the error to the frontend instead of hanging
return { status: 'error', message: e.toString() };
}
}
2. index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCASH SYSTEM</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<!-- Bootstrap Icons CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Load CSS -->
<?!= include('css'); ?>
</head>
<body>
<!-- LOADING SPINNER -->
<div id="loader" class="loader-container">
<div class="spinner"></div>
</div>
<!-- LANDING / LOGIN PAGE -->
<div id="login-section" class="main-container">
<!-- LOGIN FORM CONTAINER -->
<div id="form-login" class="login-box fade-in">
<!-- IMPORTANT: Class 'brand-logo' allows the script to update this image -->
<img src="https://lh3.googleusercontent.com/d/1Dltghw9r1Dg98lLsut5DpBqDGXa0URRc" class="brand-logo" alt="Logo">
<h3 style="margin-top: 40px;">Staff Access</h3>
<div class="input-group">
<label>Username</label>
<input type="text" id="username" placeholder="Enter username">
</div>
<div class="input-group">
<label>Password</label>
<input type="password" id="password" placeholder="Enter password">
</div>
<button onclick="handleLogin()" id="btn-login" class="btn-primary">LOGIN</button>
<p id="login-msg" class="error-msg"></p>
<div style="text-align:center; margin-top:15px; margin-bottom: 20px; font-size:12px; color:#888;">
No account? <span onclick="toggleAuthMode('register')" class="link-text">Register here</span>
</div>
<!-- INTEGRATED WALLET SECTION -->
<div class="login-divider"></div>
<div class="internal-wallet-wrapper">
<div class="internal-wallet-header" onclick="toggleWalletList()">
<div style="display:flex; align-items:center; gap:8px;">
<i class="bi bi-wallet2" style="color:var(--gold);"></i>
<span>GCash Balances</span>
</div>
<i id="wallet-arrow" class="bi bi-chevron-down" style="font-size:12px;"></i>
</div>
<div id="public-wallet-list" class="internal-wallet-content">
<div style="padding:10px; text-align:center;">
<div class="spinner" style="width:15px; height:15px; border-width:2px; margin:0 auto;"></div>
</div>
</div>
</div>
</div>
<!-- REGISTER FORM CONTAINER -->
<div id="form-register" class="login-box fade-in" style="display:none;">
<!-- IMPORTANT: Class 'brand-logo' allows the script to update this image too -->
<img src="https://lh3.googleusercontent.com/d/1Dltghw9r1Dg98lLsut5DpBqDGXa0URRc" class="brand-logo" alt="Logo">
<h3 style="margin-top: 40px;">New Registration</h3>
<div class="input-group">
<label>Display Name</label>
<input type="text" id="reg-name" placeholder="e.g. Juan Cruz">
</div>
<div class="input-group">
<label>Username</label>
<input type="text" id="reg-username" placeholder="Choose a username">
</div>
<div class="input-group">
<label>Password</label>
<input type="password" id="reg-password" placeholder="Create password">
</div>
<button onclick="handleRegister()" id="btn-register" class="btn-primary" style="background:var(--bg-page); border:1px solid var(--border-color);">REGISTER</button>
<p id="reg-msg" class="error-msg"></p>
<div style="text-align:center; margin-top:15px; font-size:12px; color:var(--text-sub);">
Have an account? <span onclick="toggleAuthMode('login')" class="link-text">Login here</span>
</div>
</div>
</div>
<!-- DASHBOARD -->
<div id="dashboard-section" class="dashboard-container" style="display:none;">
<!-- Navbar -->
<nav class="navbar">
<div class="nav-brand">
<img src="https://lh3.googleusercontent.com/d/1Dltghw9r1Dg98lLsut5DpBqDGXa0URRc"
class="nav-logo" alt="2DLR"
onclick="openCompanyModal()"
style="cursor:pointer;"
title="Company Info">
</div>
<div class="nav-user">
<div id="user-display-container" style="text-align:right; line-height:1.2;">
<div id="user-name" style="font-weight:600; font-size:13px; color:var(--text-main);">User</div>
<div id="user-role" style="font-size:10px; color:var(--gold); letter-spacing:0.5px;">Role</div>
</div>
<button onclick="triggerMasterRefresh(this)" class="btn-small" title="Recalculate System" style="color:var(--gold);">
<i id="sync-icon" class="bi bi-arrow-repeat"></i>
</button>
<button onclick="openProfileModal()" class="btn-small" title="Settings">
<i class="bi bi-gear-fill"></i>
</button>
<button onclick="toggleTheme()" class="btn-small" title="Switch Theme">
<i id="theme-icon" class="bi bi-moon-fill"></i>
</button>
<button onclick="logout()" class="btn-small" title="Logout" style="color:var(--red);">
<i class="bi bi-box-arrow-right"></i>
</button>
</div>
</nav>
<div class="content-wrapper">
<!-- WELCOME BANNER -->
<div class="welcome-banner" style="display: flex; align-items: center; gap: 20px; text-align: left; padding: 20px;">
<div class="profile-display-wrapper" style="margin: 0; width: 70px; height: 70px; flex-shrink: 0;">
<img id="dashboard-profile-img" src="" onerror="this.style.display='none'">
<div id="dashboard-profile-placeholder">
<i class="bi bi-person-fill" style="font-size: 2rem;"></i>
</div>
</div>
<div style="flex-grow: 1;">
<h2 id="welcome-text" style="margin: 0; font-size: 1.1rem; color: var(--text-main);">Loading...</h2>
<div id="banner-role-display" style="font-size: 10px; color: var(--gold); letter-spacing: 1px; font-weight: 600; margin-bottom: 5px; text-transform: uppercase;">...</div>
<div style="display: flex; align-items: center; gap: 8px; color: var(--text-sub); font-size: 11px;">
<i class="bi bi-calendar3"></i>
<span id="live-date">...</span>
<span style="opacity: 0.3;">|</span>
<i class="bi bi-clock"></i>
<span id="live-time">...</span>
</div>
</div>
</div>
<!-- Quad Balance Card -->
<div class="balance-card">
<div class="bal-row">
<small>GCASH</small>
<h2 id="gcash-bal">...</h2>
</div>
<div class="bal-divider"></div>
<div class="bal-row">
<small>CASH</small>
<h2 id="cash-bal">...</h2>
</div>
<div class="bal-divider"></div>
<div class="bal-row">
<small>BORROWINGS</small>
<h2 id="ar-bal">...</h2>
</div>
<div class="bal-divider"></div>
<div class="bal-row">
<small>CREDITS</small>
<h2 id="ar-cust-bal">...</h2>
</div>
</div>
<!-- TAB NAVIGATION -->
<div class="tab-bar">
<button class="tab-btn active" onclick="switchTab('tab-customer', this)">Customer</button>
<button class="tab-btn" onclick="switchTab('tab-management', this)">Management</button>
<button class="tab-btn" onclick="switchTab('tab-account', this)">Account</button>
<button class="tab-btn" onclick="switchTab('tab-accounting', this)">Accounting</button>
</div>
<!-- 1. CUSTOMER TAB -->
<div id="tab-customer" class="tab-content fade-in" style="display:block;">
<div class="action-grid">
<div class="action-card" onclick="openModal('Cash In')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Cash In')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-send-fill"></i></div>
<h3>Cash In</h3>
<p>Send GCash</p>
</div>
<div class="action-card" onclick="openModal('Cash Out')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Cash Out')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-wallet2"></i></div>
<h3>Cash Out</h3>
<p>Receive GCash</p>
</div>
<div class="action-card" onclick="openModal('Load')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Load')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-phone-vibrate"></i></div>
<h3>Load</h3>
<p>Airtime / Data</p>
</div>
</div>
<div class="credit-section" style="margin-top: 10px;">
<hr class="neu-divider">
<h3 style="font-size: 14px; color: var(--text-sub); margin-bottom: 15px; text-transform: uppercase; letter-spacing: 1px;">Outstanding Customer Credits</h3>
<div class="search-row">
<input type="text" id="search-credits" class="search-input" placeholder="Search Customer..." onkeyup="filterCredits()">
<button class="btn-history-log" onclick="openViewList('Pay Credit')" title="View All Credit Payments"><i class="bi bi-clipboard-data-fill"></i></button>
</div>
<!-- NEW: DYNAMIC TOTAL DISPLAY -->
<div id="cred-summary" style="text-align: right; font-size: 13px; font-weight: bold; color: var(--red); margin-bottom: 10px; padding-right: 5px;">
Total: ₱ 0.00
</div>
<div id="credit-list-container">
<p style="text-align:center; font-size:12px; color:#666;">Loading credits...</p>
</div>
</div>
</div>
<!-- 2. MANAGEMENT TAB -->
<div id="tab-management" class="tab-content fade-in">
<div class="action-grid">
<div class="action-card" onclick="openModal('Invest')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Invest')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-graph-up-arrow"></i></div>
<h3>Invest</h3>
<p>Add Capital</p>
</div>
<div class="action-card" onclick="openModal('Borrow')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Borrow')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-bank2"></i></div>
<h3>Borrow</h3>
<p>Loan Request</p>
</div>
<div class="action-card" onclick="openModal('Withdraw')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Withdraw')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-cash-coin"></i></div>
<h3>Withdraw</h3>
<p>Take Out Cash</p>
</div>
</div>
<div class="credit-section">
<hr class="neu-divider">
<h3 style="font-size: 14px; color: var(--text-sub); margin-bottom: 15px; text-transform: uppercase; letter-spacing: 1px;">Outstanding Borrowings</h3>
<div class="search-row">
<input type="text" id="search-borrowings" class="search-input" placeholder="Search Borrower..." onkeyup="filterBorrowings()">
<button class="btn-history-log" onclick="openViewList('Pay Borrowing')" title="View All Loan Payments"><i class="bi bi-clipboard-data-fill"></i></button>
</div>
<!-- NEW: DYNAMIC TOTAL DISPLAY -->
<div id="borrow-summary" style="text-align: right; font-size: 13px; font-weight: bold; color: var(--blue); margin-bottom: 10px; padding-right: 5px;">
Total: ₱ 0.00
</div>
<div id="borrowing-list-container">
<p style="text-align:center; font-size:12px; color:#666;">Loading borrowings...</p>
</div>
</div>
</div>
<!-- 3. ACCOUNT TAB -->
<div id="tab-account" class="tab-content fade-in">
<div class="action-grid">
<div class="action-card" onclick="openModal('Add GCash')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Add GCash')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-plus-circle-dotted"></i></div>
<h3>Add GCash</h3>
<p>Top-up Wallet</p>
</div>
<div class="action-card" onclick="openModal('Transfer')">
<div class="card-view-btn" onclick="event.stopPropagation(); openViewList('Transfer')"><i class="bi bi-clock-history"></i></div>
<div class="icon"><i class="bi bi-arrow-left-right"></i></div>
<h3>Transfer</h3>
<p>Wallet to Wallet</p>
</div>
<div class="action-card" onclick="openWalletModal()">
<div class="icon"><i class="bi bi-wallet-fill"></i></div>
<h3>My Wallets</h3>
<p>Add/Request</p>
</div>
<div id="btn-admin-wallets" class="action-card" onclick="openAdminWalletModal()" style="display:none; border-color: #e67e22;">
<div class="icon" style="color:#e67e22;"><i class="bi bi-shield-check"></i></div>
<h3 style="color:#e67e22;">Approvals</h3>
<p>Wallet Requests</p>
</div>
</div>
</div>
<!-- 4. ACCOUNTING TAB -->
<div id="tab-accounting" class="tab-content fade-in">
<div class="action-grid">
<div class="action-card" onclick="openReportModal('journal')">
<div class="icon" style="color:#7f8c8d;"><i class="bi bi-book"></i></div>
<h3>Journal</h3>
<p>General Ledger</p>
</div>
<div class="action-card" onclick="openReportModal('tb')">
<div class="icon" style="color:#9b59b6;"><i class="bi bi-clipboard-check"></i></div>
<h3>Audit</h3>
<p>Trial Balance</p>
</div>
<div class="action-card" onclick="openReportModal('bs')">
<div class="icon" style="color:var(--blue);"><i class="bi bi-journal-check"></i></div>
<h3>Assets</h3>
<p>Balance Sheet</p>
</div>
<div class="action-card" onclick="openReportModal('sci')">
<div class="icon" style="color:var(--green);"><i class="bi bi-file-earmark-bar-graph"></i></div>
<h3>Income</h3>
<p>Profit & Loss</p>
</div>
<div class="action-card" onclick="openReportModal('profit')">
<div class="icon" style="color:#e67e22;"><i class="bi bi-pie-chart-fill"></i></div>
<h3>Dividends</h3>
<p>Profit Sharing</p>
</div>
<div class="action-card" onclick="openReportModal('daily')">
<div class="icon" style="color:var(--gold);"><i class="bi bi-calendar-date"></i></div>
<h3>Daily</h3>
<p>Today's Stats</p>
</div>
</div>
</div>
</div>
</div>
<!-- TRANSACTION MODAL -->
<div id="txn-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">New Transaction</h2>
<span class="close-btn" onclick="closeModal()">×</span>
</div>
<div class="modal-body">
<form id="txn-form" onsubmit="submitTransaction(event)">
<input type="hidden" id="t-edit-id" value="">
<div class="form-row" style="position: relative; z-index: 20;">
<div class="form-group">
<label>Date</label>
<input type="date" id="t-date" required>
</div>
<div class="form-group" id="grp-source">
<label id="lbl-source">Source Account</label>
<div class="custom-select-wrapper" id="wrapper-source">
<input type="hidden" id="t-source">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-source')">
<span id="display-source">Select Account</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="custom-options" id="list-source"></div>
</div>
</div>
<div class="form-group" id="grp-target" style="display:none;">
<label>Target Account (To)</label>
<div class="custom-select-wrapper" id="wrapper-target">
<input type="hidden" id="t-target">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-target')">
<span id="display-target">Select Account</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="custom-options" id="list-target"></div>
</div>
</div>
</div>
<div class="form-group" id="grp-fund-source" style="display:none; margin-bottom:15px; position:relative; z-index:19;">
<label>Source of Funds</label>
<div class="custom-select-wrapper" id="wrapper-fund">
<input type="hidden" id="t-fund-select" value="7/11">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-fund')">
<span id="display-fund">7/11 Cliqq</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="custom-options">
<div class="custom-option" onclick="selectCustomOption('wrapper-fund', 't-fund-select', 'display-fund', '7/11', '7/11 Cliqq')"><i class="bi bi-shop"></i> 7/11 Cliqq</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-fund', 't-fund-select', 'display-fund', 'Bank Account', 'Bank Account')"><i class="bi bi-bank"></i> Bank Account</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-fund', 't-fund-select', 'display-fund', 'Customers', 'Customers')"><i class="bi bi-people-fill"></i> Customers</div>
</div>
</div>
</div>
<div class="form-group" id="grp-customer">
<label id="lbl-customer">Customer Name</label>
<input type="text" id="t-customer" list="customer-list" placeholder="Type or select..." onchange="checkOwnerLimit()" required>
<datalist id="customer-list"></datalist>
<small id="limit-display" style="color: #4ade80; display:none; margin-top:5px; font-weight:bold;"></small>
</div>
<div id="grp-bank-details" style="display:none;">
<div class="form-row">
<div class="form-group">
<label>Bank Name</label>
<input type="text" id="t-bank-name" placeholder="e.g. BDO, BPI">
</div>
<div class="form-group">
<label>Account Number</label>
<input type="text" id="t-bank-acc" list="bank-list" placeholder="Type or select...">
<datalist id="bank-list"></datalist>
</div>
</div>
</div>
<div class="form-group" id="grp-ref">
<label>Reference No. (Optional)</label>
<input type="text" id="t-ref" placeholder="Reference Number">
</div>
<!-- NEW: Interest Option for Borrowing -->
<div class="form-group" id="grp-interest-option" style="display:none; margin-bottom: 15px;">
<label>Interest Option</label>
<div class="custom-select-wrapper" id="wrapper-interest-opt">
<input type="hidden" id="t-interest-opt" value="No Interest">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-interest-opt')">
<span id="display-interest-opt">No Interest</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="custom-options">
<div class="custom-option" onclick="selectCustomOption('wrapper-interest-opt', 't-interest-opt', 'display-interest-opt', 'No Interest', 'No Interest')">No Interest</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-interest-opt', 't-interest-opt', 'display-interest-opt', 'With Interest', 'With Interest')">With Interest</div>
</div>
</div>
</div>
<div class="money-section" id="money-section">
<div class="form-row">
<div class="form-group">
<label>Amount</label>
<input type="number" id="t-amount" step="0.01" placeholder="0.00" oninput="calculateTotals()" required>
</div>
<div class="form-group" id="grp-fee">
<label>Charge/Fee</label>
<input type="number" id="t-fee" step="0.01" readonly class="readonly-input">
</div>
</div>
<div class="total-display">
<label id="lbl-total">TOTAL TO PAY</label>
<div id="t-total-display">0.00</div>
<input type="hidden" id="t-total">
</div>
</div>
<div id="payment-details-wrapper">
<div class="payment-section">
<div class="form-row">
<div class="form-group">
<label>Settlement</label>
<div class="custom-select-wrapper" id="wrapper-settlement">
<input type="hidden" id="t-settlement" value="Cash">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-settlement')">
<span id="display-settlement">Cash</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="custom-options">
<div class="custom-option" onclick="selectCustomOption('wrapper-settlement', 't-settlement', 'display-settlement', 'Cash', 'Cash')"><i class="bi bi-cash-stack"></i> Cash</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-settlement', 't-settlement', 'display-settlement', 'On Account', 'On Account (Credit)')"><i class="bi bi-journal-text"></i> On Account (Credit)</div>
</div>
</div>
</div>
<div class="form-group" id="grp-paid-container">
<label id="lbl-paid">Cash Received</label>
<input type="number" id="t-paid" step="0.01" placeholder="0.00" oninput="calculateChange()">
</div>
</div>
<div id="tip-change-row" class="form-row" style="display:none;">
<div class="form-group">
<label>Tip (Optional)</label>
<input type="number" id="t-tip" step="0.01" placeholder="0.00" oninput="calculateChange()">
</div>
<div class="form-group">
<label>Change</label>
<input type="number" id="t-change" step="0.01" readonly class="readonly-input">
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn-submit">PROCESS TRANSACTION</button>
</div>
</form>
</div>
</div>
</div>
<!-- VIEW TRANSACTIONS MODAL -->
<div id="view-txn-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
<div><h2 id="view-txn-title" style="margin:0; font-size:1.1rem;">History</h2></div>
<div style="display:flex; align-items:center; gap:10px;">
<div class="custom-select-wrapper" id="wrapper-limit" style="width:130px;">
<input type="hidden" id="view-limit-select" value="50">
<div class="custom-select-trigger" onclick="toggleCustomSelect('wrapper-limit')" style="padding: 8px 12px; font-size: 12px;">
<span id="display-limit">Last 50</span>
<i class="bi bi-chevron-down" style="font-size: 10px;"></i>
</div>
<div class="custom-options">
<div class="custom-option" onclick="selectCustomOption('wrapper-limit', 'view-limit-select', 'display-limit', '50', 'Last 50')">Last 50</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-limit', 'view-limit-select', 'display-limit', '100', 'Last 100')">Last 100</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-limit', 'view-limit-select', 'display-limit', '500', 'Last 500')">Last 500</div>
<div class="custom-option" onclick="selectCustomOption('wrapper-limit', 'view-limit-select', 'display-limit', '0', 'Show All')">Show All</div>
</div>
</div>
<span class="close-btn" onclick="closeViewModal()" style="line-height:1;">×</span>
</div>
</div>
<div class="modal-body" style="padding: 0;">
<div style="overflow-x: auto; max-height: 400px;">
<table class="data-table">
<thead><tr><th>Date</th><th>ID</th><th>Customer/Source</th><th class="txt-right">Amount</th><th class="txt-right">Fee</th><th class="txt-right">Total</th><th id="th-action" style="text-align:center;">Action</th></tr></thead>
<tbody id="view-txn-body"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- HISTORY DETAILS MODAL -->
<div id="history-modal" class="modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>Transaction History</h2>
<span class="close-btn" onclick="closeHistoryModal()">×</span>
</div>
<div class="modal-body">
<h3 id="hist-customer" style="margin-top:0; font-size:14px; color:var(--text-grey);">Customer</h3>
<h4 id="hist-ref" style="margin:0 0 15px; font-size:12px; color:#666;">Ref: ...</h4>
<div class="history-list-header"><span>DATE</span><span>ACTIVITY</span><span style="text-align:right">AMOUNT</span><span style="text-align:right">BALANCE</span></div>
<div id="history-list-content"></div>
</div>
</div>
</div>
<!-- PROFILE SETTINGS MODAL -->
<div id="profile-modal" class="modal">
<div class="modal-content" style="max-width: 350px;">
<div class="modal-header">
<h2>Account Settings</h2>
<span class="close-btn" onclick="document.getElementById('profile-modal').style.display='none'">×</span>
</div>
<div class="modal-body" style="text-align:center;">
<div style="margin-bottom:25px;">
<div class="profile-upload-preview">
<img id="settings-profile-img" src="" style="display:none;">
<i id="settings-profile-icon" class="bi bi-person-circle" style="font-size:4rem; color:var(--text-sub);"></i>
</div>
<input type="file" id="profile-upload-input" accept="image/*" style="display:none;" onchange="handleImageSelection(this)">
<button onclick="document.getElementById('profile-upload-input').click()" class="btn-small" style="width:auto; padding:5px 15px; font-size:11px; border-radius:20px; margin-top:10px;"><i class="bi bi-camera-fill"></i> Change Photo</button>
</div>
<hr class="neu-divider">
<p style="color:#aaa; font-size:12px; margin-bottom:15px; text-align:left;">Update Password</p>
<div class="form-group"><input type="password" id="p-new-pass" placeholder="Enter new password"></div>
<div class="form-group" style="margin-bottom:20px;"><input type="password" id="p-confirm-pass" placeholder="Repeat password"></div>
<button onclick="submitPasswordChange()" class="btn-primary">UPDATE PASSWORD</button>
</div>
</div>
</div>
<!-- USER WALLET MANAGER MODAL -->
<div id="wallet-modal" class="modal">
<div class="modal-content" style="max-width: 450px;">
<div class="modal-header">
<h2>My Wallets</h2>
<span class="close-btn" onclick="document.getElementById('wallet-modal').style.display='none'">×</span>
</div>
<div class="modal-body" style="padding: 0;">
<div id="user-wallet-list" style="padding: 15px; background: var(--bg-page); max-height: 300px; overflow-y: auto;">
<p style="text-align:center; color:#888;">Loading your wallets...</p>
</div>
<div style="padding: 20px; border-top: 1px solid var(--border-color); background: var(--bg-card);">
<h4 style="margin:0 0 10px; font-size:12px; color:var(--text-sub); text-transform:uppercase;">Request New Wallet</h4>
<div style="display:flex; gap:10px;">
<input type="text" id="w-name" placeholder="Wallet Name (e.g. My GCash)" style="flex:1;">
<button onclick="submitNewWallet()" class="btn-primary" style="width:auto; padding:0 20px;">ADD</button>
</div>
</div>
</div>
</div>
</div>
<!-- ADMIN WALLET APPROVAL MODAL -->
<div id="admin-wallet-modal" class="modal">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header" style="background: #e67e22;">
<h2>Pending Approvals</h2>
<span class="close-btn" onclick="document.getElementById('admin-wallet-modal').style.display='none'">×</span>
</div>
<div class="modal-body"><div id="admin-wallet-list"></div></div>
</div>
</div>
<!-- REPORT MODAL: DAILY -->
<div id="modal-daily" class="modal">
<div class="modal-content">
<div class="modal-header" style="flex-direction:column; align-items:stretch; gap:10px; padding-bottom:10px;">
<div style="display:flex; justify-content:space-between; align-items:center;"><h2>Daily Report</h2><span class="close-btn" onclick="closeReportModal('daily')">×</span></div>
<div class="date-nav-bar" style="display:flex; justify-content:center; align-items:center; gap:10px;">
<button onclick="changeReportDate(-1)" class="btn-small" style="width:35px; height:35px;"><i class="bi bi-chevron-left"></i></button>
<input type="date" id="accounting-date-picker" onchange="loadAccountingData()" style="width:auto; text-align:center; font-weight:bold; color:var(--gold); border:1px solid var(--border-color);">
<button onclick="changeReportDate(1)" class="btn-small" style="width:35px; height:35px;"><i class="bi bi-chevron-right"></i></button>
</div>
</div>
<div class="modal-body">
<div class="balance-card" style="flex-direction: column; align-items: stretch; gap: 15px; margin-top:0;">
<div style="text-align:center;"><small style="color:var(--text-sub); letter-spacing:1px;">TOTAL INCOME</small><h1 id="acc-daily-income" style="margin:5px 0; color:var(--gold); font-size:2.5rem;">₱0.00</h1></div>
<hr class="neu-divider">
<div style="display:flex; justify-content:space-between; text-align:center;">
<div><small style="color:var(--text-sub);">CASH SALES</small><div id="acc-cash-sales" style="color:var(--green); font-weight:bold;">₱0.00</div></div>
<div style="width:1px; background:var(--border-color);"></div>
<div><small style="color:var(--text-sub);">CREDIT SALES</small><div id="acc-credit-sales" style="color:var(--red); font-weight:bold;">₱0.00</div></div>
</div>
</div>
<div class="acct-table-wrapper" style="margin-bottom: 20px; border: 1px solid var(--border-color);">
<table class="acct-table">
<thead><tr><th style="text-align:left; color:var(--text-sub); padding-left:10px;">PROFIT SHARE (EST.)</th><th style="text-align:right; color:var(--text-sub); padding-right:10px;">AMOUNT</th></tr></thead>
<tbody id="daily-share-rows"></tbody>
</table>
</div>
<h3 class="section-title">Cash Flow</h3>
<div class="flow-container">
<div class="flow-box"><div class="flow-icon in"><i class="bi bi-arrow-down-left"></i></div><div><small>In</small><div id="flow-cash-in">₱0.00</div></div></div>
<div class="flow-box"><div class="flow-icon out"><i class="bi bi-arrow-up-right"></i></div><div><small>Out</small><div id="flow-cash-out">₱0.00</div></div></div>
</div>
<h3 class="section-title">GCash Flow</h3>
<div class="flow-container">
<div class="flow-box"><div class="flow-icon in-blue"><i class="bi bi-phone"></i></div><div><small>In</small><div id="flow-gcash-in">₱0.00</div></div></div>
<div class="flow-box"><div class="flow-icon out-blue"><i class="bi bi-send"></i></div><div><small>Out</small><div id="flow-gcash-out">₱0.00</div></div></div>
</div>
</div>
</div>
</div>
<!-- REPORT MODAL: INCOME STATEMENT -->
<div id="modal-sci" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>Income Statement (YTD)</h2><span class="close-btn" onclick="closeReportModal('sci')">×</span></div>
<div class="modal-body">
<div class="acct-table-wrapper">
<table class="acct-table">
<tr><td colspan="2" style="font-weight:bold; color:var(--text-sub); padding-top:10px;">REVENUE</td></tr>
<tr><td style="padding-left:15px;">Service Fees</td><td class="txt-right" id="sci-rev-fees">₱0.00</td></tr>
<tr><td style="padding-left:15px;">Other Income (Penalties)</td><td class="txt-right" id="sci-rev-other">₱0.00</td></tr>
<tr class="total-row"><td>TOTAL REVENUE</td><td class="txt-right" id="sci-rev-total" style="color:var(--green);">₱0.00</td></tr>
<tr><td colspan="2" style="font-weight:bold; color:var(--text-sub); padding-top:15px;">OPERATING EXPENSES</td></tr>
<tr><td style="padding-left:15px;">Transaction Costs</td><td class="txt-right" id="sci-exp-costs">₱0.00</td></tr>
<tr class="total-row"><td>TOTAL EXPENSES</td><td class="txt-right" id="sci-exp-total" style="color:var(--red);">₱0.00</td></tr>
<tr class="total-row equity" style="border-top: 3px double var(--border-color);"><td style="font-size:14px; padding-top:15px;">NET INCOME</td><td class="txt-right" id="sci-net-income" style="font-size:14px; padding-top:15px;">₱0.00</td></tr>
</table>
</div>
</div>
</div>
</div>
<!-- REPORT MODAL: BALANCE SHEET -->
<div id="modal-bs" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>Balance Sheet</h2><span class="close-btn" onclick="closeReportModal('bs')">×</span></div>
<div class="modal-body">
<div class="acct-table-wrapper">
<table class="acct-table">
<tr><td colspan="2" style="font-weight:bold; color:var(--text-sub); padding-top:5px;">ASSETS</td></tr>
<tr><td>Cash on Hand</td><td class="txt-right" id="bs-cash">₱0.00</td></tr>
<tr><td>GCash Wallet (Total)</td><td class="txt-right" id="bs-gcash" style="font-weight:bold;">₱0.00</td></tr>
<tbody id="bs-wallet-rows"></tbody>
<tr><td>Accts. Receivable (Cust)</td><td class="txt-right" id="bs-ar-cust">₱0.00</td></tr>
<tr><td>Accts. Receivable (Loans)</td><td class="txt-right" id="bs-ar-loan">₱0.00</td></tr>
<tr class="total-row"><td>TOTAL ASSETS</td><td class="txt-right" id="bs-total">₱0.00</td></tr>
<tr><td colspan="2" style="font-weight:bold; color:var(--text-sub); padding-top:15px;">OWNER'S EQUITY</td></tr>
<tbody id="bs-equity-rows"></tbody>
<tr class="total-row equity"><td>TOTAL EQUITY</td><td class="txt-right" id="bs-equity">₱0.00</td></tr>
</table>
</div>
</div>
</div>
</div>
<!-- REPORT MODAL: TRIAL BALANCE -->
<div id="modal-tb" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>Trial Balance</h2><span class="close-btn" onclick="closeReportModal('tb')">×</span></div>
<div class="modal-body">
<div class="acct-table-wrapper">
<table class="acct-table">
<thead><tr><th style="text-align:left; color:var(--text-sub);">ACCOUNT</th><th style="text-align:right; color:var(--text-sub);">DEBIT</th><th style="text-align:right; color:var(--text-sub);">CREDIT</th></tr></thead>
<tbody id="tb-rows"></tbody>
<tr class="total-row"><td>TOTAL</td><td class="txt-right" id="tb-total-dr">₱0.00</td><td class="txt-right" id="tb-total-cr">₱0.00</td></tr>
</table>
<div id="tb-status" style="text-align:center; margin-top:15px; font-weight:bold; font-size:12px;">Checking...</div>
</div>
</div>
</div>
</div>
<!-- REPORT MODAL: GENERAL JOURNAL -->
<div id="modal-journal" class="modal">
<div class="modal-content" style="max-width: 750px;">
<div class="modal-header"><h2>General Journal</h2><span class="close-btn" onclick="closeReportModal('journal')">×</span></div>
<div class="modal-body" style="padding:0;">
<!-- INCREASED HEIGHT HERE: Changed max-height: 400px to height: 65vh -->
<div style="height: 65vh; overflow-y: auto;">
<table class="data-table">
<thead style="position:sticky; top:0; background:var(--bg-page); z-index:10;">
<tr><th style="padding-left:15px;">Date / ID</th><th>Account</th><th>Description</th><th class="txt-right">Debit</th><th class="txt-right" style="padding-right:15px;">Credit</th></tr>
</thead>
<tbody id="journal-rows"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- REPORT MODAL: PROFIT SHARING -->
<div id="modal-profit" class="modal">
<div class="modal-content">
<div class="modal-header"><h2>Profit Sharing (P/L)</h2><span class="close-btn" onclick="closeReportModal('profit')">×</span></div>
<div class="modal-body">
<div class="acct-table-wrapper">
<table class="acct-table">
<thead><tr><th style="text-align:left; color:var(--text-sub);">OWNER</th><th style="text-align:right; color:var(--text-sub);">CAPITAL</th><th style="text-align:right; color:var(--text-sub);">SHARE</th><th style="text-align:right; color:var(--text-sub);">ROI</th></tr></thead>
<tbody id="profit-rows"></tbody>
</table>
<p style="font-size:10px; color:var(--text-sub); text-align:center; margin-top:20px;">* Share is calculated daily based on capital ratio at that specific date.</p>
</div>
</div>
</div>
</div>
<!-- CONFIRMATION MODAL -->
<div id="modal-confirm" class="modal" style="z-index: 3000;">
<div class="modal-content" style="max-width: 320px; text-align: center; padding: 25px;">
<div style="font-size: 3rem; color: var(--gold); margin-bottom: 10px;"><i class="bi bi-exclamation-circle-fill"></i></div>
<h3 id="confirm-title" style="margin: 0; color: var(--text-main);">Confirm Action</h3>
<p id="confirm-msg" style="color: var(--text-sub); font-size: 13px; margin: 10px 0 25px;">Are you sure?</p>
<div style="display: flex; gap: 15px;">
<button onclick="closeConfirm()" class="btn-cancel" style="border:1px solid var(--border-color); border-radius:50px; padding:10px;">Cancel</button>
<button onclick="executeConfirm()" id="btn-confirm-yes" class="btn-primary" style="padding:10px; font-size:12px;">CONFIRM</button>
</div>
</div>
</div>
<!-- COMPANY INFO MODAL -->
<div id="modal-company" class="modal">
<div class="modal-content" style="max-width: 350px; max-height: 95vh; height: auto; display: flex; flex-direction: column; transition: max-width 0.3s ease;">
<!-- HEADER -->
<div style="padding: 15px; text-align: right; border-bottom: 1px solid var(--border-color); flex-shrink: 0;">
<span class="close-btn" onclick="document.getElementById('modal-company').style.display='none'" style="cursor:pointer;">×</span>
</div>
<!-- BODY -->
<div class="modal-body" style="padding: 20px; overflow-y: auto;">
<!-- LOGO SECTION -->
<div style="position:relative; width:80px; margin:0 auto 15px auto;">
<img id="company-logo-img" src="https://lh3.googleusercontent.com/d/1Dltghw9r1Dg98lLsut5DpBqDGXa0URRc" style="width:80px; height:80px; object-fit:contain; filter: drop-shadow(0 5px 10px rgba(0,0,0,0.3));">
<input type="file" id="company-logo-upload" accept="image/*" style="display:none;" onchange="handleLogoSelection(this)">
<div id="logo-edit-overlay" onclick="document.getElementById('company-logo-upload').click()" style="display:none; position:absolute; bottom:0; right:-5px; background:var(--gold); color:black; width:25px; height:25px; border-radius:50%; align-items:center; justify-content:center; cursor:pointer; box-shadow:var(--outer-shadow);">
<i class="bi bi-camera-fill" style="font-size:12px;"></i>
</div>
</div>
<!-- VIEW MODE (Narrow & Stacked) -->
<div id="view-mode-company">
<h2 id="disp-comp-name" style="color:var(--primary-maroon); margin:0; letter-spacing:1px; font-size: 1.2rem; text-align:center;">Loading...</h2>
<h3 id="disp-comp-sub" style="color:var(--text-main); margin:5px 0 15px; font-size:0.9rem; text-align:center;">...</h3>
<div style="width:40px; height:2px; background:var(--gold); margin:0 auto 15px;"></div>
<div style="text-align:left; background:var(--bg-input); padding:12px; border-radius:12px; font-size:11px; color:var(--text-sub); border:1px solid var(--border-color); margin-bottom:15px;">
<div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;">
<i class="bi bi-geo-alt-fill" style="color:var(--gold);"></i>
<span id="disp-comp-address">...</span>
</div>
<div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;">
<i class="bi bi-telephone-fill" style="color:var(--gold);"></i>
<span id="disp-comp-contact">...</span>
</div>
<div style="display:flex; align-items:center; gap:10px;">
<i class="bi bi-envelope-fill" style="color:var(--gold);"></i>
<span id="disp-comp-email">...</span>
</div>
</div>
<div id="disp-comp-status" style="text-align:center; padding:8px; background:rgba(46, 204, 113, 0.1); border-radius:12px; font-size:10px; color:var(--green); border:1px solid var(--green); font-weight:bold;"><i class="bi bi-wifi"></i> ...</div>
<div id="admin-edit-btn" style="text-align:center; display:none; margin-top:20px;">
<button onclick="toggleCompanyEdit(true)" class="btn-small" style="width:auto; padding:5px 15px; font-size:10px; border-radius:20px;">
<i class="bi bi-pencil"></i> Edit Info
</button>
</div>
</div>
<!-- EDIT MODE (Wide & Side-by-Side) -->
<div id="edit-mode-company" style="display:none; text-align:left;">
<p style="text-align:center; font-size:9px; color:var(--gold); margin-bottom:15px;">Tap logo to change</p>
<!-- ROW 1: Name & Subtitle -->
<div class="form-row" style="margin-bottom:10px;">
<div class="form-group">
<label>Company Name</label>
<input type="text" id="edit-comp-name">
</div>
<div class="form-group">
<label>Subtitle</label>
<input type="text" id="edit-comp-sub">
</div>
</div>
<!-- ROW 2: Address & Status -->
<div class="form-row" style="margin-bottom:10px;">
<div class="form-group">
<label>Address</label>
<input type="text" id="edit-comp-address">
</div>
<div class="form-group">
<label>System Status</label>
<input type="text" id="edit-comp-status">
</div>
</div>
<!-- ROW 3: Contact & Email -->
<div class="form-row" style="margin-bottom:10px;">
<div class="form-group">
<label>Contact No.</label>
<input type="text" id="edit-comp-contact">
</div>
<div class="form-group">
<label>Email</label>
<input type="text" id="edit-comp-email">
</div>
</div>
<!-- THEME COLORS -->
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border-color);">
<label style="margin-bottom:10px; display:block; color:var(--text-main); font-weight:bold; font-size:11px;">Theme Colors</label>
<div style="display:flex; flex-wrap:wrap; justify-content:space-around; gap:10px;">
<div style="text-align:center;">
<input type="color" id="col-maroon" title="Primary" style="width:40px; height:40px; padding:0; border:2px solid var(--bg-page); outline:1px solid var(--border-color); border-radius:10px; cursor:pointer;">
<div style="font-size:9px; color:var(--text-sub); margin-top:3px;">Primary</div>
</div>
<div style="text-align:center;">
<input type="color" id="col-gold" title="Accent" style="width:40px; height:40px; padding:0; border:2px solid var(--bg-page); outline:1px solid var(--border-color); border-radius:10px; cursor:pointer;">
<div style="font-size:9px; color:var(--text-sub); margin-top:3px;">Accent</div>
</div>
<div style="text-align:center;">
<input type="color" id="col-green" title="Success" style="width:40px; height:40px; padding:0; border:2px solid var(--bg-page); outline:1px solid var(--border-color); border-radius:10px; cursor:pointer;">
<div style="font-size:9px; color:var(--text-sub); margin-top:3px;">Success</div>
</div>
<div style="text-align:center;">
<input type="color" id="col-red" title="Danger" style="width:40px; height:40px; padding:0; border:2px solid var(--bg-page); outline:1px solid var(--border-color); border-radius:10px; cursor:pointer;">
<div style="font-size:9px; color:var(--text-sub); margin-top:3px;">Danger</div>
</div>
<div style="text-align:center;">
<input type="color" id="col-blue" title="Info" style="width:40px; height:40px; padding:0; border:2px solid var(--bg-page); outline:1px solid var(--border-color); border-radius:10px; cursor:pointer;">
<div style="font-size:9px; color:var(--text-sub); margin-top:3px;">Info</div>
</div>
</div>
<div style="text-align:center; margin-top:10px;">
<span onclick="resetThemeColors()" class="link-text" style="font-size:10px; opacity:0.8;">Reset Defaults</span>
</div>
</div>
<!-- BUTTONS -->
<div style="display:flex; gap:15px; margin-top:20px;">
<button onclick="toggleCompanyEdit(false)" class="btn-cancel" style="flex:1; border:1px solid var(--border-color); border-radius:50px; padding:10px; font-size:11px;">Cancel</button>
<button onclick="saveCompanyInfo()" class="btn-primary" style="flex:1; padding:10px; font-size:11px;">SAVE CHANGES</button>
</div>
</div>
<p style="color:var(--text-sub); font-size:9px; margin-top:15px; opacity:0.4;">System v2.2</p>
</div>
</div>
</div>
<!-- TOAST CONTAINER -->
<div id="toast-container"></div>
<!-- Load JS -->
<?!= include('js'); ?>
</body>
</html>
3. css.html
4. js.html
Final Step: Deployment
Click Deploy > New deployment. Select Web app, set "Execute as" to Me, and "Who has access" to Anyone.
View Live DemoCongratulations! Your GCash Management System is now live.
