Enter your secret recovery code to reset the staff password. This is set by the owner in Settings.
Enter the owner password to continue.
Loading today's stopsβ¦
Loadingβ¦
Loadingβ¦
| Employee |
|---|
Keyword rankings, local visibility and what to fix next. Opens in the same session.
Open SEO Command Center βCustomers who uploaded an ID or confirmed a school email that needs a look. Photos are deleted as soon as you approve or reject.
Which verified groups save, on which days, on washers or dryers. Machines always get a full cycle β only the price changes.
Up to 20 lbs/week wash & fold
Up to 40 lbs/week wash & fold
Unlimited lbs/week wash & fold
Don't add these to the boxes above β those are the cash you counted. Card and app money goes to the sheet in its own columns.
Take a photo of the day's revenue note β the fields above fill in automatically. Review the numbers, then hit Save & Sync.
π¬ You can also just send a photo to the Loads of Luv Telegram bot β it reads it and adds it to the sheet automatically.
Save data to see today's summary.
| When | Machine | Amount | Source | Status | Action |
|---|---|---|---|---|---|
| Loading⦠| |||||
| Machine | Coins Today | $ Today | All-Time Coins | All-Time $ |
|---|---|---|---|---|
| Loading⦠| ||||
Pick a machine to see or update its model, serial, manual, and parts photos β saved once, used everywhere else in Repairs.
Loadingβ¦
Loadingβ¦
Step-by-step guides for running the laundromat β opening and closing, machines, cleaning, Wash & Fold, customers, policies, supplies, HR and sales. Included for every plan and every staff role.
Toggle each module on or off. Enabled modules appear as tabs in the navigation bar above.
Get a notification on this device when a new order comes in. Each device must be enabled separately β tap the button on every phone, tablet, or POS that should receive alerts.
Alerts are registered per device, not per account β enabling here turns on notifications for this phone or tablet only, and they keep working no matter who signs in on it afterwards. Turn it on once on each device that should buzz.
Machine starts from before per-customer tracking began only live in Stripe and the credit ledger. This copies them into the history so visits, new customers and lifetime value cover everything β and links old card payments to accounts where the customer typed their phone or email at checkout. Safe to run again; nothing is overwritten or charged.
Ends every staff and owner login everywhere β phones, the counter computer and iOS Shortcut tokens. Everyone signs back in with their password or PIN. Use it if a phone is lost or you think a login was shared.
Set a private recovery code only you know. Use it on the login screen if you ever forget your password.
Enter your Google Apps Script web app URL to automatically sync data to Google Sheets. Editing this link requires the owner password.
β οΈ These three need the updated Apps Script below β if you set this up before today, re-paste Step 3's code into your existing script and make a new deployment version (your Revenue tab and URL stay exactly the same).
function doPost(e) {
try {
var data = JSON.parse(e.postData.contents);
var type = data.type || 'revenue';
if (type === 'order') return writeOrderRow_(data);
if (type === 'repair') return writeRepairRow_(data);
if (type === 'clock') return writeClockRow_(data);
return writeRevenueRow_(data);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({status:'error', message:err.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}
// Unchanged from before - still writes to whichever sheet tab is active, so
// existing Revenue history keeps landing in the same place it always has.
function writeRevenueRow_(data) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var HEADERS = ['Date',
'Washers ($)','Washer Qtrs','Dryers ($)','Dryer Qtrs','Quarter Total',
'Vending','Wash & Fold','Machine Total',
'ATM Had','ATM Added','ATM Total',
'$20 Bills','$10 Bills','$5 Bills','$1 Bills','Bill Total',
'Last Updated'];
var row = [
data.date,
data.washers || 0,
data.qtrWashers || 0,
data.dryers || 0,
data.qtrDryers || 0,
data.quarterTotal || 0,
data.vending || 0,
data.washfold || 0,
data.machineTotal || 0,
data.atmHad || 0,
data.atmAdded || 0,
data.atmTotal || 0,
data.bills20 || 0,
data.bills10 || 0,
data.bills5 || 0,
data.bills1 || 0,
data.billTotal || 0,
data.savedAt || new Date().toISOString()
];
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
var hRange = sheet.getRange(1, 1, 1, HEADERS.length);
hRange.setFontWeight('bold');
hRange.setBackground('#7c3aed');
hRange.setFontColor('#ffffff');
}
// Column A may contain either a Date object (Sheets auto-parsed it) or a
// string, so normalize both sides to YYYY-MM-DD before comparing.
function normalizeDate_(v) {
if (v instanceof Date) {
var tz = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone() || 'America/New_York';
return Utilities.formatDate(v, tz, 'yyyy-MM-dd');
}
return String(v || '').slice(0, 10);
}
var targetDate = normalizeDate_(data.date);
var lastRow = sheet.getLastRow();
var existingRow = -1;
if (lastRow > 1) {
var dates = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (var i = 0; i < dates.length; i++) {
if (normalizeDate_(dates[i][0]) === targetDate) { existingRow = i + 2; break; }
}
}
if (existingRow > 0) {
sheet.getRange(existingRow, 1, 1, row.length).setValues([row]);
} else {
sheet.appendRow(row);
var newRow = sheet.getLastRow();
sheet.getRange(newRow, 1).setNumberFormat('@');
}
sheet.autoResizeColumns(1, HEADERS.length);
return ContentService
.createTextOutput(JSON.stringify({status:'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
// New - one row per order, upserted by Order ID, in its own "Orders" tab
// (auto-created the first time it's needed so it never touches Revenue).
function writeOrderRow_(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Orders') || ss.insertSheet('Orders');
var HEADERS = ['Order ID','Drop-off Date','Pickup Date','Customer','Phone',
'Service Type','Wash Style','Weight (lbs)','Bags',
'Comforters ($)','Total ($)','Amount Paid ($)','Status','Last Updated'];
var row = [
data.orderId || '', data.date || '', data.pickupDate || '',
data.customer || '', data.phone || '', data.serviceType || '', data.washStyle || '',
data.weight || 0, data.bags || '', data.comfortersTotal || 0,
data.total || 0, data.amountPaid || 0, data.status || '',
data.savedAt || new Date().toISOString()
];
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
var hRange = sheet.getRange(1, 1, 1, HEADERS.length);
hRange.setFontWeight('bold');
hRange.setBackground('#7c3aed');
hRange.setFontColor('#ffffff');
}
var lastRow = sheet.getLastRow();
var existingRow = -1;
if (lastRow > 1) {
var ids = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (var i = 0; i < ids.length; i++) {
if (String(ids[i][0]) === String(data.orderId)) { existingRow = i + 2; break; }
}
}
if (existingRow > 0) sheet.getRange(existingRow, 1, 1, row.length).setValues([row]);
else sheet.appendRow(row);
sheet.autoResizeColumns(1, HEADERS.length);
return ContentService
.createTextOutput(JSON.stringify({status:'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
// New - one row per logged issue, appended (not upserted - it's a log) to
// its own "Repairs" tab, auto-created the first time it's needed.
function writeRepairRow_(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Repairs') || ss.insertSheet('Repairs');
var HEADERS = ['Machine','Type','Issue','Status','Reported By','Logged At'];
var row = [
data.machine || '', data.machineType || '', data.issue || '',
data.status || '', data.reportedBy || '', data.savedAt || new Date().toISOString()
];
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
var hRange = sheet.getRange(1, 1, 1, HEADERS.length);
hRange.setFontWeight('bold');
hRange.setBackground('#7c3aed');
hRange.setFontColor('#ffffff');
}
sheet.appendRow(row);
sheet.autoResizeColumns(1, HEADERS.length);
return ContentService
.createTextOutput(JSON.stringify({status:'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
// New - one row per clock event (clock in/out, break/lunch start+end),
// appended (not upserted - it's a log) to its own "Clock" tab, auto-created
// the first time it's needed.
function writeClockRow_(data) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Clock') || ss.insertSheet('Clock');
var HEADERS = ['Employee','Event','Time','Duration (min)','Break (min)','Logged At'];
var row = [
data.employee || '', data.event || '', data.time || '',
data.durationMinutes || '', data.breakMinutes || '', data.savedAt || new Date().toISOString()
];
if (sheet.getLastRow() === 0) {
sheet.appendRow(HEADERS);
var hRange = sheet.getRange(1, 1, 1, HEADERS.length);
hRange.setFontWeight('bold');
hRange.setBackground('#7c3aed');
hRange.setFontColor('#ffffff');
}
sheet.appendRow(row);
sheet.autoResizeColumns(1, HEADERS.length);
return ContentService
.createTextOutput(JSON.stringify({status:'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
Step 4. Click Deploy β New Deployment (or, if you're updating an existing script, Deploy β Manage deployments β βοΈ Edit β New version).Export all orders to CSV, or clear all stored data.
How did the customer pay?
Share this link with your customer to collect payment via Stripe.
Edit anything before sending. Replace the [bracketed] blanks — you'll be warned if any are left. [payment link] generates automatically as soon as you pick a template that needs it.
Search to select existing, or type a new name below.
This will remove all items and the current customer.