Professional POS & Inventory System
A Gervic Programs Business Solution
Managing sales, tracking stock using FIFO methods, and reconciling daily cash shouldn't require expensive monthly subscriptions. This tutorial will guide you through implementing a custom-built, cloud-based Point of Sale and Inventory Management System using the power of Google Workspace.
Executive Summary
Functions: POS Terminal, FIFO Inventory Tracking, Employee HR Management, Daily Cash Reconciliation, and Sales Analytics.
Operations: The system automates stock deduction across batches, calculates profit margins, and generates real-time business insights.
System Module Overviews
Quick Restock Functionality: When stocks fall below the set Restock Value, this function becomes available within the Dashboard.
Add Product: This is where you introduce new products to the system.
View Product: This is where you view product details, history, and export product data to an excel or pdf file.
Return/Refund Functionality: Manage sold items by issuing refund or processing return upon request of customers.
Add Employee: Newly hired employees are added here. This also provide users the option to grant the employee an access to the system.
Update Employee Data: Manage sold items by issuing refund or processing return upon request of customers.
Financials: Manage employees' accountabilities and payroll through this section.
Step 1: Database Setup (Google Sheets)
Create a new Google Sheet and rename the tabs exactly as shown below. Each table must have the specific headers in the first row (Row 1) to ensure the script functions correctly.
| Sheet Name | Column Headers (Row 1) |
|---|---|
| Users | User ID | Username | Password | Role | Full Name |
| Employees | Emp ID | Name | Designation | Contact | Email | SSS | PhilHealth | TIN | Status |
| Inventory | Product ID | Name | SKU | Brand | Category | Price | Cost | Stock Qty | Reorder Level |
| Stock_Batches | Batch ID | Product ID | Date Received | Cost Price | Original Qty | Remaining Qty |
| Sales | Transaction ID | Date | Cashier Name | Total Amount | Payment Method | Items JSON |
| Cash_Reports | Report ID | Date | Reporter | Opening Bal | Receipts | System End | Actual Cash | Variance |
| Expenses | Expense ID | Date | Category | Amount | Description | Recorded By |
Step 2: Implementing the Scripts
Go to Extensions > Apps Script in your Google Sheet. Create four files to house the backend logic and the frontend interface:
1. Code.gs (Backend Engine)
/* =========================================
POS BACKEND SERVER
========================================= */
function doGet() {
var template = HtmlService.createTemplateFromFile('index');
return template.evaluate()
.setTitle('POS & Inventory System')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/* =========================================
AUTHENTICATION
========================================= */
function loginUser(username, password) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (row[1] === username && row[2] === password) {
return { status: 'success', role: row[3], username: row[1], fullname: row[4] };
}
}
return { status: 'error', message: 'Invalid username or password.' };
}
/* =========================================
3. INVENTORY CRUD (FIFO Enabled)
========================================= */
function getInventory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var invSheet = ss.getSheetByName('Inventory');
var salesSheet = ss.getSheetByName('Sales');
// 1. Calculate All-Time Sales Counts
var salesCounts = {};
if (salesSheet) {
var salesData = salesSheet.getDataRange().getValues();
// Loop through all sales
for (var j = 1; j < salesData.length; j++) {
var itemsJson = salesData[j][5]; // Col F: Items JSON
try {
var items = JSON.parse(itemsJson);
items.forEach(item => {
if (!salesCounts[item.id]) salesCounts[item.id] = 0;
salesCounts[item.id] += Number(item.qty);
});
} catch (e) {}
}
}
// 2. Build Product List
var data = invSheet.getDataRange().getValues();
var products = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
products.push({
id: row[0],
name: row[1],
sku: row[2],
brand: row[3],
category: row[4],
price: row[5],
cost: row[6],
stock: row[7],
reorderLevel: row[8],
image: row[9],
description: row[10],
supplier: row[11],
totalSold: salesCounts[row[0]] || 0
});
}
return products;
}
function addProduct(productData) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var invSheet = ss.getSheetByName('Inventory');
var batchSheet = ss.getSheetByName('Stock_Batches');
var newId = "P-" + new Date().getTime();
// 1. Add to Main Inventory
var newRow = [
newId,
productData.name,
productData.sku,
productData.brand,
productData.category,
productData.price,
productData.cost,
productData.stock, // Initial visual stock
productData.reorderLevel,
productData.image || '',
productData.description || '',
productData.supplier || ''
];
invSheet.appendRow(newRow);
// 2. CRITICAL: Create Initial Batch if Stock > 0
if (productData.stock > 0) {
var batchId = "BAT-INIT-" + new Date().getTime();
batchSheet.appendRow([
batchId,
newId,
new Date(),
productData.cost,
productData.stock, // Original
productData.stock, // Remaining
productData.price // Selling Price at time of entry
]);
}
return { status: 'success', message: 'Product and initial batch created successfully', id: newId };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
function updateProduct(productId, productData) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Inventory');
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
var currentStock = 0;
for (var i = 0; i < data.length; i++) {
if (data[i][0] == productId) {
rowIndex = i + 1;
currentStock = data[i][7];
break;
}
}
if (rowIndex === -1) return { status: 'error', message: 'Product ID not found' };
var updatedRow = [
productId,
productData.name,
productData.sku,
productData.brand,
productData.category,
productData.price,
productData.cost,
currentStock, // Keep actual stock value (FIFO)
productData.reorderLevel,
productData.image,
productData.description,
productData.supplier
];
sheet.getRange(rowIndex, 1, 1, 12).setValues([updatedRow]);
return { status: 'success', message: 'Product updated successfully' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
function deleteProduct(productId) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Inventory');
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
for (var i = 0; i < data.length; i++) {
if (data[i][0] == productId) {
rowIndex = i + 1;
break;
}
}
if (rowIndex === -1) return { status: 'error', message: 'Product not found' };
sheet.deleteRow(rowIndex);
return { status: 'success', message: 'Product deleted' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
/* =========================================
POS TRANSACTION LOGIC
========================================= */
function processSale(saleData) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(10000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var batchSheet = ss.getSheetByName('Stock_Batches');
var salesSheet = ss.getSheetByName('Sales');
var batchData = batchSheet.getDataRange().getValues();
var itemsToUpdate = [];
var finalItemsList = [];
var totalAmount = 0;
// --- FIFO PROCESSING LOOP ---
for (var j = 0; j < saleData.items.length; j++) {
var itemReq = saleData.items[j];
var qtyNeeded = itemReq.qty;
// Find all batches for this product
var productBatches = [];
for (var b = 1; b < batchData.length; b++) {
if (batchData[b][1] == itemReq.id && batchData[b][5] > 0) {
productBatches.push({
rowIndex: b + 1,
date: new Date(batchData[b][2]),
cost: batchData[b][3],
remaining: Number(batchData[b][5]),
data: batchData[b]
});
}
}
// Sort by Date (Oldest First) - The Core of FIFO
productBatches.sort((a, b) => a.date - b.date);
// Check Total Availability
var totalAvailable = productBatches.reduce((sum, b) => sum + b.remaining, 0);
if (totalAvailable < qtyNeeded) {
throw new Error("Insufficient stock for item ID: " + itemReq.id);
}
// Deduct from Batches
var qtyToFulfill = qtyNeeded;
for (var k = 0; k < productBatches.length; k++) {
if (qtyToFulfill <= 0) break;
var batch = productBatches[k];
var deduct = Math.min(batch.remaining, qtyToFulfill);
itemsToUpdate.push({
rowIndex: batch.rowIndex,
newQty: batch.remaining - deduct
});
qtyToFulfill -= deduct;
}
// Get Price from Inventory
var invSheet = ss.getSheetByName('Inventory');
var invData = invSheet.getDataRange().getValues();
var prodName = "Unknown";
var sellPrice = 0;
for(var p=1; p i.id))];
processedIds.forEach(pid => syncProductTotal(pid));
return { status: 'success', transactionId: newSaleRow[0] };
} catch (e) { return { status: 'error', message: e.message }; }
finally { lock.releaseLock(); }
}
/* =========================================
6. IMAGE UPLOADER
========================================= */
function uploadImageToDrive(base64Data, contentType, fileName) {
try {
var folderName = "POS_Images";
var folder;
var folders = DriveApp.getFoldersByName(folderName);
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = DriveApp.createFolder(folderName);
try { folder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); } catch(e) {}
}
var decoded = Utilities.base64Decode(base64Data);
var blob = Utilities.newBlob(decoded, contentType, fileName);
var file = folder.createFile(blob);
try {
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
} catch (e) {
console.log("Could not set public permission: " + e.toString());
}
var publicUrl = "https://drive.google.com/thumbnail?id=" + file.getId() + "&sz=w800";
return { status: 'success', url: publicUrl };
} catch (e) {
return { status: 'error', message: e.toString() };
}
}
/* =========================================
5. SALES HISTORY, EDIT & RETURNS LOGIC
========================================= */
function getSalesHistory() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sales');
var data = sheet.getDataRange().getValues();
var sales = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
var dateStr = row[1];
try { if (row[1] instanceof Date) dateStr = row[1].toISOString(); } catch (e) { dateStr = String(row[1]); }
sales.push({ id: row[0], date: dateStr, cashier: row[2], total: Number(row[3]), method: row[4], items: row[5] });
}
return sales.reverse();
}
function updateTransaction(transactionId, newData) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sales');
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
for (var i = 1; i < data.length; i++) {
if (data[i][0] == transactionId) { rowIndex = i + 1; break; }
}
if (rowIndex === -1) return { status: 'error', message: 'Transaction not found.' };
if (newData.date) sheet.getRange(rowIndex, 2).setValue(new Date(newData.date));
if (newData.cashier) sheet.getRange(rowIndex, 3).setValue(newData.cashier);
if (newData.method) sheet.getRange(rowIndex, 5).setValue(newData.method);
return { status: 'success', message: 'Transaction updated.' };
}
function getReturnsLog() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Returns');
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
if (data.length < 2) return [];
var logs = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (!row[0]) continue;
var dateStr = row[1];
try {
if (row[1] instanceof Date) {
dateStr = row[1].toLocaleString();
} else {
dateStr = String(row[1]);
}
} catch (e) {
dateStr = "Invalid Date";
}
logs.push({
id: row[0],
date: dateStr,
origTxn: row[2],
item: row[3],
qty: row[4],
amount: row[5],
cashier: row[6],
type: row[7],
reason: row[8] || "N/A"
});
}
return logs.reverse();
} catch (e) {
console.error("getReturnsLog Error: " + e.toString());
return [];
}
}
function logReturnToSheet(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var returnSheet = ss.getSheetByName('Returns');
if (!returnSheet) {
returnSheet = ss.insertSheet('Returns');
returnSheet.appendRow(['Return ID', 'Date', 'Orig Txn', 'Item', 'Qty', 'Amount', 'Cashier', 'Type', 'Reason']);
}
var id = "RET-" + new Date().getTime();
returnSheet.appendRow([
id, new Date(), data.origTxn, data.item, data.qty,
data.amount, data.cashier, data.type, data.reason || "N/A"
]);
}
function voidTransaction(transactionId, reason) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(10000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var salesSheet = ss.getSheetByName('Sales');
var invSheet = ss.getSheetByName('Inventory');
var batchSheet = ss.getSheetByName('Stock_Batches');
var salesData = salesSheet.getDataRange().getValues();
var invData = invSheet.getDataRange().getValues();
var saleRowIndex = -1;
var soldItems = [];
var cashierName = "Unknown";
for (var i = 1; i < salesData.length; i++) {
if (salesData[i][0] == transactionId) {
saleRowIndex = i + 1;
cashierName = salesData[i][2];
try { soldItems = JSON.parse(salesData[i][5]); } catch(e) { soldItems = []; }
break;
}
}
if (saleRowIndex === -1) return { status: 'error', message: 'Transaction not found.' };
var invMap = {};
for (var j = 1; j < invData.length; j++) {
invMap[invData[j][0]] = { rowIndex: j + 1, currentStock: invData[j][7], currentCost: invData[j][6] };
}
soldItems.forEach(function(item) {
var costToLog = 0;
if (invMap[item.id]) {
invSheet.getRange(invMap[item.id].rowIndex, 8).setValue(invMap[item.id].currentStock + item.qty);
costToLog = invMap[item.id].currentCost;
}
// Create "Return Batch"
var batchId = "RET-VOID-" + new Date().getTime();
batchSheet.appendRow([
batchId, item.id, new Date(), costToLog, item.qty, item.qty, item.price
]);
// Log to Returns Sheet
logReturnToSheet({
origTxn: transactionId,
item: item.name,
qty: item.qty,
amount: (item.price * item.qty),
cashier: cashierName,
type: "Full Void",
reason: reason
});
});
salesSheet.deleteRow(saleRowIndex);
return { status: 'success', message: 'Transaction voided, stock restored.' };
} catch (e) { return { status: 'error', message: e.toString() }; }
finally { lock.releaseLock(); }
}
function refundPartialItem(transactionId, productId, refundQty, reason) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(10000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var salesSheet = ss.getSheetByName('Sales');
var invSheet = ss.getSheetByName('Inventory');
var batchSheet = ss.getSheetByName('Stock_Batches');
var salesData = salesSheet.getDataRange().getValues();
var invData = invSheet.getDataRange().getValues();
var saleRowIndex = -1;
var saleRowData = [];
for (var i = 1; i < salesData.length; i++) {
if (salesData[i][0] == transactionId) { saleRowIndex = i + 1; saleRowData = salesData[i]; break; }
}
if (saleRowIndex === -1) return { status: 'error', message: 'Transaction not found.' };
var cashierName = saleRowData[2];
var soldItems = [];
try { soldItems = JSON.parse(saleRowData[5]); } catch(e) { return { status: 'error', message: 'Corrupt data.' }; }
var itemIndex = -1;
for (var k = 0; k < soldItems.length; k++) {
if (soldItems[k].id === productId) { itemIndex = k; break; }
}
if (itemIndex === -1) return { status: 'error', message: 'Item not found in transaction.' };
var targetItem = soldItems[itemIndex];
if (refundQty > targetItem.qty) return { status: 'error', message: 'Cannot refund more than sold.' };
// Restore Main Inventory
var invRowIndex = -1; var currentStock = 0; var currentCost = 0;
for (var j = 1; j < invData.length; j++) {
if (invData[j][0] == productId) { invRowIndex = j + 1; currentStock = Number(invData[j][7]); currentCost = Number(invData[j][6]); break; }
}
if (invRowIndex !== -1) {
invSheet.getRange(invRowIndex, 8).setValue(currentStock + refundQty);
}
// Create Return Batch
var batchId = "RET-" + new Date().getTime();
batchSheet.appendRow([
batchId, productId, new Date(), currentCost, refundQty, refundQty, targetItem.price
]);
// Log to Returns Sheet
logReturnToSheet({
origTxn: transactionId,
item: targetItem.name,
qty: refundQty,
amount: (targetItem.price * refundQty),
cashier: cashierName,
type: "Partial Refund",
reason: reason
});
// Update Sales Data
targetItem.qty -= refundQty;
if (targetItem.qty <= 0) soldItems.splice(itemIndex, 1);
if (soldItems.length === 0) {
salesSheet.deleteRow(saleRowIndex);
return { status: 'success', message: 'All items returned. Transaction deleted.' };
}
var newTotal = 0;
soldItems.forEach(item => { newTotal += (item.price * item.qty); });
salesSheet.getRange(saleRowIndex, 4).setValue(newTotal);
salesSheet.getRange(saleRowIndex, 6).setValue(JSON.stringify(soldItems));
return { status: 'success', message: 'Item refunded and returned to stock.' };
} catch (e) { return { status: 'error', message: e.toString() }; }
finally { lock.releaseLock(); }
}
/* =========================================
DASHBOARD ANALYTICS
========================================= */
function getDashboardData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var salesSheet = ss.getSheetByName('Sales');
var invSheet = ss.getSheetByName('Inventory');
var debtSheet = ss.getSheetByName('Staff_Debts');
// 1. DATA PREPARATION
var salesData = salesSheet ? salesSheet.getDataRange().getValues() : [];
var invData = invSheet ? invSheet.getDataRange().getValues() : [];
var debtData = debtSheet ? debtSheet.getDataRange().getValues() : [];
var today = new Date();
var startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
var startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
var metrics = {
// Financials
todaySales: 0,
monthSales: 0,
monthProfit: 0, // NEW
totalDebt: 0, // NEW
inventoryValue: 0, // NEW
// Inventory Health
totalProducts: 0, // NEW
lowStockCount: 0,
outOfStockCount: 0, // NEW
lowStockItems: [],
// Charts & Tables
last7Days: {},
categoryData: {},
paymentMethods: { 'Cash': 0, 'Card': 0, 'GCash': 0 }, // NEW
topProducts: {}, // NEW
topCashiers: {} // NEW
};
// 2. PROCESS INVENTORY (Map Cost for Profit Calc)
var costMap = {}; // ID -> Cost
for (var j = 1; j < invData.length; j++) {
var row = invData[j];
var id = row[0];
var category = row[4];
var price = Number(row[5]);
var cost = Number(row[6]);
var stock = Number(row[7]);
var reorder = Number(row[8]);
// Maps
costMap[id] = cost;
// Stats
metrics.totalProducts++;
metrics.inventoryValue += (cost * stock);
// Category Chart
if (!metrics.categoryData[category]) metrics.categoryData[category] = 0;
metrics.categoryData[category]++;
// Stock Alerts
if (stock <= 0) {
metrics.outOfStockCount++;
metrics.lowStockItems.push({ id: id, name: row[1], stock: stock, reorder: reorder, status: 'Out of Stock' });
} else if (stock <= reorder) {
metrics.lowStockCount++;
metrics.lowStockItems.push({ id: id, name: row[1], stock: stock, reorder: reorder, status: 'Low' });
}
}
// 3. PROCESS SALES
for (var i = 1; i < salesData.length; i++) {
var row = salesData[i];
var dateVal = new Date(row[1]);
var total = Number(row[3]);
var method = row[4];
var cashier = row[2];
var itemsJson = row[5];
// A. Date Filters
var isToday = dateVal >= startOfToday;
var isMonth = dateVal >= startOfMonth;
if (isToday) metrics.todaySales += total;
if (isMonth) {
metrics.monthSales += total;
// Calculate Gross Profit (Price - Cost) for this month
// Note: Uses current cost. For strict accounting, you'd need cost at time of sale.
try {
var items = JSON.parse(itemsJson);
items.forEach(item => {
var itemCost = costMap[item.id] || 0;
var profit = (item.price - itemCost) * item.qty;
metrics.monthProfit += profit;
// Top Products Logic
if (!metrics.topProducts[item.name]) metrics.topProducts[item.name] = 0;
metrics.topProducts[item.name] += item.qty;
});
} catch(e) {}
}
// B. Sales Trend (7 Days)
var dateKey = (dateVal.getMonth()+1) + "/" + dateVal.getDate();
if (!metrics.last7Days[dateKey]) metrics.last7Days[dateKey] = 0;
metrics.last7Days[dateKey] += total;
// C. Payment Methods (All Time or Month - Let's do Month for relevance)
if (isMonth) {
if (!metrics.paymentMethods[method]) metrics.paymentMethods[method] = 0;
metrics.paymentMethods[method] += total;
// D. Cashier Performance (Month)
if (!metrics.topCashiers[cashier]) metrics.topCashiers[cashier] = 0;
metrics.topCashiers[cashier] += total;
}
}
// 4. PROCESS DEBTS
for (var k = 1; k < debtData.length; k++) {
// Col 6 = Status (Index 5), Col 5 = Amount (Index 4)
if (debtData[k][5] === 'Unpaid') {
metrics.totalDebt += Number(debtData[k][4]);
}
}
// 5. SORT TOP LISTS
// Convert Objects to Sorted Arrays
metrics.topProducts = Object.entries(metrics.topProducts)
.sort((a,b) => b[1] - a[1])
.slice(0, 5); // Top 5
metrics.topCashiers = Object.entries(metrics.topCashiers)
.sort((a,b) => b[1] - a[1])
.slice(0, 5); // Top 5
return metrics;
}
/* =========================================
QUICK RESTOCK FUNCTION
========================================= */
function restockProduct(productId, qtyToAdd, newCost) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var batchSheet = ss.getSheetByName('Stock_Batches');
var invSheet = ss.getSheetByName('Inventory');
// 1. Get Current Cost if not provided
var costPrice = newCost;
if (!costPrice) {
var invData = invSheet.getDataRange().getValues();
for (var i = 1; i < invData.length; i++) {
if (invData[i][0] == productId) {
costPrice = invData[i][6];
break;
}
}
}
// 2. Create New Batch
var batchId = "BAT-" + new Date().getTime();
batchSheet.appendRow([
batchId, productId, new Date(), costPrice, qtyToAdd, qtyToAdd
]);
// 3. Sync Total
syncProductTotal(productId);
return { status: 'success', message: 'Batch added successfully!' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
function syncProductTotal(productId) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var batchSheet = ss.getSheetByName('Stock_Batches');
var invSheet = ss.getSheetByName('Inventory');
var batchData = batchSheet.getDataRange().getValues();
var totalQty = 0;
for (var i = 1; i < batchData.length; i++) {
if (batchData[i][1] == productId) {
totalQty += Number(batchData[i][5]);
}
}
var invData = invSheet.getDataRange().getValues();
for (var j = 1; j < invData.length; j++) {
if (invData[j][0] == productId) {
invSheet.getRange(j + 1, 8).setValue(totalQty);
break;
}
}
return totalQty;
}
function getProductTransactionHistory(productId) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var batchSheet = ss.getSheetByName('Stock_Batches');
var salesSheet = ss.getSheetByName('Sales');
var invSheet = ss.getSheetByName('Inventory');
var returnSheet = ss.getSheetByName('Returns');
var history = [];
// 0. GET PRODUCT DETAILS
var currentCost = 0;
var productName = "";
var invData = invSheet.getDataRange().getValues();
for (var k = 1; k < invData.length; k++) {
if (invData[k][0] == productId) {
productName = invData[k][1];
currentCost = Number(invData[k][6]);
break;
}
}
// 1. MAP RETURNS
var returnsMap = {};
if (returnSheet) {
var retData = returnSheet.getDataRange().getValues();
for (var r = 1; r < retData.length; r++) {
var rTxn = retData[r][2];
var rItem = retData[r][3];
var rQty = Number(retData[r][4]);
if (rItem === productName) {
if (!returnsMap[rTxn]) returnsMap[rTxn] = 0;
returnsMap[rTxn] += rQty;
}
}
}
// 2. GET "IN" TRANSACTIONS
if (batchSheet) {
var batchData = batchSheet.getDataRange().getValues();
for (var i = 1; i < batchData.length; i++) {
if (batchData[i][1] == productId) {
var batchId = batchData[i][0];
var type = "Restock";
if (String(batchId).indexOf("RET-VOID") === 0) type = "Void Restock";
else if (String(batchId).indexOf("RET-") === 0) type = "Return";
else if (String(batchId).indexOf("BAT-LEGACY") === 0 || String(batchId).indexOf("BAT-INIT-") === 0) type = "Initial Stock";
var batchCost = Number(batchData[i][3]) || currentCost;
history.push({
dateObj: new Date(batchData[i][2]),
id: batchId,
type: type,
inQty: Number(batchData[i][4]),
outQty: 0,
amount: batchCost * Number(batchData[i][4])
});
}
}
}
var processedTxnIds = new Set();
// 3. GET "OUT" TRANSACTIONS
if (salesSheet) {
var salesData = salesSheet.getDataRange().getValues();
for (var j = 1; j < salesData.length; j++) {
var row = salesData[j];
var txnId = row[0];
var itemsJson = row[5];
try {
var items = JSON.parse(itemsJson);
var soldItem = items.find(item => item.id == productId);
if (soldItem) {
processedTxnIds.add(txnId);
var returnedQty = returnsMap[txnId] || 0;
var originalQty = soldItem.qty + returnedQty;
var costAmount = currentCost * originalQty;
history.push({
dateObj: new Date(row[1]),
id: txnId,
type: "Sale",
inQty: 0,
outQty: originalQty,
amount: costAmount
});
}
} catch(e) {}
}
}
// 4. RECONSTRUCT VOIDED TRANSACTIONS
Object.keys(returnsMap).forEach(txnId => {
if (!processedTxnIds.has(txnId)) {
var txnDate = new Date();
try {
var parts = txnId.split('-');
if(parts.length > 1) {
var ts = parseInt(parts[1]);
if(!isNaN(ts)) txnDate = new Date(ts);
}
} catch(e) {}
var voidedQty = returnsMap[txnId];
history.push({
dateObj: txnDate,
id: txnId,
type: "Sale (Voided)",
inQty: 0,
outQty: voidedQty,
amount: currentCost * voidedQty
});
}
});
// 5. SORT & CALCULATE
history.sort(function(a, b) { return a.dateObj - b.dateObj; });
var currentOnHand = 0;
var finalLog = [];
history.forEach(h => {
currentOnHand = currentOnHand + h.inQty - h.outQty;
var dateStr = "N/A";
try { dateStr = h.dateObj.toLocaleDateString() + " " + h.dateObj.toLocaleTimeString(); } catch(e) {}
finalLog.push({
date: dateStr,
id: h.id,
type: h.type,
in: h.inQty > 0 ? h.inQty : "-",
out: h.outQty > 0 ? h.outQty : "-",
onHand: currentOnHand,
amount: h.amount
});
});
return finalLog;
}
/* =========================================
CASH REPORT DATABASE LOGIC
========================================= */
function saveCashReport(reportData) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Cash_Reports');
// ... (Create sheet logic same as before) ...
var data = sheet.getDataRange().getValues();
var timeZone = ss.getSpreadsheetTimeZone();
var foundRowIndex = -1;
var currentId = "CR-" + new Date().getTime();
for (var i = 1; i < data.length; i++) {
var row = data[i];
var rowDateStr = "";
if (row[1] instanceof Date) {
rowDateStr = Utilities.formatDate(row[1], timeZone, "yyyy-MM-dd");
} else {
rowDateStr = String(row[1]).substring(0, 10);
}
// CHECK DATE AND REPORTER
if (rowDateStr === reportData.date && row[2] === reportData.reporter) {
foundRowIndex = i + 1;
currentId = row[0];
break;
}
}
// ... (Rest of the saving logic matches the previous version) ...
// Just ensure rowData uses the variables above.
var rowData = [
currentId, reportData.date, reportData.reporter, reportData.openBal,
reportData.receipts, reportData.disbursements, reportData.systemEnd,
reportData.actualCash, reportData.actualCheck, reportData.totalActual,
reportData.variance, JSON.stringify(reportData.breakdown),
reportData.notes || "", "" // Reset resolution status on save
];
if (foundRowIndex > 0) {
sheet.getRange(foundRowIndex, 1, 1, 14).setValues([rowData]);
return { status: 'success', message: 'Report Updated Successfully!' };
} else {
sheet.appendRow(rowData);
return { status: 'success', message: 'Report Saved Successfully!' };
}
} catch (e) { return { status: 'error', message: e.toString() }; }
finally { lock.releaseLock(); }
}
function getCashReport(dateStr, cashierName) { // <--- ---="" 1.="" 10="" 2.="" actualcash:="" actualcheck:="" added="" and="" ash_reports="" balance="" both="" breakdownjson:="" c:="" calculator="" cashier="" cashiername="" column="" currentdatestr="" d.tostring="" d="" data.length="" data="reportSheet.getDataRange().getValues();" date="" dates="" datestr="" else="" false="" find="" for="" found:="" found="" function="" getpreviousactualbalance="" helper:="" i="" if="" instanceof="" ixed:="" match="" most="" normalize="" not_found="" notes:="" nvalid="" param="" previous="" rawdate="" recent="" report="" reporter="" reportsheet="" resolutionstatus:="" return="" row="" rowdateformatted="==" rowreporter="==" sheet="" smart="" sortedrows="data.slice(1).map(r" ss="SpreadsheetApp.getActiveSpreadsheet();" status:="" str.match="" str="" the="" this="" timezone="" toisodate="function(val)" utilities.formatdate="" val="" var="" yyyy-mm-dd=""> ({ row: r, isoDate: toIsoDate(r[1]) }))
.sort((a, b) => b.isoDate.localeCompare(a.isoDate));
var prevReportRow = null;
var prevIsoDate = "";
for (var i = 0; i < sortedRows.length; i++) {
// Condition: Date is older AND Reporter matches Cashier Name
// Column C (Index 2) is Reporter/Cashier
if (sortedRows[i].isoDate < currentDateStr && sortedRows[i].row[2] === cashierName) {
prevReportRow = sortedRows[i].row;
prevIsoDate = sortedRows[i].isoDate;
break;
}
}
if (!prevReportRow) return { found: false };
// --- 3. Get Totals ---
var cleanNum = function(val) { return parseFloat(String(val).replace(/[^0-9.-]+/g,"")) || 0; };
var prevTotalActual = cleanNum(prevReportRow[9]); // Col J: Total Actual
// --- 4. DYNAMICALLY RE-CALCULATE RECEIPTS (Filtered by Cashier) ---
// A. Sum Cash Sales
var salesSheet = ss.getSheetByName('Sales');
var totalSales = 0;
if (salesSheet) {
var sData = salesSheet.getDataRange().getValues();
for (var s = 1; s < sData.length; s++) {
// Check Date + Cash + Cashier
if (toIsoDate(sData[s][1]) === prevIsoDate && sData[s][4] === 'Cash' && sData[s][2] === cashierName) {
totalSales += Number(sData[s][3]);
}
}
}
// B. Sum Adjustments (Overages + Payments)
var adjSheet = ss.getSheetByName('Cash_Adjustments');
var totalAdjustments = 0;
if (adjSheet) {
var aData = adjSheet.getDataRange().getValues();
for (var a = 1; a < aData.length; a++) {
var aDate = toIsoDate(aData[a][4]);
var aType = aData[a][2];
var aCashier = aData[a][7]; // Col H: Cashier Name
if (aDate === prevIsoDate && aCashier === cashierName) {
if (aType === 'Cash Overage' || aType === 'Shortage Payment') {
totalAdjustments += Number(aData[a][3]);
}
}
}
}
var trueTotalReceipts = totalSales + totalAdjustments;
// --- 5. CALCULATE CARRY OVER ---
var carryOver = prevTotalActual - trueTotalReceipts;
if (carryOver < 0) carryOver = 0;
if (Math.abs(carryOver) < 0.01) carryOver = 0;
return {
found: true,
balance: carryOver,
date: prevIsoDate
};
}
function resolveVariance(data) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var resolutionNote = "";
// 1. HANDLE THE BOOKING (Existing Logic)
if (data.type === 'Shortage') {
var debtSheet = ss.getSheetByName('Staff_Debts');
if (!debtSheet) {
debtSheet = ss.insertSheet('Staff_Debts');
debtSheet.appendRow(['Date', 'Cashier', 'Report Date', 'Amount', 'Status', 'Admin Note']);
}
debtSheet.appendRow([
new Date(),
data.cashierName,
data.reportDate,
Math.abs(data.amount),
"Unpaid",
"Charged from Daily Report: " + data.notes
]);
resolutionNote = "Charged to " + data.cashierName;
}
else if (data.type === 'Overage') {
var incSheet = ss.getSheetByName('Misc_Income');
if (!incSheet) {
incSheet = ss.insertSheet('Misc_Income');
incSheet.appendRow(['Date', 'Source', 'Report Date', 'Amount', 'Notes']);
}
incSheet.appendRow([
new Date(),
"Cash Overage",
data.reportDate,
Math.abs(data.amount),
data.notes
]);
resolutionNote = "Booked as Misc Income";
}
// 2. NEW: UPDATE CASH REPORT STATUS TO "RESOLVED"
var reportSheet = ss.getSheetByName('Cash_Reports');
if (reportSheet) {
var rData = reportSheet.getDataRange().getValues();
var timeZone = ss.getSpreadsheetTimeZone();
for (var i = 1; i < rData.length; i++) {
var rowDateStr = "";
if (rData[i][1] instanceof Date) {
rowDateStr = Utilities.formatDate(rData[i][1], timeZone, "yyyy-MM-dd");
} else {
rowDateStr = String(rData[i][1]).substring(0, 10);
}
// Find the report by date and mark it
if (rowDateStr === data.reportDate) {
// Column N (Index 14) is Resolution Status.
// We use (i + 1) for row, 14 for column.
reportSheet.getRange(i + 1, 14).setValue("RESOLVED: " + resolutionNote);
break;
}
}
}
return { status: 'success', message: resolutionNote + " successfully recorded. Report locked." };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
/* =========================================
UPDATED: CHECK DEBTS ON LOGIN (Filters by ID)
========================================= */
function checkUserDebts(username) {
// Note: In your system, Username IS the Employee ID (e.g., EMP-123)
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Staff_Debts');
if (!sheet) return { hasDebt: false };
var data = sheet.getDataRange().getValues();
var debts = [];
for (var i = 1; i < data.length; i++) {
// Check Column B (Emp ID) match & Status 'Unpaid'
if (String(data[i][1]) === String(username) && data[i][5] === 'Unpaid') {
debts.push({
date: Utilities.formatDate(new Date(data[i][0]), ss.getSpreadsheetTimeZone(), "yyyy-MM-dd"),
amount: data[i][4]
});
}
}
return { hasDebt: (debts.length > 0), debtList: debts };
}
/* =========================================
7. CASH ADJUSTMENTS (Over/Short Management)
========================================= */
// A. Post an Adjustment (Cashier Action)
function postCashAdjustment(data) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Cash_Adjustments');
if (!sheet) {
sheet = ss.insertSheet('Cash_Adjustments');
// Added 'Cashier' at the end
sheet.appendRow(['Adj ID', 'Date', 'Type', 'Amount', 'Report Date', 'Status', 'Notes', 'Cashier']);
sheet.getRange(1, 1, 1, 8).setFontWeight('bold');
}
var id = "ADJ-" + new Date().getTime();
sheet.appendRow([
id,
new Date(),
data.type,
Number(data.amount),
data.reportDate,
"Pending",
data.notes,
data.cashier // <--- ---="" 0="" 1.="" 10="" 1:="" 1="" 2.="" 2:="" 3.="" 3:="" 6="" 7="" 9.="" :="" a="" action="==" active="" address:="" adjdata.length="" adjdata="" adjid="" adjrecord.amount="" adjrecord.date="" adjrecord.notes="" adjrecord="" adjrowindex="" adjsheet.appendrow="" adjsheet.getrange="" adjsheet="ss.getSheetByName(" adjustment="" adjustments="" admin="" ag-ibig="" aid="" all="" ame="" amount:="" amountval="" and="" ash_adjustments="" ashier="" at="" ate="" aves="" b.="" b="Name," back="" below="" blank="" bold="" book="" book_income="" break="" but="" by="" can="" case="" cash="" cashier:="" cashier="" catch="" ccess="" charge="" charge_custodian="" check="" col="" comparison="" contact:="" ctive="" currentidstr.indexof="" currentidstr="String(sheetData[k][0]);" data.length="" data.type="" data="" date:="" date="" datestr="" ddress="" debt="" debtsheet.appendrow="" debtsheet.getrange="" debtsheet="ss.insertSheet(" designation:="" details="" dit="" dmin="" don="" e.tostring="" e="" eceipt="" ecord="" edit="" else="" email:="" emp="" empid="" employee="" employees.push="" employees="" eport="" error="" esignation="" esolved="" eturns="" existing="" exists="" extract="" far="" fetch="" field="" finally="" find="" first="" for="" found.="" found="" from:="" function="" get="" getcashadjustments="" getemployees="" gettime="" getuserlist="" hassystemaccess:="" header="" higher="" highest="" hilhealth="" hortage="" hr="" i="" id:="" id="" if="" image:="" immediately="" in="" income="" incsheet.appendrow="" incsheet="ss.insertSheet(" instanceof="" is="" isc_income="" isnan="" it.="" it="" ixed="" k="" keep="" key="Username" leave="" let="" list="" lock.releaselock="" lock.waitlock="" lock="LockService.getScriptLock();" logic="" login="" logininfo.password="" logininfo.role="" logininfo="" look="" loop="" mage="" mail="" management="========================================" map="" max="" maxnum="-1;" merge="" message:="" mode="" mount="" mp="" mployees="" msg="" name:="" name="" new="" newid="" not="" note:="" note="" notes:="" npaid="" null="" number="" numpart="" only="" ontact="" or="" original="" otes="" ource="" overage="" pagibig:="" paid="" paid_cash="" part="" password:="" passwords="" payment="" philhealth:="" posted="" real="" record="" reference="" report.="" resolve="" resolveadjustmentitem="" results.push="" results="" return="" role:="" role="" roles="" row="" rowdatestr="==" rowdateval="" rowindex="i" rows="" s="" save="" saveemployee="" saving="" security="" see="" send="" sequential="" sers="" setfontweight="" setvalue="" sheet.appendrow="" sheet="ss.insertSheet(" sheetdata.length="" sheetdata="" shortage="" simple="" skipping="" so="" ss="SpreadsheetApp.getActiveSpreadsheet();" sss:="" staffid="" staffname="" start="" starts="" status:="" status="" structure="" success="" sync="" system="" systempassword:="" systemrole:="" t="" taff_debts="" tatus="" than="" the="" through="" timezone="" tin:="" to="" try="" type:="" u="" ui="" unchanged.="" update="" updated:="" url="" user="" userdata.length="" userdata="" usermap="" users.push="" users="" usersheet="ss.getSheetByName(" usually="" valid="" value="{password," var="" visual="" we="" what="" which="" with="" your="" yyyy-mm-dd=""> maxNum) {
maxNum = numPart;
}
}
}
var nextNum = maxNum + 1;
// Pad with zeros to ensure 6 digits (e.g., 5 -> "000005")
var paddedNum = ("000000" + nextNum).slice(-6);
empId = "EMP-" + paddedNum;
}
// -------------------------------
var rowToSave = [
empId, data.name, data.designation, data.contact, data.email,
data.address, data.sss, data.philhealth, data.pagibig, data.tin,
data.status, data.image
];
if (rowIndex > 0) {
sheet.getRange(rowIndex, 1, 1, 12).setValues([rowToSave]);
} else {
sheet.appendRow(rowToSave);
}
// --- 2. SYNC TO USERS SHEET ---
if (!userSheet) {
userSheet = ss.insertSheet('Users');
userSheet.appendRow(['User ID', 'Username', 'Password', 'Role', 'Full Name']);
}
var userData = userSheet.getDataRange().getValues();
var userRowIndex = -1;
// Find existing user by Username (Emp ID)
for (var u = 1; u < userData.length; u++) {
if (userData[u][1] == empId) {
userRowIndex = u + 1;
break;
}
}
if (data.enableLogin) {
// CREATE OR UPDATE USER
var userRow = [
"U-" + new Date().getTime(), // Internal User UUID
empId, // Username = Emp ID
data.password,
data.role,
data.name
];
if (userRowIndex > 0) {
// Update (Keep original UUID, update creds)
userSheet.getRange(userRowIndex, 2, 1, 4).setValues([[empId, data.password, data.role, data.name]]);
} else {
// Create New
userSheet.appendRow(userRow);
}
} else {
// REMOVE ACCESS (If unchecked)
if (userRowIndex > 0) {
userSheet.deleteRow(userRowIndex);
}
}
return { status: 'success', message: 'Employee & System Access saved successfully.' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
function deleteEmployee(id) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Delete from Employees Sheet
var sheet = ss.getSheetByName('Employees');
var data = sheet.getDataRange().getValues();
var found = false;
// Loop skipping header
for (var i = 1; i < data.length; i++) {
// FIX: Force String comparison to avoid Type Mismatch
if (String(data[i][0]) === String(id)) {
sheet.deleteRow(i + 1);
found = true;
break;
}
}
if (!found) return { status: 'error', message: 'Employee ID not found in database.' };
// 2. Delete from Users Sheet (Cleanup System Access)
var userSheet = ss.getSheetByName('Users');
if (userSheet) {
var userData = userSheet.getDataRange().getValues();
for (var u = 1; u < userData.length; u++) {
// FIX: Force String comparison here too
if (String(userData[u][1]) === String(id)) {
userSheet.deleteRow(u + 1);
break;
}
}
}
return { status: 'success', message: 'Employee record and system access deleted.' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}
/* =========================================
10. EMPLOYEE FINANCIALS (Fixed: Date Serialization Error)
========================================= */
function getDebtsByEmployee(empId, empName) {
console.log("Fetching debts for ID: " + empId + " / Name: " + empName);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Staff_Debts');
if (!sheet) return [];
var data = sheet.getDataRange().getValues();
if (data.length < 2) return []; // No data rows
// --- 1. DYNAMICALLY MAP HEADERS ---
var headers = data[0].map(function(h) { return String(h).toLowerCase().trim(); });
var idxId = headers.indexOf('emp id');
var idxName = -1;
if (headers.indexOf('cashier name') > -1) idxName = headers.indexOf('cashier name');
else if (headers.indexOf('cashier') > -1) idxName = headers.indexOf('cashier');
var idxDate = headers.indexOf('date');
var idxOrigin = headers.indexOf('report date');
var idxAmount = headers.indexOf('amount');
var idxStatus = headers.indexOf('status');
var idxNote = headers.indexOf('admin note');
if (idxNote === -1) idxNote = 6;
if (idxStatus === -1 || idxAmount === -1) return [];
var debts = [];
var timeZone = ss.getSpreadsheetTimeZone();
var targetId = String(empId || "").trim().toLowerCase();
var targetName = String(empName || "").trim().toLowerCase();
// --- 2. LOOP DATA ---
for (var i = 1; i < data.length; i++) {
var row = data[i];
if (!row[0]) continue;
var dbId = (idxId > -1 && row[idxId]) ? String(row[idxId]).trim().toLowerCase() : "";
var dbName = (idxName > -1 && row[idxName]) ? String(row[idxName]).trim().toLowerCase() : "";
var isMatch = false;
if (targetId !== "" && dbId === targetId) isMatch = true;
else if (targetName !== "" && dbName === targetName) isMatch = true;
if (isMatch) {
// --- FIX: SANITIZE DATES ---
var dateChargedStr = "N/A";
if (idxDate > -1) {
try { dateChargedStr = Utilities.formatDate(new Date(row[idxDate]), timeZone, "yyyy-MM-dd"); } catch (e) { dateChargedStr = String(row[idxDate]); }
}
var originDateStr = "N/A";
if (idxOrigin > -1) {
// CRITICAL FIX: Convert Origin Date (Report Date) to String before sending
try {
var rawOrigin = row[idxOrigin];
if (rawOrigin instanceof Date) {
originDateStr = Utilities.formatDate(rawOrigin, timeZone, "yyyy-MM-dd");
} else {
originDateStr = String(rawOrigin);
}
} catch (e) {
originDateStr = String(row[idxOrigin]);
}
}
// --- FIX: SANITIZE AMOUNT ---
var amt = row[idxAmount];
if (typeof amt !== 'number') {
amt = parseFloat(String(amt).replace(/[^0-9.-]+/g,"")) || 0;
}
debts.push({
rowIndex: i + 1,
date: dateChargedStr,
originDate: originDateStr, // <--- amount:="" amt="" idxnote="" idxstatus="" note:="" now="" row="" safe="" send="" status:="" to=""> -1) ? row[idxNote] : ""
});
}
}
return debts.reverse();
}
/* =========================================
STAFF DEBT SETTLEMENT LOGIC
========================================= */
function processDebtPayment(data) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Get the Debt Sheet
var debtSheet = ss.getSheetByName('Staff_Debts');
if (!debtSheet) return { status: 'error', message: 'Sheet "Staff_Debts" not found.' };
var lastRow = debtSheet.getLastRow();
var rowIndex = Number(data.rowIndex); // Ensure it's a number
// 2. Validate Row Index
if (isNaN(rowIndex) || rowIndex < 2 || rowIndex > lastRow) {
return { status: 'error', message: 'Invalid Record Index (' + rowIndex + '). Data may have changed.' };
}
// 3. Update the Sheet
// Column 6 is Status, Column 7 is Admin Note
var noteRange = debtSheet.getRange(rowIndex, 7);
var currentNote = noteRange.getValue();
var newNote = currentNote + " [Paid via " + data.method + " on " + data.paymentDate + "]";
debtSheet.getRange(rowIndex, 6).setValue("Paid");
noteRange.setValue(newNote);
// 4. Handle Cash Payment (Add to Adjustments)
if (data.method === 'Cash') {
var adjSheet = ss.getSheetByName('Cash_Adjustments');
if (!adjSheet) {
adjSheet = ss.insertSheet('Cash_Adjustments');
adjSheet.appendRow(['Adj ID', 'Date', 'Type', 'Amount', 'Report Date', 'Status', 'Notes', 'Cashier']);
}
var adjId = "PAY-" + new Date().getTime();
adjSheet.appendRow([
adjId,
new Date(),
"Shortage Payment",
Number(data.amount),
data.paymentDate,
"Resolved",
"Debt Settlement: " + data.payer,
data.payer
]);
}
return { status: 'success', message: 'Debt marked as Paid!' };
} catch (e) {
return { status: 'error', message: e.toString() };
} finally {
lock.releaseLock();
}
}--->--->--->
2. index.html (Main Interface)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>POS System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- PDF Generation Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.29/jspdf.plugin.autotable.min.js"></script>
<?!= include('css'); ?>
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" id="app-navbar" style="display:none;">
<div class="container-fluid">
<a class="navbar-brand" href="javascript:void(0)">GERVIC PROGRAMS POS AND INVENTORY SYSTEM</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="nav-dashboard"><a class="nav-link" href="javascript:void(0)" onclick="loadView('dashboard')">Dashboard</a></li>
<li class="nav-item" id="nav-inventory"><a class="nav-link" href="javascript:void(0)" onclick="loadView('inventory')">Inventory</a></li>
<li class="nav-item" id="nav-sales"><a class="nav-link" href="javascript:void(0)" onclick="loadView('sales')">Sales History</a></li>
<li class="nav-item" id="nav-returns"><a class="nav-link" href="javascript:void(0)" onclick="loadView('returns')">Returns Log</a></li>
<!-- EMPLOYEES LINK (Admin Only) -->
<li class="nav-item" id="nav-employees" style="display:none;">
<a class="nav-link" href="javascript:void(0)" onclick="loadView('employees')">
<i class="bi bi-people-fill"></i> Employees
</a>
</li>
<li class="nav-item"><a class="nav-link active" href="javascript:void(0)" onclick="loadView('pos')">POS Terminal</a></li>
<li class="nav-item" id="nav-reports"><a class="nav-link" href="javascript:void(0)" onclick="loadView('reports')">Reports</a></li>
<li class="nav-item"><a class="nav-link text-warning" href="javascript:void(0)" onclick="handleSystemRefresh()" id="nav-refresh-btn" title="Force Sync Data"><i id="nav-refresh-icon" class="bi bi-arrow-repeat"></i></a></li>
<li class="nav-item"><a class="nav-link text-danger" href="javascript:void(0)" onclick="logout()">Logout</a></li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid mt-4">
<!-- LOGIN VIEW -->
<div id="login-view" class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow mt-5">
<div class="card-header bg-primary text-white"><h4 class="mb-0">System Login</h4></div>
<div class="card-body">
<form id="login-form" onsubmit="handleLogin(event)">
<div class="mb-3"><label class="form-label">Username</label><input type="text" id="username" class="form-control" required></div>
<div class="mb-3"><label class="form-label">Password</label><input type="password" id="password" class="form-control" required></div>
<button type="submit" class="btn btn-primary w-100" id="login-btn">Login</button>
<div id="login-message" class="mt-2 text-danger text-center"></div>
</form>
</div>
</div>
</div>
</div>
<!-- DASHBOARD VIEW -->
<div id="dashboard-view" class="d-none">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="fw-bold"><i class="bi bi-speedometer2"></i> Business Analytics</h2>
<p class="text-muted mb-0">Real-time overview of your store's performance.</p>
</div>
<button class="btn btn-primary" onclick="loadDashboard()">
<i class="bi bi-arrow-clockwise"></i> Refresh Data
</button>
</div>
<!-- ROW 1: SALES & FINANCIALS -->
<div class="row g-3 mb-3">
<div class="col-md-3">
<div class="card text-white bg-primary h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50 small text-uppercase fw-bold">Today's Sales</h6>
<h3 class="fw-bold mb-0" id="dash-today">$0.00</h3>
</div>
<i class="bi bi-cash-coin fs-1 text-white-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50 small text-uppercase fw-bold">Month Revenue</h6>
<h3 class="fw-bold mb-0" id="dash-month">$0.00</h3>
</div>
<i class="bi bi-calendar-check fs-1 text-white-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-info h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-dark-50 small text-uppercase fw-bold">Est. Net Profit (Mo)</h6>
<h3 class="fw-bold mb-0 text-white" id="dash-profit">$0.00</h3>
</div>
<i class="bi bi-graph-up-arrow fs-1 text-white-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-warning h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-dark-50 small text-uppercase fw-bold">Unpaid Staff Debt</h6>
<h3 class="fw-bold mb-0 text-dark" id="dash-debt">$0.00</h3>
</div>
<i class="bi bi-exclamation-circle-fill fs-1 text-dark-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- ROW 2: INVENTORY HEALTH -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center">
<div class="bg-light rounded-circle p-3 me-3 text-primary"><i class="bi bi-box-seam fs-4"></i></div>
<div>
<h6 class="text-muted small text-uppercase fw-bold mb-0">Total Products</h6>
<h4 class="fw-bold mb-0" id="dash-total-prod">0</h4>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body d-flex align-items-center">
<div class="bg-light rounded-circle p-3 me-3 text-secondary"><i class="bi bi-tags fs-4"></i></div>
<div>
<h6 class="text-muted small text-uppercase fw-bold mb-0">Asset Value (Cost)</h6>
<h4 class="fw-bold mb-0" id="dash-asset">$0.00</h4>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-start border-4 border-danger shadow-sm h-100">
<div class="card-body d-flex align-items-center">
<div class="bg-danger bg-opacity-10 rounded-circle p-3 me-3 text-danger"><i class="bi bi-x-octagon-fill fs-4"></i></div>
<div>
<h6 class="text-muted small text-uppercase fw-bold mb-0">Out of Stock</h6>
<h4 class="fw-bold mb-0 text-danger" id="dash-out-stock">0</h4>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-start border-4 border-warning shadow-sm h-100">
<div class="card-body d-flex align-items-center">
<div class="bg-warning bg-opacity-10 rounded-circle p-3 me-3 text-warning"><i class="bi bi-exclamation-triangle-fill fs-4"></i></div>
<div>
<h6 class="text-muted small text-uppercase fw-bold mb-0">Low Stock</h6>
<h4 class="fw-bold mb-0 text-warning" id="dash-low-stock">0</h4>
</div>
</div>
</div>
</div>
</div>
<!-- ROW 3: CHARTS & TOP LISTS -->
<div class="row g-3 mb-4">
<!-- Sales Chart -->
<div class="col-md-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-primary"><i class="bi bi-bar-chart-line-fill"></i> Sales Trend (Last 7 Days)</h6>
</div>
<div class="card-body">
<canvas id="salesChart" style="height: 300px;"></canvas>
</div>
</div>
</div>
<!-- Top Products Table -->
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-success"><i class="bi bi-trophy-fill"></i> Best Sellers (This Month)</h6>
</div>
<div class="card-body p-0">
<table class="table table-striped table-hover mb-0 align-middle">
<thead class="table-light small"><tr><th>Item Name</th><th class="text-end">Sold</th></tr></thead>
<tbody id="dash-top-products"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ROW 4: DISTRIBUTION & CASHIERS -->
<div class="row g-3">
<!-- Category Chart -->
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3"><h6 class="m-0 fw-bold">Inventory by Category</h6></div>
<div class="card-body">
<canvas id="categoryChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
<!-- Payment Chart -->
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3"><h6 class="m-0 fw-bold">Payment Methods (Month)</h6></div>
<div class="card-body">
<canvas id="paymentChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
<!-- Top Cashiers -->
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="m-0 fw-bold text-info"><i class="bi bi-person-badge-fill"></i> Top Cashiers (Month)</h6>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light small"><tr><th>Name</th><th class="text-end">Sales Total</th></tr></thead>
<tbody id="dash-top-cashiers"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ROW 5: LOW STOCK ALERT TABLE -->
<div class="card shadow-sm border-danger mt-4">
<div class="card-header bg-danger text-white d-flex justify-content-between">
<h6 class="m-0"><i class="bi bi-exclamation-triangle-fill"></i> Replenishment Needed</h6>
<span class="badge bg-white text-danger" id="dash-alert-badge">0 Items</span>
</div>
<div class="table-responsive">
<table class="table table-striped mb-0 align-middle">
<thead class="table-light"><tr><th>Item Name</th><th>Current Stock</th><th>Reorder Level</th><th>Status</th><th>Action</th></tr></thead>
<tbody id="dash-low-stock-body"></tbody>
</table>
</div>
</div>
</div>
<!-- INVENTORY VIEW -->
<div id="inventory-view" class="d-none">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Inventory Management</h2>
<button class="btn btn-success" onclick="openInventoryModal('add')"><i class="bi bi-plus-circle"></i> Add Product</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<input type="text" id="inv-search-input" class="form-control" placeholder="Search Code, Name, Brand..." onkeyup="handleInventorySearch()">
</div>
<div class="col-md-6 text-end">
<label class="text-muted me-2 small">Rows per page:</label>
<select id="inv-rows-per-page" class="form-select d-inline-block w-auto" onchange="handleInventorySearch()">
<option value="5">5</option><option value="10" selected>10</option><option value="20">20</option><option value="50">50</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr><th>ID</th><th>Product Code</th><th>Name</th><th>Brand</th><th>Category</th><th>Price</th><th>Stock</th><th class="text-end">Actions</th></tr>
</thead>
<tbody id="inventory-table-body"></tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="text-muted" id="inv-page-info">Showing 0-0 of 0</span>
<nav>
<ul class="pagination mb-0">
<li class="page-item"><button class="page-link" onclick="changeInvPage(-1)" id="btn-inv-prev">Previous</button></li>
<li class="page-item"><button class="page-link" onclick="changeInvPage(1)" id="btn-inv-next">Next</button></li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- SALES HISTORY VIEW -->
<div id="sales-view" class="d-none">
<h2 class="mb-3">Sales History</h2>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<input type="text" id="sales-search" class="form-control" placeholder="Search Transaction ID or Cashier..." onkeyup="handleSalesSearch()">
</div>
<div class="col-md-6 text-end">
<label class="text-muted me-2 small">Rows per page:</label>
<select id="sales-rows-per-page" class="form-select d-inline-block w-auto" onchange="handleSalesSearch()">
<option value="5">5</option><option value="10" selected>10</option><option value="20">20</option><option value="50">50</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr><th>Date</th><th>Transaction ID</th><th>Items Sold</th><th>Total</th><th>Method</th><th>Cashier</th><th class="text-end">Actions</th></tr>
</thead>
<tbody id="sales-table-body">
<tr><td colspan="7" class="text-center">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="text-muted" id="sales-page-info">Showing 0-0 of 0</span>
<nav>
<ul class="pagination mb-0">
<li class="page-item"><button class="page-link" onclick="changeSalesPage(-1)" id="btn-sales-prev">Previous</button></li>
<li class="page-item"><button class="page-link" onclick="changeSalesPage(1)" id="btn-sales-next">Next</button></li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- RETURNS VIEW -->
<div id="returns-view" class="d-none">
<h2 class="mb-3">Returns & Refunds Log</h2>
<div class="card shadow-sm">
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<input type="text" id="ret-search-input" class="form-control" placeholder="Search Return ID, Item, Cashier..." onkeyup="handleReturnSearch()">
</div>
<div class="col-md-6 text-end">
<label class="text-muted me-2 small">Rows per page:</label>
<select id="ret-rows-per-page" class="form-select d-inline-block w-auto" onchange="handleReturnSearch()">
<option value="5">5</option><option value="10" selected>10</option><option value="20">20</option><option value="50">50</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr><th>Date</th><th>Return ID</th><th>Original Txn</th><th>Item Name</th><th class="text-center">Qty</th><th class="text-danger">Refund Amt</th><th>Type</th><th>Reason</th><th>Cashier</th><th class="text-end">Action</th></tr>
</thead>
<tbody id="returns-table-body">
<tr><td colspan="10" class="text-center">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="text-muted" id="ret-page-info">Showing 0-0 of 0</span>
<nav>
<ul class="pagination mb-0">
<li class="page-item"><button class="page-link" onclick="changeReturnPage(-1)" id="btn-ret-prev">Previous</button></li>
<li class="page-item"><button class="page-link" onclick="changeReturnPage(1)" id="btn-ret-next">Next</button></li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- POS VIEW -->
<div id="pos-view" class="d-none">
<div class="row g-0">
<div class="col-md-8 p-3">
<!-- Search & Filter Toolbar -->
<div class="row g-2 mb-3">
<!-- 1. Search: Grows to fill remaining space (Changed from col-md-4 to col) -->
<div class="col">
<input type="text" id="pos-search" class="form-control" placeholder="Scan/Search..." onkeyup="filterPosProducts()">
</div>
<!-- 2. Category Filter: Fixed Width -->
<div class="col-md-2">
<select id="filter-category" class="form-select" onchange="filterPosProducts()">
<option value="all">All Categories</option>
</select>
</div>
<!-- 3. Price Filter: Fixed Width -->
<div class="col-md-2">
<select id="filter-price" class="form-select" onchange="filterPosProducts()">
<option value="all">Any Price</option>
<option value="range1">Less than $100.00</option>
<option value="range2">$101.00 - $500.00</option>
<option value="range3">$501.00 - $1,000.00</option>
<option value="range4">More than $1,000.00</option>
</select>
</div>
<!-- 4. Bestseller: Auto Width (Shrinks to fit text) -->
<div class="col-auto">
<input type="checkbox" class="btn-check" id="filter-bestseller" autocomplete="off" onchange="filterPosProducts()">
<label class="btn btn-outline-warning text-dark fw-bold text-truncate" for="filter-bestseller">
<i class="bi bi-star-fill"></i> Top Items
</label>
</div>
<!-- 5. View Switcher: Auto Width (Decreased Width as requested) -->
<div class="col-auto">
<div class="btn-group" role="group"> <!-- Removed w-100 -->
<input type="radio" class="btn-check" name="viewMode" id="view-grid" autocomplete="off" checked onclick="switchPosView('grid')">
<label class="btn btn-outline-secondary" for="view-grid" title="Grid"><i class="bi bi-grid-fill"></i></label>
<input type="radio" class="btn-check" name="viewMode" id="view-list" autocomplete="off" onclick="switchPosView('list')">
<label class="btn btn-outline-secondary" for="view-list" title="List"><i class="bi bi-list-ul"></i></label>
<input type="radio" class="btn-check" name="viewMode" id="view-detail" autocomplete="off" onclick="switchPosView('detail')">
<label class="btn btn-outline-secondary" for="view-detail" title="Detail"><i class="bi bi-card-text"></i></label>
</div>
</div>
</div>
<div id="pos-product-grid" class="row row-cols-1 row-cols-md-3 row-cols-lg-4 g-3 pos-scroll-area"></div>
</div>
<div class="col-md-4 cart-panel shadow-sm border-start">
<div class="p-3 bg-dark text-white"><h4 class="m-0"><i class="bi bi-cart"></i> Current Order</h4><small id="cart-cashier-name">Cashier: ...</small></div>
<div class="cart-items-container" id="pos-cart-body">
<table class="table table-sm table-borderless">
<thead>
<tr class="border-bottom">
<th>Item</th><th class="text-end">Total</th><th style="width: 40px;"></th>
</tr>
</thead>
<tbody id="cart-table-rows"></tbody>
</table>
</div>
<div class="cart-footer">
<div class="d-flex justify-content-between mb-2"><h5>Total:</h5><h3 class="text-primary fw-bold" id="cart-total">$0.00</h3></div>
<div class="mb-3"><select class="form-select" id="payment-method"><option value="Cash">Cash</option><option value="Card">Card</option><option value="GCash">GCash</option></select></div>
<button class="btn btn-success w-100 btn-lg shadow" onclick="checkout()">CHECKOUT</button>
</div>
</div>
</div>
</div>
<!-- EMPLOYEES VIEW (NEW) -->
<div id="employees-view" class="d-none">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-person-badge"></i> Employee Management</h2>
<button class="btn btn-primary" onclick="openEmployeeModal('add')">
<i class="bi bi-person-plus-fill"></i> Add Employee
</button>
</div>
<!-- UPDATED SEARCH & TOGGLE TOOLBAR -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex gap-2">
<!-- Search Input: Grows to fill all available space -->
<input type="text" id="emp-search" class="form-control" placeholder="Search Name, Designation, or ID..." onkeyup="filterEmployees()">
<!-- View Toggle Buttons: Shrinks to fit content (Decreased Width) -->
<div class="btn-group flex-shrink-0" role="group">
<input type="radio" class="btn-check" name="empViewMode" id="emp-view-grid" autocomplete="off" checked onclick="switchEmployeeView('grid')">
<label class="btn btn-outline-secondary" for="emp-view-grid" title="Grid View"><i class="bi bi-grid-fill"></i></label>
<input type="radio" class="btn-check" name="empViewMode" id="emp-view-list" autocomplete="off" onclick="switchEmployeeView('list')">
<label class="btn btn-outline-secondary" for="emp-view-list" title="List View"><i class="bi bi-list-ul"></i></label>
</div>
</div>
</div>
</div>
<div id="employee-grid" class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
<div class="col-12 text-center text-muted">Waiting for data...</div>
</div>
</div>
<!-- REPORTS VIEW -->
<div id="reports-view" class="d-none">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-file-earmark-spreadsheet"></i> Daily Cash Position Report</h2>
<button class="btn btn-dark" onclick="printCashPositionPDF()"><i class="bi bi-printer"></i> Generate Official PDF</button>
</div>
<div class="row">
<!-- COLUMN 1: SYSTEM DATA (EXPECTED CASH) -->
<div class="col-md-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-primary text-white fw-bold">1. System Computation</div>
<div class="card-body">
<!-- 1. Report Date & Cashier Selector -->
<div class="row g-2 mb-3 align-items-center justify-content-end">
<div class="col-md-4 text-end fw-bold">Report Date:</div>
<div class="col-md-4">
<input type="date" id="cp-date" class="form-control" onchange="handleCashReportDateChange()">
</div>
</div>
<!-- NEW: Cashier Selector -->
<div class="row g-2 mb-3 align-items-center justify-content-end">
<div class="col-md-4 text-end fw-bold">Cashier:</div>
<div class="col-md-4">
<select id="cp-cashier-select" class="form-select" onchange="handleCashReportDateChange()">
<!-- Options populated by JS -->
</select>
</div>
</div>
<!-- 2. Opening Balance (Right Aligned, Width Reduced) -->
<div class="row g-2 mb-3 align-items-center justify-content-end">
<div class="col-md-4 text-end fw-bold">Balance Prev. Day:</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" id="cp-open-bal" class="form-control fw-bold" value="0.00" onkeyup="calculateCashPosition()" placeholder="Opening Fund">
</div>
<div class="text-end">
<small class="text-muted" style="font-size: 0.75rem;">Enter opening fund or yesterday's ending.</small>
</div>
</div>
</div>
<!-- ENUMERATION OF RECEIPTS -->
<div class="border rounded p-2 mb-2 bg-light">
<h6 class="text-success border-bottom pb-1"><i class="bi bi-plus-circle"></i> ADD: Receipts (Cash Sales)</h6>
<div style="height: 150px; overflow-y: auto;">
<table class="table table-sm table-borderless small mb-0">
<tbody id="cp-receipts-list"></tbody>
</table>
</div>
<div class="text-end fw-bold border-top mt-1 text-success">Total Receipts: <span id="cp-total-receipts">$0.00</span></div>
</div>
<!-- ENUMERATION OF DISBURSEMENTS -->
<div class="border rounded p-2 mb-2 bg-light">
<h6 class="text-danger border-bottom pb-1"><i class="bi bi-dash-circle"></i> LESS: Disbursements (Returns)</h6>
<div style="height: 100px; overflow-y: auto;">
<table class="table table-sm table-borderless small mb-0">
<tbody id="cp-disburse-list"></tbody>
</table>
</div>
<div class="text-end fw-bold border-top mt-1 text-danger">Total Disbursements: <span id="cp-total-disburse">$0.00</span></div>
</div>
<!-- THEORETICAL ENDING -->
<div class="alert alert-primary mb-0 d-flex justify-content-between align-items-center">
<span class="fw-bold">SYSTEM ENDING BALANCE:</span>
<span class="fs-4 fw-bold" id="cp-system-end">$0.00</span>
</div>
</div>
</div>
</div>
<!-- COLUMN 2: ACTUAL COUNT & BREAKDOWN -->
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
<span class="fw-bold">2. Actual Cash Breakdown</span>
<i id="db-status-icon" class="bi bi-database text-white-50" title="Database Status"></i>
</div>
<div class="card-body">
<!-- A. Cash Counter Section -->
<label class="form-label fw-bold">Cash on Hand</label>
<div class="d-flex gap-2 mb-3">
<div class="input-group input-group-lg">
<span class="input-group-text bg-success text-white">$</span>
<input type="number" id="cp-act-cash" class="form-control text-end fw-bold bg-white" placeholder="0.00" readonly>
</div>
<button class="btn btn-success" onclick="openCashCountModal()"><i class="bi bi-calculator"></i> Count</button>
</div>
<!-- B. Checks Section -->
<div class="mb-3">
<label class="form-label fw-bold">Checks / Other</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-warning">$</span>
<input type="number" id="cp-act-check" class="form-control text-end fw-bold" placeholder="0.00" onkeyup="calculateCashPosition()">
</div>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Notes / Remarks</label>
<textarea class="form-control" id="cp-notes" rows="2"></textarea>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="m-0">TOTAL ACTUAL:</h5>
<h3 class="fw-bold" id="cp-total-actual">$0.00</h3>
</div>
<div class="card text-center mb-3" id="cp-status-card">
<div class="card-header fw-bold">OVERAGE / (SHORTAGE)</div>
<div class="card-body">
<h2 class="display-5 fw-bold" id="cp-variance">$0.00</h2>
<span class="badge bg-secondary" id="cp-variance-badge">Balanced</span>
<!-- ADMIN / CASHIER ACTIONS CONTAINER -->
<div id="cp-admin-actions" class="mt-3 d-none border-top pt-2"></div>
</div>
</div>
<!-- C. Action Buttons -->
<div class="d-grid gap-2">
<button class="btn btn-primary btn-lg" onclick="saveDailyReportDb()">
<i class="bi bi-cloud-upload"></i> SAVE TO DATABASE
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div> <!-- End Main Container -->
<!-- ================= MODALS ================= -->
<!-- ADD/EDIT INVENTORY MODAL -->
<div class="modal fade" id="inventoryModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="invModalTitle">Product</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="inventory-form" onsubmit="handleInventorySubmit(event)">
<input type="hidden" id="inv-id"><input type="hidden" id="inv-mode">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Product Name</label><input type="text" class="form-control" id="inv-name" required></div>
<div class="col-md-6"><label class="form-label">Product Code</label><input type="text" class="form-control" id="inv-sku" required></div>
<div class="col-md-6"><label class="form-label">Brand</label><input type="text" class="form-control" id="inv-brand" required></div>
<div class="col-md-6">
<label class="form-label">Category</label>
<input type="text" class="form-control" id="inv-category" list="category-list" required>
<datalist id="category-list"><option value="Beverages"><option value="Bakery"><option value="Merchandise"></datalist>
</div>
<div class="col-md-4"><label class="form-label">Selling Price</label><input type="number" class="form-control" id="inv-price" step="0.01" required></div>
<div class="col-md-4"><label class="form-label">Cost Price</label><input type="number" class="form-control" id="inv-cost" step="0.01" required></div>
<div class="col-md-4"><label class="form-label">Stock</label><input type="number" class="form-control" id="inv-stock" required></div>
<div class="col-12"><label class="form-label">Reorder Level</label><input type="number" class="form-control" id="inv-reorder" value="10"></div>
<div class="col-12">
<label class="form-label">Image</label>
<input type="file" class="form-control mb-2" id="inv-image-file" accept="image/*">
<div class="input-group"><span class="input-group-text">URL</span><input type="text" class="form-control" id="inv-image" readonly></div>
</div>
</div>
<div class="col-md-6"><label class="form-label">Supplier</label><input type="text" class="form-control" id="inv-supplier"></div>
<div class="col-md-12"><label class="form-label">Description</label><textarea class="form-control" id="inv-description" rows="2"></textarea></div>
<div class="mt-4 text-end"><button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary" id="inv-save-btn">Save Product</button></div>
</form>
</div>
</div>
</div>
</div>
<!-- VIEW PRODUCT DETAILS MODAL -->
<div class="modal fade" id="viewProductModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Product Details</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-5 text-center mb-3">
<img id="view-img" src="" class="img-fluid rounded shadow-sm border" style="max-height: 300px; width: 100%; object-fit: contain;">
</div>
<div class="col-md-7">
<h3 id="view-name" class="fw-bold"></h3>
<h5 class="text-muted mb-3" id="view-brand"></h5>
<table class="table table-sm">
<tr><th width="40%">Product Code:</th><td id="view-sku"></td></tr>
<tr><th>Category:</th><td id="view-category"></td></tr>
<tr><th>Selling Price:</th><td class="fw-bold text-success" id="view-price"></td></tr>
<tr><th>Cost Price:</th><td id="view-cost"></td></tr>
<tr><th>Margin:</th><td id="view-margin"></td></tr>
<tr><th>Current Stock:</th><td id="view-stock"></td></tr>
<tr><th>Reorder Level:</th><td id="view-reorder"></td></tr>
</table>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="text-primary m-0"><i class="bi bi-clock-history"></i> Product Ledger (History)</h5>
<div>
<button class="btn btn-sm btn-outline-success me-1" onclick="downloadLedgerCSV()"><i class="bi bi-file-earmark-excel-fill"></i> Excel</button>
<button class="btn btn-sm btn-outline-danger" onclick="downloadLedgerPDF()"><i class="bi bi-file-earmark-pdf-fill"></i> PDF</button>
</div>
</div>
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
<table class="table table-bordered table-sm table-hover" style="font-size: 0.85rem;">
<thead class="table-light sticky-top">
<tr><th>Date</th><th>Transaction ID</th><th>Type</th><th class="text-success text-center">In</th><th class="text-danger text-center">Out</th><th class="text-primary text-center fw-bold">On Hand</th><th class="text-end">Amount</th></tr>
</thead>
<tbody id="view-history-rows">
<tr><td colspan="7" class="text-center text-muted">Loading history...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></div>
</div>
</div>
</div>
<!-- EDIT SALE MODAL -->
<div class="modal fade" id="editSaleModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title">Edit Transaction</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="edit-sale-form" onsubmit="handleSaleUpdate(event)">
<input type="hidden" id="edit-sale-id">
<div class="mb-3"><label class="form-label">Transaction ID</label><input type="text" class="form-control" id="display-sale-id" readonly></div>
<div class="mb-3"><label class="form-label">Date & Time</label><input type="datetime-local" class="form-control" id="edit-sale-date" required></div>
<div class="mb-3"><label class="form-label">Payment Method</label>
<select class="form-select" id="edit-sale-method"><option value="Cash">Cash</option><option value="Card">Card</option><option value="GCash">GCash</option></select>
</div>
<div class="mb-3"><label class="form-label">Cashier Name</label><input type="text" class="form-control" id="edit-sale-cashier"></div>
<div class="text-end"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Update Sale</button></div>
</form>
</div>
</div>
</div>
</div>
<!-- ITEM RETURN/REFUND MODAL -->
<div class="modal fade" id="refundModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">Manage Sold Items</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted">Select an item to return to inventory. This will adjust the sales total automatically.</p>
<input type="hidden" id="refund-txn-id">
<div class="table-responsive">
<table class="table align-middle">
<thead><tr><th>Item Name</th><th>Sold Price</th><th>Qty Sold</th><th class="text-end">Action</th></tr></thead>
<tbody id="refund-table-body"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- RESTOCK MODAL -->
<div class="modal fade" id="restockModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">Quick Restock</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="restock-form" onsubmit="handleRestockSubmit(event)">
<input type="hidden" id="restock-id">
<p class="fw-bold" id="restock-name">Product Name</p>
<div class="mb-3"><label class="form-label">Current Stock</label><input type="text" class="form-control" id="restock-current" readonly disabled></div>
<div class="mb-3"><label class="form-label">Cost per Unit (New Batch)</label><input type="number" class="form-control" id="restock-cost" step="0.01" placeholder="Leave empty to keep current cost"></div>
<div class="mb-3"><label class="form-label">Add Quantity</label><input type="number" class="form-control form-control-lg border-success" id="restock-qty" min="1" required autofocus></div>
<div class="d-grid"><button type="submit" class="btn btn-success">Confirm Add</button></div>
</form>
</div>
</div>
</div>
</div>
<!-- POS PRODUCT DETAILS MODAL -->
<div class="modal fade" id="posDetailModal" tabindex="-1">
<div class="modal-dialog" style="max-width: 500px;">
<div class="modal-content">
<div class="modal-header bg-light">
<h6 class="modal-title fw-bold">Product Info</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<img id="pos-detail-img" src="" class="img-fluid rounded mb-2" style="max-height: 150px;">
<h5 id="pos-detail-name" class="fw-bold mb-0"></h5>
<p class="text-muted small" id="pos-detail-code"></p>
<div class="text-start mt-3 bg-light p-2 rounded small">
<p class="mb-1"><strong>Supplier:</strong> <span id="pos-detail-supplier"></span></p>
<p class="mb-0"><strong>Description:</strong></p>
<p class="text-muted mb-0" id="pos-detail-desc"></p>
</div>
</div>
</div>
</div>
</div>
<!-- CASH COUNT MODAL -->
<div class="modal fade" id="cashCountModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-success text-white py-2">
<h6 class="modal-title"><i class="bi bi-cash-coin"></i> Denomination Counter</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-2 bg-light">
<h6 class="fw-bold border-bottom border-success mb-2">BILLS</h6>
<div id="den-bills-container"></div>
<h6 class="fw-bold border-bottom border-secondary mb-2 mt-3">COINS</h6>
<div id="den-coins-container"></div>
<div class="mt-3 p-2 bg-dark text-white rounded text-center">
<small>TOTAL CASH</small>
<h4 class="m-0 fw-bold" id="modal-cash-total">$0.00</h4>
</div>
</div>
<div class="modal-footer p-1 d-flex gap-1">
<button type="button" class="btn btn-outline-secondary" onclick="resetCashCount()" title="Clear All Counts" style="width: 25%;"><i class="bi bi-trash"></i></button>
<button type="button" class="btn btn-success flex-grow-1" data-bs-dismiss="modal">Confirm Count</button>
</div>
</div>
</div>
</div>
<!-- STAFF SELECTION / PAYMENT MODAL (Restored Option B) -->
<div class="modal fade" id="staffSelectModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-white py-2">
<h6 class="modal-title">Resolve Shortage</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="hidden-adj-id">
<input type="hidden" id="hidden-adj-amount">
<div class="alert alert-secondary small mb-3">
<i class="bi bi-info-circle"></i> This will record a formal debt against the employee.
If they wish to pay immediately, settle it via the <strong>Financials</strong> menu after booking.
</div>
<div class="mb-3">
<label class="form-label fw-bold">Select Staff to Charge:</label>
<select class="form-select mb-3" id="staff-select-dropdown">
<!-- Populated by JS -->
</select>
<div class="d-grid">
<button class="btn btn-primary" onclick="confirmShortageCharge()">
<i class="bi bi-person-check-fill"></i> Confirm Charge
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- EMPLOYEE PROFILE MODAL (Simplified: No Tabs) -->
<div class="modal fade" id="employeeModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="empModalTitle">Employee Profile</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="employee-form" onsubmit="handleEmployeeSubmit(event)">
<input type="hidden" id="emp-id">
<div class="row">
<div class="col-md-4 text-center border-end">
<div class="mb-3">
<label class="form-label small fw-bold">Profile Photo</label>
<div class="border rounded d-flex align-items-center justify-content-center bg-light mx-auto mb-2" style="width: 150px; height: 150px; overflow: hidden;">
<img id="emp-img-preview" src="" style="width:100%; height:100%; object-fit:cover;">
</div>
<input type="file" id="emp-image-file" class="form-control form-control-sm" accept="image/*">
<input type="hidden" id="emp-image-url">
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select class="form-select" id="emp-status"><option value="Active">Active</option><option value="On Leave">On Leave</option><option value="Resigned">Resigned</option><option value="Terminated">Terminated</option></select>
</div>
</div>
<div class="col-md-8">
<div class="row g-2">
<div class="col-md-6"><label class="form-label">Full Name</label><input type="text" class="form-control" id="emp-name" required></div>
<div class="col-md-6"><label class="form-label">Designation</label><input type="text" class="form-control" id="emp-designation" required></div>
<div class="col-12"><label class="form-label">Address</label><input type="text" class="form-control" id="emp-address"></div>
<div class="col-md-6"><label class="form-label">Contact</label><input type="text" class="form-control" id="emp-contact"></div>
<div class="col-md-6"><label class="form-label">Email</label><input type="email" class="form-control" id="emp-email"></div>
<div class="col-12"><hr class="my-2"></div>
<h6 class="text-primary">Government Numbers</h6>
<div class="col-md-6"><label class="form-label">SSS No.</label><input type="text" class="form-control" id="emp-sss"></div>
<div class="col-md-6"><label class="form-label">PhilHealth No.</label><input type="text" class="form-control" id="emp-philhealth"></div>
<div class="col-md-6"><label class="form-label">Pag-IBIG</label><input type="text" class="form-control" id="emp-pagibig"></div>
<div class="col-md-6"><label class="form-label">TIN</label><input type="text" class="form-control" id="emp-tin"></div>
<div class="col-12"><hr class="my-2"></div>
<div class="col-12">
<div class="card bg-light border-secondary">
<div class="card-body p-2">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="emp-enable-login" onchange="toggleLoginFields()">
<label class="form-check-label fw-bold" for="emp-enable-login">Enable System Login</label>
</div>
<div id="emp-login-fields" class="d-none">
<div class="row g-2">
<div class="col-md-6"><label class="form-label small">Username</label><input type="text" class="form-control form-control-sm" id="emp-login-username" readonly disabled></div>
<div class="col-md-6"><label class="form-label small">Role</label><select class="form-select form-select-sm" id="emp-login-role"><option value="Staff">Staff / Cashier</option><option value="Admin">Administrator</option></select></div>
<div class="col-12"><label class="form-label small">Password</label><input type="password" class="form-control form-control-sm" id="emp-login-password" placeholder="Set Password"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 text-end"><button type="button" class="btn btn-secondary me-1" data-bs-dismiss="modal">Close</button><button type="submit" class="btn btn-primary" id="emp-save-btn">Save Profile</button></div>
</form>
</div>
</div>
</div>
</div>
<!-- EMPLOYEE FINANCIALS MODAL (New Separate Modal) -->
<div class="modal fade" id="financialModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="finModalTitle"><i class="bi bi-wallet2"></i> Financial Records</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Hidden inputs to store context for refreshing -->
<input type="hidden" id="fin-emp-id">
<input type="hidden" id="fin-emp-name">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted m-0">Outstanding Debts & Liabilities</h6>
<button type="button" class="btn btn-sm btn-outline-success" onclick="var id=document.getElementById('fin-emp-id').value; var name=document.getElementById('fin-emp-name').value; fetchEmployeeDebts(id, name);">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle table-hover">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Origin</th>
<th>Amount</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody id="emp-debt-table">
<tr><td colspan="5" class="text-center text-muted">Loading records...</td></tr>
</tbody>
</table>
</div>
<div class="alert alert-light border small mt-3 mb-0">
<i class="bi bi-info-circle-fill text-success"></i>
<strong>Pay Cash:</strong> Adds money to today's drawer (Receipt).<br>
<strong>Payroll:</strong> Marks it for deduction in the next payslip.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- VARIANCE MODAL (This is needed to silence a potential JS error if referenced, though logic moved to StaffSelect) -->
<div class="modal fade" id="varianceModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"></div></div></div>
<!-- GENERIC YES/NO MODAL -->
<div class="modal fade" id="confirmationModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-light py-2">
<h6 class="modal-title fw-bold">Confirmation</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center py-3">
<p id="conf-modal-msg" class="mb-0">Message goes here...</p>
</div>
<div class="modal-footer p-1 justify-content-center border-top-0">
<button type="button" class="btn btn-secondary btn-sm px-3" id="btn-conf-no">No</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn-conf-yes">Yes</button>
</div>
</div>
</div>
</div><!-- GENERIC YES/NO MODAL -->
<div class="modal fade" id="confirmationModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-light py-2">
<h6 class="modal-title fw-bold">Confirmation</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center py-3">
<p id="conf-modal-msg" class="mb-0">Message goes here...</p>
</div>
<div class="modal-footer p-1 justify-content-center border-top-0">
<button type="button" class="btn btn-secondary btn-sm px-3" id="btn-conf-no">No</button>
<button type="button" class="btn btn-success btn-sm px-3" id="btn-conf-yes">Yes</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<?!= include('js'); ?>
</body>
</html>
3. css.html (Styling)
4. js.html (Logic & Interaction)
Conclusion
By following these steps, you have successfully deployed a sophisticated POS and Inventory system that operates entirely within the Google ecosystem. This setup allows for infinite scalability—you can add barcode scanners, thermal printer support, and deep financial analytics as your business grows. Stay tuned for new script on how to add these functionalities.
Demo Access Credentials
Username:
EMP-000001Password:
admin123
Username:
EMP-000002Password:
staff123
Ready to deploy? Hit the "Deploy" button in Apps Script and choose "Web App" to get your live POS and Inventory system URL!