<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>DS TRADERS V2 - Enterprise Dealership Suite</title> <!-- PWA: installable app metadata --> <link rel="manifest" href="manifest.json"> <meta name="theme-color" content="#e4002b"> <link rel="apple-touch-icon" href="icon-192.png"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-title" content="DS Traders"> <!-- QR Code Library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> <style> :root { --brand: #e4002b; --brand-dark: #b80022; --bg: #f4f6f9; --surface: #ffffff; --text: #1a1a1a; --text-muted: #666666; --border: #e2e8f0; --success: #10b981; --warning: #f59e0b; --danger: #ef4444; --radius: 10px; } * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } body { background: var(--bg); color: var(--text); padding-bottom: 70px; -webkit-tap-highlight-color: transparent; } /* HEADER & NAVBAR */ header { background: var(--brand); color: white; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .brand-title { font-size: 18px; font-weight: 800; letter-spacing: 0.5px; } .user-badge { font-size: 11px; background: rgba(255,255,255,0.2); padding: 4px 8px; border-radius: 20px; text-transform: uppercase; } nav { background: var(--surface); display: flex; overflow-x: auto; border-bottom: 1px solid var(--border); position: sticky; top: 48px; z-index: 99; scrollbar-width: none; } nav::-webkit-scrollbar { display: none; } nav button { border: none; background: transparent; padding: 12px 16px; font-weight: 600; font-size: 13px; color: var(--text-muted); white-space: nowrap; cursor: pointer; transition: 0.2s; border-bottom: 3px solid transparent; } nav button.active { color: var(--brand); border-bottom-color: var(--brand); } /* LAYOUT & CARDS */ .container { max-width: 1000px; margin: auto; padding: 12px; } .page { display: none; } .page.active { display: block; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 15px; } .card { background: var(--surface); padding: 14px; border-radius: var(--radius); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .card h4 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } .card .val { font-size: 20px; font-weight: 700; color: var(--text); } .card .val.red { color: var(--danger); } .card .val.green { color: var(--success); } /* FORMS & INPUTS */ .panel { background: var(--surface); padding: 16px; border-radius: var(--radius); border: 1px solid var(--border); margin-bottom: 15px; } .panel-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; } label { font-size: 12px; font-weight: 600; color: var(--text-muted); display: block; margin-top: 8px; margin-bottom: 4px; } input, select, textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: #fff; outline: none; } input:focus, select:focus { border-color: var(--brand); } .row-2 { display: flex; gap: 10px; } .row-2 > * { flex: 1; } /* BUTTONS */ .btn { border: none; padding: 10px 16px; border-radius: 6px; font-weight: 600; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 6px; width: 100%; margin-top: 10px; } .btn-primary { background: var(--brand); color: white; } .btn-primary:hover { background: var(--brand-dark); } .btn-secondary { background: #e2e8f0; color: var(--text); } .btn-danger { background: var(--danger); color: white; } .btn-sm { padding: 6px 10px; font-size: 12px; width: auto; margin-top: 0; } /* TABLES */ .table-wrapper { overflow-x: auto; background: var(--surface); border-radius: var(--radius); border: 1px solid var(--border); } table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; } th { background: #f8fafc; padding: 10px 12px; color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--border); white-space: nowrap; } td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; } tr:last-child td { border-bottom: none; } /* SEARCH & DROPDOWN */ .search-container { position: relative; } .dropdown-results { position: absolute; top: 100%; left: 0; right: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; max-height: 200px; overflow-y: auto; z-index: 50; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .dropdown-item { padding: 10px; border-bottom: 1px solid var(--border); cursor: pointer; font-size: 13px; } .dropdown-item:hover { background: #fff5f5; } /* LOGIN OVERLAY */ #loginOverlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; } .login-card { background: var(--surface); width: 100%; max-width: 360px; padding: 24px; border-radius: var(--radius); text-align: center; } /* ROLE-BASED VISIBILITY (added — the original had no rule making .admin-only actually hide anything) */ body.role-staff .admin-only { display: none !important; } /* PRINT RECEIPT CONTAINER (added — kept off-screen until an actual print, so a rendered receipt doesn't sit visible under the app after printing) */ #printArea { display: none; } /* PRINT MEDIA TYPES */ @media print { body * { visibility: hidden; } #printArea { display: block !important; visibility: visible; position: absolute; left: 0; top: 0; width: 100%; padding: 0; margin: 0; } #printArea * { visibility: visible; } } </style> </head> <body> <!-- LOGIN MODAL --> <div id="loginOverlay"> <div class="login-card"> <h2 style="color:var(--brand); margin-bottom: 4px;">DS TRADERS</h2> <p style="font-size: 12px; color: var(--text-muted); margin-bottom: 16px;">Coca-Cola Enterprise Suite V2</p> <label style="text-align:left;">Select User Role</label> <select id="loginRole" onchange="togglePinInput()"> <option value="staff">Staff (Billing & Stock View)</option> <option value="admin">Admin (Full Access & Accounts)</option> </select> <div id="pinContainer" style="display:none;"> <label style="text-align:left;">Admin PIN</label> <input type="password" id="loginPin" placeholder="Enter PIN (Default: 1234)"> </div> <button class="btn btn-primary" onclick="login()">Login to Portal</button> </div> </div> <!-- MAIN HEADER --> <header> <div> <div class="brand-title" id="headerTitle">DS TRADERS</div> <span style="font-size: 10px; opacity:0.8;">Distribution Management System</span> </div> <div> <span class="user-badge" id="currentUserBadge">Staff</span> <button onclick="logout()" style="background:none; border:none; color:white; font-size:16px; margin-left:8px; cursor:pointer;">🚪</button> </div> </header> <!-- NAVIGATION --> <nav id="navBar"> <button class="active" onclick="showPage('home', this)">🏠 Dashboard</button> <button onclick="showPage('billing', this)">🧾 POS Billing</button> <button onclick="showPage('shops', this)">🏪 Shops/Khata</button> <button onclick="showPage('stock', this)">📦 Inventory</button> <button class="admin-only" onclick="showPage('accounts', this)">💰 Accounts</button> <button class="admin-only" onclick="showPage('reports', this)">📊 Reports</button> <button class="admin-only" onclick="showPage('settings', this)">⚙️ Settings</button> </nav> <div class="container"> <!-- HOME DASHBOARD --> <section id="home" class="page active"> <div class="grid"> <div class="card"><h4>Today Sales</h4><div class="val green" id="dashSales">₹0</div></div> <div class="card"><h4>Bills Count</h4><div class="val" id="dashBills">0</div></div> <div class="card"><h4>Credit Due</h4><div class="val red" id="dashCredit">₹0</div></div> <div class="card"><h4>Low Stock</h4><div class="val red" id="dashLowStock">0</div></div> </div> <div class="panel"> <div class="panel-title">Quick Actions</div> <div class="row-2"> <button class="btn btn-primary" onclick="showPage('billing', document.querySelectorAll('nav button')[1])">New POS Bill</button> <button class="btn btn-secondary" onclick="showPage('stock', document.querySelectorAll('nav button')[3])">Manage Stock</button> </div> </div> </section> <!-- POS BILLING --> <section id="billing" class="page"> <div class="panel"> <div class="panel-title">Customer Selection</div> <div class="search-container"> <input id="shopSearch" placeholder="Search Shop Name or Phone..." oninput="searchShops(this.value)"> <div id="shopResults" class="dropdown-results" style="display:none;"></div> </div> <input type="hidden" id="selectedShopId"> </div> <div class="panel"> <div class="panel-title">Add Products to Cart</div> <div class="search-container"> <input id="prodSearch" placeholder="Type Product Name / SKU / Size..." oninput="searchProducts(this.value)"> <div id="prodResults" class="dropdown-results" style="display:none;"></div> </div> <div class="row-2" style="margin-top: 10px;"> <div> <label>Quantity (Cases/Units)</label> <input type="number" id="billQty" value="1" min="1"> </div> <div> <label>Custom Rate (₹)</label> <input type="number" id="billRate" placeholder="Default"> </div> </div> <button class="btn btn-secondary" onclick="addCartItem()">+ Add to Invoice</button> </div> <div class="panel"> <div class="panel-title">Cart Items</div> <div class="table-wrapper"> <table> <thead> <tr><th>Product</th><th>Qty</th><th>Rate</th><th>Total</th><th></th></tr> </thead> <tbody id="cartTable"></tbody> </table> </div> <div style="text-align:right; margin-top:12px; font-size:18px; font-weight:700;"> Subtotal: ₹<span id="cartSubtotal">0.00</span> </div> <label>Payment Method</label> <select id="paymentMethod" onchange="togglePaymentFields()"> <option value="Cash">Cash</option> <option value="UPI">UPI Digital</option> <option value="Credit">Credit Ledger (Udhaar)</option> </select> <div id="upiQrDisplay" style="text-align:center; display:none; margin:10px 0;"> <div id="posQrCode" style="display:inline-block; padding:10px; background:#fff; border:1px solid #ddd;"></div> <p style="font-size:11px; color:var(--text-muted); margin-top:4px;">Scan to pay total bill amount</p> </div> <div class="row-2"> <button class="btn btn-secondary" onclick="cancelBill()">Cancel Bill</button> <button class="btn btn-primary" onclick="processInvoice('print')">Confirm & Print Receipt</button> </div> </div> </section> <!-- SHOPS & KHATA --> <section id="shops" class="page"> <div class="panel admin-only"> <div class="panel-title">Add / Edit Shop</div> <input type="hidden" id="editShopId" value=""> <input id="shopName" placeholder="Shop Name *"> <div class="row-2"> <input id="shopArea" placeholder="Area / Route"> <input id="shopPhone" placeholder="Mobile Number"> </div> <button class="btn btn-primary" id="shopSaveBtn" onclick="saveShop()">Save Customer</button> </div> <div class="panel"> <div class="panel-title">Shop Directory & Credit Balance</div> <div class="table-wrapper"> <table> <thead> <tr><th>Shop</th><th>Area</th><th>Phone</th><th>Credit Due</th><th class="admin-only">Actions</th></tr> </thead> <tbody id="shopListTable"></tbody> </table> </div> </div> </section> <!-- INVENTORY / STOCK --> <section id="stock" class="page"> <div class="panel admin-only"> <div class="panel-title">Product Master Configuration</div> <input type="hidden" id="editProductId" value=""> <input id="pName" placeholder="Item Name (e.g., Coca Cola Original)"> <div class="row-2"> <input id="pSize" placeholder="Size (e.g., 750ml PET / 250ml Can)"> <input id="pPrice" type="number" placeholder="Selling Price (₹)"> </div> <div class="row-2"> <input id="pStock" type="number" placeholder="Current Stock (Qty)"> <input id="pGst" type="number" placeholder="GST % (e.g., 28)"> </div> <button class="btn btn-primary" id="productSaveBtn" onclick="saveProduct()">Save Product Master</button> </div> <div class="panel"> <div class="panel-title">Current Inventory</div> <div class="table-wrapper"> <table> <thead> <tr><th>Product</th><th>Size</th><th>Price</th><th>Stock</th><th>Damaged</th><th>GST%</th><th class="admin-only">Action</th></tr> </thead> <tbody id="stockTable"></tbody> </table> </div> </div> </section> <!-- ACCOUNTS --> <section id="accounts" class="page"> <div class="panel"> <div class="panel-title">Record Operating Expense</div> <input id="expTitle" placeholder="Expense Description (e.g., Fuel, Vehicle Repair)"> <input id="expAmount" type="number" placeholder="Amount (₹)"> <button class="btn btn-danger" onclick="addExpense()">Record Expense</button> </div> <div class="panel"> <div class="panel-title">Expense Ledger</div> <div class="table-wrapper"> <table> <thead> <tr><th>Date</th><th>Description</th><th>Amount</th><th>Action</th></tr> </thead> <tbody id="expenseTable"></tbody> </table> </div> </div> </section> <!-- REPORTS --> <section id="reports" class="page"> <div class="panel"> <div class="panel-title">Sales History</div> <div class="table-wrapper"> <table> <thead> <tr><th>Bill ID</th><th>Date</th><th>Customer</th><th>Type</th><th>Total</th><th>Action</th></tr> </thead> <tbody id="salesReportTable"></tbody> </table> </div> </div> </section> <!-- SETTINGS --> <section id="settings" class="page"> <div class="panel"> <div class="panel-title">Business Profile & Receipts</div> <label>Business Name</label> <input id="cfgName" value="DS TRADERS"> <label>Address</label> <input id="cfgAddress" value="Main Road, Commercial Hub"> <label>Phone Number</label> <input id="cfgPhone" value="9876543210"> <label>GSTIN</label> <input id="cfgGst" value="33AAAAA0000A1Z5"> <label>UPI ID (For Dynamic QR)</label> <input id="cfgUpi" value="dstraders@upi"> <label>Admin Security PIN</label> <input id="cfgPin" type="password" value="1234"> <label>Receipt Thermal Width</label> <select id="cfgPrintWidth"> <option value="58mm">58mm (Small Thermal Printer)</option> <option value="80mm" selected>80mm (Standard POS Thermal)</option> </select> <button class="btn btn-primary" onclick="saveSettings()">Save Configuration</button> </div> </section> </div> <!-- HIDDEN PRINT CONTAINER --> <div id="printArea"></div> <script> /* =================================================== DATABASE ENGINE (IndexedDB Wrapper) =================================================== */ class DBEngine { constructor() { this.dbName = "DSTradersV2_DB"; this.db = null; } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains("products")) db.createObjectStore("products", { keyPath: "id", autoIncrement: true }); if (!db.objectStoreNames.contains("shops")) db.createObjectStore("shops", { keyPath: "id", autoIncrement: true }); if (!db.objectStoreNames.contains("sales")) db.createObjectStore("sales", { keyPath: "id", autoIncrement: true }); if (!db.objectStoreNames.contains("expenses")) db.createObjectStore("expenses", { keyPath: "id", autoIncrement: true }); }; request.onsuccess = (e) => { this.db = e.target.result; resolve(); }; request.onerror = (e) => reject(e); }); } async getAll(storeName) { return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, "readonly"); const store = tx.objectStore(storeName); const req = store.getAll(); req.onsuccess = () => resolve(req.result || []); req.onerror = () => reject(req.error); }); } async put(storeName, item) { return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, "readwrite"); const store = tx.objectStore(storeName); const req = store.put(item); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async delete(storeName, id) { return new Promise((resolve, reject) => { const tx = this.db.transaction(storeName, "readwrite"); const store = tx.objectStore(storeName); const req = store.delete(id); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } } const db = new DBEngine(); /* =================================================== APPLICATION STATE & AUTH =================================================== */ let currentUser = { role: "staff" }; let cart = []; let activeProductSelection = null; const defaultConfig = { name: "DS TRADERS", address: "Main Distribution Hub, Sector 4", phone: "9876543210", gst: "33AAAAA0000A1Z5", upi: "dstraders@upi", pin: "1234", printWidth: "80mm" }; let config = JSON.parse(localStorage.getItem("ds_config")) || defaultConfig; window.addEventListener("DOMContentLoaded", asyn Add delete bill option after billing and payment QR method needed ip UPI means add damage stock column add and minus from whole stock and neeed it in apk format ready to install and use
window.addEventListener("DOMContentLoaded", asyn, so the current file is incomplete and cannot be built or tested as provided.manifest.json, app icons, and preferably a service worker are needed for an installable offline app.All 4 do now
~/
UPI deep link QR specification pa pn am cu transaction reference parameters NPCI
~/attachments
mkdir -p "/tmp/workspace/ds-traders-v2/src" "/tmp/workspace/ds-traders-v2/output"
~/ds-traders-v2/src/index.html
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#e4002b"><link rel="manifest" href="manifest.json"><title>DS Traders</title><style>
:root{--brand:#e4002b;--ink:#172033;--muted:#64748b;--bg:#f4f6f9;--line:#dfe5ed;--ok:#059669;--bad:#dc2626;--card:#fff}*{box-sizing:border-box;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}body{margin:0;background:var(--bg);color:var(--ink);padding-bottom:70px}header{position:sticky;top:0;z-index:10;background:var(--brand);color:#fff;padding:12px 16px;display:flex;justify-content:space-between;box-shadow:0 2px 7px #0003}.brand{font-weight:800;letter-spacing:.4px}.small{font-size:11px;opacity:.85}nav{position:sticky;top:54px;z-index:9;background:#fff;display:flex;overflow:auto;border-bottom:1px solid var(--line)}nav button{white-space:nowrap;border:0;border-bottom:3px solid transparent;background:#fff;padding:12px;color:var(--muted);font-weight:700;cursor:pointer}nav button.active{color:var(--brand);border-color:var(--brand)}main{max-width:1050px;margin:auto;padding:12px}.page{display:none}.page.active{display:block}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:10px}.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px;margin-bottom:12px}.card h4{margin:0 0 5px;color:var(--muted);font-size:11px;text-transform:uppercase}.value{font-size:21px;font-weight:800}.green{color:var(--ok)}.red{color:var(--bad)}.panel-title{font-weight:800;margin-bottom:10px;display:flex;justify-content:space-between;gap:8px;align-items:center}input,select,textarea{width:100%;padding:10px;border:1px solid var(--line);border-radius:7px;font-size:14px;margin:4px 0}label{font-size:12px;font-weight:700;color:var(--muted);display:block;margin-top:7px}.row{display:flex;gap:10px}.row>*{flex:1}.btn{border:0;border-radius:7px;padding:10px 13px;font-weight:750;cursor:pointer;margin-top:8px;background:#e8edf3;color:var(--ink)}.primary{background:var(--brand);color:white}.danger{background:var(--bad);color:#fff}.success{background:var(--ok);color:#fff}.tiny{padding:5px 8px;margin:0;font-size:11px}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:8px}table{width:100%;border-collapse:collapse;font-size:13px}th,td{padding:9px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}th{color:var(--muted);background:#f8fafc}.suggest{position:absolute;z-index:20;left:0;right:0;background:#fff;border:1px solid var(--line);border-radius:7px;max-height:180px;overflow:auto}.suggest div{padding:9px;cursor:pointer}.relative{position:relative}.right{text-align:right}.hidden{display:none!important}.notice{font-size:12px;color:var(--muted);margin-top:6px}.badge{padding:3px 7px;border-radius:12px;background:#ffffff33;font-size:11px}.admin-only{}#printArea{display:none}@media print{body>*{visibility:hidden}#printArea,#printArea *{visibility:visible}#printArea{display:block;position:absolute;inset:0;padding:8px;font:12px monospace}.no-print{display:none!important}}
</style></head><body><div id="login" style="position:fixed;inset:0;z-index:50;background:#0008;display:grid;place-items:center;padding:16px"><div class="panel" style="width:min(370px,100%);text-align:center"><h2 style="color:#e4002b;margin:4px">DS TRADERS</h2><p class="small" style="color:#64748b">Distribution Management System</p><label style="text-align:left">Role</label><select id="loginRole" onchange="pinBox.classList.toggle('hidden',this.value!=='admin')"><option value="staff">Staff</option><option value="admin">Admin</option></select><div id="pinBox" class="hidden"><label style="text-align:left">Admin PIN</label><input id="pin" type="password" placeholder="Default: 1234"></div><button class="btn primary" style="width:100%" onclick="loginApp()">Login</button></div></div>
<header><div><div class="brand" id="headName">DS TRADERS</div><div class="small">Distribution Management System</div></div><div><span class="badge" id="roleBadge">Staff</span><button class="tiny" onclick="logoutApp()">Logout</button></div></header><nav><button class="active" onclick="page('home',this)">Dashboard</button><button onclick="page('billing',this)">POS Billing</button><button onclick="page('shops',this)">Shops / Khata</button><button onclick="page('stock',this)">Inventory</button><button class="admin-only" onclick="page('accounts',this)">Accounts</button><button class="admin-only" onclick="page('reports',this)">Reports</button><button class="admin-only" onclick="page('settings',this)">Settings</button></nav>
<main>
<section id="home" class="page active"><div class="grid"><div class="card"><h4>Today Sales</h4><div class="value green" id="dSales">₹0</div></div><div class="card"><h4>Bills Count</h4><div class="value" id="dBills">0</div></div><div class="card"><h4>Credit Due</h4><div class="value red" id="dCredit">₹0</div></div><div class="card"><h4>Low Stock</h4><div class="value red" id="dLow">0</div></div></div></section>
<section id="billing" class="page"><div class="panel"><div class="panel-title">Customer Selection</div><div class="relative"><input id="shopSearch" placeholder="Search shop, phone, or use Walk-in" oninput="searchShop()"><div id="shopSuggestions" class="suggest hidden"></div></div><input id="chosenShop" type="hidden"><div class="notice" id="chosenShopText">Walk-in customer</div></div><div class="panel"><div class="panel-title">Add products</div><div class="relative"><input id="productSearch" placeholder="Search product or size" oninput="searchProduct()"><div id="productSuggestions" class="suggest hidden"></div></div><div class="row"><div><label>Quantity</label><input id="qty" min="1" type="number" value="1"></div><div><label>Rate</label><input id="rate" type="number" placeholder="Default rate"></div></div><button class="btn" onclick="addCart()">Add to invoice</button></div><div class="panel"><div class="panel-title">Cart</div><div class="table-wrap"><table><thead><tr><th>Product</th><th>Qty</th><th>Rate</th><th>Total</th><th></th></tr></thead><tbody id="cartRows"></tbody></table></div><div class="right" style="font-weight:800;margin-top:10px">Subtotal: ₹<span id="subtotal">0.00</span></div><label>Payment method</label><select id="payMethod" onchange="renderQR()"><option>Cash</option><option>UPI</option><option>Credit</option></select><div id="qrBox" class="hidden" style="text-align:center;padding:12px"><div id="qr"></div><div class="notice">Scan with any UPI app. Enter reference after payment confirmation.</div><input id="upiRef" placeholder="UPI reference / transaction ID (optional)"></div><div class="row"><button class="btn" onclick="clearCart()">Cancel</button><button class="btn primary" onclick="completeSale()">Confirm & Print</button></div></div></section>
<section id="shops" class="page"><div class="panel admin-only"><div class="panel-title">Add / Edit Shop</div><input id="shopId" type="hidden"><input id="shopName" placeholder="Shop name *"><div class="row"><input id="shopArea" placeholder="Area / Route"><input id="shopPhone" placeholder="Mobile"></div><button class="btn primary" onclick="saveShop()">Save customer</button></div><div class="panel"><div class="panel-title">Directory & credit balance</div><div class="table-wrap"><table><thead><tr><th>Shop</th><th>Area</th><th>Phone</th><th>Credit Due</th><th class="admin-only">Action</th></tr></thead><tbody id="shopRows"></tbody></table></div></div></section>
<section id="stock" class="page"><div class="panel admin-only"><div class="panel-title">Product Master</div><input id="productId" type="hidden"><input id="pName" placeholder="Item name *"><div class="row"><input id="pSize" placeholder="Size"><input id="pPrice" type="number" placeholder="Selling rate"></div><div class="row"><input id="pStock" type="number" placeholder="Total stock"><input id="pGst" type="number" placeholder="GST %"></div><button class="btn primary" onclick="saveProduct()">Save product</button></div><div class="panel"><div class="panel-title">Inventory</div><div class="notice">Available stock = Total stock minus Damaged stock. Only available stock can be billed.</div><div class="table-wrap"><table><thead><tr><th>Product</th><th>Rate</th><th>Total</th><th>Damaged</th><th>Available</th><th>GST</th><th class="admin-only">Actions</th></tr></thead><tbody id="stockRows"></tbody></table></div></div></section>
<section id="accounts" class="page"><div class="panel"><div class="panel-title">Record expense</div><input id="expenseTitle" placeholder="Description"><input id="expenseAmount" type="number" placeholder="Amount"><button class="btn danger" onclick="saveExpense()">Record expense</button></div><div class="panel"><div class="panel-title">Expenses</div><div class="table-wrap"><table><thead><tr><th>Date</th><th>Description</th><th>Amount</th><th>Action</th></tr></thead><tbody id="expenseRows"></tbody></table></div></div></section>
<section id="reports" class="page"><div class="panel"><div class="panel-title">Sales history</div><div class="notice">Deleting a bill restores inventory and reverses credit. This action cannot be undone.</div><div class="table-wrap"><table><thead><tr><th>Bill</th><th>Date</th><th>Customer</th><th>Payment</th><th>Total</th><th>Status</th><th>Action</th></tr></thead><tbody id="saleRows"></tbody></table></div></div></section>
<section id="settings" class="page"><div class="panel"><div class="panel-title">Business Profile</div><label>Business name</label><input id="cName"><label>Address</label><input id="cAddress"><label>Phone</label><input id="cPhone"><label>GSTIN</label><input id="cGst"><label>UPI ID</label><input id="cUpi"><label>Admin PIN</label><input id="cPin" type="password"><button class="btn primary" onclick="saveConfig()">Save configuration</button></div></section>
</main><div id="printArea"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script><script src="app.js"></script></body></html>~/ds-traders-v2/src/app.js
const $=id=>document.getElementById(id);let role='staff',cart=[],selectedProduct=null,config=JSON.parse(localStorage.getItem('ds_config')||'null')||{name:'DS TRADERS',address:'Main Distribution Hub',phone:'9876543210',gst:'',upi:'dstraders@upi',pin:'1234'};
class DB{constructor(){this.db=null}init(){return new Promise((ok,no)=>{let r=indexedDB.open('DSTradersV3',1);r.onupgradeneeded=e=>{let d=e.target.result;['products','shops','sales','expenses'].forEach(x=>d.objectStoreNames.contains(x)||d.createObjectStore(x,{keyPath:'id',autoIncrement:true}))};r.onsuccess=e=>{this.db=e.target.result;ok()};r.onerror=()=>no(r.error)})}all(s){return new Promise((ok,no)=>{let r=this.db.transaction(s).objectStore(s).getAll();r.onsuccess=()=>ok(r.result||[]);r.onerror=()=>no(r.error)})}put(s,o){return new Promise((ok,no)=>{let r=this.db.transaction(s,'readwrite').objectStore(s).put(o);r.onsuccess=()=>ok(r.result);r.onerror=()=>no(r.error)})}del(s,id){return new Promise((ok,no)=>{let r=this.db.transaction(s,'readwrite').objectStore(s).delete(id);r.onsuccess=()=>ok();r.onerror=()=>no(r.error)})}}
const db=new DB(), money=n=>'₹'+Number(n||0).toFixed(2), esc=s=>String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
function page(id,b){document.querySelectorAll('.page').forEach(x=>x.classList.remove('active'));$(id).classList.add('active');document.querySelectorAll('nav button').forEach(x=>x.classList.remove('active'));b?.classList.add('active');refresh()}
function loginApp(){let r=$('loginRole').value;if(r==='admin'&&$('pin').value!==config.pin)return alert('Incorrect admin PIN');role=r;document.body.classList.toggle('staff',r==='staff');document.querySelectorAll('.admin-only').forEach(x=>x.classList.toggle('hidden',r!=='admin'));$('roleBadge').textContent=r;$('login').classList.add('hidden');refresh()}function logoutApp(){$('login').classList.remove('hidden');$('pin').value=''}
async function refresh(){let [p,sh,s,e]=await Promise.all(['products','shops','sales','expenses'].map(x=>db.all(x)));renderDashboard(p,sh,s);renderShops(sh);renderStock(p);renderSales(s,sh);renderExpenses(e);$('headName').textContent=config.name}
function renderDashboard(p,sh,s){let today=new Date().toDateString(),a=s.filter(x=>!x.deleted&&new Date(x.date).toDateString()===today);$('dSales').textContent=money(a.reduce((n,x)=>n+x.total,0));$('dBills').textContent=a.length;$('dCredit').textContent=money(sh.reduce((n,x)=>n+(x.credit||0),0));$('dLow').textContent=p.filter(x=>(x.stock||0)-(x.damaged||0)<=5).length}
function renderShops(a){$('shopRows').innerHTML=a.map(x=>`<tr><td>${esc(x.name)}</td><td>${esc(x.area)}</td><td>${esc(x.phone)}</td><td>${money(x.credit)}</td><td class="admin-only ${role!=='admin'?'hidden':''}"><button class="btn tiny" onclick="editShop(${x.id})">Edit</button> <button class="btn danger tiny" onclick="deleteShop(${x.id})">Delete</button></td></tr>`).join('')||'<tr><td colspan="5">No customers added</td></tr>'}
async function saveShop(){let name=$('shopName').value.trim();if(!name)return alert('Shop name is required');let old=Number($('shopId').value), all=await db.all('shops'),x=all.find(i=>i.id===old)||{credit:0};await db.put('shops',{...x,id:old||undefined,name,area:$('shopArea').value.trim(),phone:$('shopPhone').value.trim()});['shopId','shopName','shopArea','shopPhone'].forEach(i=>$(i).value='');refresh()}async function editShop(id){let x=(await db.all('shops')).find(i=>i.id===id);$('shopId').value=x.id;$('shopName').value=x.name;$('shopArea').value=x.area;$('shopPhone').value=x.phone;page('shops',document.querySelectorAll('nav button')[2])}async function deleteShop(id){if(confirm('Delete this shop?')){await db.del('shops',id);refresh()}}
function renderStock(a){$('stockRows').innerHTML=a.map(x=>{let av=(+x.stock||0)-(+x.damaged||0);return `<tr><td>${esc(x.name)}<br><small>${esc(x.size)}</small></td><td>${money(x.price)}</td><td>${x.stock}</td><td>${x.damaged||0}</td><td class="${av<=5?'red':''}">${av}</td><td>${x.gst||0}%</td><td class="admin-only ${role!=='admin'?'hidden':''}"><button class="btn tiny" onclick="adjustDamage(${x.id},1)">+ Damage</button> <button class="btn tiny" onclick="adjustDamage(${x.id},-1)">- Damage</button><br><button class="btn tiny" onclick="editProduct(${x.id})">Edit</button> <button class="btn danger tiny" onclick="deleteProduct(${x.id})">Delete</button></td></tr>`}).join('')||'<tr><td colspan="7">No products</td></tr>'}
async function saveProduct(){let name=$('pName').value.trim(),stock=Number($('pStock').value);if(!name||Number.isNaN(stock)||stock<0)return alert('Name and non-negative total stock are required');let id=Number($('productId').value),a=await db.all('products'),x=a.find(i=>i.id===id)||{damaged:0};if((x.damaged||0)>stock)return alert('Total stock cannot be below damaged stock');await db.put('products',{...x,id:id||undefined,name,size:$('pSize').value,price:Number($('pPrice').value)||0,stock,damaged:x.damaged||0,gst:Number($('pGst').value)||0});['productId','pName','pSize','pPrice','pStock','pGst'].forEach(i=>$(i).value='');refresh()}async function adjustDamage(id,d){let x=(await db.all('products')).find(i=>i.id===id),next=(x.damaged||0)+d;if(next<0||next>x.stock)return alert('Damaged stock must be from 0 to total stock');if(confirm(`${d>0?'Add':'Remove'} one damaged unit for ${x.name}?`)){x.damaged=next;await db.put('products',x);refresh()}}async function editProduct(id){let x=(await db.all('products')).find(i=>i.id===id);$('productId').value=x.id;$('pName').value=x.name;$('pSize').value=x.size;$('pPrice').value=x.price;$('pStock').value=x.stock;$('pGst').value=x.gst;page('stock',document.querySelectorAll('nav button')[3])}async function deleteProduct(id){if(confirm('Delete product master?')){await db.del('products',id);refresh()}}
async function searchShop(){let q=$('shopSearch').value.toLowerCase(),a=await db.all('shops'),r=a.filter(x=>`${x.name} ${x.phone}`.toLowerCase().includes(q)).slice(0,8),box=$('shopSuggestions');box.innerHTML=r.map(x=>`<div onclick="chooseShop(${x.id})">${esc(x.name)} - ${esc(x.phone||'')}</div>`).join('');box.classList.toggle('hidden',!q||!r.length)}async function chooseShop(id){let x=(await db.all('shops')).find(i=>i.id===id);$('chosenShop').value=id;$('shopSearch').value=x.name;$('chosenShopText').textContent=`Selected: ${x.name} | Credit due: ${money(x.credit)}`;$('shopSuggestions').classList.add('hidden')}
async function searchProduct(){let q=$('productSearch').value.toLowerCase(),a=await db.all('products'),r=a.filter(x=>`${x.name} ${x.size}`.toLowerCase().includes(q)).slice(0,8),box=$('productSuggestions');box.innerHTML=r.map(x=>`<div onclick="chooseProduct(${x.id})">${esc(x.name)} (${esc(x.size)}) - Available: ${(x.stock||0)-(x.damaged||0)}</div>`).join('');box.classList.toggle('hidden',!q||!r.length)}async function chooseProduct(id){selectedProduct=(await db.all('products')).find(i=>i.id===id);$('productSearch').value=`${selectedProduct.name} ${selectedProduct.size}`;$('rate').value=selectedProduct.price;$('productSuggestions').classList.add('hidden')}
function addCart(){if(!selectedProduct)return alert('Select a product');let q=Number($('qty').value),rate=Number($('rate').value||selectedProduct.price),already=cart.filter(x=>x.productId===selectedProduct.id).reduce((n,x)=>n+x.qty,0),available=selectedProduct.stock-selectedProduct.damaged;if(!q||q<1||q+already>available)return alert(`Only ${available-already} available units can be added`);let old=cart.find(x=>x.productId===selectedProduct.id);if(old){old.qty+=q;old.rate=rate}else cart.push({productId:selectedProduct.id,name:selectedProduct.name,size:selectedProduct.size,qty:q,rate});selectedProduct=null;$('productSearch').value='';$('qty').value=1;$('rate').value='';renderCart()}
function renderCart(){$('cartRows').innerHTML=cart.map((x,i)=>`<tr><td>${esc(x.name)}<br><small>${esc(x.size)}</small></td><td>${x.qty}</td><td>${money(x.rate)}</td><td>${money(x.qty*x.rate)}</td><td><button class="btn danger tiny" onclick="cart.splice(${i},1);renderCart()">X</button></td></tr>`).join('')||'<tr><td colspan="5">Cart is empty</td></tr>';$('subtotal').textContent=cart.reduce((n,x)=>n+x.qty*x.rate,0).toFixed(2);renderQR()}
function upiLink(){let total=cart.reduce((n,x)=>n+x.qty*x.rate,0);return `upi://pay?pa=${encodeURIComponent(config.upi)}&pn=${encodeURIComponent(config.name)}&am=${total.toFixed(2)}&cu=INR&tn=${encodeURIComponent('DS Traders Invoice')}`}function renderQR(){let box=$('qr');box.innerHTML='';let show=$('payMethod').value==='UPI'&&cart.length;$('qrBox').classList.toggle('hidden',!show);if(show&&window.QRCode)new QRCode(box,{text:upiLink(),width:180,height:180})}
function clearCart(){cart=[];selectedProduct=null;$('chosenShop').value='';$('shopSearch').value='';$('chosenShopText').textContent='Walk-in customer';$('upiRef').value='';renderCart()}
async function completeSale(){if(!cart.length)return alert('Cart is empty');let products=await db.all('products');for(let i of cart){let p=products.find(x=>x.id===i.productId);if(!p||p.stock-p.damaged<i.qty)return alert(`Insufficient stock: ${i.name}`)}let payment=$('payMethod').value,shopId=Number($('chosenShop').value)||null;if(payment==='Credit'&&!shopId)return alert('Select a registered shop for credit sale');for(let i of cart){let p=products.find(x=>x.id===i.productId);p.stock-=i.qty;await db.put('products',p)}let total=cart.reduce((n,x)=>n+x.qty*x.rate,0),bill={date:new Date().toISOString(),billNo:'DS-'+Date.now().toString().slice(-7),shopId,payment,upiRef:$('upiRef').value.trim(),items:structuredClone(cart),total,deleted:false};await db.put('sales',bill);if(payment==='Credit'){let sh=(await db.all('shops')).find(x=>x.id===shopId);sh.credit=(sh.credit||0)+total;await db.put('shops',sh)}printBill(bill,products,shopId? (await db.all('shops')).find(x=>x.id===shopId):null);clearCart();refresh()}
function printBill(b,products,sh){$('printArea').innerHTML=`<div style="max-width:300px"><h3>${esc(config.name)}</h3><div>${esc(config.address)}<br>${esc(config.phone)}<br>GSTIN: ${esc(config.gst)}</div><hr><b>Bill: ${b.billNo}</b><br>${new Date(b.date).toLocaleString()}<br>Customer: ${esc(sh?.name||'Walk-in')}<hr>${b.items.map(i=>`${esc(i.name)} x ${i.qty} @ ${i.rate} = ${money(i.qty*i.rate)}<br>`).join('')}<hr><b>Total: ${money(b.total)}</b><br>Payment: ${esc(b.payment)}${b.upiRef?`<br>UPI Ref: ${esc(b.upiRef)}`:''}<hr>Thank you</div>`;window.print()}
function renderSales(a,shops){$('saleRows').innerHTML=[...a].reverse().map(x=>`<tr><td>${x.billNo}</td><td>${new Date(x.date).toLocaleString()}</td><td>${esc(shops.find(s=>s.id===x.shopId)?.name||'Walk-in')}</td><td>${x.payment}${x.upiRef?'<br><small>'+esc(x.upiRef)+'</small>':''}</td><td>${money(x.total)}</td><td class="${x.deleted?'red':'green'}">${x.deleted?'Deleted':'Active'}</td><td>${!x.deleted&&role==='admin'?`<button class="btn danger tiny" onclick="deleteBill(${x.id})">Delete bill</button>`:''}</td></tr>`).join('')||'<tr><td colspan="7">No bills</td></tr>'}
async function deleteBill(id){let sale=(await db.all('sales')).find(x=>x.id===id);if(!sale||sale.deleted)return;if(!confirm(`Delete ${sale.billNo}? Stock will be restored and credit reversed.`))return;let p=await db.all('products');for(let i of sale.items){let x=p.find(z=>z.id===i.productId);if(x){x.stock+=i.qty;await db.put('products',x)}}if(sale.payment==='Credit'&&sale.shopId){let x=(await db.all('shops')).find(z=>z.id===sale.shopId);if(x){x.credit=Math.max(0,(x.credit||0)-sale.total);await db.put('shops',x)}}sale.deleted=true;sale.deletedAt=new Date().toISOString();await db.put('sales',sale);refresh()}
async function saveExpense(){let title=$('expenseTitle').value.trim(),amount=Number($('expenseAmount').value);if(!title||!amount||amount<=0)return alert('Enter description and positive amount');await db.put('expenses',{title,amount,date:new Date().toISOString()});$('expenseTitle').value=$('expenseAmount').value='';refresh()}function renderExpenses(a){$('expenseRows').innerHTML=a.map(x=>`<tr><td>${new Date(x.date).toLocaleString()}</td><td>${esc(x.title)}</td><td>${money(x.amount)}</td><td><button class="btn danger tiny" onclick="db.del('expenses',${x.id}).then(refresh)">Delete</button></td></tr>`).join('')}
function saveConfig(){config={name:$('cName').value.trim()||'DS TRADERS',address:$('cAddress').value,phone:$('cPhone').value,gst:$('cGst').value,upi:$('cUpi').value.trim(),pin:$('cPin').value||'1234'};localStorage.setItem('ds_config',JSON.stringify(config));refresh();alert('Configuration saved')}function loadConfig(){Object.entries({cName:'name',cAddress:'address',cPhone:'phone',cGst:'gst',cUpi:'upi',cPin:'pin'}).forEach(([i,k])=>$(i).value=config[k]||'')}
window.addEventListener('DOMContentLoaded',async()=>{await db.init();loadConfig();renderCart();refresh();if('serviceWorker'in navigator)navigator.serviceWorker.register('sw.js')});
~/ds-traders-v2/src
~/ds-traders-v2/src/manifest.json
{
"name": "DS Traders - Distribution Management",
"short_name": "DS Traders",
"description": "Offline-first POS, inventory and customer credit management.",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#f4f6f9",
"theme_color": "#e4002b",
"icons": [
{"src":"icon-192.svg","sizes":"192x192","type":"image/svg+xml","purpose":"any maskable"},
{"src":"icon-512.svg","sizes":"512x512","type":"image/svg+xml","purpose":"any maskable"}
]
}~/ds-traders-v2/src/sw.js
const CACHE='ds-traders-v3-1';
const LOCAL=['./','./index.html','./app.js','./manifest.json','./icon-192.svg','./icon-512.svg'];
self.addEventListener('install',event=>event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(LOCAL)).then(()=>self.skipWaiting())));
self.addEventListener('activate',event=>event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())));
self.addEventListener('fetch',event=>{
if(event.request.method!=='GET') return;
event.respondWith(caches.match(event.request).then(hit=>hit||fetch(event.request).then(response=>{
if(response.ok && new URL(event.request.url).origin===location.origin){const copy=response.clone();caches.open(CACHE).then(c=>c.put(event.request,copy));}
return response;
}).catch(()=>caches.match('./index.html'))));
});~/ds-traders-v2/src/icon-192.svg
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192"><rect width="192" height="192" rx="38" fill="#e4002b"/><path d="M46 48h65c21 0 35 11 35 27 0 11-7 19-17 22 13 4 22 14 22 29 0 22-17 36-42 36H46V48zm29 24v27h31c8 0 12-5 12-14 0-8-5-13-13-13H75zm0 51v15h35c9 0 14-3 14-8 0-5-5-7-14-7H75z" fill="#fff"/><text x="96" y="178" font-family="Arial,sans-serif" font-size="16" font-weight="700" fill="#fff" text-anchor="middle">DS TRADERS</text></svg>
~/ds-traders-v2/src/icon-512.svg
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 192 192"><rect width="192" height="192" rx="38" fill="#e4002b"/><path d="M46 48h65c21 0 35 11 35 27 0 11-7 19-17 22 13 4 22 14 22 29 0 22-17 36-42 36H46V48zm29 24v27h31c8 0 12-5 12-14 0-8-5-13-13-13H75zm0 51v15h35c9 0 14-3 14-8 0-5-5-7-14-7H75z" fill="#fff"/><text x="96" y="178" font-family="Arial,sans-serif" font-size="16" font-weight="700" fill="#fff" text-anchor="middle">DS TRADERS</text></svg>
node --check "/tmp/workspace/ds-traders-v2/src/app.js" && python -m json.tool "/tmp/workspace/ds-traders-v2/src/manifest.json" >/dev/null && printf 'syntax-ok\n'
~/ds-traders-v2/README.md
# DS Traders V2 An offline-first, browser-installable distribution-management PWA. It stores products, shops, bills and expenses in the browser's IndexedDB database. ## Included functions - Admin and staff role login. Default admin PIN: `1234`. - POS cart with available-stock validation. - Dynamic UPI intent QR for the precise invoice amount. It uses `upi://pay` with the business UPI ID, merchant name, amount and INR currency. The QR starts working when the QR script is available on the device. For payment verification, check the payment in the UPI app and optionally save its reference number. - Bill deletion from Admin Reports. Deletion marks the sale as deleted, restores product stock, and reverses credit for a credit bill. - Product total stock, damaged stock, and sellable/available stock. - Add and remove damaged stock actions. Damaged units cannot exceed total stock and cannot be billed. - Customer credit balances, expense ledger, printing, business profile, PWA manifest and service worker. ## Run and install as a PWA 1. Serve the `src` folder over HTTPS, or use localhost during testing. Service workers will not activate from a `file://` URL. 2. Open the app in Chrome or Edge. 3. Use the browser's **Install app** option. 4. Login as Admin to create products and shops. Use Staff for billing and stock viewing. For a local test server, from the `src` folder run: ```bash python -m http.server 8080 ``` Then browse to `http://localhost:8080`. ## Android APK note This package is a completed installable PWA, not an Android APK. To create an APK, wrap `src` with Capacitor (or a Trusted Web Activity) on a machine with Android Studio and the Android SDK. Build the app as a signed release APK before distributing it. The application data is device-local, so it is not a multi-device cloud system. ## Data safety Browser data can be lost if site data is cleared or the app is uninstalled. For real production use, add secure authentication, a database/server backup, audit identities, and payment-provider verification. QR scanning opens a UPI app, but does not automatically prove that a payment completed.
~/ds-traders-v2
python - <<'PY' from bs4 import BeautifulSoup from pathlib import Path html=Path('/tmp/workspace/ds-traders-v2/src/index.html').read_text() js=Path('/tmp/workspace/ds-traders-v2/src/app.js').read_text() s=BeautifulSoup(html,'html.parser') ids={x.get('id') for x in s.find_all(id=True)} import re refs=set(re.findall(r"\$\('([^']+)'\)",js)) print('Missing DOM IDs:',sorted(refs-ids)) print('Duplicate IDs:',[x for x in ids if len(s.find_all(id=x))>1]) PY
cd "/tmp/workspace/ds-traders-v2" && zip -r "output/DS-Traders-V2-PWA.zip" "src" "README.md" >/dev/null && unzip -t "output/DS-Traders-V2-PWA.zip" >/dev/null && ls -lh "output/DS-Traders-V2-PWA.zip"
DS Traders V2 PWA
Archive · ZIP
upi://pay payment QR when UPI is selected1234.src folder:cd src
python -m http.server 8080
http://localhost:8080 in Chrome/Edge and use Install App. The full implementation notes are in README
MD File · MD
No zip format app format
~/
Capacitor Android official documentation build APK Android Studio
command -v java || true; command -v gradle || true; command -v adb || true; command -v sdkmanager || true; command -v node || true; ls -la "/tmp/workspace/ds-traders-v2"
.apk in this workspace because the Android build tools are not installed here: Java, Gradle, Android SDK, and Android Studio are missing. An APK cannot be safely or truthfully renamed from a ZIP or HTML file.DS-Traders.apk
I put the above zip file in vs code to make app for that give me a final proper bug free code
~/ds-traders-v2
Capacitor official install Android npm package latest add android build documentation
~/ds-traders-v2/src
npm --version && node --version
~/ds-traders-v2/package.json
{
"name": "ds-traders-v2",
"version": "1.0.0",
"private": true,
"description": "DS Traders offline POS Android application",
"scripts": {
"sync": "npx cap sync android",
"android": "npx cap open android",
"run:android": "npx cap run android",
"build:apk": "npx cap build android --androidreleasetype APK"
},
"dependencies": {
"@capacitor/android": "^8.0.0",
"@capacitor/core": "^8.0.0",
"qrcodejs": "^1.0.0"
},
"devDependencies": {
"@capacitor/cli": "^8.0.0"
}
}~/ds-traders-v2/capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.dstraders.pos',
appName: 'DS Traders',
webDir: 'src',
bundledWebRuntime: false,
android: {
allowMixedContent: false
}
};
export default config;
~/ds-traders-v2/.gitignore
node_modules/ android/.gradle/ android/build/ android/app/build/ .DS_Store *.keystore *.jks
~/ds-traders-v2/src/README-APK.md
# Build DS Traders as an Android app in VS Code ## One-time setup 1. Install current **Node.js LTS** and **Android Studio**. 2. In Android Studio, install the Android SDK, Platform Tools and a current SDK platform. Set `ANDROID_HOME` if Android Studio does not do it automatically. 3. Open this project folder in VS Code and run the commands below in the integrated terminal. ```bash npm install npx cap add android npm run sync npm run android ``` `npm run android` opens the `android` directory in Android Studio. Select a physical Android phone or emulator, then press Run. ## Make an APK For testing, in Android Studio select **Build > Build APK(s)**. The debug file is normally: ```text android/app/build/outputs/apk/debug/app-debug.apk ``` For distribution, select **Build > Generate Signed Bundle / APK > APK**, create or select your keystore, choose `release`, and finish. Do not share your keystore or passwords. After changing `src/index.html`, `src/app.js`, or other web files, run: ```bash npm run sync ``` before rebuilding in Android Studio. ## Important operational notes - Data is stored locally in IndexedDB on each device. Clearing app data or uninstalling removes it. - The UPI QR starts a payment in a UPI app but cannot confirm settlement itself. Confirm payment in the UPI app and record the transaction reference. - Change the default Admin PIN (`1234`) immediately in Settings.
~/ds-traders-v2/src/app.js
/* DS Traders V2 - offline POS application */
'use strict';
const $ = (id) => document.getElementById(id);
const DB_NAME = 'DSTradersV3';
const STORES = ['products', 'shops', 'sales', 'expenses'];
const DEFAULT_CONFIG = {
name: 'DS TRADERS', address: 'Main Distribution Hub', phone: '9876543210',
gst: '', upi: 'dstraders@upi', pin: '1234'
};
let role = 'staff';
let cart = [];
let selectedProduct = null;
let config = loadConfigValue();
function loadConfigValue() {
try { return { ...DEFAULT_CONFIG, ...JSON.parse(localStorage.getItem('ds_config') || '{}') }; }
catch { return { ...DEFAULT_CONFIG }; }
}
class Database {
constructor() { this.db = null; }
init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onupgradeneeded = (event) => {
const database = event.target.result;
STORES.forEach((name) => {
if (!database.objectStoreNames.contains(name)) {
database.createObjectStore(name, { keyPath: 'id', autoIncrement: true });
}
});
};
request.onsuccess = (event) => { this.db = event.target.result; resolve(); };
request.onerror = () => reject(request.error);
});
}
all(storeName) {
return new Promise((resolve, reject) => {
const request = this.db.transaction(storeName, 'readonly').objectStore(storeName).getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
put(storeName, value) {
return new Promise((resolve, reject) => {
const request = this.db.transaction(storeName, 'readwrite').objectStore(storeName).put(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
delete(storeName, id) {
return new Promise((resolve, reject) => {
const request = this.db.transaction(storeName, 'readwrite').objectStore(storeName).delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
/* Runs related reads and updates inside one IndexedDB transaction. */
transaction(storeNames, operation) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction(storeNames, 'readwrite');
let result;
try { result = operation(tx); } catch (error) { tx.abort(); reject(error); return; }
tx.oncomplete = () => resolve(result);
tx.onerror = () => reject(tx.error || new Error('Database transaction failed'));
tx.onabort = () => reject(tx.error || new Error('Database transaction was cancelled'));
});
}
}
const db = new Database();
const money = (value) => `₹${Number(value || 0).toFixed(2)}`;
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[char]));
const availableStock = (product) => Math.max(0, Number(product.stock || 0) - Number(product.damaged || 0));
function requireAdmin() {
if (role !== 'admin') { alert('Admin access is required.'); return false; }
return true;
}
function page(id, button) {
document.querySelectorAll('.page').forEach((element) => element.classList.remove('active'));
$(id).classList.add('active');
document.querySelectorAll('nav button').forEach((element) => element.classList.remove('active'));
if (button) button.classList.add('active');
refresh().catch(showError);
}
function showError(error) {
console.error(error);
alert(`Something went wrong: ${error.message || 'Please try again.'}`);
}
function loginApp() {
const requestedRole = $('loginRole').value;
if (requestedRole === 'admin' && $('pin').value !== config.pin) return alert('Incorrect admin PIN.');
role = requestedRole;
document.body.classList.toggle('staff', role === 'staff');
document.querySelectorAll('.admin-only').forEach((element) => element.classList.toggle('hidden', role !== 'admin'));
$('roleBadge').textContent = role === 'admin' ? 'Admin' : 'Staff';
$('login').classList.add('hidden');
refresh().catch(showError);
}
function logoutApp() {
role = 'staff';
cart = [];
selectedProduct = null;
$('pin').value = '';
$('login').classList.remove('hidden');
renderCart();
}
async function refresh() {
const [products, shops, sales, expenses] = await Promise.all(STORES.map((store) => db.all(store)));
renderDashboard(products, shops, sales);
renderShops(shops);
renderStock(products);
renderSales(sales, shops);
renderExpenses(expenses);
$('headName').textContent = config.name;
}
function renderDashboard(products, shops, sales) {
const today = new Date().toDateString();
const activeToday = sales.filter((sale) => !sale.deleted && new Date(sale.date).toDateString() === today);
$('dSales').textContent = money(activeToday.reduce((sum, sale) => sum + Number(sale.total || 0), 0));
$('dBills').textContent = activeToday.length;
$('dCredit').textContent = money(shops.reduce((sum, shop) => sum + Number(shop.credit || 0), 0));
$('dLow').textContent = products.filter((product) => availableStock(product) <= 5).length;
}
function renderShops(shops) {
$('shopRows').innerHTML = shops.map((shop) => `<tr>
<td>${escapeHtml(shop.name)}</td><td>${escapeHtml(shop.area)}</td><td>${escapeHtml(shop.phone)}</td>
<td>${money(shop.credit)}</td>
<td class="admin-only ${role !== 'admin' ? 'hidden' : ''}">
<button class="btn tiny" onclick="editShop(${shop.id})">Edit</button>
<button class="btn danger tiny" onclick="deleteShop(${shop.id})">Delete</button>
</td></tr>`).join('') || '<tr><td colspan="5">No customers added</td></tr>';
}
async function saveShop() {
if (!requireAdmin()) return;
const name = $('shopName').value.trim();
if (!name) return alert('Shop name is required.');
const id = Number($('shopId').value);
const shops = await db.all('shops');
const old = shops.find((shop) => shop.id === id);
const shop = { ...(old || { credit: 0 }), name, area: $('shopArea').value.trim(), phone: $('shopPhone').value.trim() };
if (old) shop.id = old.id;
await db.put('shops', shop);
['shopId', 'shopName', 'shopArea', 'shopPhone'].forEach((idName) => { $(idName).value = ''; });
await refresh();
}
async function editShop(id) {
if (!requireAdmin()) return;
const shop = (await db.all('shops')).find((item) => item.id === id);
if (!shop) return;
$('shopId').value = shop.id; $('shopName').value = shop.name; $('shopArea').value = shop.area || ''; $('shopPhone').value = shop.phone || '';
page('shops', document.querySelectorAll('nav button')[2]);
}
async function deleteShop(id) {
if (!requireAdmin() || !confirm('Delete this shop? Existing invoices will remain but show Walk-in.')) return;
await db.delete('shops', id); await refresh();
}
function renderStock(products) {
$('stockRows').innerHTML = products.map((product) => {
const available = availableStock(product);
return `<tr><td>${escapeHtml(product.name)}<br><small>${escapeHtml(product.size)}</small></td>
<td>${money(product.price)}</td><td>${Number(product.stock || 0)}</td><td>${Number(product.damaged || 0)}</td>
<td class="${available <= 5 ? 'red' : ''}">${available}</td><td>${Number(product.gst || 0)}%</td>
<td class="admin-only ${role !== 'admin' ? 'hidden' : ''}">
<button class="btn tiny" onclick="adjustDamage(${product.id},1)">+ Damage</button>
<button class="btn tiny" onclick="adjustDamage(${product.id},-1)">- Damage</button><br>
<button class="btn tiny" onclick="editProduct(${product.id})">Edit</button>
<button class="btn danger tiny" onclick="deleteProduct(${product.id})">Delete</button></td></tr>`;
}).join('') || '<tr><td colspan="7">No products</td></tr>';
}
async function saveProduct() {
if (!requireAdmin()) return;
const name = $('pName').value.trim();
const stock = Number($('pStock').value);
const price = Number($('pPrice').value);
if (!name || !Number.isFinite(stock) || stock < 0 || !Number.isFinite(price) || price < 0) return alert('Enter product name, valid selling rate, and non-negative total stock.');
const id = Number($('productId').value);
const old = (await db.all('products')).find((product) => product.id === id);
if (old && Number(old.damaged || 0) > stock) return alert('Total stock cannot be lower than damaged stock.');
const product = { ...(old || { damaged: 0 }), name, size: $('pSize').value.trim(), price, stock, gst: Number($('pGst').value) || 0 };
if (old) product.id = old.id;
await db.put('products', product);
['productId', 'pName', 'pSize', 'pPrice', 'pStock', 'pGst'].forEach((idName) => { $(idName).value = ''; });
await refresh();
}
async function adjustDamage(id, change) {
if (!requireAdmin()) return;
const product = (await db.all('products')).find((item) => item.id === id);
if (!product) return;
const damage = Number(product.damaged || 0) + change;
if (damage < 0 || damage > Number(product.stock || 0)) return alert('Damaged stock must be between 0 and total stock.');
if (!confirm(`${change > 0 ? 'Add' : 'Remove'} one damaged unit for ${product.name}?`)) return;
product.damaged = damage;
await db.put('products', product); await refresh();
}
async function editProduct(id) {
if (!requireAdmin()) return;
const product = (await db.all('products')).find((item) => item.id === id);
if (!product) return;
$('productId').value = product.id; $('pName').value = product.name; $('pSize').value = product.size || '';
$('pPrice').value = product.price; $('pStock').value = product.stock; $('pGst').value = product.gst || 0;
page('stock', document.querySelectorAll('nav button')[3]);
}
async function deleteProduct(id) {
if (!requireAdmin() || !confirm('Delete this product master? Existing bills will remain.')) return;
await db.delete('products', id); await refresh();
}
async function searchShop() {
const query = $('shopSearch').value.trim().toLowerCase();
const matches = (await db.all('shops')).filter((shop) => `${shop.name} ${shop.phone || ''}`.toLowerCase().includes(query)).slice(0, 8);
$('shopSuggestions').innerHTML = matches.map((shop) => `<div onclick="chooseShop(${shop.id})">${escapeHtml(shop.name)} - ${escapeHtml(shop.phone || '')}</div>`).join('');
$('shopSuggestions').classList.toggle('hidden', !query || !matches.length);
}
async function chooseShop(id) {
const shop = (await db.all('shops')).find((item) => item.id === id);
if (!shop) return;
$('chosenShop').value = shop.id; $('shopSearch').value = shop.name;
$('chosenShopText').textContent = `Selected: ${shop.name} | Credit due: ${money(shop.credit)}`;
$('shopSuggestions').classList.add('hidden');
}
async function searchProduct() {
const query = $('productSearch').value.trim().toLowerCase();
const matches = (await db.all('products')).filter((product) => `${product.name} ${product.size || ''}`.toLowerCase().includes(query)).slice(0, 8);
$('productSuggestions').innerHTML = matches.map((product) => `<div onclick="chooseProduct(${product.id})">${escapeHtml(product.name)} (${escapeHtml(product.size)}) - Available: ${availableStock(product)}</div>`).join('');
$('productSuggestions').classList.toggle('hidden', !query || !matches.length);
}
async function chooseProduct(id) {
selectedProduct = (await db.all('products')).find((item) => item.id === id);
if (!selectedProduct) return;
$('productSearch').value = `${selectedProduct.name} ${selectedProduct.size || ''}`;
$('rate').value = selectedProduct.price;
$('productSuggestions').classList.add('hidden');
}
function addCart() {
if (!selectedProduct) return alert('Select a product first.');
const quantity = Number($('qty').value);
const rate = Number($('rate').value || selectedProduct.price);
if (!Number.isInteger(quantity) || quantity < 1 || !Number.isFinite(rate) || rate < 0) return alert('Enter a whole quantity and valid rate.');
const existing = cart.find((item) => item.productId === selectedProduct.id);
const alreadyAdded = existing ? existing.qty : 0;
if (quantity + alreadyAdded > availableStock(selectedProduct)) return alert(`Only ${Math.max(0, availableStock(selectedProduct) - alreadyAdded)} unit(s) are available.`);
if (existing) { existing.qty += quantity; existing.rate = rate; }
else cart.push({ productId: selectedProduct.id, name: selectedProduct.name, size: selectedProduct.size || '', qty: quantity, rate });
selectedProduct = null; $('productSearch').value = ''; $('qty').value = 1; $('rate').value = ''; renderCart();
}
function removeCartItem(index) { cart.splice(index, 1); renderCart(); }
function totalCart() { return cart.reduce((sum, item) => sum + item.qty * item.rate, 0); }
function renderCart() {
$('cartRows').innerHTML = cart.map((item, index) => `<tr><td>${escapeHtml(item.name)}<br><small>${escapeHtml(item.size)}</small></td><td>${item.qty}</td><td>${money(item.rate)}</td><td>${money(item.qty * item.rate)}</td><td><button class="btn danger tiny" onclick="removeCartItem(${index})">X</button></td></tr>`).join('') || '<tr><td colspan="5">Cart is empty</td></tr>';
$('subtotal').textContent = totalCart().toFixed(2); renderQR();
}
function upiLink() {
return `upi://pay?pa=${encodeURIComponent(config.upi)}&pn=${encodeURIComponent(config.name)}&am=${totalCart().toFixed(2)}&cu=INR&tn=${encodeURIComponent('Invoice payment')}`;
}
function renderQR() {
const show = $('payMethod').value === 'UPI' && cart.length > 0 && config.upi;
$('qrBox').classList.toggle('hidden', !show);
const qr = $('qr'); qr.innerHTML = '';
if (show && window.QRCode) new window.QRCode(qr, { text: upiLink(), width: 180, height: 180 });
else if (show) qr.textContent = 'QR library is unavailable. Run npm install, then npm run sync.';
}
function clearCart() {
cart = []; selectedProduct = null; $('chosenShop').value = ''; $('shopSearch').value = '';
$('chosenShopText').textContent = 'Walk-in customer'; $('upiRef').value = ''; $('payMethod').value = 'Cash'; renderCart();
}
async function completeSale() {
if (!cart.length) return alert('Cart is empty.');
const payment = $('payMethod').value;
const shopId = Number($('chosenShop').value) || null;
if (payment === 'Credit' && !shopId) return alert('Select a registered shop for a credit sale.');
const bill = { date: new Date().toISOString(), billNo: `DS-${Date.now().toString().slice(-8)}`, shopId, payment, upiRef: $('upiRef').value.trim(), items: structuredClone(cart), total: totalCart(), deleted: false };
try {
await db.transaction(['products', 'shops', 'sales'], (tx) => {
const products = tx.objectStore('products');
const shops = tx.objectStore('shops');
const sales = tx.objectStore('sales');
let remaining = bill.items.length;
const finish = () => {
if (--remaining !== 0) return;
if (payment === 'Credit') {
const shopRequest = shops.get(shopId);
shopRequest.onsuccess = () => {
const shop = shopRequest.result;
if (!shop) { tx.abort(); return; }
shop.credit = Number(shop.credit || 0) + bill.total;
shops.put(shop);
sales.add(bill);
};
} else sales.add(bill);
};
bill.items.forEach((item) => {
const request = products.get(item.productId);
request.onsuccess = () => {
const product = request.result;
if (!product || availableStock(product) < item.qty) { tx.abort(); return; }
product.stock = Number(product.stock || 0) - item.qty;
products.put(product); finish();
};
});
});
} catch (error) {
return alert(error.message || 'The invoice could not be saved. Stock may have changed.');
}
const shops = await db.all('shops');
printBill(bill, shops.find((shop) => shop.id === shopId));
clearCart(); await refresh();
}
function printBill(bill, shop) {
$('printArea').innerHTML = `<div style="max-width:300px"><h3>${escapeHtml(config.name)}</h3><div>${escapeHtml(config.address)}<br>${escapeHtml(config.phone)}<br>GSTIN: ${escapeHtml(config.gst)}</div><hr><b>Bill: ${escapeHtml(bill.billNo)}</b><br>${new Date(bill.date).toLocaleString()}<br>Customer: ${escapeHtml(shop?.name || 'Walk-in')}<hr>${bill.items.map((item) => `${escapeHtml(item.name)} x ${item.qty} @ ${money(item.rate)} = ${money(item.qty * item.rate)}<br>`).join('')}<hr><b>Total: ${money(bill.total)}</b><br>Payment: ${escapeHtml(bill.payment)}${bill.upiRef ? `<br>UPI Ref: ${escapeHtml(bill.upiRef)}` : ''}<hr>Thank you</div>`;
window.print();
}
function renderSales(sales, shops) {
$('saleRows').innerHTML = [...sales].reverse().map((sale) => `<tr><td>${escapeHtml(sale.billNo)}</td><td>${new Date(sale.date).toLocaleString()}</td><td>${escapeHtml(shops.find((shop) => shop.id === sale.shopId)?.name || 'Walk-in')}</td><td>${escapeHtml(sale.payment)}${sale.upiRef ? `<br><small>${escapeHtml(sale.upiRef)}</small>` : ''}</td><td>${money(sale.total)}</td><td class="${sale.deleted ? 'red' : 'green'}">${sale.deleted ? 'Deleted' : 'Active'}</td><td>${!sale.deleted && role === 'admin' ? `<button class="btn danger tiny" onclick="deleteBill(${sale.id})">Delete bill</button>` : ''}</td></tr>`).join('') || '<tr><td colspan="7">No bills</td></tr>';
}
async function deleteBill(id) {
if (!requireAdmin()) return;
const sales = await db.all('sales');
const sale = sales.find((item) => item.id === id);
if (!sale || sale.deleted) return;
if (!confirm(`Delete ${sale.billNo}? This restores inventory and reverses credit. This cannot be undone.`)) return;
try {
await db.transaction(['products', 'shops', 'sales'], (tx) => {
const products = tx.objectStore('products'); const shops = tx.objectStore('shops'); const salesStore = tx.objectStore('sales');
let remaining = sale.items.length;
const finish = () => {
if (--remaining !== 0) return;
const saveSale = () => { sale.deleted = true; sale.deletedAt = new Date().toISOString(); salesStore.put(sale); };
if (sale.payment === 'Credit' && sale.shopId) {
const shopRequest = shops.get(sale.shopId);
shopRequest.onsuccess = () => { const shop = shopRequest.result; if (shop) { shop.credit = Math.max(0, Number(shop.credit || 0) - sale.total); shops.put(shop); } saveSale(); };
} else saveSale();
};
sale.items.forEach((item) => {
const request = products.get(item.productId);
request.onsuccess = () => { const product = request.result; if (product) { product.stock = Number(product.stock || 0) + Number(item.qty); products.put(product); } finish(); };
});
});
} catch (error) { return alert(error.message || 'Bill deletion failed.'); }
await refresh();
}
async function saveExpense() {
if (!requireAdmin()) return;
const title = $('expenseTitle').value.trim(); const amount = Number($('expenseAmount').value);
if (!title || !Number.isFinite(amount) || amount <= 0) return alert('Enter a description and positive amount.');
await db.put('expenses', { title, amount, date: new Date().toISOString() });
$('expenseTitle').value = ''; $('expenseAmount').value = ''; await refresh();
}
function renderExpenses(expenses) {
$('expenseRows').innerHTML = expenses.map((expense) => `<tr><td>${new Date(expense.date).toLocaleString()}</td><td>${escapeHtml(expense.title)}</td><td>${money(expense.amount)}</td><td>${role === 'admin' ? `<button class="btn danger tiny" onclick="deleteExpense(${expense.id})">Delete</button>` : ''}</td></tr>`).join('') || '<tr><td colspan="4">No expenses</td></tr>';
}
async function deleteExpense(id) { if (requireAdmin() && confirm('Delete this expense?')) { await db.delete('expenses', id); await refresh(); } }
function saveConfig() {
if (!requireAdmin()) return;
const pin = $('cPin').value.trim();
if (pin.length < 4) return alert('Use an Admin PIN of at least 4 characters.');
config = { name: $('cName').value.trim() || DEFAULT_CONFIG.name, address: $('cAddress').value.trim(), phone: $('cPhone').value.trim(), gst: $('cGst').value.trim(), upi: $('cUpi').value.trim(), pin };
localStorage.setItem('ds_config', JSON.stringify(config)); loadConfig(); refresh().catch(showError); alert('Configuration saved.');
}
function loadConfig() { Object.entries({ cName: 'name', cAddress: 'address', cPhone: 'phone', cGst: 'gst', cUpi: 'upi', cPin: 'pin' }).forEach(([id, key]) => { $(id).value = config[key] || ''; }); }
window.addEventListener('DOMContentLoaded', async () => {
try { await db.init(); loadConfig(); renderCart(); await refresh(); }
catch (error) { showError(error); }
if ('serviceWorker' in navigator) navigator.serviceWorker.register('./sw.js').catch((error) => console.warn('Service worker registration failed', error));
});
cd "/tmp/workspace/ds-traders-v2" && npm install --no-audit --no-fund && cp "node_modules/qrcodejs/qrcode.min.js" "src/qrcode.min.js" && node --check "src/app.js" && npx cap --version
window.addEventListener("DOMContentLoaded", asyn, so the current file is incomplete and cannot be built or tested as provided.manifest.json, app icons, and preferably a service worker are needed for an installable offline app.DS Traders V2 PWA
Archive · ZIP
upi://pay payment QR when UPI is selected1234.src folder:cd src
python -m http.server 8080
http://localhost:8080 in Chrome/Edge and use Install App. The full implementation notes are in README
MD File · MD
.apk in this workspace because the Android build tools are not installed here: Java, Gradle, Android SDK, and Android Studio are missing. An APK cannot be safely or truthfully renamed from a ZIP or HTML file.DS-Traders.apk
An error occurred (ValidationException) when calling the InvokeCodeInterpreter operation: Code interpreter session 01M22187WB9F7KRSDYHCHNXE11 is not active
We encountered an error while processing your request. Please try again or reach out to us for help.
An error occurred (ValidationException) when calling the InvokeCodeInterpreter operation: Code interpreter session 01M22187WB9F7KRSDYHCHNXE11 is not active
We encountered an error while processing your request. Please try again or reach out to us for help.
I put the above zip file in vs code to make app for that give me a final proper bug free code
~/
Capacitor official Android docs add android sync build APK 2026
~/ds-traders-v2
~/ds-traders-v2/src
python - <<'PY' from pathlib import Path root=Path('/tmp/workspace/ds-traders-v2') (root/'package.json').write_text('''{ "name": "ds-traders-v2", "version": "1.0.0", "private": true, "description": "Offline DS Traders POS Android app", "scripts": { "build": "node scripts/build.mjs", "android:add": "npm run build && npx cap add android", "sync": "npm run build && npx cap sync android", "android": "npx cap open android", "run:android": "npm run sync && npx cap run android", "apk:debug": "npm run sync && cd android && gradlew.bat assembleDebug" }, "dependencies": { "@capacitor/android": "^8.0.0", "@capacitor/core": "^8.0.0", "qrcodejs": "^1.0.0" }, "devDependencies": { "@capacitor/cli": "^8.0.0" } } ''') (root/'capacitor.config.ts').write_text('''import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'com.dstraders.pos', appName: 'DS Traders', webDir: 'src', bundledWebRuntime: false, android: { allowMixedContent: false } }; export default config; ''') (root/'.gitignore').write_text('''node_modules/\nandroid/.gradle/\nandroid/build/\nandroid/app/build/\n*.jks\n*.keystore\n.DS_Store\n''') (root/'scripts').mkdir(exist_ok=True) (root/'scripts'/'build.mjs').write_text('''import { copyFile, mkdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; const source = 'node_modules/qrcodejs/qrcode.min.js'; const target = 'src/qrcode.min.js'; if (!existsSync(source)) throw new Error('Missing qrcodejs. Run npm install first.'); await mkdir('src', { recursive: true }); await copyFile(source, target); console.log('Web assets are ready.'); ''') html=root/'src'/'index.html' s=html.read_text() s=s.replace('<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>', '<script src="qrcode.min.js"></script>') html.write_text(s) (root/'README-APK.md').write_text('''# DS Traders Android APK - VS Code instructions ## 1. Requirements Install Node.js LTS and Android Studio. In Android Studio install **Android SDK**, **Android SDK Platform-Tools**, and an Android SDK platform. Open this project root, not the `src` folder, in VS Code. ## 2. Create the Android project Run these commands in VS Code Terminal: ```bash npm install npm run android:add npm run sync npm run android ``` The last command opens Android Studio. Connect an Android phone with USB debugging enabled, or select an emulator, then click **Run**. ## 3. Create installable APK For a test APK in Android Studio use **Build > Build APK(s)**. The file is normally: ```text android/app/build/outputs/apk/debug/app-debug.apk ``` For an APK you can distribute, use **Build > Generate Signed Bundle / APK > APK**, select **release**, and create or select a keystore. Keep the keystore and password safe: you need the same keystore for app updates. ## Important - Run `npm run sync` every time you change files inside `src` before building again. - Change the default Admin PIN (`1234`) in Settings before real use. - The application is offline-first and stores records only on the device. Clearing Android app data or uninstalling deletes data. - The QR starts a UPI payment, but an offline app cannot confirm whether money was received. Verify payment in the UPI app and enter its reference number. ''') PY node --check "/tmp/workspace/ds-traders-v2/src/app.js" python -m json.tool "/tmp/workspace/ds-traders-v2/package.json" >/dev/null printf 'project-config-valid\n'
cd "/tmp/workspace/ds-traders-v2" && npm install --no-audit --no-fund && npm run build && node --check "src/app.js" && test -s "src/qrcode.min.js" && npx cap --version
cd "/tmp/workspace/ds-traders-v2" && rm -f "capacitor.config.ts" && printf '%s\n' '{' ' "appId": "com.dstraders.pos",' ' "appName": "DS Traders",' ' "webDir": "src",' ' "bundledWebRuntime": false,' ' "android": { "allowMixedContent": false }' '}' > "capacitor.config.json" && node --check "src/app.js" && npx cap --version && npx cap add android && npx cap sync android
~/ds-traders-v2
~/ds-traders-v2/src/app.js
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/src/app.js'
~/ds-traders-v2/src/app.js
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/src/app.js'
~/ds-traders-v2/src/app.js
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/src/app.js'
~/ds-traders-v2/src/app.js
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/src/app.js'
~/ds-traders-v2/src/app.js
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/src/app.js'
cd "/tmp/workspace/ds-traders-v2" && node --check "src/app.js" && npm run build && npx cap sync android && test -s "src/qrcode.min.js" && test -d "android" && printf 'android-project-verified\n'
~/ds-traders-v2/README-APK.md
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/ds-traders-v2/README-APK.md'
cd "/tmp/workspace/ds-traders-v2" && node --check "src/app.js" && npm run build && npx cap sync android && test -f "android/app/build.gradle" && test -f "android/app/src/main/assets/public/index.html" && printf 'FINAL-VALIDATION-PASSED\n'
ds traders v2
File ·
src/index.html - application interfacesrc/app.js - POS, stock, damaged stock, UPI QR, reports and bill deletion logicpackage.json - Android/Capacitor commandscapacitor.config.json - app package configurationandroid/ - generated native Android projectREADME-APK.md - exact APK build instructionsnpm install
npm run sync
npm run android
android/app/build/outputs/apk/debug/app-debug.apk
sync to copy your app files into the Android project, consistent with the official Capacitor workflow.File is still being generated
ds traders v2
File ·
ds-traders-v2 folder and run:npm install
npm run sync
npm run android
android/app/build/outputs/apk/debug/app-debug.apk
README APK
MD File · MD