Import from ERS
Pull your whole catalog, customer list and order history out of Event Rental Systems. Runs inside your own logged-in session — no password, no API key, nothing leaves your browser except the data itself.
Safe to run more than once. Records are matched on item name, customer email/phone and ERS order id, so a second run updates what changed instead of creating duplicates.
How to run it
About two minutes
- Open your ERS control panel and sign in as an administrator.
- Press F12 to open developer tools, then click the Console tab.
- Copy the script on the right and paste it into the console.
- Press Enter and leave the tab open — 1,800 customers takes a minute or two.
What it brings across
Itemsname, price, stock, category, packages, add-ons, surfaces
Categoriesincluding parent nesting
Customersdeduplicated on email then phone
Ordersline items, adjustments, payments, driver, balance due
The script
Paste this into the ERS console
/* ===========================================================================
* Marquee — ERS extractor
* ---------------------------------------------------------------------------
* Pulls your catalog, customers and order history out of Event Rental Systems.
*
* HOW TO RUN
* 1. Log in to your ERS control panel (…ourers.com/cp/) as an administrator.
* 2. Open DevTools -> Console.
* 3. Paste this whole file and press Enter.
*
* It runs entirely inside your own logged-in session. No password, no API key
* and no cookie ever leaves your browser. When it finishes it will either POST
* the data straight to your Marquee instance, or — if that is not reachable —
* save a marquee-ers-export.json file to your Downloads folder for you to
* upload manually.
* =========================================================================== */
(async () => {
const TARGET = window.MARQUEE_URL || 'http://localhost:3000';
const log = (...a) => console.log('%c[marquee]', 'color:#2a78d6;font-weight:bold', ...a);
/* ---- helpers ---------------------------------------------------------- */
const parseDoc = (html) => new DOMParser().parseFromString(html, 'text/html');
const biggestTable = (doc) =>
[...doc.querySelectorAll('table')].sort((a, b) => b.rows.length - a.rows.length)[0];
// Keep newlines: ERS packs multi-line values (order contents, payments)
// into a single cell as separate child nodes.
const cellText = (td) =>
td.innerText.replace(/ /g, ' ').split('\n').map((s) => s.trim()).filter(Boolean).join('\n');
async function grid(path) {
const res = await fetch(path, { credentials: 'include' });
if (!res.ok) throw new Error(`${path} -> ${res.status}`);
const doc = parseDoc(await res.text());
const table = biggestTable(doc);
if (!table) return { headers: [], rows: [], totalRecords: 0 };
const headers = [...table.rows[0].cells].map((c) => c.innerText.trim().replace(/\s+/g, ' '));
const rows = [...table.rows].slice(1).map((tr) => {
const cells = [...tr.cells].map(cellText);
// ERS serves item photos from a public CDN. Capture the URL so the
// catalog has real pictures instead of placeholder glyphs.
const img = tr.querySelector('img[src*="files.sysers.com"]');
cells.__image = img ? img.src : '';
return cells;
});
const m = doc.body.innerText.match(/of ([\d,]+) records/);
return { headers, rows, totalRecords: m ? Number(m[1].replace(/,/g, '')) : rows.length };
}
/** ERS paginates with ?limit_page=N and 100 rows per page. */
async function allPages(path, hardCap = 60) {
const first = await grid(path);
const per = first.rows.length || 100;
const pages = Math.min(hardCap, Math.max(1, Math.ceil(first.totalRecords / per)));
let rows = first.rows;
for (let p = 2; p <= pages; p++) {
const sep = path.includes('?') ? '&' : '?';
const next = await grid(`${path}${sep}limit_page=${p}`);
rows = rows.concat(next.rows);
log(` ${path} page ${p}/${pages} (${rows.length} rows)`);
await new Promise((r) => setTimeout(r, 120)); // be polite to ERS
}
return { headers: first.headers, rows, totalRecords: first.totalRecords };
}
/**
* ERS body rows carry an extra leading cell (the expander icon) plus a
* trailing action cell, so body index = header index + 1. Verified against
* the Items, Customer List and Order List grids.
*/
const objectify = (headers, rows, offset = 1) =>
rows.map((cells) => {
const o = {};
headers.forEach((h, i) => { if (h) o[h] = cells[i + offset] ?? ''; });
if (cells.__image) o.__image = cells.__image;
return o;
}).filter((o) => Object.values(o).some(Boolean));
/* ---- extract ---------------------------------------------------------- */
log('Reading catalog…');
const items = await allPages('/cp/items/');
log('Reading categories…');
const cats = await allPages('/cp/categories/');
log('Reading customers… (this is the slow one)');
const custs = await allPages('/cp/customer_list/');
log('Reading orders…');
const ords = await allPages('/cp/order_list/');
const payload = {
source: 'ers',
extractedAt: new Date().toISOString(),
company: document.title.replace(/^ERS CP - /, ''),
items: objectify(items.headers, items.rows),
categories: objectify(cats.headers, cats.rows),
customers: objectify(custs.headers, custs.rows),
orders: objectify(ords.headers, ords.rows),
};
log('Extracted:', {
items: payload.items.length,
categories: payload.categories.length,
customers: payload.customers.length,
orders: payload.orders.length,
});
/* ---- deliver ---------------------------------------------------------- */
try {
const res = await fetch(`${TARGET}/api/import/ers`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
const out = await res.json();
log('%cImported into Marquee ✓', 'color:#0ca30c;font-weight:bold', out);
return out;
} catch (err) {
log('Marquee not reachable — saving a file instead.', err.message);
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'marquee-ers-export.json';
a.click();
log('Saved marquee-ers-export.json to your Downloads folder.');
}
})();