Enter your PIN to access the POS
Elle Nail Bar
Client
Services
Discount
Payment
Tip
Notes
Receipt
Add services to begin

Add services to see receipt.

Today's Transactions

No transactions yet.

Cash Reconciliation

💵 Cash Counter

Count the bills in the till

Total Counted$0.00

⚙️ Till Settings

📊 From Today's Sales

$0.00
$0.00
$0.00

🧮 End-of-Day Balance

$150.00
$0.00
$0.00
$0.00
$0.00
$0.00
Expected in Till$150.00
$0.00
$150.00

Card / Digital Summary
Debit sales+HST$0.00
Debit tips$0.00
Credit sales+HST$0.00
Credit surcharge$0.00
Credit tips$0.00
Total Clover (calc)$0.00
$

💵 Cash in Envelope

Amount to pull from till after tip payout and closing float

$0.00
$150.00
$0.00
Put in Envelope
$0.00
💸 End-of-Day Tip Sheet

Today's Tip Totals

Cash Tips Collected$0.00
Card Tips Collected$0.00
Total Tips$0.00
Transactions0

Transaction Breakdown

Service Menu Editor

Edit names, prices and categories. Changes persist across sessions.

#Service NamePrice ($)Category
⚙️ Settings

Configure the Google Sheets client database connection.

Google Sheets — Client Database

The token is your private key — paste the same value into the Apps Script below. Requests without it are rejected.

📋 Setup Instructions
Step 1 — Log into your corporate Gmail → go to sheets.google.com → create a new sheet called Elle Nail Bar POS
Step 2 — Click Extensions → Apps Script
Step 3 — Delete any existing code, paste the full script below, click Save (💾)
Step 4 — In the script, replace YOUR_SECRET_TOKEN_HERE with your token
Step 5 — Click the Run button and select setupSheets — creates your 3 tabs
Step 6 — Import your Fresha CSV into the Clients tab
Step 7 — Click Deploy → New Deployment → Web App  •  Execute as: Me  •  Who has access: Anyone
Step 8 — Copy the Web App URL → paste above → Test Connection

After setup: Every transaction auto-saves to Transactions tab and Payroll tab recalculates instantly per pay period.
Apps Script — paste this into your Google Sheet's script editor
// ============================================================
//  ELLE NAIL BAR — Google Apps Script
//  Handles: Client lookup, Transaction saving, Payroll sheet
//
//  SETUP:
//  1. Paste this entire file into your Google Apps Script editor
//  2. Change SECRET_TOKEN below to match what you set in POS Settings
//  3. Deploy as Web App: Execute as Me, Anyone can access
//  4. Copy the Web App URL into POS Settings
// ============================================================

// ── CHANGE THIS TO MATCH YOUR POS SETTINGS TOKEN ─────────────
var SECRET_TOKEN = 'YOUR_SECRET_TOKEN_HERE';

// ── SHEET NAMES ───────────────────────────────────────────────
var SHEET_CLIENTS      = 'Clients';
var SHEET_TRANSACTIONS = 'Transactions';
var SHEET_PAYROLL      = 'Payroll';

// ── STAFF CONFIG ─────────────────────────────────────────────
// Format: 'Name': { commission: 0.XX, guarantee: XX (per day, 0 = none) }
var STAFF_CONFIG = {
  'Cherry':      { commission: 0.60, guarantee: 250 },
  'Quynh (Amy)': { commission: 0.55, guarantee: 100 },
  'Irene':       { commission: 0.60, guarantee: 200 },
  'Van (Vina)':  { commission: 0.55, guarantee: 100 },
  'Chloe':       { commission: 0.55, guarantee: 100 },
  'CECE':        { commission: 0.60, guarantee: 200 },
  'Kate (Chi)':  { commission: 0.50, guarantee: 50  },
  'Zen':         { commission: 0.50, guarantee: 100 },
};

