The whole operator side of the POS, redesigned on the approved Floor + Schedule direction. It covers the web POS at pos.kakiapp.co/pos/*, which is primary, and notes where the Kaki POS app (Expo) differs. Every screen sits in a landscape iPad frame because that is the counter tablet. A few native phone variants come at the end. Tap the tables, pay methods, timeline blocks and state switches to try them. All names and figures are sample data.
Operators sign in with a USERNAME that the owner set, and owners sign in with email. An operator's account has no real inbox (the synthetic address ends in .invalid), so there is no "Lupa kata sandi" for them. The owner resets their password from Lokasi.
/Web + Appapp: /sign-inOne field that accepts a username or an email. The web has Owner/Operator tabs today and the app has a single field that relabels itself, so the redesign keeps the app's single field and puts it on both. The "Lupa kata sandi?" link only appears once the input reads as an email. Switch the states to see errors.
Pemilik masuk dengan email. Kasir masuk dengan username dari pemilik.
Pemilik masuk dengan email. Kasir masuk dengan username dari pemilik.
Kami belum bisa memeriksa sesi Anda. Periksa koneksi internet tablet ini, lalu coba lagi. Tidak ada yang berubah.
/pos?location=Web + Appapp: /choose-venueThis only shows when the account can reach more than one venue, which is how owners work (Mahjong Dojo has seven). An operator is bound to one venue and goes straight to Hari ini. A remembered venue that is no longer on the account is REFUSED and named back, never swapped for another one. Otherwise money would be taken on the wrong till.
Tablet ini akan mengingat pilihan Anda sampai ada yang keluar.
Tablet ini terakhir dipakai untuk tenpai-addicts-palembang, tapi akun Anda tidak punya akses ke sana. Pilih lokasi Anda sendiri, kami tidak akan memindahkan kasir diam-diam.
Kaki tidak menjawab. Tidak ada yang berubah di akun Anda.
/pos/* (layout)Web + AppThe layout decides who may enter the POS: an owner of the venue or its operator. The account menu under the staff monogram holds Ganti lokasi (owners), Dashboard pemilik, Bagikan link booking, and Keluar. Signing out ends this till's shift: the app asks twice, and the redesign asks twice on the web too.
Akun agus@gmail.com bukan pemilik atau kasir di venue ini. POS hanya untuk staf venue.
Masuk lagi sebelum menagih. Pembayaran yang sedang berjalan tidak dibatalkan.
Live mode opens here (/pos → /pos/arrivals). It lists the day's bookings grouped by what the counter must do next, a strip of the floor right now, unpaid holds pinned in red, and the tables held at the counter. Red on this screen means only one thing: money that has not arrived.
/pos/arrivalsWeb + Appapp: /(counter)/todayGroups, in order: Di meja sekarang, Berikutnya · 18 menit lagi, Nanti hari ini, Selesai, tidak ada yang mencatat, Sudah datang, Tidak datang. A booking whose hour arrived and whose party has not checked in reads "Belum datang", never "Main". Every row shows its money state and its check-in state as two separate chips, because a booking's status is never the same word as its payment. Switch the top segment to see the floor grouped by table kind.
BOOKING_CARD_HANDOFF_ACTIONS_ENABLED=false.Meja tanpa jenis ada di grup "Belum diklasifikasi" dan boleh semua gaya.
/pos/arrivals · statesWeb + AppA failed read is never shown as an empty day. A failed WRITE says exactly that nothing changed. A demo build names itself and points to the Floor, never inventing bookings.
Memuat kedatangan hari ini…
BookingDetailSheetWeb + AppThis is opened by booking ID and re-read on every poll, so it never says "belum dibayar" after the payment landed. Sections: Kapan, Siapa, Uang, Cara booking. Only one money control shows at a time: Tagih, Tampilkan QR, or Tagih tetap (when comped, with a confirm). A cancelled booking shows "Pulihkan", or names why it cannot be restored (day over, a released hold, the slot sold since). Booking codes are case-sensitive, and the sheet says so.
Tidak bisa dipulihkan kalau harinya sudah lewat, atau kalau ini tahanan yang lepas sendiri.
Kode booking membedakan huruf besar dan kecil.
BookingQrDialog · POST /operator/bookings/:id/collectWeb + AppModes are collect and resume. Closing keeps the QR live. A live QR can be cancelled and replaced. The day-over guard allows last night's table for 60 minutes after it ends. Refusals are named: already paid, collection in progress, comped, nothing to collect, booking not live, source not collectable, gateway failed.
KK3F9A · Meja 7 · 19:30–21:30
+ biaya QRIS · QR dibuat lewat gateway pembayaran
Termasuk biaya QRIS Rp 1.000
Menunggu pembayaran · berakhir dalam 09:12
QR untuk booking ini masih aktif. Tunjukkan QR itu, atau batalkan dan buat yang baru.
Rp 121.000 lewat QRIS · Meja 7 tetap milik Dewi.
Booking kemarin masih bisa ditagih sampai 60 menit setelah selesai. Yang ini selesai Sel 23:00, jadi tidak ada QR yang dibuat.
BlockDialog · BlockSlotDialog · POST/DELETE /venues/:id/blackoutsWebApp: not builtHold a table for a named walk-in for N hours, so online customers cannot book it. The rounding rule is the owner's own example: 07:25 + 2 jam blocks slots 7, 8 AND 9. Past close is refused, a span that runs over close is clamped (and the dialog says so), midnight never wraps, and "24 jam" is refused. "Blokir slot" can close one table or the whole venue. A whole-venue hold cannot be released from a row. The app does not read blocks at all.
Mulai sekarang, 19:25
Mulai 23:40
Tahanan untuk Rina (19:25–21:25) dilepas. Pelanggan online bisa langsung membooking meja ini.
ReinstateDialog · POST /operator/bookings/:id/reinstateWebApp: noneThis brings a cancelled booking back and restores its list price. If the hour has been sold since (409 slot_taken), it moves to a "pick a new time" step, which runs the same three-source check. The done state says whether the customer was told.
KK5M4R · Budi +3 · Kam 24 Sep 16:00 · Meja 2 · Rp 60.000
Meja 2 jam 16:00 sudah dibooking orang lain. Pilih waktu lain untuk memulihkan.
Kam 17:00 · Meja 4. Budi sudah diberi tahu lewat email.
This is the approved direction. Each table is a tile, and tapping one opens its bill on the right. The floor is the only screen that can settle an OPEN TAB or close one without charging, so it must never be hidden while tabs exist. Today POS_UNREADY_NAV_ENABLED=false hides Tab and Open play on the web, and the app has no Denah at all.
/pos/floorWebApp: NotBuiltTile states: Main (open tab, felt green, running clock and total), Segera selesai (red: 10 minutes or less left, or overtime), Datang (booked, hour arrived, not checked in: gold edge), Belum bayar (unpaid hold, red chip), Kosong (until the next booking), Ditahan (block, hatched), and Tab lama (open 24 hours or more: gold warning, never auto-selected). The tile also shows the table kind (Chinese / Riichi saja). Tap any table.
/pos/floor · /pos/schedule → WalkinDialogWebApp: nonePick a table (only free tables that may host the chosen style are enabled: a Riichi-only table never takes Hong Kong or ITM, and an unclassified table takes anything), the game format, party size and the planned duration from the venue's ladder (only rungs the rules allow, capped by close and by the next booking). The rate shown comes from the price config, never an invented figure. It opens a TAB: no booking row, billed on close.
Membuka tab langsung. Dibayar saat meja ditutup.
WalkinDialog · 409 table_booked / table_busyWebOwner decision: staff are never blocked, only never surprised. table_booked asks once and re-sends with override:true. table_busy means another open tab already sits on that table, and it cannot be overridden.
Tab 2 jam akan berjalan sampai 21:41, bentrok 11 menit dengan booking KK8P2Z. Kalau yang datang memang Sari, check-in booking-nya saja.
Tab Agus sejak 19:02 belum ditutup. Tutup atau tagih tab itu dulu, atau pilih meja lain.
POST /operator/sessions/:id/extend · POST /operator/bookings/:id/extendWebApp: noneAn open tab extends by +1 jam (it is only a countdown, because a tab bills on close). A booking extension first asks for a PREVIEW quote: the API re-prices the WHOLE span, because tiers change (Sen–Kam half price, peak hours, the trading night after midnight), and charges the difference. Only the added tail is claimed, through the single three-source check. When the next booking is in the way, staff may "Perpanjang tetap", but they must type why. Price-lock reasons (already paid, collection in progress, comped, quote unavailable) are named, never silent.
Budi +3 · rencana 3 jam, s/d 19:47
KK2A7D · Rina +3 · 14:00–16:00 · lunas
Perpanjang Rudi sampai 22:00 akan bentrok 30 menit.
Pembayaran untuk booking ini sedang berjalan. Selesaikan atau batalkan QR-nya dulu, lalu perpanjang.
PaymentDialog · POST /operator/sessions/:id/closeWeb + AppQRIS mints a QR through the payment gateway and polls until it is paid, expires or is cancelled. Kartu and e-wallet are the venue's own terminal, so the POS only records them (manual:true). Tunai does not exist in the code today: the close route accepts only qris / ewallet / card. It is in the approved direction, so it is designed here and flagged as an API gap. Switch states:
QRIS · semua bank & e-wallet
Tagihan meja Rp 90.000 · termasuk biaya QRIS Rp 1.000
Menunggu pembayaran · berakhir dalam 14:32
Tidak ada uang yang ditarik. Buat QR baru untuk tagihan yang sama.
QR akan dimatikan dan tab Meja 2 tetap terbuka. Kalau pelanggan sudah membayar, tunggu saja: pembayaran akan tercatat.
BARU · API belum ada
Belum ada yang ditagih. Coba lagi, atau pakai metode lain.
Menunggu · 14:59
ReceiptDialogWeb + AppThe receipt names the method and whether the money came through Kaki (QRIS) or the venue terminal. The web shares by WhatsApp (receiptWhatsAppHref, a real link since the dead button was fixed), and the app has no share at all. Only actions that really do something are shown.
lewat QRIS · 19:52 · Meja 2 · Rudi +3
POST /operator/sessions/:id/cancelWebApp: noneThis is for a forgotten tab or a party that left. The refusals stay distinct: payment_pending means void the QR first, and already_paid means this is a refund, not a cancel. A tab left open never ages out, and its table is unsellable at the counter for as long as it stays open.
Tab dibuka 11 Sep 17:10 · 12 hari 22 jam
SessionAlerts (every /pos screen)WebApp: noneA red toast appears when an open tab or a booking reaches its end (at 10 minutes left, at 0, and in overtime). The tile flashes and a tone plays. Sound only exists after a real tap, and the sound bar tells the truth about it. After a reload it asks for one tap. It never claims "Suara aktif" over a locked audio context. A reload SHOWS every standing alarm but does not ring them again. Demo builds render no booking alarms. An iPad's hardware mute switch silences the tone, and the page cannot detect it, so the bar reminds the staff.
Budi +3 · lewat 2 menit · Rp 180.000 berjalan
Booking Tomo +3 · s/d 20:00 · lunas
The owner asked for this. Today's /pos/schedule is a time-ordered LIST of bookings, today's tabs and holds, with a source filter (Semua / Web / App / Walk-in). The new Jadwal draws one row per table with every source of occupancy on one line: bookings (paid, unpaid-hold, not-arrived), open tabs, open-play tables and blocks. It has a red now-line that scrolls itself into view, :30 starts exactly on the half hour, and a trading night that runs past midnight. Tap a block for its detail card. Tap empty space to start a walk-in, a booking or a block. The list view stays available as the "Daftar" toggle, so nothing the list does today is lost.
/pos/scheduleBARU: timelineWebApp: daftar 14 hari sajaRows are the venue's bookable tables (venues.table_count, never venue_tables), grouped and filterable by mahjong kind. The grid runs from opening to the last START plus its longest rung, so a venue closing at 01:00 still shows the 00:00–02:00 booking. The now-line scrolls to about one third of the width when the screen opens and follows the clock. Tap any block, or tap an empty span. A new-span ghost turns red and shows the seat-conflict warning when it overlaps.
/pos/schedule?date=2026-09-23 (00:35)BARUAt 00:35 on Wednesday, "today" on the till is still Tuesday's trading night until the doors shut. Starts are stored on the calendar day (00:30 is a Wednesday row) but priced on Tuesday night. The header says both so staff are never confused, and the date navigator follows the SERVER's WIB day, never the tablet's UTC date. A closed day still sells the previous night's tail: the second frame shows a Sunday that is closed but still carries Saturday night's tables until 02:00.
/pos/schedule · statesBARUA read that has not landed, or has failed, must never say "Tidak ada booking". Saying so is how an operator tells an arriving guest their reservation does not exist and then sells the table. Empty is only stated on a successful read.
ManualBookingDialog · POST /venues/:id/manual-bookingWeb + AppThis is dated with the day on screen, and the dialog keeps its day through midnight (it warns instead of moving the write). Starts come from the venue's timeline (:00 and :30). Duration uses the rungs of the table's ruleset, or the packages when the venue sells packages. Settlement is Bayar sekarang (QRIS) or Tanpa biaya (comp). Owner decision: "bayar di kasir" was removed. The app still offers "Pay at the counter", which is a parity gap.
Hari ini · Rab, 23 Sep
Dialog ini dibuka untuk Sel, 22 Sep. Sekarang sudah Rab 00:04. Booking tetap dicatat untuk malam Selasa selama slotnya masih berjalan. Slot yang sudah lewat ditolak server (in_the_past).
Seseorang baru saja membooking Meja 5 jam 20:30 lewat aplikasi. Tidak ada yang dibuat. Pilih jam atau meja lain.
Anda memilih 20:30–22:30, dan server mencatat 20:30–21:30 (paket venue berubah). Periksa dengan tamu.
RescheduleBookingDialogWebApp: nonePick a new date, start and table. The server runs the same three-source check and answers with a specific sentence per refusal: "Slot sudah mulai", "Tidak bisa dipindah lagi", "Venue tutup di jam itu", "Tinggal N kursi".
KK7Q4M · Andi +3 · Rab 19:00, 3 jam
BookingLinkDialog · ShareBookingLinkWebApp: noneThe venue's public booking page, as a link and a QR to print or show. Anyone with the link picks their own table, date and time, then pays to confirm. It belongs to ONE venue, so an owner without a chosen location is asked to pick one first.
Siapa pun dengan link ini bisa memilih meja, tanggal dan jam sendiri, lalu membayar untuk konfirmasi.
Scan untuk booking
hold-link · POST /venues/:id/booking-holdsWebApp: noneStaff pick table, start and duration, and the POS mints a held booking plus a pay link. The table is held for 15 minutes (pending occupies it), and the guest pays by QRIS on their own phone. Lapsed customer holds are swept first so they never block staff. The prefilled link carries the date shown on screen and refuses a day that is over.
Meja 5 · Rab 21:00–23:00 · Rp 120.000
Each of these routes exists on the web today. Tab and Open play are hidden from the bar by a flag. Riwayat and Peringkat sit off the bar, linked from Hari ini. In the redesign they share the rail's "Lainnya" group.
/pos/tabsWeb + Apphidden on both (flag)Each row shows the /quote figure and opens the same payment dialog as Denah. A tab older than 24 hours is flagged for review, never pre-selected. "Makanan & minuman" and "Retail" are shown as coming soon, exactly as today. A failed read says "ini bukan denah kosong".
/pos/checkinWeb + AppPOST /venues/:id/bookings/:code/checkinScan with the camera (the web starts it on tap, and the app keeps it on while the tab is focused, with a 4-second duplicate latch), or type the code. Codes are normalised, and a code check-in writes check_in_at only. Try typing KK7Q4M, KK1X7C or KK5M4R, or switch results.
Lina +3 check-in jam 15:02 di Meja 8. Tidak ada yang berubah.
Booking KK5M4R sudah dibatalkan dan dananya dikembalikan. Pastikan tamunya benar sebelum mendudukkan mereka.
Tidak ada booking dengan kode "KK7Q4N" di venue ini. Kode membedakan huruf besar-kecil. Cek juga lokasinya: booking di cabang lain tidak muncul di sini.
Anda tetap bisa check-in tamu: ketik kodenya di sebelah kiri.
/pos/open-playWebhidden (flag) · App: NotBuiltPost N tables at a start time and a style, and players claim seats in the Kaki app. The fill state is spelled out: "Sebagian bayar", "Penuh, meja ditahan sementara semua bayar: jangan dudukkan walk-in", "Waktu tambahan", "Refund terutang". A Chinese table may host several styles. A Riichi-only table sticks to Riichi.
Tidak penuh sampai jam mulai. 2 pemain sudah bayar dan menunggu refund.
Rab, 23 Sep
Kalau hanya sebagian yang berhasil: "Hanya 1 dari 2 yang terposting."
/pos/coachesWebApp: NotBuilt (visible tab)A roster of coach cards with today's booked sessions. Actions: Tambah pelatih, Jadwal (weekly opening and last start, plus time off), Ubah, Pensiunkan / Kembalikan ke roster. When sessions fail to load, the roster still shows and says only the sessions are missing. The coach dialog refuses emoji and requires a rate above Rp 0.
Jam buka dan mulai terakhir per hari
| Hari | Buka | Mulai terakhir |
|---|---|---|
| Sen | — | — |
| Sel–Jum | 16:00 | 20:00 |
| Sab–Min | 12:00 | 21:00 |
/pos/messages · /pos/messages/[conversationId]WebApp: NotBuilt (visible tab)Customers message the venue from the Kaki app. The inbox refreshes on every socket event. The thread shows read receipts ("Dibaca oleh …") and deleted messages, can attach a booking reference from "Booking terbaru", and names refusals: "Anda dibisukan di chat ini", "Tidak bisa mengirim, coba lagi", "Anda tidak punya akses ke chat ini".
/pos/historyWebApp: noneGET /operator/venues/:id/bookingsEvery booking, newest first. Filters: Semua, Datang, Tidak datang, Dibatalkan, Tidak pernah lunas, Tidak cocok. There are window chips plus "termasuk hari ini", and paging with "Tampilkan lagi". Status and money are separate columns. A test fixture is labelled as one. Tapping a row opens the detail sheet (with Pulihkan). Owners get a link to Keuangan.
| Tanggal | Kode | Tamu | Meja | Status | Uang | Jumlah |
|---|---|---|---|---|---|---|
| Sel 22 · 20:00 | KK1B5D | Tomo +3 | 2 | Datang | Lunas | Rp 120.000 |
| Sel 22 · 19:00 | KK5M4R | Budi +3 | 2 | Dibatalkan | Direfund | Rp 60.000 |
| Sen 21 · 21:30 | KK0Q2W | Eko +1 | 5 | Tidak datang | Lunas | Rp 90.000 |
| Sen 21 · 18:00 | KK9Z9Y | Nina +3 | 1 | Dibooking | Tidak pernah lunas | Rp 120.000 |
| Min 20 · 15:00 | KK4E8R | Lina +3 | 8 | Datang | Tanpa biaya | Rp 0 |
| Min 20 · 13:00 | KK2T0T | Test | 1 | Dibooking | Data uji | Rp 1.000 |
/pos/leaderboardWebApp: noneBoards: Riichi · Standar, Riichi · WRC, Riichi · 3 pemain, Hong Kong. Medal ranks use gold, silver and bronze tiles. Pills mark PEMULA and TAMU, and players who have not qualified yet sit in a separate "Belum memenuhi syarat" block. Columns show the venue rating and the global rating. States: venue unknown, failed, and "Belum ada game tercatat di sini".
| # | Pemain | Game | Di sini | Global |
|---|---|---|---|---|
| 1 | AWAndi Wijaya | 42 | 1.642 | 1.588 |
| 2 | DPDewi Putri | 38 | 1.611 | 1.570 |
| 3 | RSRina Susanti | 29 | 1.577 | 1.540 |
| 4 | BSBudi Santoso PEMULA | 12 | 1.530 | 1.512 |
| 5 | TTamu · Meja 3 TAMU | 8 | 1.504 | — |
The native app (apps/kaki-pos) is portrait-only, has no tablet layout, and is built for phones. Today its bar has five tabs: Today, Schedule, Check-in, Coaches and Messages (the last two are "Not built yet" placeholders). The redesign proposes Hari ini · Denah · Jadwal · Check-in · Lainnya, so the bar never points at an empty screen, and moves Pelatih, Pesan, Tab, Open play, Riwayat and Peringkat under Lainnya once they are built. The components are the same as on the iPad: tiles, bill, timeline, sheets.
At phone width the timeline turns into a list per hour, and the tables-per-kind filter stays. Tapping a row opens the same detail sheet as the iPad.