A Bangkok beauty clinic group with 10 branches — Siam Paragon, EmQuartier, ICONSIAM, CentralWorld, Thonglor, Phromphong, Asoke, Ari, Mega Bangna, Sathorn — was running 140+ appointments per day on a stack that looked like every Thai SME you've ever met:
- One shared Google Sheet per branch, color-coded by status
- A LINE group per branch for staff comms
- A second LINE group for management
- An Excel file at HQ trying to reconcile all 10 sheets daily
- Doctor schedules sent as JPEG screenshots in LINE
- Customer history kept in physical paper files
The owner could not answer simple questions in real time. Which branches were under-booked today? Which VVIP clients hadn't come back in 60+ days? Which services were trending? Was GoWabi (the third-party booking platform) actually profitable, or were the commissions eating the margin? Every answer required a half-day of spreadsheet archaeology.
So we built them a real operating system. One URL. Five role-specific views. Live across all 10 branches. Bilingual EN + ไทย. No login flow yet — just open it and click around.
Open the actual working build
Everything described below is live in your browser right now. Click through every tab, switch the role, change the branch — it's all real seeded data running on a CDN edge near you.
Open Beauty Clinic Console →The five role views
The top of every screen carries a role switcher: Staff · Doctor · Manager · Top Mgt · Marketing. Each role sees a totally different cockpit — same underlying data, different angles. Anyone in the clinic can flip to "Top Mgt" view to see what the owner sees. (In production we lock this behind permissions; on the live demo it's wide open so you can poke around.)
Today's arrivals
Single-branch focus. Today's appointment table, status pills, follow-up actions for late or cancelled clients. The Front-of-house cockpit.
My schedule + treatment notes
What's on your chair today, the customer's previous treatments, aftercare templates, photo library. The clinical cockpit.
Branch ops
Today + week ahead, staff schedule, stock + supply alerts, low-margin service warnings, branch P&L preview.
10-branch network
Network revenue, branch ranking, churn cohort by month, top-spender list, action items (close GoWabi? launch VIP win-back? raise a service price?).
Campaigns + audiences
Segments by lifetime value, package promotions, AI-suggested win-back queue with copy already drafted for LINE OA.
The booking status flow — 8 states, two cancel branches
Most clinic systems we benchmarked had 3 states: booked / arrived / done. Useless. A real clinic spends most of its operational energy on the weird states — the client called to cancel, the client is late but the staff doesn't know if they're still coming, the client showed up 45 min late and now the chair is overbooked.
So the booking state machine has eight nodes, and the cancellation path forks based on a single SOP question: are you willing to wait in the cancel queue?
That bottom SOP — ถามลูกค้าสะดวกเข้ามาอีกทีเมื่อไหร่ สาขาไหน — is hardcoded as the cancel-action label. Staff doesn't have to remember; the action button literally tells them what to say. The follow-up gets logged against the customer record, the customer auto-enters the "follow up" queue, and the next day's first-thing task list surfaces them.
VIP protection — a rule the spreadsheets couldn't enforce
The clinic's tier rule is simple: a client who has spent ≥ 150,000 THB lifetime is VIP or VVIP. Their previous spreadsheet had no way to enforce a tier-based rule, so high-spenders occasionally got marked as late-left when they were 30 minutes late — and the owner would find out three days later that a 600,000-baht customer had quietly churned.
In the new system, the status transition simply refuses to land on late-left for anyone above the VIP threshold:
The principle: "VIP can churn — they spend less. VIPs and above cannot leave the system as 'left'. They must surface as 'follow up' so the owner sees them." That's the whole rule. Three lines of code, but only possible because the data was unified in one place.
Multi-branch ops — what the owner finally has
"Top Mgt" view is the one the owner opens first thing every morning. It folds 10 branches' worth of state into a single screen:
The "Action today" block isn't a passive dashboard. Each row is a tappable command that opens the relevant queue with the right action pre-loaded. The owner doesn't have to navigate; the system pushes work at them.
AI-suggested upsells — drafted, not just suggested
For every customer, the upsell column doesn't just say "try Pico." It gives staff a specific, dated, priced offer to read off — already keyed off the customer's history:
- First-time client: "First-time bonus: ฿500 off next visit + welcome kit"
- Glow / Brightening regular: "Pico package · 10 sessions ฿55K (39% off retail)"
- VVIP: "Offer concierge package, free upgrade — never let them leave"
- 3+ month silence: "Win-back — AI-drafted LINE OA copy queued in Marketing tab"
The Marketing role then sees all the AI-drafted messages in one queue and either approves, edits, or sends to LINE OA in batches. No copy is ever "AI sends." Approval is always human — the AI is the copywriter, not the broadcaster.
The closure decision the owner couldn't make before
One of the most interesting outputs the new system surfaced in week one was about the clinic's GoWabi (online) revenue line. GoWabi takes a ~20% platform commission plus a daily marketing levy. The clinic had no way of seeing per-channel margin before; on the spreadsheets, GoWabi's gross looked great.
Plug the real numbers into a per-branch P&L (COGS 30%, allocated overhead, platform commission, marketing spend) and the picture flips:
The system surfaced the action: "Close GoWabi · 6 months in the red". With one click, the owner could schedule the channel closure, generate a closure-notice draft, and queue a win-back offer to the 22 platform-locked customers — "come direct, same price, double loyalty points." That decision was invisible to them before the dashboard existed.
How it's built — the boring (but honest) tech
- One HTML file. React 18 + Tailwind CSS, both via CDN. No build step. The entire app is a single
index.html. - State management: plain React
useState+useMemowith an in-memory store. No Redux, no Zustand, no overhead. - Bilingual: a single
L(en, th)helper + dictionary lookups. Language toggle at the top right. - Hosting: Cloudflare Pages on a custom domain. Global edge cache. ~50ms load time anywhere in the world.
- Seed data: realistic Thai customer names, doctor schedules, services with Thai/English category labels, and a status timeline that produces believable analytics.
- The whole thing is ~25K lines of JSX with all the seed data inlined. Could be deployed by uploading one file.
Could the same thing be done with Salesforce + Mendix + a 6-month onboarding? Yes. Would it cost ฿2.5M+ year-one and require the clinic to hire a Salesforce admin? Also yes. Custom built, this clinic's system shipped in under 3 weeks at a fraction of that, with no recurring license fees and full IP ownership of the code.
What we'd build for you
The same architecture works for any multi-location operation that's stuck in spreadsheets: dental clinics, fitness studios, salons, restaurant groups, retail chains, real-estate brokerages, dental groups, F&B (with Grab + LINE MAN Wongnai channel integration). The pattern is always the same — multi-role console, network-level dashboard, action queues that push work at the operator instead of hiding it in tabs.
Open the live system, then book a call.
Click through every role. Switch branches. Try the cancel-queue flow. If the way it handles your category isn't obvious from the demo, book a call — we'll scope a system for your operation. IP-clear contracts, Git repo handed over on final invoice, no platform lock-in.