var CREDIT_TIP_DEDUCTION = 0.16; // 16% deducted from credit tips before payout


// ============================================================
//  MAIN ROUTER
// ============================================================
function doGet(e) {
  if (!e.parameter.token || e.parameter.token !== SECRET_TOKEN) {
    return json({ ok: false, error: 'Unauthorized' });
  }
  var action = e.parameter.action || 'search';
  if (action === 'search') return handleSearch(e);
  return json({ ok: false, error: 'Unknown action' });
}

function doPost(e) {
  var params = JSON.parse(e.postData.contents);
  if (!params.token || params.token !== SECRET_TOKEN) {
    return json({ ok: false, error: 'Unauthorized' });
  }
  var action = getParam(e, 'action') || params.action || 'saveTx';
  if (action === 'saveTx')   return handleSaveTx(params);
  if (action === 'deleteTx') return handleDeleteTx(params);
  return json({ ok: false, error: 'Unknown action' });
}


// ============================================================
//  CLIENT SEARCH
// ============================================================
function handleSearch(e) {
  var ss     = SpreadsheetApp.getActiveSpreadsheet();
  var sheet  = ss.getSheetByName(SHEET_CLIENTS);
  if (!sheet) return json({ ok: false, error: 'Clients sheet not found. Make sure you have a tab named "Clients".' });

  var data    = sheet.getDataRange().getValues();
  var headers = data[0].map(function(h){ return h.toString().trim(); });

  var firstIdx  = findCol(headers, /first.?name/i);
  var lastIdx   = findCol(headers, /last.?name/i);
  var phoneIdx  = findCol(headers, /mobile|phone/i);
  var emailIdx  = findCol(headers, /email/i);

  var q = (e.parameter.q || '').toLowerCase().trim();

  var results = data.slice(1).map(function(row) {
    return {
      firstName: firstIdx >= 0 ? row[firstIdx].toString().trim() : '',
      lastName:  lastIdx  >= 0 ? row[lastIdx].toString().trim()  : '',
      phone:     phoneIdx >= 0 ? row[phoneIdx].toString().trim() : '',
      email:     emailIdx >= 0 ? row[emailIdx].toString().trim() : ''
    };
  })
  .filter(function(c){ return c.firstName || c.lastName; })
  .filter(function(c){
    if (!q) return true;
    var full      = (c.firstName + ' ' + c.lastName).toLowerCase();
    var phoneClean = c.phone.replace(/\D/g, '');
    var qClean     = q.replace(/\D/g, '');
    return full.includes(q) ||
           (qClean.length > 3 && phoneClean.includes(qClean)) ||
           c.email.toLowerCase().includes(q);
  })
  .slice(0, 20);

  return json({ ok: true, results: results });
}


// ============================================================
//  SAVE TRANSACTION
// ============================================================
function handleSaveTx(params) {
  var ss    = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);

  // Create Transactions sheet if it doesn't exist
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_TRANSACTIONS);
    var headers = [
      'Transaction ID','Date','Time','Client','Service','Tech',
      'Service Price ($)','Discount ($)','Discount Type',
      'Cash Discount ($)','HST ($)','Transaction Total ($)',
      'Payment Method','Total Tip ($)','Tip Type',
      'Tech Tip ($)','Tech Tip Type','Notes'
    ];
    sheet.appendRow(headers);
    formatHeaderRow(sheet, headers.length);
  }

  var rows = params.rows || [];
  rows.forEach(function(r) {
    sheet.appendRow([
      r.txId        || '',
      r.date        || '',
      r.time        || '',
      r.client      || '',
      r.service     || '',
      r.tech        || '',
      r.servicePrice|| 0,
      r.discountAmt || 0,
      r.discountType|| '',
      r.cashDisc    || 0,
      r.hst         || 0,
      r.total       || 0,
      r.payment     || '',
      r.totalTip    || 0,
      r.tipType     || '',
      r.techTip     || 0,
      r.techTipType || '',
      r.notes       || ''
    ]);
  });

  // Rebuild payroll after every save
  buildPayrollSheet(ss);

  return json({ ok: true, rowsSaved: rows.length });
}


