Hemijska čistiona Elegant
Dry cleaningNovi Sad, Serbia
A 207-item Excel price list became a live estimate with a ready date, before the customer books a pickup.
Status
Services
- Website · Pricing & internal tools · Booking & orders · Lead pipeline & CRM
Year
2026
Results
Not measured yet.

The problem
Every order started as a DM or a phone call. Customers couldn't learn a price or a ready date without asking a person, and pickups were arranged one message at a time. The price list lived in Excel, with turnaround times hidden in cell colours.
What we built
- A one-page site with the order form as the second section, so Instagram visitors reach it at once.
- The full price list, 207 items in 19 categories, as searchable HTML with an Add button on every row.
- A 4-step order: items, pickup, contact, review, with a live estimate and ready date as you go.
- Pickup days limited to valid working days, with three time windows to choose from.
- A request number on every order, and an email that says received, never confirmed.
- A Google Sheet with six tabs: orders, order lines, prices, settings, error log and live statistics.
- The owner edits prices, turnaround, holidays and time windows in the sheet. No developer.
- The driver filters tomorrow's pickups straight from the sheet: address, floor, window, items.
How it works
The flow end to end, as the system is built to run. Sheet and status names are the client's own.
10 steps · 1 sheet
- Instagram link
- Pick items from 207
- Live estimate + ready date
- Pickup day and window
- Contact and consent
- Server re-prices the order
- ORDERS · NOVO
- Shop and customer email
- Shop confirms by phone
- Driver's round
Under the hood
The details a developer checks first. Most of them exist so an inquiry is never lost or sent twice.
- One engine computes the customer's estimate and the stored total, so they can't drift apart.
- The browser never sends prices. The server re-prices every order from its own price sheet.
- Turnaround was hidden in Excel cell colours. The build decodes them and refuses to run on an unknown colour.
- All 207 items reconciled against the client's PDF: codes, names and prices, zero conflicts.
- The daily cutoff runs on the shop's clock in Belgrade time, not the customer's phone.
- Duplicate check, five orders per phone per hour, one write lock and a formula-injection guard on every cell.
- Orders are posted as plain text on purpose, to skip a CORS preflight Apps Script can't answer.
- A one-line switch moves orders to the shop's own software later. The API contract is already written.
Stack
What it runs on.
- HTML · CSS · ES modules · Google Apps Script · Google Sheets · MailApp · Python · Node test runner
Build QA
From our own test runs before launch. These are build numbers, not business results.
- 128 automated tests passing, 0 failing. Backend tests run the real Apps Script code.
- 27 of 28 cases from the brief pass. The 28th needs an API that doesn't exist yet.
- Browser estimate and server total identical on an end-to-end test order.
- WCAG AA contrast across 1,178 elements, 0 failures, in light and dark mode.
- 0 horizontal overflow at 12 widths, from 320 to 1920 px.
- Local page metrics: CLS 0.0000, 0 long tasks, 17 requests.
- Six independent reviewers raised 25 candidate defects, including critical timezone and colour-decoding bugs, which were fixed.
What we didn't build, on purpose
Scope is a decision. Every line here was decided, not forgotten.
- No second operational system. The shop already runs its own software, so the site is an intake layer that plugs into it later.
- No confirmed appointments online. The customer sends a request, and the shop confirms by phone.
- No stock photos. The shop had none of its own, and stock would mislead.
- No guessed facts. Hours, branches and numbers stay hidden until the client confirms them.
- No cookie banner. Analytics is wired in and switched off.
The build
Captured from our local pre-launch build. Phone numbers, email addresses, prices and photos of people from the client's site are blurred wherever they appear.



