Portfolio / Ops & Systèmes / snap

Six agences, un seul flux de réservation.
Zéro double-réservation.

SNAP est une chaîne de studios photo en libre-service présente dans six villes indonésiennes. L'ancien site était une brochure statique — les réservations se faisaient par message WhatsApp, chaque agence tenait son propre cahier, et environ une session sur douze se terminait par un conflit à l'entrée. Nous avons reconstruit le parcours client sous forme de PWA et livré un tableau de bord d'administration unique qui couvre les six agences en même temps.

ClientSNAP Self-Photo Studio
PérimètreBooking PWA · QRIS · Admin · Rapports
Périmètre6 agences · 2 types de studio · 30+ fonds
Calendrier8 semaines · 2026 · en déploiement
Ce que nous avons construit

Un système de réservation qui sait dans quelle agence vous vous trouvez.

Une PWA mobile-first côté client, une SPA desktop côté administration, une seule API et une seule base de données en dessous. Six modules portent l'ensemble de la chaîne — de l'écran de ville qu'un client voit sur Instagram au récapitulatif journalier qu'un responsable d'agence valide en fin de journée.

Module 01

Portails villes & agences

Six villes, douze agences, deux types de studios. Le parcours client saute automatiquement l'étape de sélection d'agence dans les villes à studio unique et oriente vers le bon modèle de capacité selon le lieu.

Geo-routéMulti-tenant
Module 02

Deux modèles de réservation

Les studios en box utilisent une matrice fond×horaire — chaque salle se réserve indépendamment. Les studios à fond déroulant fonctionnent sur un planning partagé avec un choix de fond purement esthétique. Même interface, deux règles de capacité différentes en dessous.

MatriceCréneau unique
Module 03

Confirmation QRIS automatique

Le client scanne un code QRIS dynamique, le callback marchand libère le créneau, le client voit une coche. Personne ne surveille l'application bancaire. Plus de message manuel « c'est payé ? ».

QRISWebhook
Module 04

Vue d'ensemble admin

Un seul tableau de bord pour le fondateur. Réservations du jour sur six agences, ventes par rapport à hier, dépenses par catégorie, taux d'occupation par salle. Clic sur n'importe quelle tuile pour accéder aux lignes brutes.

KPIs en directDrill-down
Module 05

Console opérationnelle d'agence

Les responsables d'agence ne voient que leur agence — calendrier du jour, toggle pour les walk-ins, journal de dépenses, caisse de fin de journée. Basé sur les rôles, verrouillé par défaut.

RBACClôture journalière
Module 06

Confirmations WhatsApp

Réservation confirmée → un message WhatsApp formaté part avec le créneau, l'adresse de l'agence et un lien calendrier. Rappel envoyé 2 heures avant. Les DMs ne sont plus l'endroit où vivent les réservations, mais restent celui où les clients s'attendent à avoir des nouvelles.

ModèlesRappels
Six agences · niveaux de gris devenus couleur au survol

Une gamme, une identité, six salles.

Snap / Kemang
Snap / Senopati
Snap / Bandung
Snap / Surabaya
Snap / Bali
Snap / Yogya
● Le problème

Les réservations vivaient dans une boîte de messagerie WhatsApp.

Chaque agence avait son propre numéro d'administration. Les clients envoyaient un message du type « demain 15h Kemang ? » et quelqu'un répondait s'il était disponible. Deux membres du personnel confirmaient parfois le même créneau à deux clients différents, car la boîte de messages était l'unique source de vérité.

Les week-ends chargés, environ une session sur douze se terminait par un conflit à l'entrée — l'une des parties était renvoyée, remboursée, et se plaignait sur Instagram.

« On gérait un calendrier dans une application de messagerie. Ça tenait jusqu'à ce que deux messages arrivent à la même minute. » — fondateur, SNAP
S
Snap Kemang
vu il y a 11 min
Bonjour ! Un créneau demain à 15h ?10:42
Oui, 50 % d'acompte svp10:44
Demain 15h, on passe à 4 personnes10:46
Bonjour, je voudrais aussi réserver 15h demain, encore dispo ?10:47
Une seconde, je vérifie10:51
Allô ? J'ai déjà viré l'argent11:08
??? allô ?11:24
Double-réservation
● La solution

Le créneau est une ligne en base de données, pas un message.

Le client choisit une ville, une agence, une date, une heure et un fond — chaque étape pose une réservation provisoire sur une vraie ligne. Deux téléphones qui tapotent le même créneau à la même seconde : l'un gagne, l'autre voit « déjà pris » et se voit proposer le prochain bloc de 30 minutes disponible.