// ============================================================
//  DELETE TRANSACTION
// ============================================================
function handleDeleteTx(params) {
  var txId = String(params.txId || '');
  if (!txId) return json({ ok: false, error: 'No txId provided' });

  var ss    = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
  if (!sheet) return json({ ok: false, error: 'Transactions sheet not found' });

  var data       = sheet.getDataRange().getValues();
  var txIdColIdx = data[0].findIndex(function(h){ return h.toString().trim() === 'Transaction ID'; });
  if (txIdColIdx < 0) return json({ ok: false, error: 'Transaction ID column not found' });

  // Collect row numbers to delete (reverse order so indices don't shift)
  var rowsToDelete = [];
  data.forEach(function(row, i) {
    if (i === 0) return; // skip header
    if (String(row[txIdColIdx]) === txId) rowsToDelete.push(i + 1); // +1 for 1-based sheet index
  });

  if (rowsToDelete.length === 0) return json({ ok: false, error: 'Transaction not found in sheet' });

  // Delete rows in reverse order so row numbers stay valid
  rowsToDelete.reverse().forEach(function(rowNum) {
    sheet.deleteRow(rowNum);
  });

  // Rebuild payroll after deletion
  buildPayrollSheet(ss);

  return json({ ok: true, rowsDeleted: rowsToDelete.length });
}


