036 - Promo carousel + Upcoming Match restructure
- Tanggal: 2026-08-15
- Chat/topik: Tukar banner promo Home jadi carousel gambar, pindahkan Upcoming Match ke atas Quick Actions, gating tombol Play, cron no-show/no-checkout
Ringkasan perubahan
- Header Home: kartu "Upcoming Match" di header diganti carousel gambar promo (looping, auto-advance ~4.5s, swipeable, dot indicator). Hanya promo dengan
image_urlyang tampil di carousel, diurutkandisplay_order ASC, created_at ASC. Tanpa promo bergambar aktif → slot disembunyikan, header lebih ringkas. - Kartu "Upcoming Match" (real booking terdekat) dipindah jadi section tersendiri di atas "Quick Actions", tombol "Play Live" diganti label "Play" dan hanya tampil ketika waktu sekarang berada dalam window
[start_time, end_time)booking tersebut (dicek ulang tiap ~30 detik). Tanpa jam main aktif → kartu tetap tampil info, tombol disembunyikan. - Section "Promotions" (banner teks/emoji, statis) di bagian bawah Home dihapus — sudah tercover carousel gambar di header, menghindari duplikasi.
- Backend:
Promoentity tambahimage_url+display_order;GET /content/homeactive_promos[]diurutkan sesuaidisplay_order. - Backend: cron
CleanupPastBookings(ticker 5 menit, sudah ada) diperluas — sebelumnya hanya auto-complete booking yang lewat waktu dan tidak pernah check-in (no-show); sekarang juga cover booking yang sudah check-in tapi lupa check-out. Status tetapCompleted(tidak ada enum baru). - Admin: form Promo (
/customers/promos) tambah field Image URL + Display Order untuk mengatur carousel.
Impact bisnis
Home terasa lebih hidup dengan visual promo asli (bukan teks statis), dan member langsung tahu kapan CTA "Play" relevan (bukan selalu muncul walau belum jam main). Cron yang diperluas menutup gap operasional: slot yang lupa di-checkout di kasir tidak lagi menggantung status "Confirmed" selamanya.
Epic / User stories
- Epic: Home delight + booking lifecycle hygiene
- US: Sebagai member, saya ingin melihat promo dalam bentuk gambar menarik di Home, agar saya lebih tergerak menjajal promo tersebut.
- US: Sebagai member, saya ingin tombol "Play" hanya muncul saat memang sudah waktunya main, agar saya tidak salah paham bisa langsung mulai match di luar jam booking.
- US: Sebagai staff/ops, saya ingin booking yang lupa check-out otomatis ditutup setelah jam selesai, agar data status booking tetap bersih tanpa intervensi manual.
User terdampak
Member (tab Home, Android & iOS). Staff/ops secara tidak langsung (kebersihan status booking).
Aplikasi terdampak
- mpc-be — PERLU (v1.9):
Promo.ImageURL/DisplayOrder,GetActivePromosordering,CleanupPastBookingsextended,api_contract.mdupdate. - mpc-admin — PERLU (v0.10): Prisma
Promo.imageUrl/displayOrder, form fields. - mpc-android — PERLU (v1.19, bullet tambahan chat ini):
PromoCarousel, relokasi Upcoming Match, gating Play, hapus section Promotions lama. - mpc-ios — PERLU (v1.8, bullet tambahan chat ini, parity 1:1).
- mpc-web — TIDAK — belum ada Home screen live.
- mpc-be-pos — TIDAK — semantik status booking tidak berubah (tetap
Completed, tidak ada enum baru). - mpc-owner / ksr-sync / mpc-crm / mpc-pos — TIDAK — tidak terkait domain ini.
Perubahan schema
Ya — lihat mpc-dev/docs/schema/migration/025-promo-image-carousel.md:
ALTER TABLE promos ADD COLUMN IF NOT EXISTS image_url text;
ALTER TABLE promos ADD COLUMN IF NOT EXISTS display_order integer NOT NULL DEFAULT 0;
Update lanjutan (chat yang sama) — seed 5 promo bergambar + hosting gambar
- Konten: 5 promo baru dibuat langsung di DB produksi (bukan cuma migration kosong) supaya carousel Home benar-benar terlihat: 3 Court (
Morning Court Privilege20% before 9AM,Weekend Doubles SpecialRp75.000 off,Off-Peak Court Hours15% 12-4PM) + 2 F&B (Court Cafe Refresh20% F&B,Match Day Fuel ComboRp30.000 off combo),display_order0-4. - Gambar: 5 banner digenerate (AI image generation) dengan style konsisten — background putih polos, seluruh ilustrasi/tipografi memakai warna primary tema Elegant (
#426766), ikon line-art tipis relevan tema (raket/bola, jam+net, cangkir kopi, piring), serif headline + sans-serif subtext, komposisi banner 16:9. - Hosting gambar — keputusan penting: folder
publicdimpc-admin(Next.js) ternyata di belakang auth (semua path termasuk file statis di-redirect ke/login), jadi tidak bisa dipakai untuk hotlink gambar yang harus diakses langsung oleh app member tanpa login. Solusi: tambah route publik baruGET /static/*dimpc-be(Ginrouter.Static("/static", "./static"), tanpa auth) yang serve file langsung dari disk server (/opt/mpc-be/static/).image_urlpromo memakaihttps://mpc-be.glenrh.com/static/promos/<file>.png. - Ini bukan endpoint upload — file gambar ditaruh manual via
rsyncke server saat deploy; belum ada UI upload di admin (form Promo masih inputimage_urlsebagai teks URL, sesuai desain awal di plan). - Deploy: rebuild
mpc-be(binary + folderstatic/promos), restart service; migration025yang sebelumnya cuma ada di file dijalankan manual kempcdbproduksi (AUTO_MIGRATE=falsedi server, tidak otomatis).
Perubahan kode
mpc-be/internal/promos/entity.go:Promo.ImageURL *string,Promo.DisplayOrder int.mpc-be/internal/promos/repository.go:GetActivePromosorder bydisplay_order ASC, created_at ASC.mpc-be/internal/bookings/repository.go:CleanupPastBookings— dropchecked_in_at IS NULL, ganti kondisi jadichecked_out_at IS NULL(cover no-show + lupa check-out sekaligus).mpc-dev/docs/api/be.md: dokumentasiimage_url/display_orderpadaactive_promos[]+ endpoint publikGET /static/*.mpc-be/cmd/api/main.go: tambahrouter.Static("/static", "./static"); folderstatic/promos/isi 5 gambar promo (juga di-rsync ke/opt/mpc-be/static/promos/di server).mpc-admin/.../prisma/schema.prisma:Promo.imageUrl,Promo.displayOrder.mpc-admin/.../src/app/(dashboard)/customers/promos/PromoForm.tsx+src/actions/promos.ts: input + persist field baru.- Android:
data/remote/Dtos.kt(PromoDto.imageUrl/displayOrder),data/Models.kt(BookingCourtEntry/BookingHistoryItem.startTimeIso/endTimeIso),data/remote/Mappers.kt(parseIsoInstant),feature/home/HomeSections.kt(PromoCarousel, hapusPromoBanner),feature/home/HomeScreen.kt(relokasi Upcoming Match, gating Play, re-indexAppearIn, hapus section Promotions). - iOS:
Data/Remote/Dtos.swift(PromoDto.imageUrl/displayOrder),Data/Models.swift(BookingCourtEntry/BookingHistoryItem.startTimeIso/endTimeIso),Data/Remote/Mappers.swift(parseIsoInstant),Data/Repo/MpcRepository.swift(populate ISO fields),Features/Home/HomeSections.swift(PromoCarousel, hapusPromoBanner),Features/Home/HomeScreen.swift(relokasi Upcoming Match, gating Play via ticker 30s, hapus section Promotions).
Update lanjutan (chat yang sama) — sembunyikan Upcoming Match + pull-to-refresh
- Upcoming Match disembunyikan: setelah realistic promo images live, kartu "Upcoming Match" (di body, di atas Quick Actions) untuk sementara disembunyikan lagi via flag (
SHOW_UPCOMING_MATCH_CARDAndroid /showUpcomingMatchCardiOS, defaultfalse) — mudah dinyalakan kembali tanpa perlu re-implement, tidak menghapus kode. Carousel promo di header tetap tampil seperti biasa (tidak terpengaruh flag ini). - Pull-to-refresh — Home: Android pakai
PullToRefreshBox(Material3,@OptIn(ExperimentalMaterial3Api)) membungkus konten Home; refresh memanggil ulang fungsi load yang sama dipakai saat pertama masuk (home content, open matches, upcoming booking). iOS pakai.refreshablenative padaScrollViewHome, memanggil ulangloadHome(). - Pull-to-refresh — Time Slot Booking: Android pakai
PullToRefreshBoxmembungkusLazyColumn; refresh memanggil ulanggetCourts()+ availability fetch untuk kombinasi tanggal/durasi/court/mode yang sedang aktif, tanpa mereset pilihan slot/plan yang sedang dipilih (beda dari reload otomatis saat user ganti tanggal/durasi/court, yang tetap reset pilihan seperti sebelumnya). iOS pakai.refreshablepadaScrollViewdalamTimeSlotScreen, fungsirefresh()baru yang mirror logika Android (refetch courts + availability tanpa resetselectedPlan/expandedStartHour). - Tidak ada perubahan API/schema pada update ini — murni UI state (flag hide) + client-side refetch pola yang sudah ada.
Aplikasi terdampak (update ini)
- mpc-android — PERLU (v1.19, bullet tambahan chat ini): hide Upcoming Match card, pull-to-refresh Home + Time Slot.
- mpc-ios — PERLU (v1.8, bullet tambahan chat ini, parity 1:1): sama seperti Android.
- mpc-be / mpc-be-pos / mpc-admin / mpc-web / mpc-owner / ksr-sync / mpc-crm / mpc-pos — TIDAK, tidak ada perubahan API/schema/backend pada update ini.
Update lanjutan (chat yang sama) — carousel dibuat lebih tinggi + foto promo diganti total
User feedback: gambar promo generated round pertama masih dinilai jelek/kurang meyakinkan, dan carousel-nya kurang tinggi dibanding contoh referensi (banner promo cinema chain XXI/m.tix). Diminta cek project referensi mtix_lumiere (Flutter, cinema chain) untuk nilai tinggi banner yang dipakai.
- Riset referensi:
mtix_lumiere/lib/views/pages/home/widget/section/info_section.dart— banner carousel-nya pakai formulaheight: (MediaQuery.of(context).size.width - 32) / 3.6 + 96(dp), yang secara efektif adalah rasio gambar ~3.6:1 plus buffer chrome tetap 96dp. Untuk lebar device umum (360-430dp) hasilnya ~185-210dp — jauh lebih tinggi dari carousel kita sebelumnya (fixed 128dp). - Keputusan desain: bukan replikasi literal formula (angka "+96" spesifik ke layout mereka, tidak otomatis relevan ke layout kita), tapi ambil intinya — carousel di-resize dari fixed height jadi aspect-ratio-driven (16:9), aspek rasio wide terdekat yang didukung tool image-generation kita, dan kebetulan hasil tingginya sangat mendekati angka mtix pada lebar device umum (dalam ~10dp) sekaligus otomatis adaptif ke semua ukuran device. Header Home yang membungkus carousel disesuaikan agar tinggi totalnya ikut dinamis (bukan konstanta
228.dp/228tetap seperti sebelumnya) —100.dpchrome tetap (padding + greeting row + spacer) + tinggi carousel yang dihitung dari lebar layar. - Root-cause "gambar sampah": gambar promo lama di-generate di rasio ~1.5:1 (dekat persegi), lalu di-crop
ContentScale.Crop/.aspectRatio(.fill)paksa ke box yang jauh lebih wide/pendek (128dp height) — bagian besar foto (termasuk judul teks yang dibakar ke gambar) ter-crop habis, jadi kelihatan pecah/acak. Setelah box carousel di-resize ke 16:9, foto baru digenerate juga di 16:9 supaya pas tanpa crop agresif. - Refactor besar — teks tidak lagi dibakar ke gambar: setelah 2x percobaan image-generation untuk banner bergaya "advertising" (logo + headline + CTA pill ala referensi XXI) berulang kali menghasilkan flyer neon/cluttered (bertentangan dengan
ux-high-class.mdc) atau bahkan kebocoran teks/brand pihak ketiga yang tidak diminta, diputuskan pivot: generate foto murni fotografi tanpa teks/watermark, lalu judul promo + badge diskon digambar native oleh app (ComposeText+badge Android, SwiftUIText+badge iOS) di atas scrim gradient gelap di bagian bawah foto — berdasarkan data livePromo.name/type/value(bukan dibakar ke PNG). Konsekuensi baik: admin bisa ganti nama/harga promo dari admin site tanpa perlu generate ulang gambar; typography & warna badge otomatis konsisten brand (AppColors.Secondary/Primary, font Playfair). Dot indicator carousel dipindah ke pojok kanan-atas supaya tidak numpuk dengan teks di kiri-bawah. - 5 foto promo diganti total (real photography, tanpa teks, 16:9):
promo-court-morning.png(aksi rally pagi),promo-court-weekend.png(dua teman jalan bareng bawa raket ke court, senyum),promo-court-offpeak.png(pemain solo di court kosong malam),promo-fnb-cafe.png(meja cafe elegan),promo-fnb-matchday.png(combo burger+fries+minuman). File di-rsync overwrite ke/opt/mpc-be/static/promos/(path/image_urltidak berubah, jadi tidak perlu update DB). - Catatan proses image-generation: percobaan foto "high-five doubles" berulang kali gagal — model image-gen secara konsisten menghasilkan foto mirip liputan turnamen Premier Padel asli (nama pemain, logo sponsor Qatar Airways/Alpine/dsb ikut muncul) ketika prompt menyebut "padel doubles high-five", kemungkinan bias dataset. Diselesaikan dengan mengganti konsep visual (dua orang jalan santai bawa raket, bukan gestur high-five) supaya tidak memicu pattern tersebut.
- Tidak ada perubahan API/schema — murni UI sizing (aspect ratio, bukan fixed dp) + rendering (badge native, bukan baked-in) + aset gambar.
Aplikasi terdampak (update ini)
- mpc-android — PERLU:
PromoCarouselaspect-ratio 16:9 + overlay native (HomeSections.kt), header height dinamis (HomeScreen.kt). - mpc-ios — PERLU (parity 1:1): sama,
HomeSections.swift+HomeScreen.swift. - mpc-be — TIDAK ada perubahan kode; hanya replace file gambar di
static/promos/(url sama, konten beda) via rsync ke server produksi. - mpc-admin / mpc-be-pos / mpc-web / mpc-owner / ksr-sync / mpc-crm / mpc-pos — TIDAK terkait.
Update lanjutan (chat yang sama) — carousel dikecilkan 24px, tanpa ganti gambar
User feedback: carousel 16:9 masih dinilai kurang pas ukurannya (kebesaran), minta dikecilkan 24 pixel — eksplisit tanpa generate ulang gambar, cukup crop bagian bawah foto yang sudah ada.
- Implementasi: bukan lagi
Modifier.aspectRatio(16f/9f)mandiri di dalamPromoCarousel. Tinggi sekarang dihitung sekali diHomeScreen(carouselHeightDpAndroid /carouselHeightiOS) =(lebar layar - 48) / (16/9) - 24.dp/pt, diteruskan sebagai parameterheighteksplisit kePromoCarousel(Android: lewatHomeHeader→PromoCarousel(height = ...); iOS:HomeHeader.carouselHeight→PromoCarousel(height:)). Header total (maxHeaderDp/maxHeader) otomatis ikut 24px lebih pendek karena pakai nilai yang sama. - Tidak ada aset baru: 5 file foto promo di
static/promos/tidak diubah sama sekali. KarenaAsyncImage/SmartImagememakaiContentScale.Crop/fill-crop, box yang 24px lebih pendek otomatis memotong sedikit lebih banyak dari foto yang sama (efeknya crop tambahan di bagian bawah/atas foto, tidak ada re-generate atau re-upload). - Verifikasi: build Android (
compileDebugKotlin) + iOS (xcodebuild) sukses; APK debug diinstall ulang di device tanpa clear data (tidak logout) dan screenshot dicek — carousel terlihat lebih pendek, promo "Off-Peak Court Hours" (15% OFF) tetap terbaca jelas, "Quick Actions" langsung kelihatan lebih banyak tanpa scroll. - Tidak ada perubahan API/schema/backend — murni sizing UI di kedua app.
Aplikasi terdampak (update ini)
- mpc-android — PERLU:
PromoCarousel(height:)eksplisit +HomeHeaderplumbing (HomeSections.kt,HomeScreen.kt). - mpc-ios — PERLU (parity 1:1):
PromoCarousel(height:)+HomeHeader.carouselHeight(HomeSections.swift,HomeScreen.swift). - mpc-be / mpc-admin / mpc-be-pos / mpc-web / mpc-owner / ksr-sync / mpc-crm / mpc-pos — TIDAK terkait, tidak ada perubahan.