La confirmation part ensuite par WhatsApp, de sorte que les clients reçoivent une réponse là où ils s'y attendent. Le message est désormais le reçu — pas la réservation.

9:41 ●●●● ◐ ▮▮▮
Snap / Kemang
Sam 14 fév · studio unique
Étape 4/6
Choisir un horaire · 30 min 8 libres
10:00
10:30
11:00
11:30
12:00
12:30
13:00
13:30
14:00
14:30
15:00
15:30
Choisir un fond · esthétique uniquement
Rp 120,00013:30 · Blue
Continuer →
● Deuxième problème

« Fonds » voulait dire deux choses différentes.

La moitié des agences sont des studios en box — chaque fond est dans sa propre salle, donc deux fonds signifient deux sessions en parallèle. L'autre moitié sont des studios à fond déroulant — plusieurs fonds accrochés dans une seule salle, mais une seule session peut se dérouler à la fois.

L'ancien site les traitait de façon identique. Des clients réservaient quatre fonds simultanément dans une agence à fond déroulant et arrivaient pour découvrir que c'était impossible.

Kemang · Sat
13:00 — Adi (4 pax)
13:30 — Sarah ✓
Senopati · Sat
Box A — 1pm Tia
Box B — 1pm Rai
Box C — empty
Bandung
payment? — DP 50
cash 70 — sisanya?
Surabaya
walk-in 2 pax
cust ngantri ←
Bali · Sun
refund 1 booking
double again 😩
Yogya
expense ice 25k
listrik 280k
cleaning 60k
● La solution

Le modèle de capacité est attaché au créneau, pas au fond.

Chaque agence est taguée multi ou single en base de données. Les studios en box affichent une grille fond×horaire — cliquer sur une cellule, c'est choisir une salle et un horaire en un seul geste. Les studios à fond déroulant proposent un planning partagé unique et un choix de fond séparé, purement esthétique.

Même shell React, mêmes composants, forme de données différente depuis un seul endpoint. Le client n'a jamais besoin de connaître la différence.

9:41 ●●●● ◐ ▮▮▮
Snap / Senopati
Sam 14 fév · box · 4 salles
Multi
Choisir une salle & un horaire 9 libres
·
12
12:30
13
13:30
14
A
×
×
B
×
C
×
×
D
×

○ libre    × pris    ● sélectionné

Salle B · 13:00Senopati · 30 min
Continuer →
● Troisième problème

Quelqu'un devait surveiller l'application bancaire.

Le client fait un virement, prend une capture d'écran de la preuve et l'envoie par WhatsApp. L'administrateur ouvre l'application bancaire, cherche le montant correspondant, répond « ok confirmé » et note le créneau dans le cahier. Cinq minutes par réservation un jour calme, bien plus un samedi.

Si l'administrateur était à déjeuner, le client attendait. Si le client attendait trop longtemps, il annulait.

9:41 ●●●● ◐ ▮▮▮
Payer par QRIS
Réservation #SNP-2614
Étape 6/6
Rp 120,000
Snap Kemang · 13:30
14:32 restantes

Confirmation automatique.
Pas de capture d'écran nécessaire.

En attente du paiementdétection auto via callback QRIS
Live
Avant & après

À quoi ressemblait un samedi matin.

Pour un administrateur d'agence le samedi entre l'ouverture et midi — la période la plus chargée. La même personne, le même poste, avant et après la refonte.

Avant Sam · 4h de jonglage
  • 09:00ouvertureOuvrir WhatsApp Web. 17 messages non lus arrivés dans la nuit.
  • 09:20+20 minRecouper le cahier avec les réservations de la veille. Deux créneaux en conflit.
  • 10:05+1 hUn client écrit « j'ai viré ». Ouvrir l'appli bancaire. Chercher le montant. Répondre « ok ».
  • 10:48+1h 48Deux téléphones, deux clients, les deux veulent 11:30. Choisir l'un. Rembourser l'autre.
  • 11:30+2h 30Un walk-in arrive. Le cahier dit que la salle est libre. Le téléphone dit qu'elle est réservée. Dispute.
  • 12:55+3h 55Pause déjeuner repoussée encore une fois.