// ============================================================
//  PAYROLL SHEET BUILDER
// ============================================================
function buildPayrollSheet(ss) {
  var txSheet = ss.getSheetByName(SHEET_TRANSACTIONS);
  if (!txSheet) return;

  var data = txSheet.getDataRange().getValues();
  if (data.length < 2) return;

  var headers = data[0];
  var col = {};
  headers.forEach(function(h, i){ col[h.toString().trim()] = i; });

  // Group rows by period (1-15 or 16-end) and tech
  var periods = {};

  data.slice(1).forEach(function(row) {
    var dateStr = row[col['Date']] ? row[col['Date']].toString() : '';
    if (!dateStr) return;
    var date    = new Date(dateStr);
    var year    = date.getFullYear();
    var month   = date.getMonth() + 1;
    var day     = date.getDate();
    var half    = day <= 15 ? 1 : 2;
    var monthStr= year + '-' + pad(month);
    var periodKey = monthStr + (half === 1 ? ' (1-15)' : ' (16-end)');

    var tech       = (row[col['Tech']] || '').toString().trim();
    var svcPrice   = parseFloat(row[col['Service Price ($)']])  || 0;
    var techTip    = parseFloat(row[col['Tech Tip ($)']])        || 0;
    var techTipType= (row[col['Tech Tip Type']] || '').toString().trim().toLowerCase();

    if (!tech) return;

    if (!periods[periodKey]) periods[periodKey] = {};
    if (!periods[periodKey][tech]) {
      periods[periodKey][tech] = {
        days: {},
        totalSales: 0,
        cashTips: 0,
        creditTips: 0
      };
    }

    var entry = periods[periodKey][tech];
    entry.totalSales += svcPrice;
    if (techTipType === 'cash')  entry.cashTips   += techTip;
    if (techTipType === 'card')  entry.creditTips  += techTip;

    // Track days worked (unique dates with sales)
    if (svcPrice > 0 && dateStr) entry.days[dateStr] = true;
  });

  // Write payroll sheet
  var pSheet = ss.getSheetByName(SHEET_PAYROLL);
  if (pSheet) ss.deleteSheet(pSheet);
  pSheet = ss.insertSheet(SHEET_PAYROLL);

  var allRows = [];
  var periodKeys = Object.keys(periods).sort();

  periodKeys.forEach(function(period) {
    // Period header
    allRows.push([period, '', '', '', '', '', '', '', '', '', '', '', '']);
    allRows.push([
      'Staff','Commission %','Daily Guarantee',
      'Days Worked','Total Sales ($)','Commission Pay ($)',
      'Guaranteed Pay ($)','Pre-tip Pay ($)',
      'Cash Tips ($)','Credit Tips (gross $)',
      'Credit Tips (after 16%) ($)','Total Cash Payout ($)',
      'Notes'
    ]);

    var techs = Object.keys(periods[period]).sort();
    techs.forEach(function(tech) {
      var d      = periods[period][tech];
      var cfg    = STAFF_CONFIG[tech] || { commission: 0.55, guarantee: 0 };
      var daysWorked    = Object.keys(d.days).length;
      var commissionPay = +(d.totalSales * cfg.commission).toFixed(2);
      var guaranteedPay = cfg.guarantee > 0 ? +(cfg.guarantee * daysWorked).toFixed(2) : 0;
      var preTipPay     = Math.max(commissionPay, guaranteedPay);
      var creditTipsNet = +(d.creditTips * (1 - CREDIT_TIP_DEDUCTION)).toFixed(2);
      var totalCash     = +(preTipPay + d.cashTips + creditTipsNet).toFixed(2);

      allRows.push([
        tech,
        (cfg.commission * 100).toFixed(0) + '%',
        cfg.guarantee > 0 ? '$' + cfg.guarantee + '/day' : 'None',
        daysWorked,
        d.totalSales.toFixed(2),
        commissionPay.toFixed(2),
        guaranteedPay > 0 ? guaranteedPay.toFixed(2) : 'N/A',
        preTipPay.toFixed(2),
        d.cashTips.toFixed(2),
        d.creditTips.toFixed(2),
        creditTipsNet.toFixed(2),
        totalCash.toFixed(2),
        commissionPay >= guaranteedPay ? 'Commission' : 'Guaranteed'
      ]);
    });

    // Period totals
    var totals = { sales:0, commPay:0, guarPay:0, preTip:0, cashTips:0, creditGross:0, creditNet:0, totalCash:0 };
    techs.forEach(function(tech){
      var d   = periods[period][tech];
      var cfg = STAFF_CONFIG[tech] || { commission:0.55, guarantee:0 };
      var dw  = Object.keys(d.days).length;
      var cp  = d.totalSales * cfg.commission;
      var gp  = cfg.guarantee > 0 ? cfg.guarantee * dw : 0;
      var ptp = Math.max(cp, gp);
      var ctn = d.creditTips * (1 - CREDIT_TIP_DEDUCTION);
      totals.sales       += d.totalSales;
      totals.commPay     += cp;
      totals.guarPay     += gp;
      totals.preTip      += ptp;
      totals.cashTips    += d.cashTips;
      totals.creditGross += d.creditTips;
      totals.creditNet   += ctn;
      totals.totalCash   += ptp + d.cashTips + ctn;
    });
    allRows.push([
      'TOTAL','','','',
      totals.sales.toFixed(2),
      totals.commPay.toFixed(2),
      totals.guarPay.toFixed(2),
      totals.preTip.toFixed(2),
      totals.cashTips.toFixed(2),
      totals.creditGross.toFixed(2),
      totals.creditNet.toFixed(2),
      totals.totalCash.toFixed(2),
      ''
    ]);

    allRows.push(['', '', '', '', '', '', '', '', '', '', '', '', '']); // spacer
  });

  if (allRows.length === 0) {
    pSheet.appendRow(['No transaction data yet.']);
    return;
  }

  // Write all rows at once (faster than one at a time)
  pSheet.getRange(1, 1, allRows.length, 13).setValues(allRows);

  // Format the sheet
  pSheet.setColumnWidths(1, 13, 130);
  pSheet.setColumnWidth(1, 160);

  // Style period headers and column headers
  var rowNum = 1;
  periodKeys.forEach(function(period) {
    // Period header row
    pSheet.getRange(rowNum, 1, 1, 13)
      .setBackground('#2C1810')
      .setFontColor('#FAF7F2')
      .setFontWeight('bold')
      .setFontSize(11);
    rowNum++;

    // Column header row
    pSheet.getRange(rowNum, 1, 1, 13)
      .setBackground('#C9786A')
      .setFontColor('#FFFFFF')
      .setFontWeight('bold')
      .setFontSize(10);
    rowNum++;

    var techCount = Object.keys(periods[period]).length;
    // Alternate row shading for staff rows
    for (var i = 0; i < techCount; i++) {
      var bg = i % 2 === 0 ? '#FFF9F4' : '#FAF7F2';
      pSheet.getRange(rowNum, 1, 1, 13).setBackground(bg);
      rowNum++;
    }

    // Totals row
    pSheet.getRange(rowNum, 1, 1, 13)
      .setBackground('#8B3D31')
      .setFontColor('#FFFFFF')
      .setFontWeight('bold');
    rowNum++;
    rowNum++; // spacer
  });

  // Freeze top row equivalent - freeze column headers of first period
  pSheet.setFrozenRows(2);
  SpreadsheetApp.flush();
}


// ============================================================
//  UTILITY FUNCTIONS
// ============================================================
function json(obj) {
  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

function findCol(headers, pattern) {
  return headers.findIndex(function(h){ return pattern.test(h); });
}

function getParam(e, key) {
  return e && e.parameter ? e.parameter[key] : null;
}

function pad(n) {
  return n < 10 ? '0' + n : '' + n;
}

function formatHeaderRow(sheet, numCols) {
  var cols = numCols || 18;
  sheet.getRange(1, 1, 1, cols)
    .setBackground('#2C1810')
    .setFontColor('#FAF7F2')
    .setFontWeight('bold')
    .setFontSize(10);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, cols, 120);
  sheet.setColumnWidth(1, 160); // Transaction ID
  sheet.setColumnWidth(4, 150); // Client
  sheet.setColumnWidth(5, 200); // Service
  sheet.setColumnWidth(6, 130); // Tech
}


// ============================================================
//  MANUAL TRIGGER — run this once to set up sheets
// ============================================================
function setupSheets() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  // Clients sheet
  if (!ss.getSheetByName(SHEET_CLIENTS)) {
    var cs = ss.insertSheet(SHEET_CLIENTS);
    cs.appendRow(['First Name','Last Name','Full Name','Blocked','Mobile Number','Email']);
    cs.getRange(1,1,1,6).setBackground('#2C1810').setFontColor('#FAF7F2').setFontWeight('bold');
    cs.setFrozenRows(1);
  }

  // Transactions sheet
  if (!ss.getSheetByName(SHEET_TRANSACTIONS)) {
    var ts = ss.insertSheet(SHEET_TRANSACTIONS);
    var txHeaders = [
      'Transaction ID','Date','Time','Client','Service','Tech',
      'Service Price ($)','Discount ($)','Discount Type',
      'Cash Discount ($)','HST ($)','Transaction Total ($)',
      'Payment Method','Total Tip ($)','Tip Type',
      'Tech Tip ($)','Tech Tip Type','Notes'
    ];
    ts.appendRow(txHeaders);
    formatHeaderRow(ts, txHeaders.length);
  }

  // Payroll sheet placeholder
  if (!ss.getSheetByName(SHEET_PAYROLL)) {
    var ps = ss.insertSheet(SHEET_PAYROLL);
    ps.appendRow(['Payroll data will appear here automatically after transactions are saved.']);
  }

  SpreadsheetApp.getUi().alert('✓ Elle Nail Bar sheets are set up and ready!');
}
Staff PIN

The 4-digit PIN staff enter to open the POS. Default is 1234 — change it now.

Clear All Data

Wipe all transactions from this device. Use this to remove demo/test data before going live. This cannot be undone.

Requires current admin password to confirm.