~4 h
Après Sam · 35 min d'admin
  • 09:00ouvertureOuvrir le tableau de bord. Le calendrier du jour est déjà rempli depuis la nuit.
  • 09:05+5 minParcourir le fil WhatsApp pour repérer les anomalies. Aucune.
  • 10:00+1 hUn walk-in arrive. Appuyer sur « ouvrir un créneau walk-in ». Le client paie par QRIS au comptoir. Créneau enregistré.
  • 11:30+2h 30Coup d'œil au widget d'occupation des salles. Les quatre salles tournent. Rien à faire.
  • 12:00+3 hPause déjeuner.
~35 min · ~14 % d'avant
Comment nous y sommes arrivés

Huit semaines, trois déploiements.

01

Semaines 1–2 — cartographier le chaos

Passé un samedi entier avec deux administrateurs d'agence. Observé le cahier se remplir, les messages se jongler, l'appli bancaire être rafraîchie. Noté chaque étape. Supprimé tout ce qui n'aurait pas à exister si une base de données existait.

02

Semaines 3–6 — construire le parcours client

PWA React + Vite, Express + MySQL en backend. Deux modèles de réservation derrière un seul endpoint pour éviter que l'interface ne se bifurque. Intégration QRIS marchand avec fallback webhook pour qu'un réseau instable ne bloque jamais un client en plein paiement.

03

Semaines 7–8 — admin + lancement progressif

SPA admin : vue d'ensemble, calendrier de réservations, dépenses, ventes, rapports, paramètres. Déploiement sur une agence d'abord (Kemang), suivi pendant une semaine, puis intégration des autres deux par deux. Les cahiers retirés agence par agence.

Résultats · chiffres de déploiement initial

Première agence sur le système depuis ~6 semaines.

Les chiffres ci-dessous proviennent de la base de données en production pour l'agence pilote (Kemang) ainsi que les deux agences connectées par la suite. Les chiffres sur l'ensemble de la chaîne ne seront publiés ici qu'une fois qu'il y aura suffisamment de mois de données réelles.

0
Conflits à l'entrée depuis le lancement
Agence pilote · 6 semaines en production · auparavant ~1 session sur 12 les week-ends se terminait par un conflit.
~85%
Temps d'admin récupéré
La plage du samedi matin de l'administrateur est passée de ~4 h à ~35 min — agence pilote uniquement, pour l'instant.
< 6sec
Confirmation QRIS médiane
Du scan au callback « payé ». Il fallait 3–5 min quand un humain surveillait l'appli bancaire.
3 / 6
Agences intégrées
Kemang, Senopati, Bandung. Trois autres en cours d'intégration, à raison d'une tous les quinze jours.
Sous le capot

Une tech simple, délibérément.

Six agences dans six villes, un fondateur qui doit pouvoir lire le tableau de bord sur son téléphone en faisant la queue dans un café. Chaque décision a répondu à une question : que se passe-t-il quand le wi-fi d'une agence tombe au milieu d'un samedi chargé ?

Client

React PWA, mobile-first

Installable depuis l'écran d'accueil. Transitions de pages via Framer Motion. CSS Modules et une palette strictement noir et blanc — les photos apportent la couleur, l'interface reste en retrait.

React 18 · Vite · Framer Motion · CSS Modules
Backend

Express + MySQL, un seul tenant

Une seule base de données, les agences en tant que lignes, le modèle de capacité en enum. Les écritures de créneaux passent par un seul endpoint qui verrouille la ligne pour empêcher deux réservations simultanées de prendre la même cellule.

Node 20 · Express · MySQL 8 · row-level locks
Paiements

QRIS dynamique + webhook

Code QRIS dynamique par réservation avec un TTL de 15 minutes. Le webhook de règlement libère le créneau ; en cas d'échec du webhook, un poller réconcilie en moins d'une minute. Dans les deux cas, le client voit la même coche.

QRIS dynamic · webhook · poll fallback
Admin

La même app React, une interface différente

Pas de codebase admin séparé. La connexion fondateur atterrit dans la mise en page desktop avec sidebar ; la connexion agence atterrit dans une vue mono-agence. RBAC au niveau de l'API, pas de l'interface — naviguer ne peut jamais accorder ce que le serveur n'a pas déjà approuvé.

RBAC · server-checked routes · desktop SPA
Suivant → GhostKitchen

Les réservations
débordent ?

Appel de cadrage gratuit. On vous dit la chose la moins chère qui règle le problème.

Des projets comme celui-ci s'appuient sur la Core Platform + modules sur mesure — la plupart des systèmes de réservation se situent entre $2 500 et $4 500. Tarifs complets →