Initial commit
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
PORT=3000
|
||||
ADMIN_TOKEN=admin-dev-token
|
||||
DEVICE_TOKEN=device-dev-token
|
||||
TRACE_HEADER=x-request-id
|
||||
IDEMPOTENCY_TTL_MS=300000
|
||||
INTEGRATION_WEBHOOK_SECRET=dev-callback-secret
|
||||
MQTT_PUBLISH_FORCE_FAIL_ALL=false
|
||||
MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS=
|
||||
MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS=15000
|
||||
|
||||
# PostgreSQL settings
|
||||
PGHOST=127.0.0.1
|
||||
PGPORT=5432
|
||||
PGUSER=postgres
|
||||
PGPASSWORD=postgres
|
||||
PGDATABASE=qris_soundbox_platform
|
||||
# Optional alternative:
|
||||
# DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/qris_soundbox_platform
|
||||
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
.env
|
||||
79
01-executive-blueprint.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Executive Blueprint - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Ringkasan
|
||||
Platform ini dirancang sebagai **Merchant Aggregator QRIS + Payment Orchestration + Soundbox/TMS Platform**.
|
||||
|
||||
Sistem menangani:
|
||||
- onboarding merchant
|
||||
- koneksi ke bank, issuer, acquirer, atau partner QRIS
|
||||
- konfigurasi SNAP BI / QRIS integration
|
||||
- pemrosesan transaksi QRIS statis dan dinamis
|
||||
- ledger internal dan perhitungan hak merchant
|
||||
- settlement/disbursement ke merchant
|
||||
- manajemen device soundbox lintas vendor
|
||||
- notifikasi pembayaran realtime ke device
|
||||
- dashboard admin, ops, finance, dan merchant
|
||||
|
||||
## 2. Objective
|
||||
Tujuan utama sistem:
|
||||
1. mendukung banyak jenis soundbox tanpa vendor lock-in
|
||||
2. memusatkan kontrol payment dan orkestrasi payout di backend perusahaan, sementara pencairan diproses sesuai rekening/flow payout milik merchant (non-tersentral pada MVP)
|
||||
3. memastikan transaksi, ledger, dan settlement dapat diaudit
|
||||
4. memberi notifikasi realtime ke merchant melalui soundbox
|
||||
5. menyediakan fondasi yang bisa dicicil implementasinya per fase
|
||||
|
||||
## 3. Prinsip Kunci
|
||||
- backend adalah pusat logika sistem
|
||||
- device adalah channel, bukan pusat source of truth
|
||||
- callback/webhook payment partner adalah sumber status transaksi eksternal
|
||||
- ledger internal adalah sumber kebenaran finansial internal (pending settlement, fee, dan entitlement)
|
||||
- settlement/payout dipisah dari payment event dan dapat dikoordinasikan via partner payout merchant
|
||||
- device support berbasis capability, bukan hardcode per vendor
|
||||
- TMS adalah domain inti, bukan tambahan
|
||||
- semua aksi kritikal harus memiliki audit trail
|
||||
|
||||
## 4. Tipe Device yang Didukung
|
||||
### 4.1 Soundbox Static
|
||||
- QR merchant statis
|
||||
- transaksi masuk via callback partner
|
||||
- backend melakukan mapping ke merchant/outlet/terminal/device
|
||||
- notifikasi sukses dikirim ke device via MQTT
|
||||
|
||||
### 4.2 Soundbox Dynamic MQTT-only
|
||||
- device membuat request QR dynamic via MQTT
|
||||
- backend memproses request dan mengirim hasil via MQTT
|
||||
- payment success tetap berasal dari callback partner
|
||||
- notifikasi sukses ke device via MQTT
|
||||
|
||||
### 4.3 Soundbox Dynamic API-direct
|
||||
- device dapat hit API backend langsung
|
||||
- request-response create QR dilakukan via API
|
||||
- payment success tetap berasal dari callback partner
|
||||
- notifikasi sukses ke device via MQTT
|
||||
|
||||
## 5. Domain Inti
|
||||
- Merchant & Onboarding
|
||||
- Payment Integration
|
||||
- Transaction
|
||||
- Ledger & Settlement
|
||||
- Device/TMS
|
||||
- Notification
|
||||
- Operations & Governance
|
||||
|
||||
## 6. Keputusan Arsitektur Final
|
||||
1. one unified backend
|
||||
2. support multi-protocol ingress untuk device
|
||||
3. use capability-based routing untuk flow device
|
||||
4. use QRIS/payment callback as external payment status trigger
|
||||
5. use internal ledger for merchant payable and settlement
|
||||
6. support admin portal dan merchant portal terpisah
|
||||
7. design all critical integration points with idempotency and auditability
|
||||
|
||||
## 7. Hasil yang Diharapkan dari Paket Ini
|
||||
Dokumen ini menjadi fondasi untuk:
|
||||
- diskusi arsitektur
|
||||
- desain UI/UX
|
||||
- breakdown sprint engineering
|
||||
- definisi database awal
|
||||
- definisi API dan kontrak MQTT
|
||||
- roadmap delivery bertahap
|
||||
126
02-system-architecture.md
Normal file
@ -0,0 +1,126 @@
|
||||
# System Architecture - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Posisi Sistem
|
||||
Platform diposisikan sebagai:
|
||||
- merchant aggregator
|
||||
- payment orchestrator
|
||||
- device orchestration platform untuk soundbox lintas vendor
|
||||
|
||||
## 2. Context Eksternal
|
||||
### Pihak eksternal
|
||||
- Merchant
|
||||
- Customer payer
|
||||
- Bank / Issuer / Acquirer / Aggregator QRIS
|
||||
- SNAP BI / QRIS integration provider
|
||||
- Device vendor / soundbox devices
|
||||
|
||||
## 3. Layer Arsitektur
|
||||
|
||||
### Layer 1. Experience Layer
|
||||
- Admin Web Portal
|
||||
- Merchant Web Portal
|
||||
- Ops / Finance Dashboard
|
||||
- Device UI / Soundbox UI
|
||||
|
||||
### Layer 2. Access Layer
|
||||
- API Gateway
|
||||
- MQTT Broker
|
||||
- Webhook Receiver
|
||||
- Auth Gateway
|
||||
|
||||
### Layer 3. Device Abstraction Layer
|
||||
- MQTT Adapter
|
||||
- API Adapter
|
||||
- Capability Resolver
|
||||
- Command Router
|
||||
- Notification Router
|
||||
- Payload Normalizer
|
||||
|
||||
### Layer 4. Core Business Layer
|
||||
- Merchant Service
|
||||
- Outlet Service
|
||||
- Terminal Service
|
||||
- Device/TMS Service
|
||||
- QRIS Service
|
||||
- Transaction Service
|
||||
- Ledger Service
|
||||
- Settlement Service
|
||||
- Notification Service
|
||||
- Reconciliation Service
|
||||
- Audit Service
|
||||
|
||||
### Layer 5. Integration Layer
|
||||
- Bank Connector
|
||||
- QRIS Partner Connector
|
||||
- SNAP BI Connector
|
||||
- Payout / Disbursement Connector
|
||||
|
||||
### Layer 6. Data Layer
|
||||
- Merchant Database
|
||||
- Transaction Database
|
||||
- Ledger Database
|
||||
- Device Database
|
||||
- Audit Log Store
|
||||
- Queue / Event Store
|
||||
- Configuration Store
|
||||
|
||||
## 4. Arsitektur Logis
|
||||
```text
|
||||
Merchant/Admin/Finance/Device
|
||||
|
|
||||
v
|
||||
Access Layer (API Gateway / MQTT Broker / Webhook Receiver)
|
||||
|
|
||||
v
|
||||
Device Abstraction + Auth + Capability Resolver
|
||||
|
|
||||
v
|
||||
Core Business Services
|
||||
|
|
||||
+--> Payment/QRIS Integrations
|
||||
+--> Ledger & Settlement
|
||||
+--> TMS & Notification
|
||||
|
|
||||
v
|
||||
Databases / Event Store / Audit Logs
|
||||
```
|
||||
|
||||
## 5. Prinsip Routing Device
|
||||
### Static device
|
||||
- tidak perlu create QR request
|
||||
- device mainly menerima payment notification
|
||||
|
||||
### Dynamic MQTT-only device
|
||||
- uplink via MQTT
|
||||
- downlink via MQTT
|
||||
- async success via MQTT
|
||||
|
||||
### Dynamic API-direct device
|
||||
- create QR via API
|
||||
- async success via MQTT
|
||||
- ops/config dapat via API atau MQTT tergantung capability
|
||||
|
||||
## 6. Prinsip Integrasi Payment
|
||||
- semua request ke bank/partner dilakukan oleh backend kita
|
||||
- semua callback payment diterima oleh backend kita
|
||||
- device tidak pernah menjadi source of truth transaksi
|
||||
- status transaksi final berasal dari backend setelah verifikasi callback atau reconciliation
|
||||
|
||||
## 7. Prinsip Finansial
|
||||
- dana customer dihimpun sesuai flow partner/integrasi payout merchant (tanpa vault settlement pusat di awal MVP)
|
||||
- ledger internal mencatat hak merchant, fee, penyesuaian, dan status reconciliation payout
|
||||
- settlement/disbursement dieksekusi sebagai domain terpisah dari transaction event
|
||||
|
||||
## 8. Prinsip Operasional
|
||||
- semua perubahan entity penting harus diaudit
|
||||
- onboarding merchant harus punya status workflow
|
||||
- device harus punya heartbeat, last seen, config version, dan binding history
|
||||
- delivery notification harus dapat di-track dan di-retry
|
||||
|
||||
## 9. NFR Utama
|
||||
- idempotency untuk create QR, callback, payout, notification
|
||||
- observability untuk payment, device, settlement
|
||||
- auditability untuk finance dan merchant changes
|
||||
- multi-tenant isolation
|
||||
- secure device auth
|
||||
- HA untuk broker, callback receiver, dan transaction services
|
||||
132
03-domain-modules.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Domain Modules - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Merchant & Onboarding Domain
|
||||
### Tanggung jawab
|
||||
- merchant registration
|
||||
- KYC / KYB workflow
|
||||
- document collection
|
||||
- approval / rejection
|
||||
- fee profile assignment
|
||||
- settlement destination registration (bank/partner payout reference)
|
||||
- outlet and terminal creation
|
||||
|
||||
### Submodule
|
||||
- Merchant Registry
|
||||
- Onboarding Workflow
|
||||
- Document Service
|
||||
- Fee Profile Management
|
||||
- Settlement Destination Management
|
||||
- Outlet Management
|
||||
- Terminal Management
|
||||
|
||||
## 2. Payment Integration Domain
|
||||
### Tanggung jawab
|
||||
- partner QRIS integration
|
||||
- bank integration
|
||||
- SNAP BI configuration
|
||||
- callback handling
|
||||
- signature verification
|
||||
- reconciliation feed import/export
|
||||
|
||||
### Submodule
|
||||
- Partner Credential Vault
|
||||
- QRIS Connector
|
||||
- SNAP BI Connector
|
||||
- Callback Receiver
|
||||
- Signature Validator
|
||||
- Reconciliation Importer
|
||||
|
||||
## 3. Transaction Domain
|
||||
### Tanggung jawab
|
||||
- create transaction
|
||||
- static vs dynamic QR transaction lifecycle
|
||||
- transaction status management
|
||||
- timeout, expiry, and cancel flow
|
||||
- idempotency control
|
||||
|
||||
### Submodule
|
||||
- Transaction Registry
|
||||
- Transaction State Machine
|
||||
- QR Generation Orchestrator
|
||||
- Transaction Query Service
|
||||
- Refund/Dispute Hook Interface
|
||||
|
||||
## 4. Ledger & Settlement Domain
|
||||
### Tanggung jawab
|
||||
- mencatat efek finansial transaksi
|
||||
- menghitung fee platform dan merchant payable
|
||||
- membuat settlement batch
|
||||
- menginisiasi payout request ke partner payout (bukan menampung dana di perusahaan)
|
||||
- menangani payout failure
|
||||
- support reconciliation
|
||||
|
||||
### Submodule
|
||||
- Ledger Entry Service
|
||||
- Merchant Balance Calculator
|
||||
- Fee Engine
|
||||
- Settlement Batch Service
|
||||
- Payout Execution Service
|
||||
- Adjustment Service
|
||||
- Reconciliation Service
|
||||
|
||||
## 5. Device / TMS Domain
|
||||
### Tanggung jawab
|
||||
- device registration
|
||||
- provisioning
|
||||
- auth credential management
|
||||
- merchant/outlet/terminal binding
|
||||
- heartbeat and presence
|
||||
- config management
|
||||
- command delivery
|
||||
- capability registry
|
||||
- firmware and compatibility tracking
|
||||
|
||||
### Submodule
|
||||
- Device Registry
|
||||
- Provisioning Service
|
||||
- Device Binding Service
|
||||
- Heartbeat Service
|
||||
- Device Auth Service
|
||||
- Capability Profile Service
|
||||
- Config Management Service
|
||||
- Command Center Service
|
||||
- Firmware Tracking Service
|
||||
|
||||
## 6. Notification Domain
|
||||
### Tanggung jawab
|
||||
- payment success notification
|
||||
- audio/display payload preparation
|
||||
- routing based on device capability
|
||||
- retry and ack tracking
|
||||
- duplicate suppression
|
||||
|
||||
### Submodule
|
||||
- Notification Orchestrator
|
||||
- Payload Formatter
|
||||
- Delivery Tracker
|
||||
- Retry Worker
|
||||
- Ack Processor
|
||||
|
||||
## 7. Operations & Governance Domain
|
||||
### Tanggung jawab
|
||||
- admin portal actions
|
||||
- audit logging
|
||||
- alerting
|
||||
- role-based access
|
||||
- compliance visibility
|
||||
- fraud / risk checks
|
||||
|
||||
### Submodule
|
||||
- Admin Management
|
||||
- RBAC Service
|
||||
- Audit Log Service
|
||||
- Monitoring & Alerting
|
||||
- Risk/Fraud Rule Service
|
||||
- Incident Tracker
|
||||
|
||||
## 8. Domain Boundary Notes
|
||||
- Payment Integration tidak boleh menampung logic device
|
||||
- Device/TMS tidak boleh menampung source of truth transaksi
|
||||
- Ledger & Settlement tidak boleh bergantung pada notifikasi device
|
||||
- Notification hanya boleh membaca transaction state final/eligible
|
||||
- Admin UI harus memakai policy dan RBAC lintas domain
|
||||
93
04-device-flows.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Device Flows - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Soundbox Static Flow
|
||||
### Karakteristik
|
||||
- QR merchant bersifat statis
|
||||
- customer input nominal di aplikasi bank/e-wallet
|
||||
- device menerima notifikasi hasil transaksi
|
||||
|
||||
### Sequence
|
||||
1. customer scan QR statis merchant
|
||||
2. customer input nominal dan submit pembayaran
|
||||
3. partner/bank memproses pembayaran
|
||||
4. callback payment masuk ke backend
|
||||
5. backend verifikasi signature dan status
|
||||
6. transaction state diupdate
|
||||
7. ledger internal diupdate
|
||||
8. backend lookup binding merchant -> outlet -> terminal -> device
|
||||
9. notification event dibuat
|
||||
10. MQTT push ke device
|
||||
11. device memainkan audio dan/atau menampilkan nominal
|
||||
|
||||
### Catatan
|
||||
- challenge utama adalah mapping transaksi ke device yang tepat
|
||||
- perlu binding yang presisi dan fallback jika device offline
|
||||
|
||||
## 2. Soundbox Dynamic MQTT-only Flow
|
||||
### Karakteristik
|
||||
- device tidak bisa hit API langsung
|
||||
- request create QR lewat MQTT
|
||||
- response dan success notification lewat MQTT
|
||||
|
||||
### Sequence
|
||||
1. merchant input nominal di device
|
||||
2. device publish request create QR ke topic uplink
|
||||
3. backend MQTT adapter menerima request
|
||||
4. device auth dan capability check dilakukan
|
||||
5. backend validasi nominal, merchant binding, idempotency key
|
||||
6. backend request QR dynamic ke partner QRIS
|
||||
7. transaction dibuat dengan status pending/awaiting_payment
|
||||
8. response QR payload dipublish ke topic device
|
||||
9. device render QR di layar
|
||||
10. customer scan dan bayar
|
||||
11. callback payment masuk ke backend
|
||||
12. backend verifikasi dan update transaction
|
||||
13. ledger internal diupdate
|
||||
14. notification success dipublish ke device
|
||||
15. device memainkan audio sukses
|
||||
|
||||
### Catatan
|
||||
- wajib ada request_id / correlation_id
|
||||
- retry device tidak boleh menimbulkan double create
|
||||
- MQTT handler jangan jadi pusat logic, hanya ingress adapter
|
||||
|
||||
## 3. Soundbox Dynamic API-direct Flow
|
||||
### Karakteristik
|
||||
- device dapat call API backend langsung
|
||||
- create QR dilakukan secara sinkron via API
|
||||
- success notification tetap realtime via MQTT
|
||||
|
||||
### Sequence
|
||||
1. merchant input nominal di device
|
||||
2. device call API create dynamic QR
|
||||
3. backend auth device dan validasi payload
|
||||
4. backend check capability dan idempotency
|
||||
5. backend request QR dynamic ke partner QRIS
|
||||
6. transaction dibuat dengan status pending
|
||||
7. response API berisi QR payload + expiry + transaction reference
|
||||
8. device render QR
|
||||
9. customer scan dan bayar
|
||||
10. callback payment masuk ke backend
|
||||
11. backend verifikasi dan update transaction
|
||||
12. ledger internal diupdate
|
||||
13. success notification dikirim via MQTT
|
||||
14. device memainkan audio sukses
|
||||
|
||||
### Catatan
|
||||
- ini flow paling bersih untuk engineering
|
||||
- request-response lebih natural, observability lebih baik
|
||||
|
||||
## 4. Heartbeat Flow
|
||||
1. device kirim heartbeat secara periodik
|
||||
2. backend update last_seen_at
|
||||
3. backend simpan health metrics yang tersedia
|
||||
4. status device diturunkan menjadi online/stale/offline/degraded
|
||||
5. ops dashboard menampilkan alert bila perlu
|
||||
|
||||
## 5. Provisioning Flow
|
||||
1. device didaftarkan di TMS
|
||||
2. credential/token/certificate dibuat
|
||||
3. device dibinding ke merchant/outlet/terminal
|
||||
4. config profile ditetapkan
|
||||
5. device mengambil config awal atau dikirimkan via channel yang tersedia
|
||||
6. device aktif dan mulai heartbeat
|
||||
194
05-api-contract-draft.md
Normal file
@ -0,0 +1,194 @@
|
||||
# API Contract Draft - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Prinsip API
|
||||
- gunakan HTTPS
|
||||
- semua endpoint device memakai auth khusus device
|
||||
- semua endpoint admin/merchant memakai RBAC
|
||||
- endpoint create yang sensitif wajib support idempotency key
|
||||
- response harus punya request_id / trace_id
|
||||
|
||||
## 2. Admin / Merchant APIs
|
||||
|
||||
### Merchant
|
||||
- `POST /admin/merchants`
|
||||
- `GET /admin/merchants`
|
||||
- `GET /admin/merchants/{merchantId}`
|
||||
- `PATCH /admin/merchants/{merchantId}`
|
||||
- `POST /admin/merchants/{merchantId}/approve`
|
||||
- `POST /admin/merchants/{merchantId}/reject`
|
||||
|
||||
#### Create Merchant
|
||||
- `POST /admin/merchants`
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"legal_name": "Toko Indo",
|
||||
"brand_name": "Toko Indo",
|
||||
"settlement_account_reference": "bank:9876543210",
|
||||
"settlement_account_type": "merchant_bank_account",
|
||||
"payout_mode": "merchant_direct",
|
||||
"fee_profile_id": "fee_basic"
|
||||
}
|
||||
```
|
||||
|
||||
`payout_mode`:
|
||||
- `merchant_direct` (default): payout mengikuti rekening/referensi milik merchant.
|
||||
- `manual`: payout dilakukan manual/offline oleh tim operasi.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "m_123",
|
||||
"merchant_code": "m_123abc",
|
||||
"legal_name": "Toko Indo",
|
||||
"brand_name": "Toko Indo",
|
||||
"settlement_account_reference": "bank:9876543210",
|
||||
"settlement_account_type": "merchant_bank_account",
|
||||
"payout_mode": "merchant_direct"
|
||||
},
|
||||
"request_id": "req_001",
|
||||
"timestamp": "2026-05-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Catatan penting:
|
||||
- Pada fase awal, settlement tidak ditarik ke rekening perusahaan.
|
||||
- Pencairan merchant dilakukan via `payout_account_reference` milik merchant.
|
||||
- Untuk merchant yang belum punya integrasi payout otomatis, gunakan `payout_mode: manual`.
|
||||
|
||||
### Outlet
|
||||
- `POST /admin/merchants/{merchantId}/outlets`
|
||||
- `GET /admin/outlets`
|
||||
- `GET /admin/outlets/{outletId}`
|
||||
- `PATCH /admin/outlets/{outletId}`
|
||||
|
||||
### Terminal
|
||||
- `POST /admin/outlets/{outletId}/terminals`
|
||||
- `GET /admin/terminals`
|
||||
- `GET /admin/terminals/{terminalId}`
|
||||
- `PATCH /admin/terminals/{terminalId}`
|
||||
|
||||
### Device / TMS
|
||||
- `POST /admin/devices`
|
||||
- `GET /admin/devices`
|
||||
- `GET /admin/devices/{deviceId}`
|
||||
- `PATCH /admin/devices/{deviceId}`
|
||||
- `POST /admin/devices/{deviceId}/bind`
|
||||
- `POST /admin/devices/{deviceId}/unbind`
|
||||
- `POST /admin/devices/{deviceId}/commands`
|
||||
- `GET /admin/devices/{deviceId}/commands`
|
||||
- `GET /admin/devices/{deviceId}/commands/{commandId}`
|
||||
- `GET /admin/devices/{deviceId}/heartbeats`
|
||||
- `GET /admin/devices/{deviceId}/notifications`
|
||||
- `POST /admin/seed`
|
||||
|
||||
### Transactions
|
||||
- `GET /admin/transactions`
|
||||
- `GET /admin/transactions/{transactionId}`
|
||||
- `POST /admin/transactions/{transactionId}/retry-notification`
|
||||
|
||||
### Settlements
|
||||
- `GET /admin/settlements`
|
||||
- `GET /admin/settlements/{settlementId}`
|
||||
- `POST /admin/settlements/run`
|
||||
- `POST /admin/settlements/{settlementId}/retry-payout`
|
||||
- Catatan: endpoint ini dipakai untuk batch/reconciliation status; eksekusi payout dilakukan sesuai konfigurasi `payout_mode` merchant.
|
||||
|
||||
## 3. Device APIs
|
||||
|
||||
### Provisioning
|
||||
- `POST /device/provision/activate`
|
||||
- `POST /device/provision/refresh-credential`
|
||||
|
||||
### Heartbeat
|
||||
- `POST /device/heartbeat`
|
||||
|
||||
Sample request:
|
||||
```json
|
||||
{
|
||||
"device_id": "sbx_001",
|
||||
"timestamp": "2026-05-23T10:00:00Z",
|
||||
"firmware_version": "1.0.3",
|
||||
"network_strength": 78,
|
||||
"battery_level": 92,
|
||||
"state": "idle"
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
- `GET /device/config`
|
||||
- `POST /device/config/ack`
|
||||
- `POST /device/commands/ack`
|
||||
|
||||
### Device command payload ack
|
||||
Device mengirim:
|
||||
```json
|
||||
{
|
||||
"device_id": "sbx_001",
|
||||
"command_id": "cmd_123",
|
||||
"status": "delivered",
|
||||
"reason": "ok",
|
||||
"result_payload": {
|
||||
"payment_result": "ok"
|
||||
}
|
||||
}
|
||||
```
|
||||
`status` untuk ACK: `delivered` | `failed` | `timeout`.
|
||||
|
||||
### Dynamic QR create
|
||||
- `POST /device/transactions/dynamic-qr`
|
||||
|
||||
Headers:
|
||||
- `Authorization: Bearer <device-token>`
|
||||
- `Idempotency-Key: <uuid>`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"device_id": "sbx_001",
|
||||
"terminal_id": "term_001",
|
||||
"amount": 50000,
|
||||
"currency": "IDR",
|
||||
"request_id": "req_123"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"request_id": "req_123",
|
||||
"transaction_id": "tx_123",
|
||||
"qr_type": "dynamic",
|
||||
"qr_payload": "000201010212...",
|
||||
"expires_at": "2026-05-23T10:05:00Z",
|
||||
"status": "awaiting_payment"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Merchant Portal APIs
|
||||
- `GET /merchant/me`
|
||||
- `GET /merchant/dashboard/summary`
|
||||
- `GET /merchant/transactions`
|
||||
- `GET /merchant/transactions/{transactionId}`
|
||||
- `GET /merchant/settlements`
|
||||
- `GET /merchant/settlements/{settlementId}`
|
||||
- `GET /merchant/devices`
|
||||
- `GET /merchant/outlets`
|
||||
|
||||
## 5. Webhook Receiver
|
||||
- `POST /integrations/qris/callback`
|
||||
- `POST /integrations/bank/payout-callback`
|
||||
|
||||
## 6. Catatan Error Model
|
||||
Gunakan error code yang konsisten, misalnya:
|
||||
- `DEVICE_UNAUTHORIZED`
|
||||
- `DEVICE_NOT_BOUND`
|
||||
- `DEVICE_CAPABILITY_NOT_SUPPORTED`
|
||||
- `INVALID_AMOUNT`
|
||||
- `DUPLICATE_REQUEST`
|
||||
- `PARTNER_TIMEOUT`
|
||||
- `TRANSACTION_NOT_FOUND`
|
||||
- `SETTLEMENT_NOT_ELIGIBLE`
|
||||
- `MERCHANT_PAYOUT_NOT_CONFIGURED`
|
||||
103
06-mqtt-contract-draft.md
Normal file
@ -0,0 +1,103 @@
|
||||
# MQTT Contract Draft - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Prinsip MQTT
|
||||
- topic harus namespaced per device atau tenant
|
||||
- device hanya boleh publish/subscribe ke topic yang diizinkan
|
||||
- semua pesan request harus punya request_id
|
||||
- semua pesan response harus punya correlation_id
|
||||
- notifikasi harus punya event_id untuk dedup
|
||||
|
||||
## 2. Topic Convention
|
||||
### Uplink dari device
|
||||
- `devices/{deviceId}/uplink/heartbeat`
|
||||
- `devices/{deviceId}/uplink/dynamic-qr/request`
|
||||
- `devices/{deviceId}/uplink/config/ack`
|
||||
- `devices/{deviceId}/uplink/command/ack`
|
||||
|
||||
### Downlink ke device
|
||||
- `devices/{deviceId}/downlink/dynamic-qr/response`
|
||||
- `devices/{deviceId}/downlink/payment/success`
|
||||
- `devices/{deviceId}/downlink/config/push`
|
||||
- `devices/{deviceId}/downlink/command`
|
||||
|
||||
## 3. Heartbeat Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "heartbeat",
|
||||
"device_id": "sbx_001",
|
||||
"timestamp": "2026-05-23T10:00:00Z",
|
||||
"firmware_version": "1.0.3",
|
||||
"state": "idle",
|
||||
"network_strength": 78,
|
||||
"battery_level": 92
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Dynamic QR Request Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "dynamic_qr_request",
|
||||
"request_id": "req_123",
|
||||
"device_id": "sbx_001",
|
||||
"terminal_id": "term_001",
|
||||
"amount": 50000,
|
||||
"currency": "IDR",
|
||||
"created_at": "2026-05-23T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Dynamic QR Response Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "dynamic_qr_response",
|
||||
"correlation_id": "req_123",
|
||||
"transaction_id": "tx_123",
|
||||
"status": "success",
|
||||
"qr_payload": "000201010212...",
|
||||
"expires_at": "2026-05-23T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Payment Success Notification Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "payment_success",
|
||||
"event_id": "evt_123",
|
||||
"transaction_id": "tx_123",
|
||||
"merchant_name": "Toko Berkah",
|
||||
"amount": 50000,
|
||||
"currency": "IDR",
|
||||
"paid_at": "2026-05-23T10:02:10Z",
|
||||
"audio_text": "Pembayaran diterima lima puluh ribu rupiah",
|
||||
"display_text": "Pembayaran diterima Rp50.000"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Config Push Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "config_push",
|
||||
"config_version": 3,
|
||||
"settings": {
|
||||
"volume": 80,
|
||||
"language": "id-ID",
|
||||
"heartbeat_interval_seconds": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Command Payload
|
||||
```json
|
||||
{
|
||||
"message_type": "command",
|
||||
"command_id": "cmd_123",
|
||||
"command_name": "test_speaker",
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. QoS dan Delivery
|
||||
- heartbeat dapat memakai QoS rendah sesuai kebutuhan
|
||||
- payment success sebaiknya QoS lebih tinggi
|
||||
- retained message jangan dipakai untuk notifikasi sukses transaksi
|
||||
- ack command dan ack config harus dicatat
|
||||
222
07-database-schema-draft.md
Normal file
@ -0,0 +1,222 @@
|
||||
# Database Schema Draft - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. merchants
|
||||
- id
|
||||
- merchant_code
|
||||
- legal_name
|
||||
- brand_name
|
||||
- status
|
||||
- onboarding_status
|
||||
- fee_profile_id
|
||||
- settlement_account_reference
|
||||
- settlement_account_type
|
||||
- payout_mode
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
## 2. merchant_documents
|
||||
- id
|
||||
- merchant_id
|
||||
- document_type
|
||||
- document_url
|
||||
- verification_status
|
||||
- created_at
|
||||
|
||||
## 3. outlets
|
||||
- id
|
||||
- merchant_id
|
||||
- outlet_code
|
||||
- name
|
||||
- address
|
||||
- status
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
## 4. terminals
|
||||
- id
|
||||
- outlet_id
|
||||
- terminal_code
|
||||
- qr_mode
|
||||
- partner_reference
|
||||
- status
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
## 5. devices
|
||||
- id
|
||||
- device_code
|
||||
- serial_number
|
||||
- vendor
|
||||
- model
|
||||
- firmware_version
|
||||
- communication_mode
|
||||
- capability_profile_json
|
||||
- auth_method
|
||||
- status
|
||||
- last_seen_at
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
## 6. device_bindings
|
||||
- id
|
||||
- device_id
|
||||
- merchant_id
|
||||
- outlet_id
|
||||
- terminal_id
|
||||
- active_flag
|
||||
- bound_at
|
||||
- unbound_at
|
||||
|
||||
## 7. device_heartbeats
|
||||
- id
|
||||
- device_id
|
||||
- received_at
|
||||
- network_strength
|
||||
- battery_level
|
||||
- firmware_version
|
||||
- state
|
||||
- payload_json
|
||||
|
||||
## 8. device_configs
|
||||
- id
|
||||
- device_id
|
||||
- config_version
|
||||
- config_json
|
||||
- active_flag
|
||||
- created_at
|
||||
|
||||
## 9. device_commands
|
||||
- id
|
||||
- device_id
|
||||
- command_name
|
||||
- command_payload_json
|
||||
- command_status
|
||||
- requested_by
|
||||
- requested_at
|
||||
- ack_at
|
||||
|
||||
## 10. transactions
|
||||
- id
|
||||
- transaction_code
|
||||
- merchant_id
|
||||
- outlet_id
|
||||
- terminal_id
|
||||
- device_id
|
||||
- qr_mode
|
||||
- initiation_mode
|
||||
- partner_reference
|
||||
- amount
|
||||
- currency
|
||||
- status
|
||||
- created_at
|
||||
- paid_at
|
||||
- expired_at
|
||||
- updated_at
|
||||
|
||||
## 11. transaction_events
|
||||
- id
|
||||
- transaction_id
|
||||
- event_type
|
||||
- source
|
||||
- payload_json
|
||||
- created_at
|
||||
|
||||
## 12. notifications
|
||||
- id
|
||||
- transaction_id
|
||||
- device_id
|
||||
- delivery_channel
|
||||
- payload_type
|
||||
- delivery_status
|
||||
- retry_count
|
||||
- ack_status
|
||||
- sent_at
|
||||
- ack_at
|
||||
|
||||
## 13. ledger_entries
|
||||
- id
|
||||
- merchant_id
|
||||
- transaction_id
|
||||
- entry_type
|
||||
- amount
|
||||
- direction
|
||||
- balance_effect
|
||||
- created_at
|
||||
|
||||
## 14. settlements
|
||||
- id
|
||||
- merchant_id
|
||||
- settlement_period_start
|
||||
- settlement_period_end
|
||||
- gross_amount
|
||||
- fee_amount
|
||||
- net_amount
|
||||
- payout_status
|
||||
- payout_reference
|
||||
- created_at
|
||||
- paid_at
|
||||
|
||||
## 15. settlement_items
|
||||
- id
|
||||
- settlement_id
|
||||
- transaction_id
|
||||
- gross_amount
|
||||
- fee_amount
|
||||
- net_amount
|
||||
|
||||
## 16. payout_attempts
|
||||
- id
|
||||
- settlement_id
|
||||
- partner_reference
|
||||
- amount
|
||||
- status
|
||||
- response_payload_json
|
||||
- attempted_at
|
||||
|
||||
## 17. reconciliation_records
|
||||
- id
|
||||
- reconciliation_type
|
||||
- entity_type
|
||||
- entity_id
|
||||
- external_reference
|
||||
- internal_amount
|
||||
- external_amount
|
||||
- status
|
||||
- notes
|
||||
- created_at
|
||||
|
||||
## 18. audit_logs
|
||||
- id
|
||||
- actor_type
|
||||
- actor_id
|
||||
- action
|
||||
- entity_type
|
||||
- entity_id
|
||||
- before_json
|
||||
- after_json
|
||||
- source_ip
|
||||
- created_at
|
||||
|
||||
## 19. users
|
||||
- id
|
||||
- name
|
||||
- email
|
||||
- password_hash
|
||||
- role_id
|
||||
- status
|
||||
- created_at
|
||||
|
||||
## 20. roles
|
||||
- id
|
||||
- name
|
||||
- permissions_json
|
||||
- created_at
|
||||
|
||||
## 21. Index penting
|
||||
- transactions(partner_reference)
|
||||
- transactions(merchant_id, created_at)
|
||||
- notifications(device_id, delivery_status)
|
||||
- device_heartbeats(device_id, received_at)
|
||||
- device_bindings(device_id, active_flag)
|
||||
- settlements(merchant_id, payout_status)
|
||||
- ledger_entries(merchant_id, created_at)
|
||||
510
08-implementation-roadmap.md
Normal file
@ -0,0 +1,510 @@
|
||||
# Implementation Roadmap - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Prinsip Delivery
|
||||
- kerjakan fondasi domain lebih dulu
|
||||
- prioritaskan flow yang paling cepat memberi nilai bisnis
|
||||
- buat modul yang bisa dipakai ulang untuk static, dynamic MQTT, dan dynamic API
|
||||
- selesaikan admin visibility lebih awal untuk membantu operasi
|
||||
|
||||
## 2. Fase 0 - Persiapan Pra-Pelaksanaan
|
||||
### Tujuan
|
||||
menyelaraskan fondasi teknis, governance, dan alur kerja supaya pembangunan fase berikutnya stabil dan konsisten
|
||||
|
||||
### Deliverable Awal
|
||||
- Tim dan role pelaksana disepakati (PO, Tech Lead, Backend, Infra, QA, Ops)
|
||||
- Definisi source control workflow dan branching strategy
|
||||
- CI/CD base pipeline siap (build, lint, security scan dasar)
|
||||
- Lingkungan dev/test/staging dipastikan terpisah
|
||||
- Standar kontrak siap pakai:
|
||||
- API contract final v1
|
||||
- MQTT topic dan payload final v1
|
||||
- schema migration baseline
|
||||
- Skema status, kode error, dan idempotency key standar disepakati
|
||||
- Definisi tracing & logging baseline (request_id, trace_id, event_id, correlation_id)
|
||||
- Definition of Done (DoD) tim sinkron untuk tiap fase
|
||||
|
||||
### Setup Operasional
|
||||
- Issue tracker dan sprint board aktif
|
||||
- Template ticket siap (story, acceptance criteria, checklist testing)
|
||||
- Test environment data seed untuk merchant, outlet, terminal, device baseline
|
||||
- Device simulator untuk static + MQTT-only + API-direct
|
||||
- Monitoring awal:
|
||||
- uptime endpoint
|
||||
- callback receiver readiness check
|
||||
- MQ publish/subscribe smoke test
|
||||
|
||||
### Risiko Persiapan
|
||||
- Scope creep pada Fase 0 -> batasi sampai API/MQTT contract dan baseline infrastruktur
|
||||
- Ketidaksinkronan data model -> gunakan satu schema owner untuk review harian
|
||||
- Keterlambatan perangkat test -> sediakan mock/simulator lebih awal
|
||||
- Akses keamanan belum stabil -> pastikan IAM/token policy baseline sebelum endpoint sensitive dibuka
|
||||
|
||||
### DoD Fase 0
|
||||
- [ ] semua dokumen kontrak dan skema siap untuk implementasi
|
||||
- [ ] repositori, pipeline, dan environment awal siap dipakai
|
||||
- [ ] template sprint dan ticket aktif
|
||||
- [ ] device simulator bisa mensimulasikan callback dan heartbeat
|
||||
- [ ] tim siap masuk Fase 1 tanpa menunggu keputusan besar
|
||||
|
||||
## 3. Fase 1 - Foundation MVP
|
||||
### Tujuan
|
||||
mengaktifkan merchant, device registry dasar, callback payment, dan static soundbox notification
|
||||
|
||||
### Scope
|
||||
- merchant registry dasar
|
||||
- outlet dan terminal management dasar
|
||||
- device registry/TMS dasar
|
||||
- device binding
|
||||
- callback/webhook receiver
|
||||
- transaction registry
|
||||
- notification service dasar
|
||||
- MQTT push basic
|
||||
- admin dashboard basic
|
||||
- transaction monitoring basic
|
||||
|
||||
### Output
|
||||
- static soundbox flow berjalan end-to-end
|
||||
- admin dapat melihat merchant, device, transaksi, status notif
|
||||
|
||||
### Breakdown Implementasi Fase 1
|
||||
#### Sprint 1 — Auth & Core Platform
|
||||
- Setup repo struktur service dan pipeline
|
||||
- Setup observability dasar (request_id/trace_id, structured log)
|
||||
- Setup RBAC baseline untuk admin dan service-to-service auth
|
||||
- Setup auth device token + rotation policy awal
|
||||
- Setup migrasi awal sesuai schema MVP
|
||||
- Endpoint onboarding merchant basic (`/admin/merchants` CRUD minimal)
|
||||
- Endpoint outlet dan terminal basic (`/admin/merchants/{merchantId}/outlets`, `/admin/outlets/{outletId}/terminals`)
|
||||
- Kriteria Selesai: data merchant/outlet/terminal bisa dibuat, diupdate, ditampilkan, dan dihapus sesuai kebutuhan flow
|
||||
|
||||
#### Sprint 2 — Device Foundation & Binding
|
||||
- Endpoint device registry (`/admin/devices` CRUD)
|
||||
- Endpoint konfigurasi awal device (`/admin/devices/{deviceId}/bind`, `/unbind`)
|
||||
- Simpan binding aktif ke merchant, outlet, terminal dengan history
|
||||
- Endpoint heartbeat device (`/device/heartbeat`) + tabel `device_heartbeats`
|
||||
- Endpoint get device dan list heartbeat untuk ops
|
||||
- Status device derived: online/stale/offline (berdasarkan `last_seen_at`)
|
||||
- Kriteria Selesai: device bisa didaftarkan, di-bind, kirim heartbeat, dan terlihat statusnya di dashboard ops
|
||||
|
||||
#### Sprint 3 — Transaction Engine + Webhook Static
|
||||
- Model transaksi inti untuk mode static (`transactions` + `transaction_events`)
|
||||
- Webhook callback (`/integrations/qris/callback`) dengan verifikasi signature
|
||||
- Idempotency key untuk callback dan penanganan duplicate callback
|
||||
- State machine transaksi minimal: `initiated`, `paid`, `failed`, `expired`
|
||||
- Audit dasar pada perubahan status transaksi
|
||||
- Kriteria Selesai: callback memutakhirkan status transaksi dan terekam eventnya
|
||||
|
||||
#### Sprint 4 — Notifikasi Berbasis MQTT
|
||||
- Event notification orchestrator sederhana dari status transaksi `paid`
|
||||
- Publisher ke topic device `devices/{deviceId}/downlink/payment/success`
|
||||
- Format payload sesuai kontrak MQTT draft
|
||||
- Tabel `notifications` + status pengiriman (`queued/sent/acknowledged/failed`)
|
||||
- Retry basic untuk status gagal dengan backoff sederhana (tanpa DLQ penuh di sprint ini)
|
||||
- Kriteria Selesai: transaksi static memicu notifikasi sukses ke device yang terikat
|
||||
|
||||
#### Sprint 5 — Admin Monitoring & Readiness
|
||||
- Admin transaction list + detail (`/admin/transactions`, `/admin/transactions/{transactionId}`)
|
||||
- Admin device/merchant list dasar
|
||||
- Admin dashboard KPI minimal (tx hari ini, success rate, device online, pending notif)
|
||||
- Endpoint retry notifikasi (`/admin/transactions/{transactionId}/retry-notification`)
|
||||
- Monitoring kegagalan callback dan notifikasi
|
||||
- Kriteria Selesai: alur static end-to-end terlihat utuh di admin (input callback -> status tx -> notif -> ack/failed)
|
||||
|
||||
#### Acceptance Criteria Fase 1
|
||||
- Static QR flow end-to-end berjalan untuk minimal 1 merchant dan 1 device
|
||||
- Callback duplicate tidak menambah transaksi ganda
|
||||
- transaksi yang berhasil bayar tercatat `paid` dan membuat ledger entry placeholder
|
||||
- status device `online` tersimpan saat heartbeat berhasil
|
||||
- notifikasi sukses terkirim dengan event_id
|
||||
- audit log mencatat aksi CRUD penting dan callback state changes
|
||||
|
||||
#### Dependency Diagram (Ringkas)
|
||||
- Auth & Core dibutuhkan sebelum semua API/device flow
|
||||
- Device Binding dibutuhkan sebelum transaksi menargetkan device
|
||||
- Webhook dipasang sebelum notifikasi payment
|
||||
- Notifikasi MQTT dipasang setelah transaction status `paid` stabil
|
||||
- Admin monitoring dipasang setelah data transaksi dan notification pipeline berfungsi
|
||||
|
||||
#### Risiko Awal & Mitigasi (Fase 1)
|
||||
- Mapping merchant-transaksi-device salah -> hard binding + validasi binding aktif saat kirim notifikasi
|
||||
- Callback retry menyebabkan duplicate -> key composite pada partner reference + event idempotent check
|
||||
- Device offline saat notify -> queue retry + status `retrying`
|
||||
- Data konsistensi merchant/outlet/terminal tidak stabil -> blokir pembuatan transaksi jika binding tidak valid
|
||||
- Audit minim -> tambahkan actor/action/entity id di setiap state mutasi penting
|
||||
|
||||
#### Definisi Selesai Fase 1
|
||||
- [ ] static flow: scan-pay callback sukses -> transaksi `paid` -> notifikasi success diterima
|
||||
- [ ] dashboard ops melihat transaksi, merchant, device, notification status
|
||||
- [ ] retry basic untuk callback dan notification tersedia
|
||||
- [ ] skenario utama: normal, duplicate callback, device tanpa binding, webhook invalid signature, device offline
|
||||
|
||||
## 4. Fase 2 - Dynamic QR Enablement
|
||||
### Scope
|
||||
- QRIS dynamic orchestration
|
||||
- MQTT uplink request flow
|
||||
- API direct create QR flow
|
||||
- capability resolver
|
||||
- heartbeat dan online status
|
||||
- config management dasar
|
||||
|
||||
### Output
|
||||
- support 3 jenis device flow
|
||||
- dynamic QR bisa berjalan melalui MQTT-only dan API-direct
|
||||
|
||||
### Breakdown Implementasi Fase 2
|
||||
#### Sprint 1 — Capability & Routing
|
||||
- Implementasi capability profile pada `devices.capability_profile_json`
|
||||
- Resolver untuk menentukan flow:
|
||||
- `STATIC` device: tidak boleh create QR
|
||||
- `DYNAMIC_MQTT` device: create via MQTT request
|
||||
- `DYNAMIC_API` device: create via API langsung
|
||||
- Middleware validasi capabilty dan izin per endpoint/action
|
||||
- Kriteria Selesai: request invalid akan reject dengan error yang jelas (`DEVICE_CAPABILITY_NOT_SUPPORTED`, `DEVICE_NOT_BOUND`)
|
||||
|
||||
#### Sprint 2 — API-direct Dynamic QR
|
||||
- Finalisasi endpoint `POST /device/transactions/dynamic-qr`
|
||||
- Validasi payload: `amount`, `terminal_id`, `request_id`, device binding, capabilty
|
||||
- Idempotency handling pada request device (idempotency key / request_id)
|
||||
- Orchestrator QRIS dynamic:
|
||||
- panggil partner QRIS
|
||||
- simpan `transactions` status `awaiting_payment`
|
||||
- set `transaction_events`
|
||||
- Response standard:
|
||||
- `transaction_id`, `qr_payload`, `expires_at`, `status`, `request_id`
|
||||
- Kriteria Selesai: device API mendapatkan QR dinamis dan dapat memantau status pembayaran via callback
|
||||
|
||||
#### Sprint 3 — MQTT Dynamic Uplink/Downlink
|
||||
- Implementasi handler uplink MQTT untuk request:
|
||||
- `devices/{deviceId}/uplink/dynamic-qr/request`
|
||||
- Mapping `request_id` -> `correlation_id` untuk response
|
||||
- Publish response ke:
|
||||
- `devices/{deviceId}/downlink/dynamic-qr/response`
|
||||
- Idempotent request handling untuk MQTT retry
|
||||
- Kriteria Selesai: device MQTT-only bisa membuat QR sukses minimal untuk normal flow
|
||||
|
||||
#### Sprint 4 — Heartbeat & Status Online yang Lebih Akurat
|
||||
- Peningkatan endpoint heartbeat:
|
||||
- `POST /device/heartbeat` + MQTT heartbeat ingest terstandar
|
||||
- health score dari `network_strength`, `battery_level`, `last_seen_at`
|
||||
- Derivasi status:
|
||||
- `online`, `degraded`, `stale`, `offline`
|
||||
- Filter admin untuk status dan rentang health metric
|
||||
- Kriteria Selesai: ops bisa memfilter device berdasarkan health dan mengurangi retry pada kondisi degraded
|
||||
|
||||
#### Sprint 5 — Konfigurasi Device Dasar
|
||||
- Endpoint config:
|
||||
- `GET /device/config`
|
||||
- `POST /device/config/ack`
|
||||
- Command config push via MQTT:
|
||||
- `devices/{deviceId}/downlink/config/push`
|
||||
- Versioned config (`config_version`) dan idempotent ACK
|
||||
- Kriteria Selesai: perubahan config tidak memutus flow pembayaran dan bisa disinkronkan ulang
|
||||
|
||||
#### Acceptance Criteria Fase 2
|
||||
- 3 mode device berfungsi:
|
||||
- static: hanya notifikasi
|
||||
- dynamic-MQTT: request dan response QR via broker
|
||||
- dynamic-API: request dan response via API
|
||||
- duplikasi request tidak membuat transaksi ganda
|
||||
- setiap request punya korelasi penuh `request_id/correlation_id`
|
||||
- callback tetap jadi sumber truth status bayar
|
||||
- notifikasi sukses tetap melalui MQTT dengan payload terstandar
|
||||
|
||||
#### Dependency Diagram (Ringkas)
|
||||
- Fase 2 dimulai setelah Fase 1 merchant/device/binding dan callback stabil
|
||||
- API-direct bisa mulai setelah capability resolver dan transaksi core siap
|
||||
- MQTT uplink membutuhkan MQTT adapter dan auth device yang siap dari Fase 1
|
||||
|
||||
#### DoD Fase 2
|
||||
- [ ] Fitur create dynamic QR untuk API dan MQTT berfungsi
|
||||
- [ ] dynamic-qr request/reply korelatif dan idempotent
|
||||
- [ ] heartbeat/status device dipakai untuk operasional dan retry
|
||||
- [ ] config push/ack dasar berjalan
|
||||
- [ ] semua path mencatat trace/request IDs konsisten
|
||||
|
||||
## 5. Fase 3 - Finance Core
|
||||
### Scope
|
||||
- ledger entries
|
||||
- merchant payable
|
||||
- fee engine
|
||||
- settlement batch
|
||||
- payout/disbursement
|
||||
- settlement monitoring
|
||||
- reconciliation basic
|
||||
|
||||
### Output
|
||||
- merchant balance dan settlement dapat dihitung dan dibayarkan
|
||||
|
||||
### Breakdown Implementasi Fase 3
|
||||
#### Sprint 1 — Ledger Foundation
|
||||
- Finalisasi skema `ledger_entries` dan index query balance
|
||||
- Implementasi akun per-merchant saldo running (materialized atau computed-on-read)
|
||||
- Masukkan entri ledger saat transaksi berubah ke `paid`:
|
||||
- `gross_income`
|
||||
- `platform_fee`
|
||||
- `merchant_payable`
|
||||
- Integrasi reconciliation placeholder agar entri ledger bisa ditandai `reconciled=false`
|
||||
- Kriteria Selesai: setiap transaksi sukses menambah entri ledger yang benar
|
||||
|
||||
#### Sprint 2 — Fee Engine
|
||||
- Mapping fee profile per merchant (fixed + percentage jika berlaku)
|
||||
- Formula payout per transaksi:
|
||||
- `merchant_payable = gross - fee`
|
||||
- Validasi konsistensi nilai (rounding, precision uang)
|
||||
- Simpan audit metadata fee calculation
|
||||
- Kriteria Selesai: perubahan fee profile langsung memengaruhi simulasi payable
|
||||
|
||||
#### Sprint 3 — Settlement Batch
|
||||
- Buat job pembuat settlement periodik (harian/mingguan/period custom)
|
||||
- Generate `settlements` dan `settlement_items`
|
||||
- Auto-status: draft -> ready_to_pay -> paid / failed
|
||||
- API basic:
|
||||
- `POST /admin/settlements/run`
|
||||
- `GET /admin/settlements`
|
||||
- `GET /admin/settlements/{settlementId}`
|
||||
- Kriteria Selesai: batch untuk periode tertentu bisa dibuat dan diekspor
|
||||
|
||||
#### Sprint 4 — Disbursement/Payout
|
||||
- Implementasi eksekusi payout via connector bank/disbursement
|
||||
- Simpan `payout_attempts` (attempt, payload, response, status)
|
||||
- Retry payout dasar + endpoint retry payout
|
||||
- Fallback status jika payout gagal / pending
|
||||
- Kriteria Selesai: payout attempt sukses atau tercatat gagal dengan alasan terstruktur
|
||||
|
||||
#### Sprint 5 — Monitoring Finance Dasar
|
||||
- Dashboard settlement: gross, fee, net, status payout
|
||||
- Reconciliation basic:
|
||||
- compare internal `gross/net` vs feed partner
|
||||
- status `matched`, `mismatch`, `missing_internal`, `missing_external`
|
||||
- Kriteria Selesai: perbedaan finansial terlihat dan bisa di-escalate
|
||||
|
||||
#### Acceptance Criteria Fase 3
|
||||
- saldo merchant dapat dihitung dari transaksi settled
|
||||
- settlement batch dan payout tidak bergantung pada notifikasi device
|
||||
- failed payout tidak menghapus transaksi dan tetap terjaga jejaknya
|
||||
- fitur finance dapat dijalankan dengan idempotency dasar
|
||||
|
||||
#### Dependency Diagram (Ringkas)
|
||||
- Bergantung penuh pada Fase 1–2 untuk transaction lifecycle + terminal/merchant/terminal binding
|
||||
- Fee engine perlu fee profile dan transaksi yang valid
|
||||
- Payout memerlukan settlement batch dan reconciliation record yang konsisten
|
||||
|
||||
#### DoD Fase 3
|
||||
- [ ] ledger entries otomatis terbentuk saat transaksi paid
|
||||
- [ ] settlement batch bisa dibuat, dipreview, dan dieksekusi
|
||||
- [ ] payout attempt dan status payout terekam
|
||||
- [ ] laporan mismatch reconciliation dasar tersedia
|
||||
- [ ] audit log menutup operasi finance kritis
|
||||
|
||||
## 6. Fase 4 - Operations Hardening
|
||||
### Scope
|
||||
- alerting
|
||||
- audit log lengkap
|
||||
- command center device
|
||||
- duplicate suppression matang
|
||||
- retry policies formal
|
||||
- reconciliation exception handling
|
||||
- role-based access matang
|
||||
|
||||
### Output
|
||||
- sistem siap dioperasikan dengan volume lebih tinggi
|
||||
|
||||
### Breakdown Implementasi Fase 4
|
||||
#### Sprint 1 — Audit & Governance
|
||||
- Audit log lengkap pada aksi kritis:
|
||||
- onboarding approve/reject
|
||||
- binding/unbinding device
|
||||
- Capture `before_json` dan `after_json`
|
||||
- Searchable audit view dengan filter actor/entity/action/time
|
||||
- Kriteria Selesai: perubahan konfigurasi penting dapat ditelusuri 1 klik
|
||||
|
||||
#### Sprint 2 — Alerting & Observability
|
||||
- Alert dasar:
|
||||
- callback failure rate naik
|
||||
- notification failed spike
|
||||
- heartbeat timeout massal
|
||||
- payout failed beruntun
|
||||
- Integrasi dashboard metrics dasar + error budget sederhana
|
||||
- Kriteria Selesai: alert operasional muncul ke channel ops
|
||||
|
||||
#### Sprint 3 — Retry & Duplicate Suppression Matang
|
||||
- Idempotency layer diperluas:
|
||||
- callback
|
||||
- dynamic QR request
|
||||
- MQ downlink
|
||||
- Retry policy configurable per class event
|
||||
- DLQ / dead-letter untuk kegagalan permanen
|
||||
- Kriteria Selesai: event gagal tidak membeku dan dapat ditindaklanjuti
|
||||
|
||||
#### Sprint 4 — Device Command Center
|
||||
- Endpoint command:
|
||||
- `POST /admin/devices/{deviceId}/commands`
|
||||
- track ack melalui `device_commands`
|
||||
- Command push via MQTT
|
||||
- Retry command terbatas + timeout handling
|
||||
- Kriteria Selesai: ops bisa mengirim perintah tes/diagnostic
|
||||
|
||||
#### Sprint 5 — Security & RBAC Matang
|
||||
- RBAC matrix admin ops / finance / support
|
||||
- Audit permission perubahan role
|
||||
- Peningkatan token/device auth (exp, rotasi, revocation)
|
||||
- Kriteria Selesai: akses sesuai prinsip least privilege
|
||||
|
||||
#### Acceptance Criteria Fase 4
|
||||
- kejadian kritis punya jejak audit lengkap
|
||||
- sistem lebih stabil pada kondisi gagal sementara
|
||||
- device command operasional tersedia
|
||||
- kontrol akses sesuai tugas/otoritas
|
||||
|
||||
#### Dependency Diagram (Ringkas)
|
||||
- Audit dan RBAC dimulai paralel, paling optimal setelah model user-role stabil
|
||||
- Alerting memerlukan observability baseline dari phase sebelumnya
|
||||
- Retry matang mengunci keandalan Fase 2–3
|
||||
|
||||
#### DoD Fase 4
|
||||
- [ ] audit log searchable dan immutable
|
||||
- [ ] alert operasional aktif untuk skenario kegagalan utama
|
||||
- [ ] duplicate suppression menutup 95% replay issue normal
|
||||
- [ ] command center mencatat status end-to-end
|
||||
- [ ] role permission change tervalidasi dan tercatat
|
||||
|
||||
## 7. Fase 5 - Merchant Experience
|
||||
### Scope
|
||||
- merchant portal
|
||||
- merchant transaction view
|
||||
- merchant settlement view
|
||||
- device visibility untuk merchant
|
||||
- support/help flow
|
||||
|
||||
### Output
|
||||
- merchant bisa self-serve melihat transaksi, settlement, dan device miliknya
|
||||
|
||||
### Breakdown Implementasi Fase 5
|
||||
#### Sprint 1 — Portal Dasar
|
||||
- `GET /merchant/me`, `GET /merchant/dashboard/summary`
|
||||
- Tabel transaksi dan settlement merchant
|
||||
- Akses terbatas ke entitas milik merchant
|
||||
- Kriteria Selesai: merchant dapat login dan melihat saldo/riwayat dasar
|
||||
|
||||
#### Sprint 2 — Transaction Detail & Device Visibility
|
||||
- Detail transaksi merchant (status timeline + raw callback summary)
|
||||
- Tabel device merchant, status online, last seen
|
||||
- Pembatasan data per tenant/merchant
|
||||
- Kriteria Selesai: merchant bisa menyaring sendiri transaksi/perangkatnya
|
||||
|
||||
#### Sprint 3 — Settlement & Support
|
||||
- Halaman settlement history + settlement detail
|
||||
- Informasi payout status dan retry reason
|
||||
- Support center untuk tiket/FAQ awal
|
||||
- Kriteria Selesai: merchant bisa memantau proses pencairan
|
||||
|
||||
#### Sprint 4 — Self-Serve Preferences
|
||||
- Pengaturan notifikasi merchant dan outlet/terminal sederhana
|
||||
- Delegasi role akses team merchant
|
||||
- Kriteria Selesai: pengelolaan akun merchant lebih mandiri
|
||||
|
||||
#### Acceptance Criteria Fase 5
|
||||
- merchant self-serve tidak bergantung pada ops untuk data transaksi normal
|
||||
- hak akses tenant kuat, tidak bocor ke merchant lain
|
||||
- settlement & device informasi cukup untuk operasional harian merchant
|
||||
|
||||
#### Dependency Diagram (Ringkas)
|
||||
- Fase 5 menunggu Fase 1–4 untuk data transaksi, settlement, binding, audit, dan RBAC
|
||||
- Fitur support berjenjang setelah data core stabil
|
||||
|
||||
#### DoD Fase 5
|
||||
- [ ] merchant login dan melihat dashboard, transaksi, settlements
|
||||
- [ ] tenant isolation valid di semua query merchant
|
||||
- [ ] device visibility dan riwayat pembayaran sesuai tenant
|
||||
- [ ] dokumentasi onboarding pengguna internal merchant siap dipakai
|
||||
|
||||
## 8. Rekomendasi Urutan Pengerjaan Tim
|
||||
1. finalisasi entity dan schema awal
|
||||
2. finalisasi API dan MQTT contract
|
||||
3. bangun admin/tms/transaction monitoring dasar
|
||||
4. bangun static notification flow
|
||||
5. bangun dynamic QR flows
|
||||
6. bangun ledger dan settlement
|
||||
7. bangun merchant portal
|
||||
|
||||
## 9. Execution Now (Tanpa Jadwal)
|
||||
### Fase 0 — Segera Dijalankan Hari Ini
|
||||
- finalisasi dokumen API contract dan MQTT contract ke versi stabil untuk coding
|
||||
- finalize ownership field API: siapa yang maintain `merchant`, `transaction`, `device`, `ledger`, `settlement`
|
||||
- finalisasi error code standar dan mapping HTTP status
|
||||
- buat branch awal `feat/phase-0` dan template commit message
|
||||
- siapkan repositori pipeline baseline untuk lint/build/test smoke
|
||||
- setup environment list: dev, staging, production target
|
||||
- siapkan dataset seed:
|
||||
- minimal 2 merchant
|
||||
- minimal 2 outlet dan terminal
|
||||
- minimal 3 device: static, dynamic-mqtt, dynamic-api
|
||||
- siapkan MQTT broker topik minimal:
|
||||
- `devices/{deviceId}/downlink/payment/success`
|
||||
- `devices/{deviceId}/uplink/dynamic-qr/request`
|
||||
- `devices/{deviceId}/downlink/dynamic-qr/response`
|
||||
- `devices/{deviceId}/downlink/config/push`
|
||||
- simpan semua hasil keputusan di `DECISIONS_LOG.md` (buat file baru nanti)
|
||||
|
||||
### Fase 1 — Langkah Eksekusi Urutan Langsung
|
||||
#### Step 1 — Core Foundation
|
||||
- setup service skeleton sesuai arsitektur layer
|
||||
- implement RBAC baseline admin + device auth
|
||||
- implement migrasi DB awal untuk merchant, outlet, terminal, device, binding, transaksi, notifications
|
||||
- implement `request_id`, `trace_id`, `event_id` generator standar
|
||||
- buat endpoint create/get/list merchant
|
||||
- buat endpoint create/get/list outlet dan terminal
|
||||
- buat endpoint binding device (bind/unbind)
|
||||
- acceptance: CRUD merchant/device/terminal bisa dipakai dari admin
|
||||
|
||||
#### Referensi Step 1 Detail
|
||||
- gunakan [12-fase1-step1-core-foundation-spec.md](/home/wira/work/codex/qris-soundbox-platform/12-fase1-step1-core-foundation-spec.md) untuk API detail, schema, dan acceptance per paket
|
||||
- keputusan teknis wajib mengikuti [DECISIONS_LOG.md](/home/wira/work/codex/qris-soundbox-platform/DECISIONS_LOG.md)
|
||||
|
||||
#### Step 2 — Callback dan State Transaction
|
||||
- implement webhook callback `/integrations/qris/callback` dengan signature check
|
||||
- implement idempotency callback dan event store `transaction_events`
|
||||
- implement state machine transaksi minimal
|
||||
- simpan data callback raw di event
|
||||
- acceptance: duplicate callback tidak duplikasi state akhir
|
||||
|
||||
#### Referensi Step 2 Detail
|
||||
- gunakan [13-fase1-step2-callback-transaction-spec.md](/home/wira/work/codex/qris-soundbox-platform/13-fase1-step2-callback-transaction-spec.md) untuk validasi event, idempotency callback, dan state machine
|
||||
|
||||
#### Step 3 — Static Notification
|
||||
- implement notification orchestrator pada state `paid`
|
||||
- implement publish MQTT success ke device target
|
||||
- simpan delivery state dan retry sekali-dua kali
|
||||
- implement endpoint retry notification admin
|
||||
- acceptance: static flow menghasilkan notifikasi sukses ke device bound
|
||||
|
||||
#### Referensi Step 3 Detail
|
||||
- gunakan [14-fase1-step3-notification-spec.md](/home/wira/work/codex/qris-soundbox-platform/14-fase1-step3-notification-spec.md) untuk event flow, payload, status retry, dan endpoint retry
|
||||
|
||||
#### Step 4 — Device & Ops Monitoring
|
||||
- implement heartbeat endpoint dan status derivation
|
||||
- implement admin list device, transaction, merchant + detail basic
|
||||
- implement dashboard KPI minimal
|
||||
- acceptance: operator bisa melihat health device, transaksi hari ini, dan notifikasi pending
|
||||
|
||||
#### Referensi Step 4 Detail
|
||||
- gunakan [16-fase1-step4-monitoring-spec.md](/home/wira/work/codex/qris-soundbox-platform/16-fase1-step4-monitoring-spec.md) untuk endpoint heartbeat, status derivation, KPI dashboard, dan retry-notification
|
||||
|
||||
### Cara Mulai Kode Hari Pertama (Checklist)
|
||||
- [ ] buat modul `shared` untuk idempotency + tracing + error format
|
||||
- [ ] buat modul `auth` untuk admin dan device token
|
||||
- [ ] buat modul `merchant` dan `location` (outlet/terminal) terlebih dulu
|
||||
- [ ] buat modul `device` dan binding service
|
||||
- [ ] buat modul `transaction` state machine dan callback handler
|
||||
- [ ] buat modul `notification` publisher MQTT
|
||||
- [ ] buat modul `monitoring` KPI dasar admin
|
||||
- [ ] verifikasi Fase 1 DoD sebelum lanjut Fase 2
|
||||
- [ ] ikuti urutan kerja di [17-fase1-implementation-task-pack.md](/home/wira/work/codex/qris-soundbox-platform/17-fase1-implementation-task-pack.md)
|
||||
|
||||
### Aturan Kerja Eksekusi
|
||||
- jangan buka perangkat lain jika API contract belum final
|
||||
- jangan lanjut dynamic QR sebelum callback + static notification stabil
|
||||
- setiap PR harus menyertakan:
|
||||
- skenario happy path
|
||||
- 1 edge case
|
||||
- 1 idempotency case
|
||||
110
09-screen-inventory.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Screen Inventory - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Admin / Ops Screens
|
||||
1. Login
|
||||
2. Dashboard Overview
|
||||
3. Merchant List
|
||||
4. Merchant Detail
|
||||
5. Merchant Onboarding Review
|
||||
6. Outlet List
|
||||
7. Outlet Detail
|
||||
8. Terminal List
|
||||
9. Terminal Detail
|
||||
10. Device Registry List
|
||||
11. Device Detail
|
||||
12. Device Provisioning
|
||||
13. Device Command Center
|
||||
14. Device Monitoring
|
||||
15. Transaction List
|
||||
16. Transaction Detail
|
||||
17. Notification Delivery Monitor
|
||||
18. Ledger Dashboard
|
||||
19. Settlement Batch List
|
||||
20. Settlement Detail
|
||||
21. Reconciliation Screen
|
||||
22. Integration Settings
|
||||
23. Fee & Pricing Management
|
||||
24. Audit Logs
|
||||
25. Role & Access Management
|
||||
26. Incident / Alert Center
|
||||
|
||||
## 2. Merchant Portal Screens
|
||||
1. Merchant Login
|
||||
2. Merchant Dashboard
|
||||
3. Transaction History
|
||||
4. Transaction Detail
|
||||
5. Settlement History
|
||||
6. Settlement Detail
|
||||
7. Outlet List
|
||||
8. Outlet Detail
|
||||
9. Device List
|
||||
10. Device Detail
|
||||
11. QR Management
|
||||
12. Profile & Business Info
|
||||
13. Settlement Account
|
||||
14. Team Access
|
||||
15. Support Center
|
||||
16. Notification Preferences
|
||||
|
||||
## 3. Merchant Onboarding Screens
|
||||
1. Registration Start
|
||||
2. Business Information Form
|
||||
3. PIC Information Form
|
||||
4. Settlement Account Form
|
||||
5. Document Upload
|
||||
6. Review Submission
|
||||
7. Onboarding Status Tracker
|
||||
|
||||
## 4. Finance / Internal Ops Screens
|
||||
1. Payable Summary
|
||||
2. Disbursement Queue
|
||||
3. Failed Disbursement Monitor
|
||||
4. Manual Adjustment
|
||||
5. Refund / Dispute Review
|
||||
6. Reconciliation Exception Detail
|
||||
|
||||
## 5. Device-side Screens
|
||||
### Static device
|
||||
1. Idle
|
||||
2. Payment Received
|
||||
3. Network Issue
|
||||
4. Bound Device Info
|
||||
|
||||
### Dynamic device
|
||||
1. Idle
|
||||
2. Input Amount
|
||||
3. Generating QR
|
||||
4. QR Display
|
||||
5. Payment Success
|
||||
6. Payment Expired
|
||||
7. Retry/Error
|
||||
8. Network Disconnected
|
||||
|
||||
## 6. Prioritas Desain Tahap Awal
|
||||
### Prioritas 1
|
||||
- Admin Dashboard
|
||||
- Merchant List
|
||||
- Merchant Detail
|
||||
- Onboarding Review
|
||||
- Device Registry List
|
||||
- Device Detail
|
||||
- Transaction List
|
||||
- Transaction Detail
|
||||
- Settlement Batch List
|
||||
- Settlement Detail
|
||||
|
||||
### Prioritas 2
|
||||
- Merchant Dashboard
|
||||
- Merchant Transaction History
|
||||
- Merchant Settlement History
|
||||
- Merchant Device List
|
||||
- Device Monitoring
|
||||
- Notification Delivery Monitor
|
||||
|
||||
### Prioritas 3
|
||||
- Reconciliation
|
||||
- Audit Logs
|
||||
- Role Management
|
||||
- Command Center
|
||||
- Integration Settings
|
||||
- Fee Management
|
||||
133
10-design-blueprint.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Design Blueprint - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Objective
|
||||
Desain harus mendukung 4 persona utama:
|
||||
- Admin Ops
|
||||
- Finance Ops
|
||||
- Merchant
|
||||
- Field / Device Operations
|
||||
|
||||
Fokus desain:
|
||||
- data heavy tapi cepat dipahami
|
||||
- workflow operasional jelas
|
||||
- merchant portal sederhana dan terpercaya
|
||||
- device monitoring tidak membingungkan
|
||||
|
||||
## 2. Visual Direction
|
||||
### Style keyword
|
||||
- operational SaaS
|
||||
- trustworthy fintech
|
||||
- modern enterprise
|
||||
- data-centric
|
||||
- clean and fast
|
||||
|
||||
### Warna yang disarankan
|
||||
- Primary: `#2563EB`
|
||||
- Primary dark: `#1D4ED8`
|
||||
- Success: `#16A34A`
|
||||
- Warning: `#F59E0B`
|
||||
- Danger: `#DC2626`
|
||||
- Info: `#0EA5E9`
|
||||
- Slate 900: `#0F172A`
|
||||
- Slate 700: `#334155`
|
||||
- Slate 500: `#64748B`
|
||||
- Slate 200: `#E2E8F0`
|
||||
- Slate 100: `#F1F5F9`
|
||||
- Background: `#F8FAFC`
|
||||
|
||||
### Typography
|
||||
- Inter / Plus Jakarta Sans / Geist
|
||||
- heading semibold
|
||||
- body regular
|
||||
- gunakan tabular numbers untuk nominal dan metric
|
||||
|
||||
## 3. Layout Rules
|
||||
- sidebar tetap di area admin dan merchant portal desktop
|
||||
- topbar tinggi 72px
|
||||
- page padding 24px
|
||||
- 12-column desktop grid
|
||||
- card padding 20-24px
|
||||
- table row 52-56px
|
||||
|
||||
## 4. Komponen Utama
|
||||
- KPI card
|
||||
- metric card with trend
|
||||
- data table with sticky header
|
||||
- filter bar
|
||||
- detail drawer
|
||||
- status chips
|
||||
- timeline vertical
|
||||
- alert rail
|
||||
- audit block / raw payload viewer
|
||||
- command modal
|
||||
- config panel
|
||||
|
||||
## 5. UX Rules
|
||||
- tabel utama harus punya filter dan export
|
||||
- detail transaksi dan device idealnya punya tab + side summary
|
||||
- gunakan drawer untuk quick inspection, full page untuk deep work
|
||||
- nominal, status, dan device health harus mudah dipindai
|
||||
- raw payload harus bisa copy
|
||||
- empty state harus selalu ada CTA
|
||||
|
||||
## 6. Page Groups
|
||||
### Admin
|
||||
- Dashboard
|
||||
- Merchant Management
|
||||
- Onboarding Review
|
||||
- Device/TMS
|
||||
- Transactions
|
||||
- Ledger & Settlement
|
||||
- Reconciliation
|
||||
- Integration Settings
|
||||
- Audit & Access Control
|
||||
|
||||
### Merchant Portal
|
||||
- Dashboard
|
||||
- Transactions
|
||||
- Settlements
|
||||
- Devices
|
||||
- Outlets
|
||||
- Profile & Bank Account
|
||||
- Support
|
||||
|
||||
### Device UI
|
||||
- Static notification screens
|
||||
- Dynamic QR flow screens
|
||||
- Error and offline states
|
||||
|
||||
## 7. Figma Pages Recommendation
|
||||
1. Cover
|
||||
2. Foundations
|
||||
3. Components
|
||||
4. Admin Dashboard
|
||||
5. Merchant Portal
|
||||
6. Device UI
|
||||
7. Flows
|
||||
8. Archive
|
||||
|
||||
## 8. Komponen Status Chips yang Wajib
|
||||
- active
|
||||
- inactive
|
||||
- pending
|
||||
- approved
|
||||
- rejected
|
||||
- online
|
||||
- offline
|
||||
- degraded
|
||||
- paid
|
||||
- expired
|
||||
- failed
|
||||
- retrying
|
||||
- settled
|
||||
- payout_failed
|
||||
|
||||
## 9. Prioritas Desain Pertama
|
||||
1. Admin Dashboard
|
||||
2. Merchant Detail
|
||||
3. Device Registry + Device Detail
|
||||
4. Transaction List + Detail
|
||||
5. Settlement Batch + Detail
|
||||
6. Merchant Dashboard
|
||||
7. Merchant Transaction History
|
||||
8. Dynamic Device UI states
|
||||
119
11-low-fi-wireframes.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Low-Fi Wireframes - QRIS Soundbox Platform v1
|
||||
|
||||
## 1. Admin Dashboard Overview
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Sidebar | Topbar: Search | Date | Alerts | Profile |
|
||||
|---------+--------------------------------------------------------------------------------------|
|
||||
| Overview| KPI: Merchant | Device Online | Tx Today | Success Rate | Pending Settlement |
|
||||
| Merchant|--------------------------------------------------------------------------------------|
|
||||
| Outlet | Tx Trend Chart | Device Health Chart |
|
||||
| Device |------------------------------------+---------------------------------------------------|
|
||||
| Tx | Pending Onboarding Table | Alerts / Incident Rail |
|
||||
| Ledger |--------------------------------------------------------------------------------------|
|
||||
| Settle | Latest Transactions Table |
|
||||
| Recon |--------------------------------------------------------------------------------------|
|
||||
| Audit | Failed Notifications Table |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 2. Merchant Detail
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Header: Merchant Name | Status | Fee Profile | Settlement Status | Actions |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Summary Cards: GMV | Tx Count | Active Outlet | Active Device | Pending Settlement |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Tabs: Profile | Outlets | Terminals | Devices | Transactions | Settlements | Audit |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Content Area |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 3. Device Detail
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Header: Device ID | Vendor/Model | Online Status | Last Seen | Actions |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Cards: Merchant Binding | Firmware | Comm Mode | Capability Profile | Heartbeat Health |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Tabs: Overview | Heartbeat | Config | Commands | Notifications | Binding History |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Content Area |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 4. Transaction Detail
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Header: Tx ID | Amount | Status | Merchant | Device | Partner Ref |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Timeline: Created -> QR Generated -> Paid -> Notified -> Settled |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Left: Summary / Callback Payload / Partner Response |
|
||||
| Right: Ledger Impact / Notification Delivery / Device Ack |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 5. Settlement Batch List
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Filters: Period | Status | Merchant | Export |
|
||||
|------------------------------------------------------------------------------------------------|
|
||||
| Table: Batch ID | Period | Merchant Count | Gross | Fee | Net | Status | Action |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 6. Merchant Portal Dashboard
|
||||
```text
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
| Sidebar | Topbar: Date | Profile |
|
||||
|---------+--------------------------------------------------------------------------------------|
|
||||
| Home | KPI: Tx Today | Settlement Pending | Active Devices | Success Rate |
|
||||
| Tx |--------------------------------------------------------------------------------------|
|
||||
| Settle | Recent Transactions Table |
|
||||
| Device |--------------------------------------------------------------------------------------|
|
||||
| Outlet | Recent Settlements Table |
|
||||
| Profile |--------------------------------------------------------------------------------------|
|
||||
| Support | Device Status Mini Panel |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 7. Dynamic Device UI
|
||||
```text
|
||||
+-------------------------------------------+
|
||||
| Merchant Name |
|
||||
|-------------------------------------------|
|
||||
| Input Nominal |
|
||||
| [ 50.000 ] |
|
||||
| |
|
||||
| [Buat QR] |
|
||||
|-------------------------------------------|
|
||||
| Status / hint text |
|
||||
+-------------------------------------------+
|
||||
```
|
||||
|
||||
```text
|
||||
+-------------------------------------------+
|
||||
| Merchant Name |
|
||||
|-------------------------------------------|
|
||||
| [ QR IMAGE ] |
|
||||
| |
|
||||
| Nominal: Rp50.000 |
|
||||
| Expired in: 04:12 |
|
||||
| |
|
||||
| Menunggu pembayaran... |
|
||||
+-------------------------------------------+
|
||||
```
|
||||
|
||||
```text
|
||||
+-------------------------------------------+
|
||||
| Pembayaran diterima |
|
||||
| |
|
||||
| Rp50.000 |
|
||||
| |
|
||||
| Referensi: TX-20260523-001 |
|
||||
| |
|
||||
| Terima kasih |
|
||||
+-------------------------------------------+
|
||||
```
|
||||
206
12-fase1-step1-core-foundation-spec.md
Normal file
@ -0,0 +1,206 @@
|
||||
# Fase 1 — Step 1: Core Foundation (Spesifikasi Implementasi)
|
||||
|
||||
Dokumen ini memecah Step 1 menjadi paket kerja per service, API, schema, dan acceptance detail agar bisa langsung jadi ticket development.
|
||||
|
||||
## 1) Service yang Dibangun Dulu (Urutan Implementasi)
|
||||
|
||||
1. `shared` — tracing, idempotency key store, error envelope, request context
|
||||
2. `identity` — admin auth + device auth + RBAC baseline
|
||||
3. `merchant` — merchant onboarding dasar
|
||||
4. `location` — outlet dan terminal
|
||||
5. `device-tms` — registrasi device dan binding
|
||||
6. `core-db` — migrasi schema MVP + seed data
|
||||
7. `ingress` — endpoint admin yang membutuhkan service di atas
|
||||
|
||||
## 2) Kontrak Data dan ID Standard
|
||||
|
||||
### 2.1 ID
|
||||
- `merchant_id`, `outlet_id`, `terminal_id`, `device_id`, `binding_id`, `transaction_id`, `notification_id`
|
||||
- format rekomendasi: ULID/UUID v4
|
||||
|
||||
### 2.2 Standard Trace/Event
|
||||
- Setiap request HTTP dan MQTT message harus membawa:
|
||||
- `request_id` (wajib di inbound entry API / webhook / mqtt)
|
||||
- `trace_id` (inherit antar service)
|
||||
- `event_id` (untuk emission / audit event)
|
||||
|
||||
### 2.3 Idempotency
|
||||
- Table `idempotency_keys` baru:
|
||||
- `id`
|
||||
- `scope` (`merchant_create`, `outlet_create`, `terminal_create`, dll)
|
||||
- `key` (unique)
|
||||
- `response_hash`
|
||||
- `created_at`, `expires_at`
|
||||
- Rule: hit yang sama + key sama => response deterministic dari cache/replay.
|
||||
|
||||
## 3) Skema DB (MVP Step 1) — Detail Migrasi
|
||||
|
||||
### 3.1 Tabel Inti yang Harus Ada
|
||||
- `merchants`
|
||||
- `outlets`
|
||||
- `terminals`
|
||||
- `devices`
|
||||
- `device_bindings`
|
||||
- `device_heartbeats`
|
||||
- `transactions`
|
||||
- `transaction_events`
|
||||
- `notifications`
|
||||
- `audit_logs`
|
||||
- `users`
|
||||
- `roles`
|
||||
- `idempotency_keys` (tambah)
|
||||
|
||||
### 3.2 Field Wajib Step 1 per Tabel
|
||||
|
||||
#### `merchants`
|
||||
- id, merchant_code, legal_name, brand_name
|
||||
- status, onboarding_status
|
||||
- settlement_account_reference, settlement_account_type, fee_profile_id
|
||||
- created_at, updated_at
|
||||
|
||||
#### `outlets`
|
||||
- id, merchant_id(FK), outlet_code, name, address, status
|
||||
- created_at, updated_at
|
||||
|
||||
#### `terminals`
|
||||
- id, outlet_id(FK), terminal_code, qr_mode, partner_reference, status
|
||||
- created_at, updated_at
|
||||
|
||||
#### `devices`
|
||||
- id, device_code, serial_number, vendor, model
|
||||
- communication_mode, capability_profile_json, auth_method, status
|
||||
- last_seen_at, firmware_version
|
||||
- created_at, updated_at
|
||||
|
||||
#### `device_bindings`
|
||||
- id, device_id(FK), merchant_id(FK), outlet_id(FK), terminal_id(FK)
|
||||
- active_flag, bound_at, unbound_at
|
||||
- unique index: (`device_id`, `active_flag` where active_flag=true)
|
||||
|
||||
#### `device_heartbeats`
|
||||
- id, device_id(FK), received_at
|
||||
- network_strength, battery_level, firmware_version, state, payload_json
|
||||
|
||||
#### `transactions`
|
||||
- id, transaction_code, merchant_id(FK), outlet_id(FK), terminal_id(FK), device_id(FK)
|
||||
- qr_mode, initiation_mode, partner_reference, amount, currency, status, created_at, paid_at, expired_at, updated_at
|
||||
|
||||
#### `transaction_events`
|
||||
- id, transaction_id(FK), event_type, source, payload_json, created_at
|
||||
|
||||
#### `notifications`
|
||||
- id, transaction_id(FK), device_id(FK), delivery_channel, payload_type
|
||||
- delivery_status, retry_count, ack_status, sent_at, ack_at
|
||||
|
||||
#### `audit_logs`
|
||||
- id, actor_type, actor_id, action, entity_type, entity_id
|
||||
- before_json, after_json, source_ip, created_at
|
||||
|
||||
#### `users` / `roles` (minimal)
|
||||
- users: id, name, email, password_hash, role_id, status, created_at
|
||||
- roles: id, name, permissions_json, created_at
|
||||
|
||||
### 3.3 Index Prioritas Step 1
|
||||
- `device_bindings(device_id, active_flag)`
|
||||
- `transactions(partner_reference)`
|
||||
- `transactions(merchant_id, created_at)`
|
||||
- `transactions(status, created_at)`
|
||||
- `notifications(device_id, delivery_status)`
|
||||
- `device_heartbeats(device_id, received_at)`
|
||||
- `audit_logs(entity_type, entity_id, created_at)`
|
||||
|
||||
## 4) Modul API Step 1 (Spesifikasi Ringkas)
|
||||
|
||||
### 4.1 Admin/Auth
|
||||
- `POST /auth/login` (bila auth service belum ada, temporary token stub boleh pakai untuk sprint 1)
|
||||
- `POST /auth/token/refresh` (opsional)
|
||||
- Validasi RBAC untuk endpoint admin:
|
||||
- `admin` minimal bisa akses semua CRUD Fase 1
|
||||
- Error standar:
|
||||
- `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`
|
||||
|
||||
### 4.2 Merchant
|
||||
- `POST /admin/merchants`
|
||||
- body:
|
||||
- `legal_name`, `brand_name`, `settlement_account_reference`, `settlement_account_type`, `fee_profile_id`
|
||||
- `settlement_account_type` contoh: `merchant_virtual_account`, `merchant_bank_account`, `partner_payout_reference`
|
||||
- `payout_mode` (opsional): `merchant_direct` (default), `manual`
|
||||
- `merchant_direct` = payout diarahkan via reference payout merchant
|
||||
- `manual` = pencairan dibantu operasional, belum otomatis
|
||||
- validasi: `legal_name` required
|
||||
- idempotency: `Idempotency-Key` optional untuk create
|
||||
- `GET /admin/merchants`
|
||||
- `GET /admin/merchants/{merchantId}`
|
||||
- `PATCH /admin/merchants/{merchantId}`
|
||||
- `DELETE /admin/merchants/{merchantId}` (opsional; bisa soft delete via status=inactive)
|
||||
|
||||
### 4.3 Outlet
|
||||
- `POST /admin/merchants/{merchantId}/outlets`
|
||||
- `GET /admin/outlets`
|
||||
- `GET /admin/outlets/{outletId}`
|
||||
- `PATCH /admin/outlets/{outletId}`
|
||||
|
||||
### 4.4 Terminal
|
||||
- `POST /admin/outlets/{outletId}/terminals`
|
||||
- `GET /admin/terminals`
|
||||
- `GET /admin/terminals/{terminalId}`
|
||||
- `PATCH /admin/terminals/{terminalId}`
|
||||
|
||||
### 4.5 Device & Binding
|
||||
- `POST /admin/devices`
|
||||
- `GET /admin/devices`
|
||||
- `GET /admin/devices/{deviceId}`
|
||||
- `PATCH /admin/devices/{deviceId}`
|
||||
- `POST /admin/devices/{deviceId}/bind`
|
||||
- body: `{ merchant_id, outlet_id, terminal_id }`
|
||||
- rule: endpoint ini membuat record `device_bindings` baru dan menonaktifkan binding aktif lama
|
||||
- `POST /admin/devices/{deviceId}/unbind`
|
||||
- body: `{ reason }`
|
||||
|
||||
## 5) Acceptance Criteria per Paket
|
||||
|
||||
### 5.1 Merchant/Location
|
||||
- merchant, outlet, terminal bisa dibuat, diubah, list, detail
|
||||
- semua response memuat `request_id`
|
||||
- duplikasi create request dengan idempotency key yang sama menghasilkan resource yang sama
|
||||
|
||||
### 5.2 Device Binding
|
||||
- bind mengikat hanya satu binding aktif/device pada saat yang sama
|
||||
- unbind membuat binding aktif jadi nonaktif dan merekam `unbound_at`
|
||||
- binding referensi invalid menyebabkan endpoint device/transaction terkait ditolak di tahap berikutnya
|
||||
|
||||
### 5.3 Observability/Platform
|
||||
- semua response error mengikuti envelope:
|
||||
- `code`, `message`, `details`, `request_id`
|
||||
- log audit tersimpan saat create/update pada merchant, device, bind/unbind
|
||||
|
||||
### 5.4 Operasional
|
||||
- `last_seen_at` device berubah saat heartbeat valid (step 2 nantinya lebih lengkap)
|
||||
- pipeline dasar bisa menjalankan 10 request bersamaan tanpa crash (smoke test)
|
||||
|
||||
## 6) Definisi File/Artefak
|
||||
|
||||
### 6.1 Yang Harus Dibuat di Step 1
|
||||
- `12-fase1-step1-core-foundation-spec.md` (dokumen ini)
|
||||
- `DECISIONS_LOG.md` (dibuat bersama)
|
||||
- migration file untuk tabel Step 1
|
||||
- endpoint handlers sesuai list di atas
|
||||
- seed file:
|
||||
- 2 merchant
|
||||
- 2 outlet + 2 terminal
|
||||
- 3 devices (static, dynamic-mqtt, dynamic-api)
|
||||
|
||||
### 6.2 Testing Wajib (manual smoke, tanpa alat tambahan)
|
||||
- login/admin auth
|
||||
- CRUD merchant
|
||||
- CRUD outlet/terminal
|
||||
- bind/unbind device
|
||||
- seed device bisa dilihat di GET list
|
||||
|
||||
## 7) Handover ke Step 2
|
||||
|
||||
Step 2 tidak dimulai sampai:
|
||||
- endpoint binding dan auth stabil
|
||||
- transaksi memiliki struktur minimal `transactions + transaction_events`
|
||||
- event/error format terstandardisasi
|
||||
- idempotency baseline aktif
|
||||
156
13-fase1-step2-callback-transaction-spec.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Fase 1 — Step 2: Transaction Engine + Webhook Callback (Spesifikasi Implementasi)
|
||||
|
||||
Dokumen ini merinci implementasi callback QRIS, state machine transaksi, dan eventing dasar untuk alur static flow.
|
||||
|
||||
## 1) Tujuan Step 2
|
||||
- Menerima callback pembayaran dari partner secara aman dan idempotent
|
||||
- Menyimpan state transaksi dari `initiated` sampai `paid/failed/expired`
|
||||
- Menyimpan jejak event untuk audit dan observability
|
||||
- Menyiapkan output event agar notifikasi MQTT dan pelaporan ops bisa dipicu
|
||||
|
||||
## 2) Alur State Transaksi (Static Flow)
|
||||
|
||||
1. `initiated`
|
||||
2. `awaiting_payment`
|
||||
3. `paid`
|
||||
4. `failed`
|
||||
5. `expired`
|
||||
6. `reversed` (opsional untuk fase awal)
|
||||
|
||||
### Transisi Utama
|
||||
- `initiated` -> `awaiting_payment`
|
||||
- terjadi saat transaksi static dibuat dari mapping merchant/outlet/terminal
|
||||
- saat ini boleh dilakukan di Step 2 sebagai fallback/manual test
|
||||
- `awaiting_payment` -> `paid`
|
||||
- terjadi saat callback partner valid dengan status success
|
||||
- `awaiting_payment` -> `failed`
|
||||
- status partner gagal/declined/rejected
|
||||
- `awaiting_payment` -> `expired`
|
||||
- expiry waktu lewat atau event timeout dari partner
|
||||
- `failed` / `paid` adalah terminal state untuk Step 1
|
||||
|
||||
## 3) API Endpoint: Webhook Callback
|
||||
|
||||
### Endpoint
|
||||
- `POST /integrations/qris/callback`
|
||||
|
||||
### Request Headers
|
||||
- `X-Partner-Signature`: signature HMAC (mandatory)
|
||||
- `X-Partner-Event`: jenis event
|
||||
- `Idempotency-Key` (jika tersedia dari partner)
|
||||
- `X-Request-Id` (opsional, gunakan untuk trace)
|
||||
|
||||
### Behavior
|
||||
- Validasi signature sebelum payload diproses
|
||||
- Jika signature invalid: response `401` dan tidak membuat transaksi
|
||||
- Parsing event_type:
|
||||
- sukses -> candidate `paid`
|
||||
- gagal -> candidate `failed`
|
||||
- expired / timeout -> candidate `expired`
|
||||
- Kunci idempotency:
|
||||
- key = kombinasi partner_reference + signature_reference/transaction_reference + status event
|
||||
- Jika sudah diproses:
|
||||
- kembalikan response sukses idempotent (tanpa mengubah state lagi)
|
||||
|
||||
### Request/Response Format
|
||||
- Response sukses:
|
||||
- `status`: `accepted`
|
||||
- `request_id`
|
||||
- `event_id`
|
||||
- `timestamp`
|
||||
- Response error:
|
||||
- envelope dari keputusan `D-003`
|
||||
|
||||
## 4) Validasi Callback
|
||||
|
||||
### Required Fields (minimum)
|
||||
- `partner_reference`
|
||||
- `amount`
|
||||
- `currency` (default `IDR`)
|
||||
- `status` / `payment_status`
|
||||
- `paid_at`
|
||||
- `merchant_id` / `terminal_id` / mapping key lain dari payload partner
|
||||
- `signature`
|
||||
|
||||
### Validasi Data
|
||||
- `amount > 0`
|
||||
- transaksi ditemukan: mapping `partner_reference` ke `transactions.partner_reference` (atau lookup `merchant_reference`)
|
||||
- status yang tidak dikenali -> log warning dan set ke `failed` dengan reason `UNKNOWN_STATUS`
|
||||
- cek expiry jika ada (`expired_at`) saat status sukses masuk -> jika expired, set `failed` / `expired`
|
||||
|
||||
## 5) Idempotency Strategy
|
||||
|
||||
### Tabel Idempotency
|
||||
- gunakan `idempotency_keys` yang sudah didefinisikan di Step 1
|
||||
- scope: `callback_processing`
|
||||
- key value: hash dari `(partner_reference + payment_status + partner_txn_id)`
|
||||
- jika key sudah ada:
|
||||
- return hasil callback yang sama (replay safe)
|
||||
|
||||
## 6) Transaction Store yang Diperlukan
|
||||
|
||||
### `transactions`
|
||||
- fields yang harus dipopulasi di Step 2:
|
||||
- `merchant_id`, `outlet_id`, `terminal_id`, `device_id` (jika static device direct binding)
|
||||
- `qr_mode` = `static`
|
||||
- `initiation_mode` = `static`
|
||||
- `partner_reference`
|
||||
- `amount`, `currency`, `status`
|
||||
- `created_at`, `paid_at`, `expired_at`, `updated_at`
|
||||
|
||||
### `transaction_events`
|
||||
- event wajib untuk setiap perubahan state:
|
||||
- `event_type`: `INITIATED`, `STATE_CHANGED`, `CALLBACK_RECEIVED`, `CALLBACK_REJECTED`, `CALLBACK_DUPLICATE`, `PUSH_QUEUED`
|
||||
- `source`: `webhook`, `system`, `admin`
|
||||
- `payload_json`: raw payload callback + context
|
||||
- urutan event dipakai untuk debugging urut transaksi
|
||||
|
||||
## 7) Integrasi Ke Step 3 (Notifikasi)
|
||||
- ketika state transaksi berubah ke `paid`, emit event internal:
|
||||
- `transaction.paid`
|
||||
- payload ringan: `transaction_id`, `merchant_id`, `device_id`, `amount`, `currency`, `partner_reference`, `paid_at`
|
||||
- event ini menjadi trigger awal untuk notification orchestrator Step 3
|
||||
- jika transaction tidak punya binding aktif:
|
||||
- event tetap tercatat dengan reason `NO_ACTIVE_BINDING`
|
||||
|
||||
## 8) Error Code (saran)
|
||||
- `WEBHOOK_SIGNATURE_INVALID`
|
||||
- `TRANSACTION_NOT_FOUND`
|
||||
- `PAYMENT_STATUS_INVALID`
|
||||
- `DUPLICATE_WEBHOOK`
|
||||
- `CALLBACK_PARTNER_DATA_INVALID`
|
||||
- `IDEMPOTENCY_MISSING_KEY`
|
||||
|
||||
## 9) Contoh Pseudocode Alur Handler
|
||||
|
||||
1. terima payload
|
||||
2. validasi signature
|
||||
3. normalisasi `request_id` dan `event_id`
|
||||
4. ambil `partner_reference` -> cari transaksi
|
||||
5. cek idempotency callback
|
||||
6. jika duplikat: simpan `transaction_events` duplicate dan return success
|
||||
7. validasi transaksi + status
|
||||
8. update status + paid_at jika sukses
|
||||
9. simpan event (`CALLBACK_RECEIVED`, `STATE_CHANGED`)
|
||||
10. emit `transaction.paid` (hanya saat sukses)
|
||||
11. commit
|
||||
12. return response success
|
||||
|
||||
## 10) API/Query untuk Operasional (minimal)
|
||||
- `GET /admin/transactions` tetap support filter `status`, `merchant_id`, `from`, `to`, `partner_reference`
|
||||
- `GET /admin/transactions/{transactionId}` menampilkan event timeline
|
||||
- `GET /admin/transactions/{transactionId}/events` opsional di Step 2 untuk debug
|
||||
|
||||
## 11) Acceptance Criteria Step 2
|
||||
- webhook menerima callback valid dan mengubah state transaksi ke `paid`
|
||||
- callback duplicate tidak menambah perubahan state tambahan
|
||||
- signature invalid ditolak `401`
|
||||
- jika partner reference tidak ditemukan, response tetap deterministik dan tidak crash
|
||||
- setiap perubahan state menghasilkan `transaction_events` minimal satu baris
|
||||
- setiap perubahan ke `paid` menghasilkan event internal `transaction.paid` untuk Step 3
|
||||
|
||||
## 12) Keberhasilan Handoff ke Step 3
|
||||
- state machine stabil dan bisa dipakai unit/integration smoke
|
||||
- transaksi berhasil dan gagal terekam lengkap
|
||||
- callback replay aman
|
||||
- notifikasi orchestrator dapat subscribe pada event `transaction.paid`
|
||||
118
14-fase1-step3-notification-spec.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Fase 1 — Step 3: Notifikasi MQTT Dasar (Static Payment)
|
||||
|
||||
Dokumen ini memandu implementasi notifikasi success payment ke device berbasis event transaksi `paid`.
|
||||
|
||||
## 1) Tujuan Step 3
|
||||
- Memastikan setiap transaksi `paid` memicu notifikasi sukses ke device yang valid
|
||||
- Menyimpan status delivery dan retry agar operasional bisa dipantau
|
||||
- Menyediakan endpoint admin untuk retry manual
|
||||
|
||||
## 2) Event Source
|
||||
- Trigger dari Step 2: event internal `transaction.paid`
|
||||
- Sumber wajib: event bus internal / service method call dari transaction service (untuk fase awal)
|
||||
|
||||
## 3) Mapping Notifikasi
|
||||
- Input: `transaction_id`, `merchant_id`, `device_id`, `amount`, `currency`, `paid_at`, `reference`
|
||||
- Output: payload MQTT `payment_success` ke topik:
|
||||
- `devices/{deviceId}/downlink/payment/success`
|
||||
- Hanya kirim jika device memiliki binding aktif (`device_bindings.active_flag = true`)
|
||||
|
||||
## 4) Tabel dan Status
|
||||
|
||||
### `notifications`
|
||||
Kolom inti:
|
||||
- `id`
|
||||
- `transaction_id`
|
||||
- `device_id`
|
||||
- `delivery_channel` = `mqtt`
|
||||
- `payload_type` = `payment_success`
|
||||
- `delivery_status`: `queued | sent | acknowledged | failed | retrying`
|
||||
- `retry_count`
|
||||
- `ack_status`: `pending | received | not_supported | not_needed`
|
||||
- `sent_at`
|
||||
- `ack_at`
|
||||
|
||||
Status transisi:
|
||||
- dibuat -> `queued`
|
||||
- publish sukses -> `sent`
|
||||
- no ack handler tersedia pada fase awal -> `acknowledged` otomatis atau `failed` sesuai konfigurasi broker
|
||||
- publish gagal -> `retrying`
|
||||
- retry 3x gagal -> `failed`
|
||||
|
||||
## 5) Payload Kontrak MQTT (Step 3)
|
||||
```json
|
||||
{
|
||||
"message_type": "payment_success",
|
||||
"event_id": "evt_123",
|
||||
"transaction_id": "tx_123",
|
||||
"merchant_name": "Toko Berkah",
|
||||
"amount": 50000,
|
||||
"currency": "IDR",
|
||||
"paid_at": "2026-05-23T10:02:10Z",
|
||||
"audio_text": "Pembayaran diterima lima puluh ribu rupiah",
|
||||
"display_text": "Pembayaran diterima Rp50.000"
|
||||
}
|
||||
```
|
||||
|
||||
## 6) Alur Proses Notifikasi
|
||||
1. Step 2 publish event `transaction.paid`
|
||||
2. Notification Orchestrator menerima event
|
||||
3. Validasi `device_id` ada dan memiliki binding aktif
|
||||
4. Buat record `notifications` dengan status `queued`
|
||||
5. Publish ke `devices/{deviceId}/downlink/payment/success`
|
||||
6. Jika sukses, update `sent` dan `sent_at`
|
||||
7. Jika gagal:
|
||||
- hitung retry (exponential/linear simple backoff)
|
||||
- update `retrying`
|
||||
- lanjut retry maksimum 3x
|
||||
8. Jika 3x gagal tetap `failed`
|
||||
9. Return result ke endpoint retry bila dipanggil manual
|
||||
|
||||
## 7) Retry Policy Fase 1 (Sederhana)
|
||||
- max_attempt = 3
|
||||
- retry_interval_seconds = 15, 30, 60 (tetap untuk fase awal)
|
||||
- retry_count disimpan di `notifications.retry_count`
|
||||
- tiap percobaan harus idempotent via `event_id` dan `transaction_id`
|
||||
|
||||
## 8) Endpoint Admin
|
||||
- `POST /admin/transactions/{transactionId}/retry-notification`
|
||||
- validasi transaksi status harus `paid`
|
||||
- akan membuat attempt publish baru jika `delivery_status` bukan `acknowledged`
|
||||
- response:
|
||||
- `transaction_id`
|
||||
- `notification_id`
|
||||
- `delivery_status`
|
||||
- `next_retry_at`
|
||||
|
||||
## 9) Endpoint Ops Monitoring Minimal
|
||||
- `GET /admin/devices/{deviceId}/notifications`
|
||||
- list notif by device + `delivery_status`
|
||||
- `GET /admin/transactions/{transactionId}`
|
||||
- tampilkan link notification + timeline status
|
||||
|
||||
## 10) Idempotency dan De-Dupe
|
||||
- kombinasi key notifikasi:
|
||||
- `transaction_id + event_id`
|
||||
- jika event duplicate:
|
||||
- tidak membuat record baru
|
||||
- return existing notif status
|
||||
- publish duplicate harus ditolak di orchestrator (guard).
|
||||
|
||||
## 11) Error Code (saran)
|
||||
- `NOTIFICATION_DEVICE_UNAVAILABLE`
|
||||
- `NOTIFICATION_NO_ACTIVE_BINDING`
|
||||
- `NOTIFICATION_PUBLISH_FAILED`
|
||||
- `NOTIFICATION_RETRY_EXHAUSTED`
|
||||
|
||||
## 12) Acceptance Criteria Step 3
|
||||
- transaksi `paid` menghasilkan event `payment_success` ke topik device
|
||||
- notification record terbentuk untuk setiap transaksi `paid`
|
||||
- perangkat yang tidak terikat aktif tidak dipush (state `failed` dengan reason)
|
||||
- retry berjalan saat publish gagal
|
||||
- endpoint retry admin merubah status dari `failed/retrying` menjadi `sent` atau tetap `failed` setelah limit
|
||||
|
||||
## 13) Catatan Implementasi Step 3 (Tanpa Design)
|
||||
- gunakan QoS 1 untuk topic payment success
|
||||
- hindari retained message untuk event sukses pembayaran
|
||||
- logging harus menyertakan `transaction_id`, `device_id`, `event_id`, `request_id`
|
||||
- payload audit (audio/display) boleh berupa default ID locale
|
||||
55
15-pr-template-fase1-step1-2-3.md
Normal file
@ -0,0 +1,55 @@
|
||||
# PR Template — Fase 1 (Step 1–3)
|
||||
|
||||
Gunakan template ini untuk tiap PR agar konsisten dan cepat di-review.
|
||||
|
||||
## 1) Judul dan Lingkup
|
||||
- [ ] Scope: Fase 1 Step:
|
||||
- [ ] Step 1 (Core Foundation)
|
||||
- [ ] Step 2 (Callback & Transaction State)
|
||||
- [ ] Step 3 (Notification)
|
||||
- [ ] Ringkas apa yang diubah
|
||||
|
||||
## 2) Perubahan Teknis
|
||||
- [ ] Service/module yang berubah
|
||||
- [ ] Endpoint yang ditambah/diubah
|
||||
- [ ] Tabel/model yang ditambah/diubah
|
||||
- [ ] Event yang ditambah/diubah
|
||||
- [ ] Kontrak error/response yang ikut berubah
|
||||
|
||||
## 3) Acceptance (wajib minimal 3)
|
||||
Isi satu checklist dari scope PR:
|
||||
- [ ] Happy path utama sesuai dokumentasi:
|
||||
- [ ] Step 1: CRUD entity dan binding berhasil
|
||||
- [ ] Step 2: callback sukses -> status `paid`
|
||||
- [ ] Step 3: paid -> notifikasi record + publish MQTT
|
||||
- [ ] Edge case:
|
||||
- [ ] duplikasi idempotency
|
||||
- [ ] invalid signature (callback)
|
||||
- [ ] no active binding
|
||||
- [ ] Idempotency case:
|
||||
- [ ] request/callback duplikat tidak menambah state/record tidak sesuai aturan
|
||||
|
||||
## 4) Kesesuaian Dokumen
|
||||
- [ ] Mengikuti [12-fase1-step1-core-foundation-spec.md](/home/wira/work/codex/qris-soundbox-platform/12-fase1-step1-core-foundation-spec.md)
|
||||
- [ ] Mengikuti [13-fase1-step2-callback-transaction-spec.md](/home/wira/work/codex/qris-soundbox-platform/13-fase1-step2-callback-transaction-spec.md)
|
||||
- [ ] Mengikuti [14-fase1-step3-notification-spec.md](/home/wira/work/codex/qris-soundbox-platform/14-fase1-step3-notification-spec.md)
|
||||
- [ ] Decision references:
|
||||
- [ ] [D-005] Idempotency Rule
|
||||
- [ ] [D-010] Webhook Signature dan Parsing
|
||||
- [ ] [D-012] Evented Transaction State Change
|
||||
- [ ] [D-013] Trigger Notification dari event `paid`
|
||||
|
||||
## 5) Bukti Eksekusi
|
||||
- [ ] Screenshot/request log dari endpoint manual smoke test
|
||||
- [ ] Log contoh transaksi sukses (`callback` -> `paid` -> `notification sent`)
|
||||
- [ ] Lampirkan payload/response sample untuk endpoint utama
|
||||
|
||||
## 6) Dampak
|
||||
- [ ] Ada migration schema?
|
||||
- [ ] Ada backward compatibility concern?
|
||||
- [ ] Berikan langkah rollback sederhana (jika perlu)
|
||||
|
||||
## 7) Catatan Security/Compliance
|
||||
- [ ] Signature verification tetap aman
|
||||
- [ ] trace/error format tetap konsisten
|
||||
- [ ] audit log untuk operasi penting tercatat
|
||||
154
16-fase1-step4-monitoring-spec.md
Normal file
@ -0,0 +1,154 @@
|
||||
# Fase 1 — Step 4: Device & Ops Monitoring
|
||||
|
||||
Dokumen ini merinci implementasi heartbeat, status device, list/detail monitoring, dan KPI dasar admin untuk selesai dari Step 4.
|
||||
|
||||
## 1) Tujuan Step 4
|
||||
- Menyediakan visibilitas operasional untuk device dan transaksi di admin
|
||||
- Menjadikan heartbeat sebagai sumber status online device
|
||||
- Menyediakan metrik minimum yang dipakai tim operasi harian
|
||||
- Menyediakan jalur retry/penelusuran notifikasi pending
|
||||
|
||||
## 2) Heartbeat Ingestion
|
||||
|
||||
### Endpoint
|
||||
- `POST /device/heartbeat`
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"device_id": "sbx_001",
|
||||
"timestamp": "2026-05-23T10:00:00Z",
|
||||
"firmware_version": "1.0.3",
|
||||
"network_strength": 78,
|
||||
"battery_level": 92,
|
||||
"state": "idle"
|
||||
}
|
||||
```
|
||||
|
||||
### Rules
|
||||
- Validate token device.
|
||||
- Simpan ke `device_heartbeats`.
|
||||
- Update `devices.last_seen_at = timestamp`.
|
||||
- Jika field missing, simpan `null` dan tetap catat validasi non-kritis:
|
||||
- `network_strength` / `battery_level` boleh null
|
||||
- Return response:
|
||||
- `request_id`
|
||||
- `device_id`
|
||||
- `server_time`
|
||||
|
||||
### Status Derivation (Step 4)
|
||||
- `online`: `last_seen_at >= now - 90 detik`
|
||||
- `degraded`: `network_strength < 40` **atau** `battery_level < 20`
|
||||
- `stale`: `last_seen_at < now - 90 detik` dan >= `now - 15 menit`
|
||||
- `offline`: `last_seen_at < now - 15 menit`
|
||||
- Kombinasi prioritas: `offline`/`stale`/`degraded`/`online` (offline paling dominan)
|
||||
|
||||
## 3) Endpoint Monitoring Dasar
|
||||
|
||||
### List Device
|
||||
- `GET /admin/devices`
|
||||
- query:
|
||||
- `status`
|
||||
- `vendor`
|
||||
- `communication_mode`
|
||||
- `merchant_id`
|
||||
- `q` (search)
|
||||
- response: include:
|
||||
- `status`
|
||||
- `last_seen_at`
|
||||
- `heartbeat_count_24h`
|
||||
- binding current summary
|
||||
|
||||
### Device Detail
|
||||
- `GET /admin/devices/{deviceId}`
|
||||
- response include:
|
||||
- metadata device
|
||||
- binding aktif (merchant/outlet/terminal)
|
||||
- latest heartbeat
|
||||
- derived status + device health
|
||||
- `notifications` latest
|
||||
|
||||
### Heartbeat List
|
||||
- `GET /admin/devices/{deviceId}/heartbeats`
|
||||
- filter:
|
||||
- `from`, `to`, `state`
|
||||
- sort: latest first
|
||||
|
||||
### Transaction List/Detail (minimal lanjutan Step 4)
|
||||
- `GET /admin/transactions`
|
||||
- filter `status`, `merchant_id`, `from`, `to`, `partner_reference`
|
||||
- `GET /admin/transactions/{transactionId}`
|
||||
- include `transaction_events` timeline
|
||||
|
||||
## 4) KPI Dashboard Minimal (admin)
|
||||
|
||||
### Endpoint
|
||||
- `GET /admin/dashboard/summary`
|
||||
|
||||
### Metrics
|
||||
- `transactions_today`
|
||||
- `success_rate_today` (paid / total attempt)
|
||||
- `active_devices`
|
||||
- `pending_notifications`
|
||||
- `devices_stale`
|
||||
- `devices_offline`
|
||||
|
||||
### Data Source
|
||||
- transaksi hari ini dari `transactions.created_at`
|
||||
- device status dari `devices.last_seen_at`
|
||||
- pending notification dari `notifications.delivery_status in ('queued','retrying')`
|
||||
|
||||
## 5) Admin Retry-notification (Step 4)
|
||||
|
||||
### Endpoint
|
||||
- `POST /admin/transactions/{transactionId}/retry-notification`
|
||||
|
||||
### Flow
|
||||
1. validasi transaksi exists dan status `paid`
|
||||
2. ambil latest notification `delivery_status` != `acknowledged`
|
||||
3. jika `delivery_status` = `queued/sent/failed/retrying`:
|
||||
- trigger publish ulang
|
||||
- tambah `retry_count`
|
||||
4. simpan hasil status setelah publish
|
||||
5. return:
|
||||
- `transaction_id`
|
||||
- `notification_id`
|
||||
- `delivery_status`
|
||||
- `next_retry_at`
|
||||
|
||||
## 6) Monitoring Notifikasi Gagal
|
||||
- endpoint ops:
|
||||
- `GET /admin/notifications/failed`
|
||||
- filter:
|
||||
- `device_id`
|
||||
- `from`
|
||||
- `to`
|
||||
- response:
|
||||
- `notification_id`, `transaction_id`, `device_id`, `delivery_status`, `retry_count`, `reason`
|
||||
|
||||
## 7) Query Efficiency (Prioritas)
|
||||
- buat index:
|
||||
- `devices(status, last_seen_at)`
|
||||
- `device_heartbeats(device_id, received_at desc)`
|
||||
- `transactions(status, created_at)`
|
||||
- `notifications(delivery_status, created_at)`
|
||||
|
||||
## 8) Acceptance Criteria Step 4
|
||||
- status device berubah sesuai aturan `online/degraded/stale/offline`
|
||||
- heartbeat tercatat dan `devices.last_seen_at` terupdate
|
||||
- dashboard summary menampilkan 5 metrik minimal
|
||||
- `GET /admin/transactions` dan device/transaction detail menampilkan data yang dibutuhkan
|
||||
- endpoint retry-notification memicu publish ulang untuk status non-final
|
||||
|
||||
## 9) Error Handling
|
||||
- `DEVICE_NOT_FOUND`
|
||||
- `INVALID_HEARTBEAT_PAYLOAD`
|
||||
- `TRANSACTION_NOT_ELIGIBLE_FOR_RETRY`
|
||||
- `DASHBOARD_CALCULATION_ERROR` (fallback graceful: return nilai default 0 jika query gagal)
|
||||
|
||||
## 10) Keluaran Step 4 (End-State)
|
||||
- operator bisa cek:
|
||||
- device aktif/offline
|
||||
- transaksi dan status payment
|
||||
- kegagalan notifikasi
|
||||
- sistem tetap bisa operasi manual meski tanpa UI dashboard kompleks
|
||||
157
17-fase1-implementation-task-pack.md
Normal file
@ -0,0 +1,157 @@
|
||||
# Fase 1 — Task Pack (Urutan Implementasi Langsung)
|
||||
|
||||
Dokumen ini adalah daftar kerja yang bisa langsung dipakai sebagai urutan PR/ticket.
|
||||
|
||||
## Paket A — Foundation Core (Step 1)
|
||||
|
||||
## A.1 PR: Shared Foundation + Middleware
|
||||
- scope: `shared`, tracing, request/error wrapper, idempotency table baseline, audit helper
|
||||
- target file:
|
||||
- shared error formatter
|
||||
- request_id/trace_id generator
|
||||
- idempotency service CRUD/read
|
||||
- acceptance:
|
||||
- semua response punya `request_id` di body meta
|
||||
- idempotency write/read bisa ditest di local endpoint sample
|
||||
|
||||
## A.2 PR: Auth Baseline
|
||||
- scope: login/admin token minimal, auth middleware, role dasar
|
||||
- acceptance:
|
||||
- endpoint admin menolak tanpa token (`401`)
|
||||
- token valid memberi akses CRUD Step 1
|
||||
|
||||
## A.3 PR: Merchant CRUD
|
||||
- scope: `POST /admin/merchants`, `GET /admin/merchants`, `GET /admin/merchants/{id}`, `PATCH /admin/merchants/{id}`
|
||||
- acceptance:
|
||||
- create/read/update berhasil
|
||||
- duplikasi create dengan idempotency key tidak membuat duplicate
|
||||
|
||||
## A.4 PR: Outlet + Terminal
|
||||
- scope:
|
||||
- `POST /admin/merchants/{merchantId}/outlets`
|
||||
- `GET /admin/outlets`, `GET /admin/outlets/{id}`, `PATCH /admin/outlets/{id}`
|
||||
- `POST /admin/outlets/{outletId}/terminals`
|
||||
- `GET /admin/terminals`, `GET /admin/terminals/{id}`, `PATCH /admin/terminals/{id}`
|
||||
- acceptance:
|
||||
- outlet/terminal berkait dengan merchant/outlet parent
|
||||
- error jika parent tidak valid
|
||||
|
||||
## A.5 PR: Device Registry + Binding
|
||||
- scope:
|
||||
- `POST /admin/devices`, `GET /admin/devices`, `GET /admin/devices/{id}`, `PATCH /admin/devices/{id}`
|
||||
- `POST /admin/devices/{id}/bind`
|
||||
- `POST /admin/devices/{id}/unbind`
|
||||
- acceptance:
|
||||
- bind menghasilkan `device_bindings` aktif tunggal
|
||||
- unbind menonaktifkan binding aktif
|
||||
|
||||
## A.6 PR: Migration + Seed Phase 1
|
||||
- scope:
|
||||
- tables dari Step 1 MVP
|
||||
- seed: 2 merchant, 2 outlet, 2 terminal, 3 device
|
||||
- user/admin seed baseline
|
||||
- acceptance:
|
||||
- sistem startup clean dengan data seed
|
||||
- query dasar berjalan
|
||||
|
||||
## Paket B — Transaction + Webhook (Step 2)
|
||||
|
||||
## B.1 PR: Transaction Core & State Machine
|
||||
- scope:
|
||||
- tabel `transactions`, `transaction_events`
|
||||
- states: initiated, awaiting_payment, paid, failed, expired
|
||||
- acceptance:
|
||||
- create transaksi dasar
|
||||
- timeline event tercatat saat state berubah
|
||||
|
||||
## B.2 PR: Callback Receiver
|
||||
- scope: `POST /integrations/qris/callback`
|
||||
- rules:
|
||||
- signature validation
|
||||
- idempotency callback
|
||||
- duplicate replay handling
|
||||
- acceptance:
|
||||
- valid callback -> state paid
|
||||
- duplicate callback tidak mengubah state
|
||||
- signature salah -> 401
|
||||
|
||||
## B.3 PR: Event Emission (`transaction.paid`)
|
||||
- scope:
|
||||
- emit internal event saat status bayar sukses
|
||||
- acceptance:
|
||||
- setiap successful callback menghasilkan event sekali (idempotent)
|
||||
|
||||
## Paket C — Notification (Step 3)
|
||||
|
||||
## C.1 PR: Notification Orchestrator
|
||||
- scope:
|
||||
- listen `transaction.paid`
|
||||
- buat `notifications` record
|
||||
- acceptance:
|
||||
- transaksi paid menghasilkan record notification
|
||||
|
||||
## C.2 PR: MQTT Publisher
|
||||
- scope:
|
||||
- publish ke `devices/{deviceId}/downlink/payment/success`
|
||||
- payload sesuai kontrak
|
||||
- acceptance:
|
||||
- publish sukses update `delivery_status=sent`
|
||||
- fallback jika publish gagal -> `retrying/failed`
|
||||
|
||||
## C.3 PR: Retry + Admin Retry API
|
||||
- scope:
|
||||
- retry loop (15/30/60)
|
||||
- `POST /admin/transactions/{transactionId}/retry-notification`
|
||||
- acceptance:
|
||||
- status update berjalan dari retrying ke sent/gagal
|
||||
|
||||
## Paket D — Device & Ops Monitoring (Step 4)
|
||||
|
||||
## D.1 PR: Heartbeat Ingestion
|
||||
- scope:
|
||||
- `POST /device/heartbeat`
|
||||
- update `devices.last_seen_at`
|
||||
- simpan `device_heartbeats`
|
||||
- acceptance:
|
||||
- status heartbeat dan latest heartbeat terlihat per device
|
||||
|
||||
## D.2 PR: Device Listing + Detail
|
||||
- scope:
|
||||
- `GET /admin/devices`
|
||||
- `GET /admin/devices/{id}`
|
||||
- derived status online/stale/offline/degraded
|
||||
- acceptance:
|
||||
- list/detail bisa difilter dan menampilkan status
|
||||
|
||||
## D.3 PR: Transaction List/Detail + Heartbeat History
|
||||
- scope:
|
||||
- `GET /admin/transactions`, `GET /admin/transactions/{id}`
|
||||
- `GET /admin/devices/{id}/heartbeats`
|
||||
- acceptance:
|
||||
- timeline event dan heartbeat history tampil
|
||||
|
||||
## D.4 PR: Dashboard KPI + Failed Notification View
|
||||
- scope:
|
||||
- `GET /admin/dashboard/summary`
|
||||
- `GET /admin/notifications/failed`
|
||||
- acceptance:
|
||||
- 5 metrik minimum tampil
|
||||
- daftar notifikasi failed bisa difilter
|
||||
|
||||
## Urutan Eksekusi Terbuka
|
||||
1. A.1 → A.2 → A.6 (mendukung semua service berikutnya)
|
||||
2. A.3 → A.4 → A.5
|
||||
3. B.1 → B.2 → B.3
|
||||
4. C.1 → C.2 → C.3
|
||||
5. D.1 → D.2 → D.3 → D.4
|
||||
|
||||
## Aturan Dependensi
|
||||
- B paket tidak boleh dimulai jika A.5 belum selesai (binding + transaksi dasar belum valid)
|
||||
- C paket tidak boleh dimulai jika B.3 belum siap
|
||||
- D paket bisa jalan paralel dengan C jika A dan B core sudah jalan
|
||||
|
||||
## Exit Criteria Fase 1 (siap Step 2)
|
||||
- [ ] static paid flow jalan end-to-end
|
||||
- [ ] tidak ada duplicate state via callback
|
||||
- [ ] notification ada untuk paid tx
|
||||
- [ ] device heartbeat dan status ops visible
|
||||
55
CODEX_HANDOFF.md
Normal file
@ -0,0 +1,55 @@
|
||||
# CODEx Handoff — QRIS Soundbox Platform
|
||||
|
||||
## Current status
|
||||
- Fokus terakhir: sinkronisasi UI dan smoke test pada stack yang sudah aktif.
|
||||
- Implementasi backend dan UI sudah mulai dikerjakan di repository (tidak lagi hanya dokumentasi).
|
||||
- Smoke test Fase1 jalannya:
|
||||
- `smoke:cleanup` ✅
|
||||
- `smoke:flow` ❌ saat dijalankan langsung (karena server belum jalan di `localhost:3100`)
|
||||
- `smoke:e2e` ✅ setelah server auto-start di port 3100 (cleanup + full flow berhasil).
|
||||
|
||||
## Files baru/terbaru yang sudah dibuat
|
||||
- [UI: admin-system-dashboard](/home/wira/work/codex/qris-soundbox-platform/ui/admin-system-dashboard/index.html)
|
||||
- Dashboard API wiring dipastikan terhubung ke backend untuk token admin, endpoint summary, dan retry/realtime UI.
|
||||
- [UI: merchant-onboarding-flow](/home/wira/work/codex/qris-soundbox-platform/ui/merchant-onboarding-flow/index.html)
|
||||
- Form onboarding disinkron ke API (create merchant, outlet, terminal, device, binding) + status badge flow.
|
||||
- [UI: merchant-detail-view](/home/wira/work/codex/qris-soundbox-platform/ui/merchant-detail-view/index.html)
|
||||
- Detail merchant kini ambil data API untuk merchant/outlet/transactions list.
|
||||
- [UI: device-technical-detail](/home/wira/work/codex/qris-soundbox-platform/ui/device-technical-detail/index.html)
|
||||
- Device detail sinkronisasi data API: detail device, binding terbaru, heartbeats, events, metrics device, dan stream log.
|
||||
- [UI: transaction-history-monitoring](/home/wira/work/codex/qris-soundbox-platform/ui/transaction-history-monitoring/index.html)
|
||||
- Search/filter outlet-terminal dan path transaksi sudah memakai endpoint API admin.
|
||||
- [README](/home/wira/work/codex/qris-soundbox-platform/README.md)
|
||||
- Sudah ada script dan langkah smoke test (`smoke:cleanup`, `smoke:flow`, `smoke:e2e`) siap dipakai.
|
||||
- [DECISIONS_LOG.md](/home/wira/work/codex/qris-soundbox-platform/DECISIONS_LOG.md)
|
||||
- Sudah memuat keputusan merchant bank account: kini arah keputusan ke rekening milik merchant (bukan escrow/terpusat) agar menghindari kebutuhan izin tambahan di awal.
|
||||
|
||||
## Keputusan penting yang harus diikuti saat lanjut
|
||||
1. Fase 1 Step 1–4 harus tetap jalan berurutan sebelum pengembangan Fase 2.
|
||||
2. Backend target Postgres di local (`qris_soundbox_platform`) sudah dipakai di smoke test.
|
||||
3. Jalankan smoke dari kondisi bersih (`smoke:cleanup`) untuk hasil yang konsisten.
|
||||
4. Untuk sementara, pencairan dana mengikuti pola rekening merchant sendiri (sesuai permintaan terakhir), bukan rekening terpusat.
|
||||
5. Pertahankan format error API yang konsisten: `code`, `message`, `details`, `request_id`, `timestamp`.
|
||||
|
||||
## Urutan kerja selanjutnya (disarankan)
|
||||
1. Backend/backend sanity lanjut dari titik terakhir:
|
||||
- Pastikan endpoint untuk sinkronisasi screen sudah stabil (terutama filter/search transaksi dan heartbeat/ events).
|
||||
- Lengkapi pemeriksaan 1–3 (dalam flow kamu, yaitu smoke point 1–3) yang belum dites manual via UI.
|
||||
2. Ambil data smoke yang sudah tercipta di e2e (`merchant`, `device`, `transaction`) lalu smoke-test:
|
||||
- Merchant detail page
|
||||
- Merchant list/filter
|
||||
- Device technical detail
|
||||
- Device list + heartbeat view
|
||||
- Transaction history + outlet/terminal filter
|
||||
3. Jika ada regresi, cek log server di `/tmp/qris-smoke-e2e-server.log`.
|
||||
4. Setelah UI flow stabil, lanjut fitur ops:
|
||||
- `A.6 Migration + Seed` (jika ada gap)
|
||||
- `B.1–B.3` + `C.1–C.3` + `D.1–D.4` untuk full DoD Fase 1.
|
||||
|
||||
## Note kalau meneruskan sesi berikutnya
|
||||
- Kode dan screen yang sudah dimodifikasi tidak perlu diulang dari nol; lanjut dari state saat ini.
|
||||
- Prioritas saat lanjut: verifikasi “jalur UI sinkron API” lalu lanjutkan smoke flow end-to-end berkala.
|
||||
- Gunakan [DECISIONS_LOG.md] sebagai rujukan wajib untuk keputusan yang sudah disepakati.
|
||||
|
||||
## Selesai untuk off
|
||||
- Sudah ada gabungan perubahan di repo: doc + UI + API integration + smoke validation.
|
||||
194
DECISIONS_LOG.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Decisions Log — QRIS Soundbox Platform
|
||||
|
||||
Log keputusan arsitektur dan implementasi yang harus dijadikan acuan eksekusi.
|
||||
|
||||
## Format Entri
|
||||
- ID: [D-XXX]
|
||||
- Tanggal:
|
||||
- Keputusan:
|
||||
- Alasan:
|
||||
- Dampak / implikasi:
|
||||
- Status:
|
||||
|
||||
## D-001 — Basis Implementasi Fase 0 dan 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Menjalankan eksekusi langsung berdasarkan fase roadmap, tanpa pembuatan jadwal rinci.
|
||||
- Alasan:
|
||||
- Tim sudah punya pembagian fase yang jelas dan siap mulai implementasi langsung.
|
||||
- Dampak / implikasi:
|
||||
- Fokus pada deliverable per fase dan DoD, bukan timeline.
|
||||
- Status: Active
|
||||
|
||||
## D-002 — Scope Fase 1 Step 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Step 1 Fase 1 dibatasi pada core foundation: auth baseline, schema MVP, merchant/outlet/terminal/device/binding, observability dasar.
|
||||
- Alasan:
|
||||
- Memastikan jalur static payment bisa dipersiapkan stabil sebelum dynamic flow.
|
||||
- Dampak / implikasi:
|
||||
- Fitur lain (settlement, dynamic QR, merchant portal) ditunda sampai Step 1 stabil.
|
||||
- Status: Active
|
||||
|
||||
## D-003 — API Contract Error Standard
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Semua response error mengikuti envelope seragam:
|
||||
- `code`, `message`, `details`, `request_id`, `timestamp`.
|
||||
- Alasan:
|
||||
- Memudahkan debug dan tracing lintas service/device.
|
||||
- Dampak / implikasi:
|
||||
- Semua handler API harus mematuhi middleware response formatter yang sama.
|
||||
- Status: Active
|
||||
|
||||
## D-004 — Traceability Requirement
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Semua request harus membawa `request_id`; webhook/device callback harus dihubungkan dengan `trace_id` jika multi-step.
|
||||
- Alasan:
|
||||
- Root-cause analysis perlu konteks end-to-end dari callback sampai notifikasi.
|
||||
- Dampak / implikasi:
|
||||
- Framework logging dan parser event wajib men-generate field ini secara konsisten.
|
||||
- Status: Active
|
||||
|
||||
## D-005 — Idempotency Rule
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Setiap path sensitif terhadap duplicate (create merchant/create device/binding/webhook request/dynamic QR nanti) wajib men-support idempotency key/table.
|
||||
- Alasan:
|
||||
- Menghindari double create, double binding, dan state drift akibat retry.
|
||||
- Dampak / implikasi:
|
||||
- Menambah kebutuhan service/DB untuk `idempotency_keys` sejak awal Fase 1.
|
||||
- Status: Active
|
||||
|
||||
## D-006 — Device Binding
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Notifikasi pembayaran hanya boleh dipush ke device dari binding aktif yang valid (active_flag=true).
|
||||
- Alasan:
|
||||
- Mencegah notifikasi salah kirim jika device pernah dipindahkan antar outlet/terminal.
|
||||
- Dampak / implikasi:
|
||||
- Query notification selalu harus resolve binding aktif secara explicit.
|
||||
- Status: Active
|
||||
|
||||
## D-007 — Fallback Strategy untuk Fase 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Retry MQTT pada Fase 1 memakai retry sederhana (fixed/backoff pendek), tanpa DLQ kompleks.
|
||||
- Alasan:
|
||||
- Menghemat effort awal sambil menjaga fitur core berjalan.
|
||||
- Dampak / implikasi:
|
||||
- Fase 4 mengelola policy DLQ dan retry matang.
|
||||
- Status: Active
|
||||
|
||||
## D-008 — Minimal RBAC untuk Mulai Eksekusi
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Implement RBAC baseline minimum di Fase 1 (admin-only) sambil menjaga token device terpisah.
|
||||
- Alasan:
|
||||
- Mengurangi risiko akses silang awal tanpa memperlambat pengembangan.
|
||||
- Dampak / implikasi:
|
||||
- Permission matrix akan disempurnakan di Fase 4.
|
||||
- Status: Active
|
||||
|
||||
## D-009 — Dokumentasi Eksekusi Step 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Spesifikasi detail Step 1 dituangkan di `12-fase1-step1-core-foundation-spec.md`.
|
||||
- Alasan:
|
||||
- Menghindari ketidakpastian saat tim mulai coding.
|
||||
- Dampak / implikasi:
|
||||
- Semua implementer wajib merujuk dokumen ini saat menyiapkan branch dan PR.
|
||||
- Status: Active
|
||||
|
||||
## D-010 — Webhook Signature dan Parsing
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Semua callback harus divalidasi signature-nya terlebih dahulu; jika tidak valid, respons harus `401` dan tidak boleh melakukan perubahan state apapun.
|
||||
- Alasan:
|
||||
- Payment callback adalah sumber trust utama dan harus dijaga dari spoofing/replay.
|
||||
- Dampak / implikasi:
|
||||
- Service callback wajib mengimplementasikan validator HMAC (atau skema cryptographic sesuai partner) sebelum mapping transaksi.
|
||||
- Status: Active
|
||||
|
||||
## D-011 — Callback Idempotency di Level Transaksi
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Callback duplikat harus diidentifikasi deterministik dari `partner_reference + payment_status + partner_event_id` dan diperlakukan idempotent.
|
||||
- Alasan:
|
||||
- Callback retry sangat umum dari partner dan dapat memicu double state update.
|
||||
- Dampak / implikasi:
|
||||
- state transition dilakukan hanya jika perubahan state valid sesuai mesin state.
|
||||
- Status: Active
|
||||
|
||||
## D-012 — Evented Transaction State Change
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Setiap perubahan state transaksi harus menghasilkan `transaction_events` agar Step 3 dan observability bisa berjalan.
|
||||
- Alasan:
|
||||
- Auditability dan troubleshooting menuntut timeline per transaksi.
|
||||
- Dampak / implikasi:
|
||||
- update transaksi tanpa menulis event dianggap bug di Step 1.
|
||||
- Status: Active
|
||||
|
||||
## D-013 — Terminal Event `paid` sebagai Trigger Notification
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Notifikasi pembayaran di Step 3 harus dipicu dari event internal status `paid`, bukan dari polling callback.
|
||||
- Alasan:
|
||||
- Menghindari race dan duplikasi notifikasi.
|
||||
- Dampak / implikasi:
|
||||
- Implementasi Step 3 harus subscribe dan consume event transaction paid.
|
||||
- Status: Active
|
||||
|
||||
## D-014 — Retry Notifikasi Fase 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Step 3 menggunakan retry dasar maksimum 3 kali dengan jadwal 15/30/60 detik.
|
||||
- Alasan:
|
||||
- Menyeimbangkan reliability dan kecepatan operasional tanpa kompleksitas DLQ penuh pada fase awal.
|
||||
- Dampak / implikasi:
|
||||
- `notifications.retry_count` wajib ditulis dan dibatasi maksimal 3.
|
||||
- Status: Active
|
||||
|
||||
## D-015 — Notifikasi Tanpa Ack Fase 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Pada fase 1, absence of MQTT ack tidak dianggap error akhir; status sukses ditentukan dari publish outcome, bukan ack dari device.
|
||||
- Alasan:
|
||||
- Device ecosystem belum konsisten untuk ack schema, dan goal fase 1 adalah stabilitas flow utama.
|
||||
- Dampak / implikasi:
|
||||
- `ack_status` bisa `not_supported`, dan operasi tetap lanjut dengan monitoring via retry/publish status.
|
||||
- Status: Active
|
||||
|
||||
## D-016 — Heartbeat Status Threshold Fase 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Definisi status device ditetapkan: online (<90s), degraded (signal/battery buruk), stale (<15 menit), offline (>15 menit) berdasarkan `last_seen_at`.
|
||||
- Alasan:
|
||||
- Konsistensi status dibutuhkan untuk ops triage cepat.
|
||||
- Dampak / implikasi:
|
||||
- Setiap list/detail device menampilkan status yang dihitung dari rule yang sama.
|
||||
- Status: Active
|
||||
|
||||
## D-017 — Dashboard KPI Fase 1
|
||||
- Tanggal: 2026-05-23
|
||||
- Keputusan:
|
||||
- Dashboard ops minimum menampilkan: transaksi hari ini, success rate hari ini, active devices, pending notifications, stale/offline counts.
|
||||
- Alasan:
|
||||
- Tim operasi perlu indikator cepat tanpa menunggu custom analytics.
|
||||
- Dampak / implikasi:
|
||||
- Endpoint summary dashboard wajib dihitung dari data transaksi/device/notification inti.
|
||||
- Status: Active
|
||||
|
||||
## D-018 — Pencairan Dana Non-Tersentral per Merchant
|
||||
- Tanggal: 2026-05-24
|
||||
- Keputusan:
|
||||
- Pencairan dana tidak dilakukan di rekening perusahaan; setiap merchant memakai rekening tujuan sendiri (atau reference payout miliknya) untuk settlement.
|
||||
- Alasan:
|
||||
- Menghindari ketergantungan izin/operasional PJP di tahap awal dan mempercepat launch MVP.
|
||||
- Dampak / implikasi:
|
||||
- Schema dan model onboarding merchant memakai `payout_account_reference`/rekening merchant, bukan rekening vault pusat.
|
||||
- Modul settlement platform difokuskan ke rekonsiliasi, status payout, dan visibility, bukan pengelolaan rekening pusat.
|
||||
- Callback payout dan payout execution dianggap partner-oriented (tergantung integrasi penyedia), bukan core di fase awal.
|
||||
- Status: Active
|
||||
112
README.md
Normal file
@ -0,0 +1,112 @@
|
||||
# QRIS Soundbox Platform Package
|
||||
|
||||
Paket ini berisi blueprint final v1 untuk platform merchant aggregator QRIS + soundbox universal.
|
||||
|
||||
## Isi paket
|
||||
- 01-executive-blueprint.md
|
||||
- 02-system-architecture.md
|
||||
- 03-domain-modules.md
|
||||
- 04-device-flows.md
|
||||
- 05-api-contract-draft.md
|
||||
- 06-mqtt-contract-draft.md
|
||||
- 07-database-schema-draft.md
|
||||
- 08-implementation-roadmap.md
|
||||
- 09-screen-inventory.md
|
||||
- 10-design-blueprint.md
|
||||
- 11-low-fi-wireframes.md
|
||||
|
||||
## Tujuan
|
||||
Dokumen ini dibuat supaya tim bisa langsung mulai:
|
||||
- desain UI/UX
|
||||
- breakdown engineering
|
||||
- desain backend
|
||||
- integrasi device
|
||||
- cicil implementasi per fase
|
||||
|
||||
## Quick Start Implementasi (Lanjutan dari CODEx Handoff)
|
||||
|
||||
- Backend bootstrap Fase 1 sudah dibuat di `src/`.
|
||||
- Fitur awal yang sudah aktif:
|
||||
- request context + `request_id` di middleware
|
||||
- error envelope (`code`, `message`, `details`, `request_id`, `timestamp`)
|
||||
- auth token minimal untuk endpoint admin
|
||||
- middleware idempotency untuk endpoint sensitif
|
||||
- endpoint awal:
|
||||
- `GET /health`
|
||||
- `GET /admin/health` (dengan `Authorization: Bearer <token>`)
|
||||
- `POST /admin/login`
|
||||
- `POST /admin/sample-idempotent`
|
||||
- `POST /admin/merchants`
|
||||
- `GET /admin/merchants`
|
||||
- `GET /admin/merchants/{id}`
|
||||
- `PATCH /admin/merchants/{id}`
|
||||
- `POST /admin/merchants/{merchantId}/outlets`
|
||||
- `POST /admin/merchants/{merchantId}/approve`
|
||||
- `POST /admin/merchants/{merchantId}/reject`
|
||||
- `GET /admin/outlets`
|
||||
- `GET /admin/outlets/{id}`
|
||||
- `POST /admin/outlets/{outletId}/terminals`
|
||||
- `GET /admin/terminals`
|
||||
- `GET /admin/terminals/{id}`
|
||||
- `POST /admin/devices`
|
||||
- `GET /admin/devices`
|
||||
- `GET /admin/devices/{id}`
|
||||
- `POST /admin/devices/{id}/bind`
|
||||
- `POST /admin/devices/{id}/unbind`
|
||||
- `POST /admin/devices/{id}/commands`
|
||||
- `GET /admin/devices/{id}/commands`
|
||||
- `GET /admin/devices/{id}/commands/{commandId}`
|
||||
- `GET /admin/devices/{id}/notifications`
|
||||
- `GET /admin/transactions`
|
||||
- `GET /admin/transactions/{transactionId}`
|
||||
- `POST /admin/transactions`
|
||||
- `GET /admin/transactions/{transactionId}/events`
|
||||
- `POST /admin/transactions/{transactionId}/retry-notification`
|
||||
- `POST /admin/seed`
|
||||
|
||||
### Menjalankan lokal
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
npm run build && npm start
|
||||
```
|
||||
|
||||
### Cleanup data smoke test
|
||||
|
||||
```bash
|
||||
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:cleanup
|
||||
```
|
||||
|
||||
Cleanup hanya menarget entitas smoke (`Smoke Merchant`, `PR-`, `DEV-`) agar data seed demo tidak ikut terhapus.
|
||||
|
||||
```bash
|
||||
PORT=3100 ADMIN_TOKEN=admin-dev-token DEVICE_TOKEN=device-dev-token INTEGRATION_WEBHOOK_SECRET=dev-callback-secret PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:flow
|
||||
```
|
||||
|
||||
Smoke flow akan melakukan create merchant/device/transaction + heartbeat + callback paid + verifikasi event/heartbeat/notification.
|
||||
|
||||
### Smoke test end-to-end (bootstrap + flow + cleanup)
|
||||
|
||||
```bash
|
||||
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform npm run smoke:e2e
|
||||
```
|
||||
|
||||
Perintah ini menjalankan:
|
||||
|
||||
- cleanup data smoke
|
||||
- start server lokal di port 3100
|
||||
- wait sampai `/health` aktif
|
||||
- jalankan flow smoke lengkap
|
||||
- hentikan server setelah selesai
|
||||
|
||||
### Endpoint device lain
|
||||
|
||||
- `POST /device/commands/ack`
|
||||
|
||||
### Quick screen preview
|
||||
- `GET /ui` => katalog halaman UI dari seluruh `design/*`.
|
||||
- `GET /ui/:page` => buka halaman berdasarkan slug (contoh: `/ui/admin-login`, `/ui/admin-dashboard-overview`, `/ui/merchant-login`).
|
||||
|
||||
Langkah berikutnya sesuai handoff: lanjut ke Task Pack B.1 (transaction core), lalu C.1–C.3.
|
||||
448
design/admin_application_review_detail/code.html
Normal file
@ -0,0 +1,448 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-container": "#54647a",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary": "#943700",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"on-surface": "#191b23",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"slate-900": "#0F172A",
|
||||
"surface-tint": "#0053db",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"inverse-surface": "#2e3039",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md overflow-hidden h-screen flex">
|
||||
<!-- Sidebar (Shared Component Style) -->
|
||||
<aside class="docked left-0 top-0 h-full w-64 border-r border-slate-200 bg-surface-container-lowest flex flex-col py-page-padding overflow-y-auto z-50">
|
||||
<div class="px-6 mb-8">
|
||||
<span class="font-headline-md text-headline-md font-bold text-primary">FinOps Admin</span>
|
||||
<p class="text-secondary text-label-md">System Management</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-body-md">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-primary font-bold border-r-4 border-primary bg-surface-container-low rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">pending_actions</span>
|
||||
<span class="font-body-md">Onboarding Queue</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
<span class="font-body-md">Merchant Directory</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">speaker_group</span>
|
||||
<span class="font-body-md">Device Fleet</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">assignment</span>
|
||||
<span class="font-body-md">Audit Logs</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="px-6 mt-auto pt-6 border-t border-slate-100 flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-primary-container flex items-center justify-center text-on-primary font-bold">JD</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface font-bold">Admin User</p>
|
||||
<p class="text-[10px] text-secondary">Level 3 Access</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 flex flex-col h-screen overflow-hidden relative">
|
||||
<!-- TopAppBar (Shared Component Style) -->
|
||||
<header class="h-topbar-height border-b border-slate-200 bg-surface-container-lowest flex justify-between items-center px-gutter z-40">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full transition-all">
|
||||
<span class="material-symbols-outlined">arrow_back</span>
|
||||
</button>
|
||||
<h1 class="font-headline-md text-headline-md font-black text-primary">Review: Coffee & Co. Jakarta</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 px-3 py-1 bg-warning/10 text-warning rounded-full">
|
||||
<span class="material-symbols-outlined text-[18px]">timer</span>
|
||||
<span class="text-label-md font-bold uppercase tracking-wider">Pending Review</span>
|
||||
</div>
|
||||
<div class="h-8 w-[1px] bg-slate-200"></div>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container rounded-full p-2 transition-all">notifications</button>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container rounded-full p-2 transition-all">help_outline</button>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container rounded-full p-2 transition-all">account_circle</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Split Screen Layout -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left Side: Information Details -->
|
||||
<div class="w-1/2 border-r border-slate-200 overflow-y-auto custom-scrollbar p-gutter bg-white">
|
||||
<div class="max-w-2xl mx-auto space-y-8">
|
||||
<!-- Section: Business Details -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="material-symbols-outlined text-primary">business</span>
|
||||
<h2 class="font-headline-md text-headline-md text-slate-700">Business Details</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-y-4 gap-x-8 bg-slate-50 p-6 rounded-xl border border-slate-200">
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Legal Business Name</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold">PT. Kopi Nusantara Abadi</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Brand Name</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold">Coffee & Co. Jakarta</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Business Category</label>
|
||||
<p class="font-body-lg text-slate-900">Food & Beverage (Café)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Business Type</label>
|
||||
<p class="font-body-lg text-slate-900">Limited Liability (PT)</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-label-md text-slate-500 block">Business Address</label>
|
||||
<p class="font-body-lg text-slate-900">Jl. Senopati No. 12, Kebayoran Baru, Jakarta Selatan, 12190</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Section: PIC Information -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="material-symbols-outlined text-primary">person</span>
|
||||
<h2 class="font-headline-md text-headline-md text-slate-700">Person in Charge (PIC)</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-y-4 gap-x-8 bg-slate-50 p-6 rounded-xl border border-slate-200">
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Full Name</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold">Bambang Wijaya</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Identity Number (KTP)</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold tracking-widest">3174092803850001</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Email Address</label>
|
||||
<p class="font-body-lg text-slate-900">bambang.w@coffeeandco.id</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Phone Number</label>
|
||||
<p class="font-body-lg text-slate-900">+62 812 3456 7890</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Section: Bank Account -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="material-symbols-outlined text-primary">account_balance</span>
|
||||
<h2 class="font-headline-md text-headline-md text-slate-700">Bank Settlement Info</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-y-4 gap-x-8 bg-slate-50 p-6 rounded-xl border border-slate-200">
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Bank Name</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold">Bank Central Asia (BCA)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-label-md text-slate-500 block">Account Number</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold tracking-wider">8820 123 456</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-label-md text-slate-500 block">Account Holder Name</label>
|
||||
<p class="font-body-lg text-slate-900 font-semibold uppercase">PT KOPI NUSANTARA ABADI</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Section: System Metadata -->
|
||||
<section class="pb-12">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="material-symbols-outlined text-primary">info</span>
|
||||
<h2 class="font-headline-md text-headline-md text-slate-700">Application Audit</h2>
|
||||
</div>
|
||||
<div class="bg-slate-900 p-4 rounded-xl font-mono text-xs text-slate-300">
|
||||
<div class="flex justify-between items-center mb-2 border-b border-slate-700 pb-2">
|
||||
<span>RAW PAYLOAD v2.4</span>
|
||||
<button class="text-primary hover:text-primary-fixed transition-colors flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[14px]">content_copy</span> Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre class="overflow-x-auto">{
|
||||
"application_id": "APP-2023-9912",
|
||||
"submission_date": "2023-11-24T10:22:31Z",
|
||||
"device_provisioning": true,
|
||||
"mcc_code": "5812",
|
||||
"risk_score": 0.12,
|
||||
"onboarding_channel": "Web Portal Direct"
|
||||
}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Side: Document Viewer -->
|
||||
<div class="w-1/2 overflow-y-auto custom-scrollbar p-gutter bg-slate-100">
|
||||
<div class="max-w-2xl mx-auto space-y-6">
|
||||
<h2 class="font-headline-md text-headline-md text-slate-700 flex items-center gap-2 mb-6">
|
||||
<span class="material-symbols-outlined">description</span>
|
||||
Document Verification
|
||||
</h2>
|
||||
<!-- Document: KTP -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="p-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
|
||||
<span class="font-label-md font-bold text-slate-700">IDENTITY CARD (KTP)</span>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="mr-3 text-label-md font-semibold text-slate-600">VERIFY</span>
|
||||
<div class="relative">
|
||||
<input checked="" class="sr-only peer" type="checkbox"/>
|
||||
<div class="w-11 h-6 bg-slate-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-success"></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="aspect-video relative overflow-hidden bg-slate-200 flex items-center justify-center group">
|
||||
<img class="w-full h-full object-cover" data-alt="A professional high-resolution scan of an Indonesian national identity card, known as KTP, placed on a neutral gray surface. The document is clearly visible with sharp text and a passport-style portrait of a man. The lighting is bright and even, highlighting the security watermarks and holograms. The style is clean, corporate, and functional for a banking verification interface." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCL7J17BsfNKQMf3o0iI0BpGl1Hh8YgbPq7EZR3bXzrNnpmSTlHwZPVoh26pCXFg88E-a8g922nUcI7AvOJwcw5S0Gsno58wLXKwwiSR5FcV-BFAm4Q08wr_Kcs8nKpxc3M4nkta8Y7-4Yt0peaX49sBp-zEzkT6xqWkPpyE93jprWPygz4Gh5D9yBkRZ4jV1qN9D9uwCoHo4WxPGFjispOoZEGArWYjwaItc4dvgSBxbQkjjzduJ-JyjsDIP06hmex1F0QsX6Srdw"/>
|
||||
<div class="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<button class="bg-white/20 backdrop-blur-md p-3 rounded-full text-white hover:bg-white/40 transition-all">
|
||||
<span class="material-symbols-outlined">zoom_in</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Document: NPWP -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="p-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
|
||||
<span class="font-label-md font-bold text-slate-700">TAX ID (NPWP)</span>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="mr-3 text-label-md font-semibold text-slate-600">VERIFY</span>
|
||||
<div class="relative">
|
||||
<input class="sr-only peer" type="checkbox"/>
|
||||
<div class="w-11 h-6 bg-slate-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-success"></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="aspect-video relative overflow-hidden bg-slate-200 flex items-center justify-center group">
|
||||
<img class="w-full h-full object-cover" data-alt="A clear, macro photograph of a business tax registration card, NPWP, displayed on a minimalist white desk. The card features a signature, a long identification number, and the Indonesian tax authority logo. The mood is professional and sterile, with high-key lighting that ensures every detail of the text is legible for administrative review. The color palette consists of soft grays and sharp blacks." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBjQ7TKbJxqbHDYoXjh1cDh_XtpuxnUJJV3DsHw6sfUkvnmVGX51TZ_yfk34jHWPVtoZ0pfkuy9TvMPy-8_uVVPZvGGNM4-uM_dQ1ggjDGlb2dgnMipAg08LKGP3IxTZo51_celEUR-PbyIpIyvW6Dqq7xcfLZjz7C7fHDvcdJELR3OwhyK0Wbqk1k_G_28yndui4EnizH02keVCIMQXnTyc26y98DJzThqw3Q3i-dDyosK9CJORMJGln6PPc0UHzCHfi8PpJkjtAY"/>
|
||||
<div class="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<button class="bg-white/20 backdrop-blur-md p-3 rounded-full text-white hover:bg-white/40 transition-all">
|
||||
<span class="material-symbols-outlined">zoom_in</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Document: Store Photo -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden mb-24">
|
||||
<div class="p-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
|
||||
<span class="font-label-md font-bold text-slate-700">STORE FRONT PHOTO</span>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="mr-3 text-label-md font-semibold text-slate-600">VERIFY</span>
|
||||
<div class="relative">
|
||||
<input class="sr-only peer" type="checkbox"/>
|
||||
<div class="w-11 h-6 bg-slate-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-success"></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="aspect-video relative overflow-hidden bg-slate-200 flex items-center justify-center group">
|
||||
<img class="w-full h-full object-cover" data-alt="A high-quality wide-angle shot of a modern urban coffee shop storefront in Jakarta. The aesthetic is industrial-chic with black steel frames, large glass windows, and a prominent 'Coffee & Co.' sign illuminated by warm LED lighting. The image is taken during a bright day with clear blue skies. The scene is clean, organized, and provides a clear view of the establishment's legitimacy for merchant onboarding." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBwAmK-1t1yF_JTlRNxt3Hyq3jE6w5HKj9Guxbj2r6wTo88dw5Y4d1EUdhY4U6irCWigI-M3m515dLa_GwHDGgdNMqxMGzKLwjKkH6QAMB2MGcPercYSuTOWJ1zNKYNMOHLohpJ7QUI902A4t73aQwxVwCaj66ihYR671dASPRgZo2EjAi5kBKHd8UqlDREPJXYsx0Wi4FnVFUVKWJQtyz4KTyW30Vta94fRvIv8blgHgKILVCYC8oNEyd8kbTrhaidYR06YnrMzTw"/>
|
||||
<div class="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<button class="bg-white/20 backdrop-blur-md p-3 rounded-full text-white hover:bg-white/40 transition-all">
|
||||
<span class="material-symbols-outlined">zoom_in</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sticky Bottom Bar -->
|
||||
<footer class="absolute bottom-0 left-0 right-0 h-20 bg-white border-t border-slate-200 flex items-center justify-between px-gutter z-50">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-label-md text-slate-500 font-bold uppercase">Assigned To</span>
|
||||
<span class="text-body-md text-slate-900 font-semibold">Self (Admin #042)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="px-8 py-3 border-2 border-danger text-danger font-bold rounded-lg hover:bg-danger/5 transition-all flex items-center gap-2" onclick="toggleModal('rejectModal')">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
REJECT APPLICATION
|
||||
</button>
|
||||
<button class="px-8 py-3 bg-primary text-on-primary font-bold rounded-lg hover:bg-primary-container transition-all flex items-center gap-2 shadow-lg shadow-primary/20">
|
||||
<span class="material-symbols-outlined">check_circle</span>
|
||||
APPROVE MERCHANT
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Rejection Modal Overlay -->
|
||||
<div class="hidden fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" id="rejectModal">
|
||||
<div class="bg-white w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200">
|
||||
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 class="text-headline-md font-bold text-slate-900">Rejection Reason</h3>
|
||||
<button class="material-symbols-outlined text-slate-400 hover:text-slate-600" onclick="toggleModal('rejectModal')">close</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<p class="text-body-md text-slate-600">Please select the primary reason for rejecting this application. This will be sent to the merchant.</p>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50 cursor-pointer transition-colors">
|
||||
<input class="text-primary focus:ring-primary h-4 w-4" name="reject_reason" type="radio"/>
|
||||
<span class="text-body-md font-medium">Incomplete/Blurry Documents</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50 cursor-pointer transition-colors">
|
||||
<input class="text-primary focus:ring-primary h-4 w-4" name="reject_reason" type="radio"/>
|
||||
<span class="text-body-md font-medium">Invalid Tax ID (NPWP)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50 cursor-pointer transition-colors">
|
||||
<input class="text-primary focus:ring-primary h-4 w-4" name="reject_reason" type="radio"/>
|
||||
<span class="text-body-md font-medium">Address Mismatch</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50 cursor-pointer transition-colors">
|
||||
<input class="text-primary focus:ring-primary h-4 w-4" name="reject_reason" type="radio"/>
|
||||
<span class="text-body-md font-medium">Restricted Category</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="text-label-md font-bold text-slate-500 mb-2 block">ADDITIONAL COMMENTS</label>
|
||||
<textarea class="w-full border-slate-200 rounded-lg focus:ring-primary focus:border-primary text-body-md" placeholder="Explain the specific issue here..." rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 bg-slate-50 flex gap-3">
|
||||
<button class="flex-1 py-3 text-slate-600 font-bold hover:bg-slate-200 rounded-lg transition-colors" onclick="toggleModal('rejectModal')">CANCEL</button>
|
||||
<button class="flex-1 py-3 bg-danger text-white font-bold rounded-lg hover:bg-danger/90 transition-colors">CONFIRM REJECTION</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
function toggleModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal.classList.contains('hidden')) {
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
} else {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
}
|
||||
|
||||
// Add some interaction to the verify toggles
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const card = this.closest('.bg-white');
|
||||
if (this.checked) {
|
||||
card.classList.add('ring-2', 'ring-success', 'ring-inset');
|
||||
} else {
|
||||
card.classList.remove('ring-2', 'ring-success', 'ring-inset');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_application_review_detail/screen.png
Normal file
|
After Width: | Height: | Size: 851 KiB |
599
design/admin_dashboard_overview/code.html
Normal file
@ -0,0 +1,599 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops - Admin Console</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"warning": "#F59E0B",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface": "#faf8ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"outline": "#737686",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"info": "#0EA5E9",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-bright": "#faf8ff",
|
||||
"surface-container": "#ededf9",
|
||||
"error-container": "#ffdad6",
|
||||
"slate-900": "#0F172A",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-700": "#334155",
|
||||
"slate-200": "#E2E8F0",
|
||||
"on-background": "#191b23",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"danger": "#DC2626",
|
||||
"on-primary-container": "#eeefff",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-surface-variant": "#434655",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"primary-container": "#2563eb",
|
||||
"background": "#F8FAFC",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-secondary-container": "#54647a",
|
||||
"slate-100": "#F1F5F9"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"gutter": "24px",
|
||||
"topbar-height": "72px",
|
||||
"card-padding": "20px",
|
||||
"row-height": "52px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md text-body-md overflow-x-hidden">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">Soundbox Ops</h1>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant opacity-70">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<!-- Active Tab: Overview -->
|
||||
<a class="bg-secondary-container dark:bg-secondary text-on-secondary-container dark:text-on-secondary font-bold rounded-lg flex items-center gap-3 px-3 py-2.5 transition-transform active:scale-95" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
<span>Merchant Management</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span>Device Registry</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span>Transactions</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
<span>Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
<span>Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto pt-6 border-t border-slate-100 dark:border-slate-800 space-y-1">
|
||||
<button class="w-full bg-primary text-on-primary py-2.5 rounded-lg font-bold mb-4 flex items-center justify-center gap-2 hover:opacity-90 active:scale-95 transition-all">
|
||||
<span class="material-symbols-outlined" data-icon="add">add</span>
|
||||
Register New Device
|
||||
</button>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2 rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- TopNavBar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] bg-surface-container-lowest dark:bg-slate-900 flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding z-40 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-6 flex-1">
|
||||
<div class="relative w-full max-w-md">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-body-lg">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-slate-100 dark:bg-slate-800 border-none rounded-full focus:ring-2 focus:ring-primary/20 text-body-md" placeholder="Search devices, merchants, or transactions..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="text-primary dark:text-primary-fixed border-b-2 border-primary h-[72px] flex items-center px-2 font-bold" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:text-primary transition-colors h-[72px] flex items-center px-2" href="#">System Health</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-on-surface-variant relative">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full"></span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-body-md leading-none">Admin User</p>
|
||||
<p class="text-label-md text-on-surface-variant opacity-70 leading-none mt-1">Super Administrator</p>
|
||||
</div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full border-2 border-primary/10" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB61eQv0UesiiqGw5OUHVQbaA_dyExJL4b7KTMpoWwbtef5ADmEto1ZpJkVAh1u1v3gjZ4jWeIcJxM3QEAc5Lbb_RiJRBzspl31-ArZ_BOk81uoa33eL3GnXH4FQKEPtgNy56dMsXrd4pnpPsXM2KL0-S9UFwfsxrXUHcnjlDarnrsdlP5lbKfQJmTVO2kF1h-uQxj5OxwomQxAJLT-B9Zy3ZCWaEsh9DYUtAp7zrAcvDT1PoMzxfS1012kLlCi3xUH0GrWeChmQXY"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="ml-64 pt-[72px] min-h-screen p-page-padding max-w-[1600px]">
|
||||
<!-- Dashboard Header & Welcome -->
|
||||
<div class="mb-8 flex justify-between items-end">
|
||||
<div>
|
||||
<h2 class="font-display-lg text-display-lg text-on-surface mb-1">Operational Overview</h2>
|
||||
<p class="text-body-lg text-on-surface-variant">Real-time status of your QRIS soundbox ecosystem.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors font-bold text-body-md">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="filter_list">filter_list</span>
|
||||
Filter View
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-primary text-on-primary rounded-lg hover:opacity-90 transition-colors font-bold text-body-md">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="download">download</span>
|
||||
Export Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bento Layout: KPI Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||||
<!-- Total Merchants -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="p-2 bg-primary/10 rounded-lg text-primary">
|
||||
<span class="material-symbols-outlined" data-icon="store">store</span>
|
||||
</div>
|
||||
<span class="text-success font-metric-sm flex items-center gap-1">
|
||||
+4.2% <span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Total Merchants</p>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">1,240</p>
|
||||
</div>
|
||||
<!-- Devices Online -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="p-2 bg-success/10 rounded-lg text-success">
|
||||
<span class="material-symbols-outlined" data-icon="sensors">sensors</span>
|
||||
</div>
|
||||
<span class="text-on-surface-variant font-label-md">94.4% Active</span>
|
||||
</div>
|
||||
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Devices Online</p>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">850<span class="text-body-lg text-slate-400 font-normal"> / 900</span></p>
|
||||
</div>
|
||||
<!-- Today's Volume -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="p-2 bg-warning/10 rounded-lg text-warning">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
</div>
|
||||
<span class="text-success font-metric-sm flex items-center gap-1">
|
||||
+12% <span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Today's Transactions</p>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">Rp450M</p>
|
||||
</div>
|
||||
<!-- Success Rate -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="p-2 bg-info/10 rounded-lg text-info">
|
||||
<span class="material-symbols-outlined" data-icon="verified">verified</span>
|
||||
</div>
|
||||
<span class="text-success font-metric-sm flex items-center gap-1">
|
||||
Stable <span class="material-symbols-outlined text-[16px]" data-icon="check_circle">check_circle</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Success Rate</p>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">99.2%</p>
|
||||
</div>
|
||||
<!-- Pending Settlements -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow border-l-4 border-l-warning">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="p-2 bg-error/10 rounded-lg text-error">
|
||||
<span class="material-symbols-outlined" data-icon="hourglass_empty">hourglass_empty</span>
|
||||
</div>
|
||||
<button class="text-primary font-bold text-label-md hover:underline">View All</button>
|
||||
</div>
|
||||
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Pending Settlements</p>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">24</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Chart & Sidebar Rail Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 mb-8">
|
||||
<!-- Left: Chart & Table Column -->
|
||||
<div class="lg:col-span-8 space-y-8">
|
||||
<!-- Transaction Trend Chart -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 class="text-headline-md font-headline-md text-on-surface">Transaction Volume Trend</h3>
|
||||
<p class="text-body-md text-on-surface-variant">Last 7 days performance metrics</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 bg-primary rounded-full"></span>
|
||||
<span class="text-label-md font-label-md">Current Period</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 bg-slate-200 rounded-full"></span>
|
||||
<span class="text-label-md font-label-md text-on-surface-variant">Previous Period</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chart Placeholder -->
|
||||
<div class="h-[320px] w-full flex items-end gap-2 px-4">
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[40%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[70%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Mon</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[60%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[85%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Tue</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[55%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[65%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Wed</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[80%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[95%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Thu</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[70%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[80%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Fri</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[45%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[50%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400">Sat</span>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-end gap-1">
|
||||
<div class="w-full bg-slate-100 rounded-t h-[90%] relative group">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[100%] group-hover:bg-primary transition-colors cursor-pointer"></div>
|
||||
</div>
|
||||
<span class="text-center text-label-md text-slate-400 font-bold text-primary">Sun</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pending Merchant Table -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-headline-md font-headline-md text-on-surface">Pending Merchant Onboarding</h3>
|
||||
<p class="text-body-md text-on-surface-variant">New applications requiring review</p>
|
||||
</div>
|
||||
<button class="text-primary font-bold text-body-md hover:underline">View Full Queue</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50">
|
||||
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Merchant Name</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Category</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Submission Date</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">KB</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Kopi Bahagia</p>
|
||||
<p class="text-label-md text-slate-400">ID: MERCH-9021</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">F&B - Cafe</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">Oct 24, 2023</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
|
||||
Pending Review
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">JM</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Jaya Mart</p>
|
||||
<p class="text-label-md text-slate-400">ID: MERCH-8843</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">Retail - Grocery</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">Oct 23, 2023</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
|
||||
Pending Review
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">AL</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Apotek Lestari</p>
|
||||
<p class="text-label-md text-slate-400">ID: MERCH-8712</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">Healthcare - Pharma</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">Oct 23, 2023</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
|
||||
Pending Review
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Health & Alerts Rail Column -->
|
||||
<div class="lg:col-span-4 space-y-8">
|
||||
<!-- Device Health Distribution -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
|
||||
<h3 class="text-headline-md font-headline-md text-on-surface mb-6">Device Health</h3>
|
||||
<div class="flex justify-center mb-8 relative">
|
||||
<svg class="w-48 h-48 transform -rotate-90">
|
||||
<circle class="text-slate-100" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-width="20"></circle>
|
||||
<circle class="text-success" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="50.2" stroke-width="20"></circle>
|
||||
<circle class="text-warning" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="440" stroke-width="20"></circle>
|
||||
<circle class="text-danger" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="480" stroke-width="20"></circle>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<p class="text-metric-lg font-metric-lg leading-none">94%</p>
|
||||
<p class="text-label-md text-slate-400 mt-1">Healthy</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 bg-success rounded-full"></span>
|
||||
<span class="text-body-md">Online / Ready</span>
|
||||
</div>
|
||||
<span class="font-bold">850</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 bg-warning rounded-full"></span>
|
||||
<span class="text-body-md">Degraded / Slow</span>
|
||||
</div>
|
||||
<span class="font-bold">35</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 bg-danger rounded-full"></span>
|
||||
<span class="text-body-md">Offline / Error</span>
|
||||
</div>
|
||||
<span class="font-bold">15</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recent Alerts / Incidents -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl flex flex-col">
|
||||
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 class="text-headline-md font-headline-md text-on-surface">Recent Alerts</h3>
|
||||
<span class="bg-error/10 text-error px-2 py-0.5 rounded text-label-md font-bold">2 Critical</span>
|
||||
</div>
|
||||
<div class="p-4 space-y-4 max-h-[480px] overflow-y-auto custom-scrollbar">
|
||||
<!-- Alert Item -->
|
||||
<div class="p-4 rounded-lg bg-error/5 border border-error/10">
|
||||
<div class="flex gap-3">
|
||||
<span class="material-symbols-outlined text-error" data-icon="error">error</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-bold text-on-surface text-body-md">Terminal X-009 Offline</p>
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Location: Outlet Y (Sudirman Mall)</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-label-md text-slate-400">2 mins ago</span>
|
||||
<button class="text-primary font-bold text-label-md">Dispatch Tech</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Alert Item -->
|
||||
<div class="p-4 rounded-lg bg-warning/5 border border-warning/10">
|
||||
<div class="flex gap-3">
|
||||
<span class="material-symbols-outlined text-warning" data-icon="warning">warning</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-bold text-on-surface text-body-md">Network Latency Spike</p>
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Impact: Cluster Jakarta Selatan</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-label-md text-slate-400">14 mins ago</span>
|
||||
<button class="text-primary font-bold text-label-md">Investigate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Alert Item -->
|
||||
<div class="p-4 rounded-lg bg-error/5 border border-error/10">
|
||||
<div class="flex gap-3">
|
||||
<span class="material-symbols-outlined text-error" data-icon="error">error</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-bold text-on-surface text-body-md">Repeated Auth Failure</p>
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Merchant: IndoFresh Mart #44</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-label-md text-slate-400">45 mins ago</span>
|
||||
<button class="text-primary font-bold text-label-md">Lock Device</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Info Alert -->
|
||||
<div class="p-4 rounded-lg bg-info/5 border border-info/10">
|
||||
<div class="flex gap-3">
|
||||
<span class="material-symbols-outlined text-info" data-icon="info">info</span>
|
||||
<div class="flex-1">
|
||||
<p class="font-bold text-on-surface text-body-md">New FW Update Available</p>
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Version 2.4.1 (Stable Build)</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-label-md text-slate-400">2 hours ago</span>
|
||||
<button class="text-primary font-bold text-label-md">Deploy Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full py-4 text-on-surface-variant hover:bg-slate-50 transition-colors font-bold text-body-md border-t border-slate-100">
|
||||
Clear All Notifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- System Activity Log (Compact Footer-level detail) -->
|
||||
<div class="bg-slate-900 text-white rounded-xl p-6 mb-8 overflow-hidden relative">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-headline-md font-headline-md">Audit Activity Stream</h3>
|
||||
<div class="flex gap-2">
|
||||
<span class="px-2 py-1 bg-white/10 rounded text-[10px] font-bold uppercase tracking-widest">Live Stream</span>
|
||||
<span class="px-2 py-1 bg-success/20 text-success rounded text-[10px] font-bold uppercase tracking-widest">Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-mono text-[13px] space-y-2 opacity-80">
|
||||
<p><span class="text-primary-fixed-dim">[14:32:11]</span> <span class="text-success">SUCCESS:</span> Settlement triggered for Cluster-B (Rp12.4M handled)</p>
|
||||
<p><span class="text-primary-fixed-dim">[14:31:05]</span> <span class="text-info">INFO:</span> Device X-292 ping response received (latency 42ms)</p>
|
||||
<p><span class="text-primary-fixed-dim">[14:29:44]</span> <span class="text-warning">WARN:</span> Merchant ID 9921 failed KYC validation step 3</p>
|
||||
<p><span class="text-primary-fixed-dim">[14:28:12]</span> <span class="text-success">SUCCESS:</span> New Admin 'DevOps_Main' logged in via MFA</p>
|
||||
</div>
|
||||
<!-- Decorative backdrop for "raw data" feel -->
|
||||
<div class="absolute -right-4 -bottom-4 opacity-10 pointer-events-none">
|
||||
<span class="material-symbols-outlined text-[120px]" data-icon="terminal">terminal</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
// Micro-interactions for hovering and state visual feedback
|
||||
document.querySelectorAll('.hover\\:shadow-lg').forEach(card => {
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transform = 'translateY(-2px)';
|
||||
card.style.transition = 'all 0.3s ease';
|
||||
});
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
|
||||
// Simple real-time clock indicator in TopBar
|
||||
const updateTime = () => {
|
||||
// Logic for a real-time system clock could go here
|
||||
};
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_dashboard_overview/screen.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
567
design/admin_fee_pricing_management/code.html
Normal file
@ -0,0 +1,567 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Fee & Pricing Management | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.mono-font { font-family: 'JetBrains Mono', monospace; }
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
/* Low-contrast outline for cards */
|
||||
.card-border { border: 1px solid #E2E8F0; }
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"danger": "#DC2626",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"error-container": "#ffdad6",
|
||||
"outline": "#737686",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"warning": "#F59E0B",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"error": "#ba1a1a",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-error": "#ffffff",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-surface": "#191b23",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"on-background": "#191b23",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"surface-bright": "#faf8ff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-primary": "#ffffff",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-container": "#54647a"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px",
|
||||
"card-padding": "20px",
|
||||
"gutter": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background font-body-md text-on-surface antialiased">
|
||||
<!-- SideNavBar Anchor -->
|
||||
<aside class="flex flex-col fixed left-0 top-0 h-full p-4 gap-2 bg-white border-r border-slate-200 w-64 z-[60] hidden md:flex">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md text-primary font-bold">Soundbox Admin</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500">Fintech Ops Suite</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
<span class="font-label-md text-label-md">Reconciliation</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="security">security</span>
|
||||
<span class="font-label-md text-label-md">Audit Logs</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 bg-secondary-container text-on-secondary-container rounded-xl font-bold" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
<span class="font-label-md text-label-md">Fee Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span class="font-label-md text-label-md">Settlements</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="router">router</span>
|
||||
<span class="font-label-md text-label-md">Device Health</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="contact_support">contact_support</span>
|
||||
<span class="font-label-md text-label-md">Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto pt-4 border-t border-slate-100">
|
||||
<button class="w-full bg-primary text-white py-3 rounded-xl font-label-md text-label-md font-bold mb-4 active:opacity-90">
|
||||
Generate Report
|
||||
</button>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-danger hover:bg-red-50 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="md:ml-64 min-h-screen">
|
||||
<!-- TopNavBar Anchor -->
|
||||
<header class="flex justify-between items-center h-[72px] px-page-padding w-full sticky top-0 z-50 bg-surface border-b border-slate-200">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</div>
|
||||
<nav class="hidden lg:flex items-center gap-6">
|
||||
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-md text-body-md" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-md text-body-md" href="#">Merchants</a>
|
||||
<a class="text-primary border-b-2 border-primary font-bold pb-1 font-body-md text-body-md" href="#">Operations</a>
|
||||
<a class="text-on-surface-variant hover:text-primary transition-colors font-body-md text-body-md" href="#">Audit</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative group">
|
||||
<input class="bg-slate-100 border-none rounded-full px-4 py-2 text-body-md w-64 focus:ring-2 focus:ring-primary transition-all" placeholder="Search operations..." type="text"/>
|
||||
<span class="material-symbols-outlined absolute right-3 top-2 text-slate-400">search</span>
|
||||
</div>
|
||||
<button class="p-2 text-slate-500 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="p-2 text-slate-500 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
</button>
|
||||
<img alt="Administrator Avatar" class="w-8 h-8 rounded-full object-cover border-2 border-primary-container" data-alt="A professional headshot of a corporate administrator in a modern tech office setting. The person is smiling confidently, wearing business-casual attire. The background features soft bokeh with office glass and warm interior lighting, reflecting a reliable and precise fintech environment. High-key lighting, corporate portrait style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAhJ_b15g9taoZ2e12vFpcTrcSaFJHkdDkWsrFySkJKhqRP4sQaB9PVayTxwSA9Z7_SIdPgWY7sWxcgFYYWzhxhZb3t-hkRiTtpvi8XeDKczdOf24TxrBNaYN5pfafEfMSjixcdZ5eYihLlfn8rsCGAq5-vg8zo9fEyg3EDBW8-0bvwYBLPm5xSE8XpsM2x8h1NJx82SPEOaRF_5xqrXSE0Uqm2GApDe1vF-pIVW7XghgwW6L8bxw8mzAtZ1gGfK9WGxgh5UJe_Tsw"/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-page-padding max-w-[1600px] mx-auto space-y-gutter">
|
||||
<!-- Header & Page Title -->
|
||||
<div class="flex justify-between items-end">
|
||||
<div>
|
||||
<h2 class="font-headline-lg text-headline-lg text-slate-900">Fee & Pricing Management</h2>
|
||||
<p class="font-body-lg text-body-lg text-slate-500 mt-1">Configure transaction MDR, platform fees, and subscription tiers.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-xl font-label-md text-label-md text-secondary hover:bg-slate-50 transition-all">
|
||||
<span class="material-symbols-outlined text-base" data-icon="history">history</span>
|
||||
View Log
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-6 py-2 bg-primary text-white rounded-xl font-label-md text-label-md font-bold shadow-sm hover:shadow-md transition-all active:scale-95">
|
||||
<span class="material-symbols-outlined text-base" data-icon="add">add</span>
|
||||
New Pricing Tier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bento Grid for Core Content -->
|
||||
<div class="grid grid-cols-12 gap-gutter">
|
||||
<!-- Left Column: Pricing Tiers Selection -->
|
||||
<div class="col-span-12 lg:col-span-4 space-y-4">
|
||||
<h3 class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Active Pricing Tiers</h3>
|
||||
<div class="space-y-3">
|
||||
<!-- Tier Card -->
|
||||
<div class="bg-white p-card-padding rounded-xl card-border border-l-4 border-l-primary shadow-sm cursor-pointer hover:bg-slate-50 transition-all">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="px-2 py-0.5 bg-secondary-container text-on-secondary-container text-xs font-bold rounded">DEFAULT</span>
|
||||
<span class="material-symbols-outlined text-slate-300">more_vert</span>
|
||||
</div>
|
||||
<h4 class="font-headline-md text-headline-md text-slate-900">SME Basic</h4>
|
||||
<p class="text-body-md text-slate-500 mb-4">Optimized for small-volume merchants.</p>
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-slate-100 pt-4">
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Base MDR</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">0.75%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Subscription</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">$12/mo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding rounded-xl card-border shadow-sm cursor-pointer hover:bg-slate-50 transition-all opacity-80 border-l-4 border-l-transparent">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="px-2 py-0.5 bg-slate-100 text-slate-500 text-xs font-bold rounded">PREMIUM</span>
|
||||
<span class="material-symbols-outlined text-slate-300">more_vert</span>
|
||||
</div>
|
||||
<h4 class="font-headline-md text-headline-md text-slate-900">Enterprise Plus</h4>
|
||||
<p class="text-body-md text-slate-500 mb-4">High-volume corporate accounts.</p>
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-slate-100 pt-4">
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Base MDR</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">0.45%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Subscription</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">$150/mo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding rounded-xl card-border shadow-sm cursor-pointer hover:bg-slate-50 transition-all opacity-80 border-l-4 border-l-transparent">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="px-2 py-0.5 bg-slate-900 text-white text-xs font-bold rounded">CUSTOM</span>
|
||||
<span class="material-symbols-outlined text-slate-300">more_vert</span>
|
||||
</div>
|
||||
<h4 class="font-headline-md text-headline-md text-slate-900">Government Special</h4>
|
||||
<p class="text-body-md text-slate-500 mb-4">Public sector specific regulations.</p>
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-slate-100 pt-4">
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Base MDR</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">0.10%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 uppercase font-bold">Subscription</div>
|
||||
<div class="font-metric-sm text-metric-sm text-primary">$0/mo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column: Detail Configuration & Bulk -->
|
||||
<div class="col-span-12 lg:col-span-8 space-y-gutter">
|
||||
<!-- Configuration Form Card -->
|
||||
<div class="bg-white rounded-xl card-border shadow-sm overflow-hidden">
|
||||
<div class="bg-slate-50 p-page-padding border-b border-slate-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md text-slate-900">Configure Tier: SME Basic</h3>
|
||||
<p class="text-body-md text-slate-500">Managing global transaction rules for all assigned merchants.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-4 py-2 bg-slate-200 text-slate-700 rounded-lg font-label-md text-label-md hover:bg-slate-300 transition-colors">Reset</button>
|
||||
<button class="px-6 py-2 bg-success text-white rounded-lg font-label-md text-label-md font-bold shadow-sm hover:opacity-90 transition-all">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-page-padding grid grid-cols-2 gap-x-8 gap-y-10">
|
||||
<!-- MDR Section -->
|
||||
<div class="col-span-2 md:col-span-1 space-y-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="percent">percent</span>
|
||||
<h4 class="font-label-md text-label-md font-bold uppercase tracking-widest text-slate-400">Merchant Discount Rate (MDR)</h4>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Standard QRIS (%)</label>
|
||||
<div class="relative">
|
||||
<input class="w-full pl-4 pr-12 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="0.75"/>
|
||||
<span class="absolute right-4 top-3.5 text-slate-400 font-bold">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Credit Card (Global) (%)</label>
|
||||
<div class="relative">
|
||||
<input class="w-full pl-4 pr-12 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="2.10"/>
|
||||
<span class="absolute right-4 top-3.5 text-slate-400 font-bold">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Debit Card (%)</label>
|
||||
<div class="relative">
|
||||
<input class="w-full pl-4 pr-12 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="1.00"/>
|
||||
<span class="absolute right-4 top-3.5 text-slate-400 font-bold">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fixed & Subs Section -->
|
||||
<div class="col-span-2 md:col-span-1 space-y-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="payments">payments</span>
|
||||
<h4 class="font-label-md text-label-md font-bold uppercase tracking-widest text-slate-400">Fixed Fees & Subscription</h4>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Processing Fee (Fixed)</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-4 top-3.5 text-slate-400 font-bold">$</span>
|
||||
<input class="w-full pl-10 pr-4 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="0.25"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Monthly Subscription</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-4 top-3.5 text-slate-400 font-bold">$</span>
|
||||
<input class="w-full pl-10 pr-4 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="12.00"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-body-md font-bold text-slate-700 mb-2">Settlement Transfer Fee</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-4 top-3.5 text-slate-400 font-bold">$</span>
|
||||
<input class="w-full pl-10 pr-4 py-3 bg-slate-50 border-slate-200 rounded-xl focus:ring-primary focus:border-primary" step="0.01" type="number" value="0.50"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advanced Logic Section -->
|
||||
<div class="col-span-2 pt-6 border-t border-slate-100">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="font-label-md text-label-md font-bold text-slate-900 uppercase">Tier Logic Restrictions</h4>
|
||||
<button class="text-primary font-label-md text-label-md hover:underline">+ Add Custom Rule</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="flex items-center gap-4 p-4 bg-slate-50 rounded-xl border border-dashed border-slate-300">
|
||||
<div class="p-2 bg-white rounded-lg shadow-sm">
|
||||
<span class="material-symbols-outlined text-warning" data-icon="terminal">terminal</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Cap Maximum Fee</p>
|
||||
<p class="text-xs text-slate-500">Limit total MDR to $25.00 per TRX</p>
|
||||
</div>
|
||||
<label class="ml-auto inline-flex relative items-center cursor-pointer">
|
||||
<input checked="" class="sr-only peer" type="checkbox" value=""/>
|
||||
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 bg-slate-50 rounded-xl border border-dashed border-slate-300">
|
||||
<div class="p-2 bg-white rounded-lg shadow-sm">
|
||||
<span class="material-symbols-outlined text-info" data-icon="event_repeat">event_repeat</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Prorated First Month</p>
|
||||
<p class="text-xs text-slate-500">Calculate sub-fee based on join date</p>
|
||||
</div>
|
||||
<label class="ml-auto inline-flex relative items-center cursor-pointer">
|
||||
<input class="sr-only peer" type="checkbox" value=""/>
|
||||
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bulk Update & Audit Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-gutter">
|
||||
<!-- Bulk Update Section -->
|
||||
<div class="bg-slate-900 text-white p-page-padding rounded-xl shadow-lg relative overflow-hidden group">
|
||||
<!-- Background Decor -->
|
||||
<div class="absolute -right-8 -bottom-8 opacity-10 group-hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-outlined text-[160px]" data-icon="update">update</span>
|
||||
</div>
|
||||
<div class="relative z-10 space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-inverse-primary" data-icon="published_with_changes">published_with_changes</span>
|
||||
<h3 class="font-headline-md text-headline-md">Bulk Fee Update</h3>
|
||||
</div>
|
||||
<p class="text-body-md text-slate-400">Apply a flat-rate adjustment or percentage shift to multiple tiers or specific merchant clusters instantly.</p>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button class="flex-1 bg-white/10 hover:bg-white/20 px-4 py-2 rounded-lg font-label-md text-label-md transition-all">Select Target Groups</button>
|
||||
<button class="px-4 py-2 bg-primary text-white rounded-lg font-label-md text-label-md font-bold shadow-sm active:scale-95 transition-all">Start Wizard</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Audit History Table Block -->
|
||||
<div class="bg-white rounded-xl card-border shadow-sm overflow-hidden flex flex-col">
|
||||
<div class="p-4 border-b border-slate-200 flex justify-between items-center bg-slate-50/50">
|
||||
<h3 class="font-label-md text-label-md font-bold text-slate-900 uppercase tracking-widest">History of Fee Changes</h3>
|
||||
<span class="text-xs text-primary cursor-pointer hover:underline">Full Log</span>
|
||||
</div>
|
||||
<div class="overflow-auto scrollbar-hide flex-1">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead class="sticky top-0 bg-white border-b border-slate-200 z-10">
|
||||
<tr class="h-10">
|
||||
<th class="px-4 font-label-md text-[10px] text-slate-400 uppercase">Timestamp</th>
|
||||
<th class="px-4 font-label-md text-[10px] text-slate-400 uppercase">Change</th>
|
||||
<th class="px-4 font-label-md text-[10px] text-slate-400 uppercase">Admin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
<tr class="hover:bg-slate-50 transition-colors cursor-pointer group">
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-900 mono-font">2023-11-24 14:02</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-600">SME Basic QRIS <span class="text-danger font-bold">0.65% → 0.75%</span></div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<img class="w-5 h-5 rounded-full" data-alt="A profile picture of a male fintech administrator in professional lighting. The image captures a sense of reliability and technical expertise, with a clean office background and soft focused details. Modern, corporate aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCxKAv4VWZ9OCCWi88dAZObuF661dRUde_5BLaKeE3UrXBW9pB-rVmDIn_CN2v4sn5YbzCc5d3xrsHBFHzrB7_x-ZCPFaI2S6G8GM4IjV2zb7OXaKWIHp0gVPeQrjyX6jqsB_W5KYF7sTFmmXLo5m1LPG9oUKc-Z9p-Sd4ifd_a1zJpNrPrNjK0oMH1gBOpxd9FIm1S4MGfW9cInBsleK9ahacD7CvnW8e16_wsgBY-4m584k3j3UPvnLWc8jA596_lFDCE_vuu1lI"/>
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">J. DOE</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors cursor-pointer">
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-900 mono-font">2023-11-23 09:15</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-600">New Tier Created: <span class="text-success font-bold">Gov Special</span></div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<img class="w-5 h-5 rounded-full" data-alt="A portrait of a focused female technology specialist in a minimalist setting. The mood is calm and efficient, with high-quality daylight illuminating her profile. The styling is professional and modern, perfectly fitting a fintech operational environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAP0GXCeQomMoUl9_5bZfYL-05neftDyRV4m1EZaCT5_ftxnpdgjGsBAuX3jTZPZK6crUGyPBMwJNidpJtn506WQKZGl-Fl2arsXiAqgVAKv_Dma3l_c7eWHW0l_4KQVkbVPSApt1RIUFMRq3IIyC8LGKXXK7uE4YrLTqRqZ8mQG1p7o8O_sFghhIQE67kbzLGUUAWYgsx4O_71BBq14geWnJsRZaLzpsl_8570ILAqV6M3iIKV3eWFsqlITs6NiNNlVs8TIdUZbjA"/>
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">A. CHEN</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors cursor-pointer">
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-900 mono-font">2023-11-22 17:44</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-xs text-slate-600">Enterprise Subs <span class="text-info font-bold">$125 → $150</span></div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-5 h-5 rounded-full bg-slate-200 flex items-center justify-center text-[8px] font-bold">SYS</div>
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">SYSTEM</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Raw Payload Viewer (Audit Block) -->
|
||||
<div class="bg-slate-900 rounded-xl overflow-hidden shadow-inner relative group">
|
||||
<div class="px-4 py-2 border-b border-slate-800 flex justify-between items-center">
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Active Configuration JSON</span>
|
||||
<button class="flex items-center gap-1 text-[10px] text-slate-400 hover:text-white transition-colors">
|
||||
<span class="material-symbols-outlined text-xs" data-icon="content_copy">content_copy</span>
|
||||
Copy Payload
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-x-auto">
|
||||
<pre class="mono-font text-sm text-green-400 scrollbar-hide">{
|
||||
"tier_id": "tier_sme_001",
|
||||
"tier_name": "SME Basic",
|
||||
"pricing_logic": {
|
||||
"mdr": {
|
||||
"qris": 0.0075,
|
||||
"credit_card": 0.0210,
|
||||
"debit_card": 0.0100
|
||||
},
|
||||
"fixed_fees": {
|
||||
"processing": 0.25,
|
||||
"settlement": 0.50
|
||||
},
|
||||
"subscription": {
|
||||
"monthly": 12.00,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
"constraints": {
|
||||
"max_fee_cap": 25.00,
|
||||
"prorated_subscription": false
|
||||
},
|
||||
"last_updated": "2023-11-24T14:02:11Z"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Detail Drawer Placeholder (Right slide-in simulation) -->
|
||||
<div class="fixed right-0 top-0 h-full w-[400px] bg-white shadow-2xl z-[100] transform translate-x-full transition-transform duration-300 ease-in-out" id="audit-drawer">
|
||||
<div class="p-6 h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="font-headline-md text-headline-md text-slate-900">Change Details</h3>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full transition-colors" onclick="document.getElementById('audit-drawer').classList.add('translate-x-full')">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 space-y-8 overflow-y-auto pr-2">
|
||||
<section>
|
||||
<label class="font-label-md text-label-md text-slate-400 uppercase block mb-3">Timeline</label>
|
||||
<div class="relative pl-6 space-y-6 before:content-[''] before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-[2px] before:bg-slate-100">
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[23px] top-1 w-4 h-4 rounded-full bg-success border-4 border-white"></div>
|
||||
<p class="text-sm font-bold">Approved by Finance Dir</p>
|
||||
<p class="text-xs text-slate-500">2023-11-24 15:30</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[23px] top-1 w-4 h-4 rounded-full bg-primary border-4 border-white"></div>
|
||||
<p class="text-sm font-bold">Requested by J. Doe</p>
|
||||
<p class="text-xs text-slate-500">2023-11-24 14:02</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[23px] top-1 w-4 h-4 rounded-full bg-slate-300 border-4 border-white"></div>
|
||||
<p class="text-sm font-bold text-slate-400">Previous Value Verified</p>
|
||||
<p class="text-xs text-slate-500">2023-11-24 13:45</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-4 bg-slate-50 rounded-xl space-y-3">
|
||||
<label class="font-label-md text-label-md text-slate-400 uppercase block">Rationale</label>
|
||||
<p class="text-sm text-slate-700 leading-relaxed italic">"Adjusting QRIS MDR to reflect new interbank processing costs and maintain margin for small-volume SME segment."</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="pt-6 border-t border-slate-100 mt-auto">
|
||||
<button class="w-full py-3 bg-slate-900 text-white rounded-xl font-bold font-label-md">Download Audit PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Micro-interaction for drawer
|
||||
document.querySelector('tbody tr').addEventListener('click', function() {
|
||||
document.getElementById('audit-drawer').classList.remove('translate-x-full');
|
||||
});
|
||||
|
||||
// Dashboard Interaction
|
||||
const cards = document.querySelectorAll('.lg\\:col-span-4 .bg-white');
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
cards.forEach(c => {
|
||||
c.classList.remove('border-l-primary');
|
||||
c.classList.add('border-l-transparent', 'opacity-80');
|
||||
});
|
||||
card.classList.remove('border-l-transparent', 'opacity-80');
|
||||
card.classList.add('border-l-primary');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_fee_pricing_management/screen.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
260
design/admin_login_portal/code.html
Normal file
@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="id"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Login Admin | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.login-gradient {
|
||||
background: radial-gradient(circle at 50% 50%, rgba(37, 99, 235, 0.05) 0%, transparent 100%);
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-primary": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"on-background": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"on-secondary-container": "#54647a",
|
||||
"secondary": "#505f76",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"danger": "#DC2626",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"outline": "#737686",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"slate-100": "#F1F5F9",
|
||||
"error-container": "#ffdad6",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"warning": "#F59E0B",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"info": "#0EA5E9",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-error": "#ffffff",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"metric-sm": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"metric-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md min-h-screen flex items-center justify-center p-4">
|
||||
<!-- Main Container -->
|
||||
<main class="w-full max-w-[1100px] grid grid-cols-1 md:grid-cols-2 bg-surface-container-lowest rounded-xl shadow-2xl overflow-hidden border border-slate-200">
|
||||
<!-- Left Side: Login Form -->
|
||||
<div class="p-8 md:p-16 flex flex-col justify-center relative overflow-hidden login-gradient">
|
||||
<div class="mb-12">
|
||||
<img alt="Soundbox Ops Logo" class="h-12 w-auto mb-8" src="https://lh3.googleusercontent.com/aida/ADBb0ug9HaB2AriUP4nVrKMOARoZTCm9CjAvMRfjT5wYuZ28c5NkPI6L46e_UFThUQPqPv-K4M6u1kyZquW5BDLZyRJSF9YLnwi_mI5uLm2wJEBGLDxQHiD_V9fmLgPaggsISesqP8Emgu71sqr05ARxSf4H2YOhq5u6sFUSvOIIL82WsxLajMZ-nih3D86dJlFa-trY0J3Rj5JyOW9El0dy9BF4-npXWTYUDjwfMVDeEGMw93XUNIHdxQmPkAg"/>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-2">Admin Portal</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Silakan masuk untuk mengelola infrastruktur Soundbox Ops.</p>
|
||||
</div>
|
||||
<form action="#" class="space-y-6" onsubmit="return false;">
|
||||
<!-- Email Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block font-label-md text-label-md text-slate-700" for="email">Alamat Email</label>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-[20px]">mail</span>
|
||||
<input class="w-full pl-10 pr-4 py-3 rounded-xl border border-slate-200 bg-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none font-body-md text-body-md" id="email" name="email" placeholder="admin@soundboxops.id" required="" type="email"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Password Field -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="block font-label-md text-label-md text-slate-700" for="password">Kata Sandi</label>
|
||||
<a class="font-label-md text-label-md text-primary hover:underline transition-all" href="#">Lupa kata sandi?</a>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-[20px]">lock</span>
|
||||
<input class="w-full pl-10 pr-12 py-3 rounded-xl border border-slate-200 bg-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none font-body-md text-body-md" id="password" name="password" placeholder="••••••••" required="" type="password"/>
|
||||
<button class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-primary transition-colors" type="button">
|
||||
<span class="material-symbols-outlined text-[20px]">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Remember Me Toggle -->
|
||||
<div class="flex items-center">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input class="sr-only peer" type="checkbox" value=""/>
|
||||
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
<span class="ml-3 font-body-md text-body-md text-on-surface-variant">Ingat saya untuk 30 hari</span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<button class="w-full py-4 bg-primary text-on-primary rounded-xl font-headline-md text-headline-md hover:bg-surface-tint active:scale-[0.98] transition-all flex justify-center items-center gap-2 shadow-lg shadow-primary/20 group" type="submit">
|
||||
Masuk Sekarang
|
||||
<span class="material-symbols-outlined group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-12 pt-8 border-t border-slate-100 flex justify-between items-center">
|
||||
<p class="font-label-md text-label-md text-slate-500">© 2024 Soundbox Ops</p>
|
||||
<div class="flex gap-4">
|
||||
<a class="font-label-md text-label-md text-slate-500 hover:text-primary" href="#">Bantuan</a>
|
||||
<a class="font-label-md text-label-md text-slate-500 hover:text-primary" href="#">Kebijakan</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Side: Visual Illustration -->
|
||||
<div class="hidden md:block relative bg-slate-900 overflow-hidden">
|
||||
<!-- Background Image with prompt-driven alt -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img class="w-full h-full object-cover opacity-40 mix-blend-luminosity" data-alt="A highly detailed close-up of a futuristic fintech operations dashboard displaying real-time digital ledger transactions and IoT device health. The aesthetic is clean and corporate, featuring a palette of deep slate blacks and vibrant neon blue accents. Soft, cinematic lighting illuminates the scene, creating a professional and trustworthy enterprise SaaS atmosphere with high-tech data visualizations." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCcMCYxVWHKiu6zABQgGjvipDpzzMs1ZestQF1PJTvFG9A0Dxx6aJ8VRvuUHopGtVEA053iEK1dIYWWuOM8Ut2ExY4Fh2Cwa6UpbYcNjc36uovWAmJ2_j3uxJKo5g9GW4GcUwozOwmSZZ8kQukgjxJzhsUnkbuXwzZbFIJj4FkLhRzTiacxOOtPg4XhSagS5vh3UuH-zE3b1zbi0cFoD8iwtqxTZnC3StYK3QN7ydapksOXkwShOok9-zNUGmAoNHEf2pONmss3gmE"/>
|
||||
</div>
|
||||
<!-- Overlay Content -->
|
||||
<div class="relative z-10 h-full flex flex-col justify-end p-12 text-white bg-gradient-to-t from-slate-900 via-transparent to-transparent">
|
||||
<div class="glass-panel p-8 rounded-xl border-white/10">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="p-3 bg-primary rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-white">security</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md leading-tight">Keamanan Enterprise</h3>
|
||||
<p class="font-body-md text-body-md text-white/70">Terlindung oleh enkripsi AES-256 dan 2FA.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center py-2 border-b border-white/5">
|
||||
<span class="text-white/60 font-label-md text-label-md uppercase tracking-wider">Uptime Sistem</span>
|
||||
<span class="text-success font-metric-sm text-metric-sm flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-success animate-pulse"></span>
|
||||
99.99%
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-white/5">
|
||||
<span class="text-white/60 font-label-md text-label-md uppercase tracking-wider">Settlement Hari Ini</span>
|
||||
<span class="font-metric-sm text-metric-sm">4,829 Transaksi</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<span class="text-white/60 font-label-md text-label-md uppercase tracking-wider">Device Online</span>
|
||||
<span class="font-metric-sm text-metric-sm">12,042 Unit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 text-center">
|
||||
<p class="font-body-md text-body-md italic text-white/50">"Presisi. Andal. Proaktif."</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Abstract UI Elements for Decoration -->
|
||||
<div class="absolute top-10 right-10 w-32 h-32 bg-primary/20 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div class="absolute bottom-1/4 left-0 w-48 h-48 bg-info/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Background Decorative Blobs -->
|
||||
<div class="fixed top-0 left-0 w-full h-full -z-10 pointer-events-none opacity-20">
|
||||
<div class="absolute top-[-10%] right-[-5%] w-[400px] h-[400px] bg-primary rounded-full blur-[120px]"></div>
|
||||
<div class="absolute bottom-[-10%] left-[-5%] w-[300px] h-[300px] bg-secondary-container rounded-full blur-[100px]"></div>
|
||||
</div>
|
||||
<script>
|
||||
// Simple Interaction for Password Toggle
|
||||
const toggleBtn = document.querySelector('button[type="button"]');
|
||||
const passInput = document.getElementById('password');
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const icon = toggleBtn.querySelector('.material-symbols-outlined');
|
||||
if (passInput.type === 'password') {
|
||||
passInput.type = 'text';
|
||||
icon.textContent = 'visibility_off';
|
||||
} else {
|
||||
passInput.type = 'password';
|
||||
icon.textContent = 'visibility';
|
||||
}
|
||||
});
|
||||
|
||||
// Add loading state simulation on form submit
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', (e) => {
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<span class="animate-spin material-symbols-outlined">progress_activity</span> Memproses...`;
|
||||
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_login_portal/screen.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
528
design/admin_onboarding_review_queue/code.html
Normal file
@ -0,0 +1,528 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Onboarding Review Queue</title>
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-container": "#54647a",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary": "#943700",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"on-surface": "#191b23",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"slate-900": "#0F172A",
|
||||
"surface-tint": "#0053db",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"inverse-surface": "#2e3039",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="font-body-md text-on-background antialiased overflow-hidden flex h-screen">
|
||||
<!-- SideNavBar Integration -->
|
||||
<aside class="flex flex-col h-full py-page-padding overflow-y-auto bg-surface-container-lowest dark:bg-surface-dim docked left-0 top-0 w-64 border-r border-slate-200 dark:border-outline-variant z-50">
|
||||
<div class="px-6 mb-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">FinOps Admin</h1>
|
||||
<p class="text-on-surface-variant font-label-md text-label-md">System Management</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-secondary dark:text-on-surface-variant hover:bg-slate-100 dark:hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span class="font-body-md text-body-md">Dashboard</span>
|
||||
</a>
|
||||
<!-- Active Tab: Onboarding Queue -->
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-primary dark:text-primary-fixed font-bold border-r-4 border-primary dark:border-primary-fixed bg-surface-container-low dark:bg-surface-container-high transition-all duration-150" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="pending_actions">pending_actions</span>
|
||||
<span class="font-body-md text-body-md">Onboarding Queue</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-secondary dark:text-on-surface-variant hover:bg-slate-100 dark:hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="store">store</span>
|
||||
<span class="font-body-md text-body-md">Merchant Directory</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-secondary dark:text-on-surface-variant hover:bg-slate-100 dark:hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span class="font-body-md text-body-md">Device Fleet</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-secondary dark:text-on-surface-variant hover:bg-slate-100 dark:hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="assignment">assignment</span>
|
||||
<span class="font-body-md text-body-md">Audit Logs</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="px-4 mt-auto">
|
||||
<button class="w-full flex items-center justify-center gap-2 py-3 bg-primary text-on-primary rounded-xl font-body-md text-body-md hover:opacity-90 transition-all">
|
||||
<span class="material-symbols-outlined" data-icon="add">add</span>
|
||||
New Application
|
||||
</button>
|
||||
<div class="mt-6 flex items-center gap-3 px-2 border-t border-slate-200 pt-6">
|
||||
<img alt="Admin User Profile" class="w-10 h-10 rounded-full bg-surface-container-high" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBdbiIwfk-ly7bNvqJvZRBEPcxgDxbiR5WJPG7vOQbUZ0O1ywOu-UXP_udMCARcUIH9HVZnLkbCDH7ERlB11QvmPMJSIWpi4rfkPaZhcUZ6AU1RUhFqjl9gOBbMoNqx6FbobMHKJ1vn50byWjrAgmpIJYBLxQPla5o-JDYcznG3UdqJNAAa-QVBV0BRQ_qh2ZJkTg2h-X-f3lVeBUxUWpQGbdeOTX7jQgcLFTnhelfjyZn3qlTUUOLjfGmV5EqmBIpBVWIotb-vTZM"/>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Alex Rivera</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Lead Reviewer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="flex-1 flex flex-col min-w-0 overflow-hidden relative">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center h-topbar-height px-gutter z-40 bg-surface-container-lowest dark:bg-surface-dim border-b border-slate-200 dark:border-outline-variant">
|
||||
<div class="flex items-center flex-1 max-w-md">
|
||||
<div class="relative w-full">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant" data-icon="search">search</span>
|
||||
<input class="w-full bg-surface-container-low border-none rounded-full pl-10 pr-4 py-2 text-body-md focus:ring-2 focus:ring-primary/20" placeholder="Search applications..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="hover:bg-surface-container dark:hover:bg-surface-container-highest rounded-full p-2 text-on-surface-variant transition-all active:scale-95 duration-100">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="hover:bg-surface-container dark:hover:bg-surface-container-highest rounded-full p-2 text-on-surface-variant transition-all active:scale-95 duration-100">
|
||||
<span class="material-symbols-outlined" data-icon="help_outline">help_outline</span>
|
||||
</button>
|
||||
<button class="hover:bg-surface-container dark:hover:bg-surface-container-highest rounded-full p-2 text-on-surface-variant transition-all active:scale-95 duration-100">
|
||||
<span class="material-symbols-outlined" data-icon="account_circle">account_circle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Page Content -->
|
||||
<div class="flex-1 overflow-y-auto p-page-padding bg-background">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-1">Onboarding Review Queue</h2>
|
||||
<p class="text-on-surface-variant font-body-lg text-body-lg">Verify and authorize new merchant accounts for the soundbox ecosystem.</p>
|
||||
</div>
|
||||
<!-- Summary Cards Bento Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Pending Reviews Card -->
|
||||
<div class="bg-surface-container-lowest p-card-padding rounded-xl border border-slate-200 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">Pending Reviews</span>
|
||||
<span class="material-symbols-outlined text-warning" data-icon="pending">pending</span>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg text-on-surface">142</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-1 text-danger font-metric-sm text-metric-sm">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="trending_up">trending_up</span>
|
||||
<span>+8% from yesterday</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Approved Today Card -->
|
||||
<div class="bg-surface-container-lowest p-card-padding rounded-xl border border-slate-200 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">Approved Today</span>
|
||||
<span class="material-symbols-outlined text-success" data-icon="check_circle">check_circle</span>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg text-on-surface">28</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-1 text-success font-metric-sm text-metric-sm">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="check">check</span>
|
||||
<span>Daily quota met</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Average Review Time Card -->
|
||||
<div class="bg-surface-container-lowest p-card-padding rounded-xl border border-slate-200 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">Avg. Review Time</span>
|
||||
<span class="material-symbols-outlined text-info" data-icon="timer">timer</span>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg text-on-surface">4.2h</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-1 text-success font-metric-sm text-metric-sm">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="trending_down">trending_down</span>
|
||||
<span>-12m improvement</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Table Section -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-slate-200 flex items-center justify-between bg-surface-container-lowest sticky top-0 z-10">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Review Applications</h3>
|
||||
<div class="flex gap-2">
|
||||
<span class="px-3 py-1 bg-surface-container-high text-on-surface-variant rounded-full text-label-md font-label-md">All: 142</span>
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-label-md font-label-md border border-warning/20">Urgent: 12</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-lg text-body-md hover:bg-slate-50 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="filter_list">filter_list</span>
|
||||
Filter
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-lg text-body-md hover:bg-slate-50 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="sort">sort</span>
|
||||
Sort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr class="bg-surface-container-low text-on-surface-variant font-label-md text-label-md uppercase tracking-wider border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-semibold">Merchant Details</th>
|
||||
<th class="px-6 py-4 font-semibold">Submission Date</th>
|
||||
<th class="px-6 py-4 font-semibold">Category</th>
|
||||
<th class="px-6 py-4 font-semibold text-center">Status</th>
|
||||
<th class="px-6 py-4 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded bg-primary/10 flex items-center justify-center text-primary font-bold">BK</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Bistro Kopi Central</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">ID: MER-49210</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface font-body-md text-body-md">Oct 24, 2023 09:12 AM</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-label-md font-label-md">Food & Beverage</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-label-md font-label-md flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending Review
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-4 py-1.5 bg-primary text-on-primary rounded-lg font-body-md text-body-md hover:bg-primary-container transition-all shadow-sm" onclick="toggleDrawer()">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded bg-tertiary/10 flex items-center justify-center text-tertiary font-bold">MS</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Metro Supermarket</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">ID: MER-49211</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface font-body-md text-body-md">Oct 24, 2023 10:45 AM</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-label-md font-label-md">Retail</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-label-md font-label-md flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending Review
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-4 py-1.5 bg-primary text-on-primary rounded-lg font-body-md text-body-md hover:bg-primary-container transition-all shadow-sm">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded bg-info/10 flex items-center justify-center text-info font-bold">TL</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">TechLogistics Solutions</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">ID: MER-49212</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface font-body-md text-body-md">Oct 24, 2023 01:20 PM</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-label-md font-label-md">Courier Services</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-label-md font-label-md flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending Review
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-4 py-1.5 bg-primary text-on-primary rounded-lg font-body-md text-body-md hover:bg-primary-container transition-all shadow-sm">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded bg-success/10 flex items-center justify-center text-success font-bold">FP</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Fresh Produce Mart</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">ID: MER-49213</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-on-surface font-body-md text-body-md">Oct 24, 2023 03:05 PM</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-label-md font-label-md">Grocery</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-label-md font-label-md flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending Review
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-4 py-1.5 bg-primary text-on-primary rounded-lg font-body-md text-body-md hover:bg-primary-container transition-all shadow-sm">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-slate-200 bg-surface-container-low flex items-center justify-between">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Showing 4 of 142 pending applications</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_left">chevron_left</span>
|
||||
</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_right">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FAB: Only for relevant screens, here it could be for manual entry -->
|
||||
<!-- Suppressed on review page as per guidelines -->
|
||||
</main>
|
||||
<!-- Detail Drawer (Hidden by default) -->
|
||||
<div class="fixed inset-y-0 right-0 w-[450px] bg-white shadow-2xl z-50 transform translate-x-full transition-transform duration-300 ease-in-out border-l border-slate-200 overflow-y-auto" id="reviewDrawer">
|
||||
<div class="p-6 border-b border-slate-200 flex items-center justify-between sticky top-0 bg-white z-20">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Application Review</h3>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full transition-colors" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-8">
|
||||
<!-- Merchant Profile Header -->
|
||||
<div class="flex items-center gap-4">
|
||||
<img alt="Merchant Branding" class="w-16 h-16 rounded-xl bg-slate-50 border border-slate-200" data-alt="A clean and modern cafe logo design featuring a stylized coffee bean integrated with a minimal building silhouette, using a professional palette of navy blue and cream white. The aesthetic is corporate yet approachable, suitable for a boutique coffee bistro in a metropolitan business district." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDYO-drQb2ROHjmD_cyYqfpNjwjJHYc0vDNoMcbyZZMbSh49q011wnPXp4EBwywXlUDF3lLTQ0zl0oDGgKsx36vmr95g-NKwe1OTPzPa5UIl9-CSS4pr3wMAxf57XWmnVDzNbd-gAyF_gNa2iONosLcTlIBjbjxjwrtEozE3imd2if846_M0L5CrzesCE8b4DX4U5TE9I4xSqlvctFSNVO6h7iIhzu6_rm5fB5wm8Lz6MDYqplo-h7xRDeRqa8l5RgKPzw9iTKtVzk"/>
|
||||
<div>
|
||||
<h4 class="font-headline-md text-headline-md text-on-surface">Bistro Kopi Central</h4>
|
||||
<span class="px-2 py-0.5 bg-warning/10 text-warning rounded text-label-md font-label-md">High Priority Review</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity Verification Section -->
|
||||
<section class="space-y-4">
|
||||
<h5 class="font-body-md text-body-md font-bold text-on-surface-variant uppercase tracking-wider flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="verified_user">verified_user</span>
|
||||
Entity Verification
|
||||
</h5>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-surface-container-low rounded-lg">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1">Business Registration</p>
|
||||
<p class="font-body-md text-body-md font-semibold">REG-2023-99120</p>
|
||||
</div>
|
||||
<div class="p-3 bg-surface-container-low rounded-lg">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1">Tax ID / PAN</p>
|
||||
<p class="font-body-md text-body-md font-semibold">BKC991204X</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border border-slate-200 rounded-lg flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-info" data-icon="description">description</span>
|
||||
<p class="font-body-md text-body-md">Incorporation_Doc.pdf</p>
|
||||
</div>
|
||||
<button class="text-primary font-body-md text-body-md font-semibold hover:underline">View File</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Raw Audit Block -->
|
||||
<section class="space-y-4">
|
||||
<h5 class="font-body-md text-body-md font-bold text-on-surface-variant uppercase tracking-wider flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="code">code</span>
|
||||
Risk Score Payload
|
||||
</h5>
|
||||
<div class="bg-slate-900 rounded-xl p-4 relative font-mono text-sm text-slate-300">
|
||||
<button class="absolute top-3 right-3 text-slate-500 hover:text-white transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="content_copy">content_copy</span>
|
||||
</button>
|
||||
<pre class="whitespace-pre-wrap leading-relaxed">{
|
||||
"risk_assessment": {
|
||||
"score": 14,
|
||||
"rating": "LOW",
|
||||
"signals": [
|
||||
"geofence_match: true",
|
||||
"kyc_verification: pass",
|
||||
"velocity_check: pass"
|
||||
],
|
||||
"last_check": "2023-10-24T09:12:04Z"
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Timeline of Submission -->
|
||||
<section class="space-y-4">
|
||||
<h5 class="font-body-md text-body-md font-bold text-on-surface-variant uppercase tracking-wider flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="history">history</span>
|
||||
Timeline
|
||||
</h5>
|
||||
<div class="space-y-6 relative before:content-[''] before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-[2px] before:bg-slate-200">
|
||||
<div class="relative pl-8">
|
||||
<span class="absolute left-0 top-1 w-6 h-6 rounded-full bg-success flex items-center justify-center text-white">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="check" style="font-variation-settings: 'FILL' 1;">check</span>
|
||||
</span>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Application Submitted</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Oct 24, 09:12 AM by Merchant</p>
|
||||
</div>
|
||||
<div class="relative pl-8">
|
||||
<span class="absolute left-0 top-1 w-6 h-6 rounded-full bg-info flex items-center justify-center text-white">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="robot" style="font-variation-settings: 'FILL' 1;">robot</span>
|
||||
</span>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Automated KYC Check</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Oct 24, 09:13 AM - System Approved</p>
|
||||
</div>
|
||||
<div class="relative pl-8">
|
||||
<span class="absolute left-0 top-1 w-6 h-6 rounded-full border-2 border-warning bg-white flex items-center justify-center text-warning">
|
||||
<span class="w-2 h-2 rounded-full bg-warning"></span>
|
||||
</span>
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Pending Manual Review</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Assigning to Alex Rivera...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-200 bg-surface-container-lowest sticky bottom-0 z-20 flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 border border-danger text-danger rounded-xl font-body-md text-body-md font-bold hover:bg-danger/5 transition-all" onclick="toggleDrawer()">
|
||||
Reject Account
|
||||
</button>
|
||||
<button class="flex-[2] px-4 py-3 bg-success text-on-primary rounded-xl font-body-md text-body-md font-bold hover:opacity-90 transition-all shadow-lg shadow-success/20">
|
||||
Approve & Activate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Overlay for Drawer -->
|
||||
<div class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-40 hidden opacity-0 transition-opacity duration-300" id="drawerOverlay" onclick="toggleDrawer()"></div>
|
||||
<script>
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('reviewDrawer');
|
||||
const overlay = document.getElementById('drawerOverlay');
|
||||
const isOpen = !drawer.classList.contains('translate-x-full');
|
||||
|
||||
if (isOpen) {
|
||||
drawer.classList.add('translate-x-full');
|
||||
overlay.classList.add('hidden');
|
||||
overlay.classList.remove('opacity-100');
|
||||
document.body.style.overflow = '';
|
||||
} else {
|
||||
drawer.classList.remove('translate-x-full');
|
||||
overlay.classList.remove('hidden');
|
||||
setTimeout(() => overlay.classList.add('opacity-100'), 10);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_onboarding_review_queue/screen.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
557
design/admin_reconciliation_management/code.html
Normal file
@ -0,0 +1,557 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Reconciliation | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"danger": "#DC2626",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"error-container": "#ffdad6",
|
||||
"outline": "#737686",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"warning": "#F59E0B",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"error": "#ba1a1a",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-error": "#ffffff",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-surface": "#191b23",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"on-background": "#191b23",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"surface-bright": "#faf8ff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-primary": "#ffffff",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-container": "#54647a"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px",
|
||||
"card-padding": "20px",
|
||||
"gutter": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { background-color: #F8FAFC; color: #191B23; -webkit-font-smoothing: antialiased; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; display: inline-block; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.glass-effect { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="font-body-md text-body-md overflow-x-hidden">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="flex flex-col fixed left-0 top-0 h-full p-4 gap-2 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 w-64 z-50">
|
||||
<div class="mb-8 px-2 flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-white" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md text-primary leading-tight">Soundbox Admin</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500">Fintech Ops Suite</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-3 bg-secondary-container dark:bg-on-secondary-fixed-variant text-on-secondary-container dark:text-on-secondary-fixed rounded-xl font-bold transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
<span class="font-label-md text-label-md">Reconciliation</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="security">security</span>
|
||||
<span class="font-label-md text-label-md">Audit Logs</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
<span class="font-label-md text-label-md">Fee Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span class="font-label-md text-label-md">Settlements</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="router">router</span>
|
||||
<span class="font-label-md text-label-md">Device Health</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="contact_support">contact_support</span>
|
||||
<span class="font-label-md text-label-md">Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto space-y-4">
|
||||
<button class="w-full bg-primary text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="description">description</span>
|
||||
<span class="font-label-md text-label-md">Generate Report</span>
|
||||
</button>
|
||||
<div class="h-px bg-slate-200"></div>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-danger hover:bg-red-50 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64 min-h-screen">
|
||||
<!-- TopNavBar -->
|
||||
<header class="flex justify-between items-center h-[72px] px-page-padding w-full sticky top-0 z-40 bg-surface dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-8">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary dark:text-inverse-primary">Reconciliation</h2>
|
||||
<nav class="hidden lg:flex items-center gap-6">
|
||||
<a class="text-primary dark:text-inverse-primary border-b-2 border-primary font-bold pb-1 transition-colors" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Merchants</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Operations</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Audit</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative group">
|
||||
<input class="pl-10 pr-4 py-2 rounded-full border border-slate-200 focus:outline-none focus:ring-2 focus:ring-primary/20 w-64 bg-slate-50 transition-all focus:bg-white text-body-md" placeholder="Search transactions..." type="text"/>
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" data-icon="search">search</span>
|
||||
</div>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors text-slate-600">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors text-slate-600">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
</button>
|
||||
<div class="h-8 w-px bg-slate-200 mx-2"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<img alt="Administrator Avatar" class="w-10 h-10 rounded-full border border-slate-200" data-alt="A professional headshot of a senior financial operations manager in a minimalist, bright office setting. The person exudes competence and calm, dressed in corporate business attire. Soft, high-key lighting illuminates the scene, consistent with a modern fintech dashboard aesthetic using a clean white and blue color palette." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDhfFggnhz_VMmYf0ijSR2I_RiQAO21qPqG33SGjrEM2wAQ79xWw-6u77CyKPUuRePmDC7vc-pftCEMyfLbsNUg8xnKqZ_gy0ei1QmvnyDRu8Hj1ytkFZLGH0awGL-1pJj1epvjRyJpVPdORX_03QJwW9ZzpXdv_WDhH_wvr5dWzrEPQm0IYyls85ZJbKM0jq73rP2IRY4IlwS-nTc2aBF4bKfgSM7v40-NDHULJgRYLxpa56nDBrXDs76c2_zmj8CtP7vOa3TNnwo"/>
|
||||
<div class="hidden xl:block">
|
||||
<p class="font-label-md text-label-md font-bold text-on-surface">Admin Ops</p>
|
||||
<p class="text-[11px] text-slate-500 uppercase tracking-wider">Level 4</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-page-padding space-y-6">
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-gutter">
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-slate-500">Total Matched</span>
|
||||
<div class="p-2 bg-success/10 rounded-lg">
|
||||
<span class="material-symbols-outlined text-success" data-icon="check_circle">check_circle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">42,892</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-success text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
<span class="font-metric-sm text-metric-sm text-success">+12.4%</span>
|
||||
<span class="text-[12px] text-slate-400 ml-1">vs last period</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-slate-500">Discrepancies</span>
|
||||
<div class="p-2 bg-danger/10 rounded-lg">
|
||||
<span class="material-symbols-outlined text-danger" data-icon="error_outline">error_outline</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">148</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-danger text-[16px]" data-icon="trending_down">trending_down</span>
|
||||
<span class="font-metric-sm text-metric-sm text-danger">-2.1%</span>
|
||||
<span class="text-[12px] text-slate-400 ml-1">Requires attention</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-slate-500">Pending Verification</span>
|
||||
<div class="p-2 bg-warning/10 rounded-lg">
|
||||
<span class="material-symbols-outlined text-warning" data-icon="hourglass_empty">hourglass_empty</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">1,024</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-slate-400 text-[16px]" data-icon="history">history</span>
|
||||
<span class="font-metric-sm text-metric-sm text-slate-600">Avg 4h processing</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="font-label-md text-label-md text-slate-500">Bank Statements</span>
|
||||
<div class="p-2 bg-info/10 rounded-lg">
|
||||
<span class="material-symbols-outlined text-info" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">842</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-success text-[16px]" data-icon="cloud_done">cloud_done</span>
|
||||
<span class="font-metric-sm text-metric-sm text-slate-600">12 API Connections</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filter Bar -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4 flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-100 transition-colors">
|
||||
<span class="material-symbols-outlined text-slate-500 text-[20px]" data-icon="calendar_today">calendar_today</span>
|
||||
<span class="font-body-md text-body-md">Oct 01, 2023 - Oct 31, 2023</span>
|
||||
</div>
|
||||
<select class="px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg font-body-md text-body-md focus:ring-primary/20 focus:border-primary">
|
||||
<option>All Bank Partners</option>
|
||||
<option>HSBC Corporate</option>
|
||||
<option>JPMorgan Chase</option>
|
||||
<option>Citibank Enterprise</option>
|
||||
</select>
|
||||
<select class="px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg font-body-md text-body-md focus:ring-primary/20 focus:border-primary">
|
||||
<option>All Statuses</option>
|
||||
<option>Matched</option>
|
||||
<option>Exception</option>
|
||||
<option>Pending</option>
|
||||
</select>
|
||||
<div class="ml-auto flex items-center gap-3">
|
||||
<button class="px-4 py-2 text-primary font-bold border border-primary/20 hover:bg-primary/5 transition-colors rounded-lg flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="merge">merge</span>
|
||||
Manual Match
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-slate-900 text-white font-bold hover:bg-slate-800 transition-colors rounded-lg flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="file_download">file_download</span>
|
||||
Export Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Comparison Table -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200 sticky top-0 z-10">
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Transaction Details</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-primary/5">System Record (Internal)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-info/5">Bank Record (External)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Variance</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1: Matched -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283471</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 24, 2023 • 14:22:10</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 14,500.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 14,500.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">0.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success text-[12px] font-bold rounded-full">MATCHED</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="more_vert">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2: Discrepancy -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-88273412</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 24, 2023 • 11:05:45</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 8,240.50</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 8,245.50</td>
|
||||
<td class="px-6 py-4 tabular-nums text-danger font-bold">- 5.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-danger/10 text-danger text-[12px] font-bold rounded-full">EXCEPTION</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-3 py-1 bg-primary text-white text-[12px] font-bold rounded hover:bg-primary/90 transition-colors">
|
||||
Resolve
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3: Pending -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90112456</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 23:18:02</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 1,20,000.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium italic text-slate-400">Not Found</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">Pending</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning text-[12px] font-bold rounded-full">PENDING</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="refresh">refresh</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4: Matched -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283472</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 18:45:30</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 450.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 450.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">0.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success text-[12px] font-bold rounded-full">MATCHED</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="more_vert">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5: Exception (Fee Miscalc) -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283478</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 16:12:11</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 22,000.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 21,560.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-danger font-bold">- 440.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-danger/10 text-danger text-[12px] font-bold rounded-full">EXCEPTION</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-3 py-1 bg-primary text-white text-[12px] font-bold rounded hover:bg-primary/90 transition-colors">
|
||||
Resolve
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Table Pagination/Footer -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex items-center justify-between">
|
||||
<p class="text-label-md text-slate-500">Showing 1 to 5 of 42,892 entries</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-400 hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_left">chevron_left</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-primary bg-primary text-white text-label-md font-bold">1</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-label-md">2</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-label-md">3</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_right">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bento Layout Footer Widgets -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-gutter pb-8">
|
||||
<!-- System Logs / Audit Blocks -->
|
||||
<div class="lg:col-span-2 bg-slate-900 rounded-xl overflow-hidden p-6 relative">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-white font-headline-md text-[16px] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-info" data-icon="terminal">terminal</span>
|
||||
Raw Reconciliation Payload (Last Match)
|
||||
</h3>
|
||||
<button class="text-slate-400 hover:text-white transition-colors flex items-center gap-1 text-[12px]">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="content_copy">content_copy</span>
|
||||
Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="font-mono text-[13px] text-success leading-relaxed h-[200px] overflow-y-auto hide-scrollbar">
|
||||
<pre>{
|
||||
"reconciliation_id": "RECON-00492-AX",
|
||||
"timestamp": "2023-10-24T14:22:10.452Z",
|
||||
"system_ledger": {
|
||||
"entry_id": "SL-90283471",
|
||||
"amount": 14500.00,
|
||||
"currency": "INR",
|
||||
"hash": "8f3e22...a1"
|
||||
},
|
||||
"bank_statement": {
|
||||
"ref_num": "BANK-TX-5512",
|
||||
"posted_date": "2023-10-24",
|
||||
"amount": 14500.00,
|
||||
"match_confidence": 0.998
|
||||
},
|
||||
"result": "AUTO_MATCH_SUCCESS",
|
||||
"strategy": "EXACT_AMOUNT_REF_MATCH"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Verification Timeline -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-6">
|
||||
<h3 class="font-headline-md text-[16px] mb-6">Recent Resolution Activity</h3>
|
||||
<div class="space-y-6">
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-success ring-4 ring-success/10 z-10"></div>
|
||||
<div class="absolute left-1.5 top-5 w-[2px] h-full bg-slate-100"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">TXN-88273412 Resolved</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Manual match confirmed by Admin A12</p>
|
||||
<p class="text-[11px] text-slate-400">12 minutes ago</p>
|
||||
</div>
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-primary ring-4 ring-primary/10 z-10"></div>
|
||||
<div class="absolute left-1.5 top-5 w-[2px] h-full bg-slate-100"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">Report Exported</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Full Oct report generated (PDF)</p>
|
||||
<p class="text-[11px] text-slate-400">45 minutes ago</p>
|
||||
</div>
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-slate-300 z-10"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">HSBC Sync Completed</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">2,400 statements fetched via API</p>
|
||||
<p class="text-[11px] text-slate-400">1 hour ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Interactive Detail Drawer (Hidden by default) -->
|
||||
<div class="fixed inset-y-0 right-0 w-[450px] bg-white shadow-2xl translate-x-full transition-transform duration-300 z-[100] border-l border-slate-200 hidden" id="detailDrawer">
|
||||
<div class="h-[72px] px-6 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 class="font-headline-md text-[18px]">Transaction Details</h3>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200">
|
||||
<p class="text-label-md text-slate-500 uppercase tracking-wider mb-2">Reconciliation Status</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-4 py-1.5 bg-danger/10 text-danger text-[14px] font-bold rounded-full">EXCEPTION FOUND</span>
|
||||
<span class="text-slate-400 tabular-nums text-[12px]">Variance: ₹ 5.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="font-label-md font-bold text-slate-700 mb-2">Audit Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-white border border-slate-100 rounded-lg">
|
||||
<p class="text-[11px] text-slate-400 uppercase">Merchant ID</p>
|
||||
<p class="font-bold text-on-surface">M-92834</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white border border-slate-100 rounded-lg">
|
||||
<p class="text-[11px] text-slate-400 uppercase">Gateway</p>
|
||||
<p class="font-bold text-on-surface">Razorpay</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-label-md font-bold text-slate-700 mb-2">Internal Payload</h4>
|
||||
<pre class="bg-slate-900 text-info p-4 rounded-xl text-[12px] font-mono">{
|
||||
"amount": 8240.50,
|
||||
"fee_deducted": 4.50,
|
||||
"net_settled": 8236.00
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6 border-t border-slate-200 space-y-3">
|
||||
<button class="w-full bg-primary text-white py-3 rounded-xl font-bold hover:opacity-90 transition-opacity">Resolve with Manual Match</button>
|
||||
<button class="w-full bg-white text-danger border border-danger/20 py-3 rounded-xl font-bold hover:bg-red-50 transition-colors">Mark as Fraudulent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Micro-interactions
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
drawer.classList.toggle('hidden');
|
||||
setTimeout(() => {
|
||||
drawer.classList.toggle('translate-x-full');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Add click events to 'Resolve' buttons for demo
|
||||
document.querySelectorAll('button').forEach(btn => {
|
||||
if (btn.innerText === 'Resolve') {
|
||||
btn.addEventListener('click', toggleDrawer);
|
||||
}
|
||||
});
|
||||
|
||||
// Simple fade in for cards on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const cards = document.querySelectorAll('.bg-white.p-card-padding');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(10px)';
|
||||
card.style.transition = 'all 0.4s ease-out ' + (index * 0.1) + 's';
|
||||
setTimeout(() => {
|
||||
card.style.opacity = '1';
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_reconciliation_management/screen.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
517
design/admin_system_audit_logs/code.html
Normal file
@ -0,0 +1,517 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Audit Logs | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Plus+Jakarta+Sans:wght@600;700;800&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"danger": "#DC2626",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"error-container": "#ffdad6",
|
||||
"outline": "#737686",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"warning": "#F59E0B",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"error": "#ba1a1a",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-error": "#ffffff",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-surface": "#191b23",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"on-background": "#191b23",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"surface-bright": "#faf8ff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-primary": "#ffffff",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-container": "#54647a"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px",
|
||||
"card-padding": "20px",
|
||||
"gutter": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"mono": ["JetBrains Mono", "monospace"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.data-table-container { height: calc(100vh - 280px); }
|
||||
.tab-active { @apply bg-secondary-container text-on-secondary-container font-bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md selection:bg-primary-container selection:text-white">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="flex flex-col fixed left-0 top-0 h-full p-4 gap-2 bg-white border-r border-slate-200 w-64 z-50">
|
||||
<div class="flex items-center gap-3 px-2 py-4 mb-4">
|
||||
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-white" style="font-variation-settings: 'FILL' 1;">shield</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md text-primary leading-tight">Soundbox Admin</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Fintech Ops Suite</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">account_balance_wallet</span>
|
||||
<span class="font-label-md">Reconciliation</span>
|
||||
</a>
|
||||
<!-- ACTIVE TAB: Audit Logs -->
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container rounded-xl font-bold transition-all scale-98" href="#">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">security</span>
|
||||
<span class="font-label-md">Audit Logs</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">payments</span>
|
||||
<span class="font-label-md">Fee Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">receipt_long</span>
|
||||
<span class="font-label-md">Settlements</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">router</span>
|
||||
<span class="font-label-md">Device Health</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary hover:bg-slate-100 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">contact_support</span>
|
||||
<span class="font-label-md">Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<button class="mb-4 w-full bg-primary text-white font-label-md py-3 rounded-xl hover:shadow-lg transition-all active:scale-95">
|
||||
Generate Report
|
||||
</button>
|
||||
<div class="border-t border-slate-100 pt-4">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-danger hover:bg-error-container/30 rounded-xl transition-all" href="#">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="font-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 min-h-screen flex flex-col">
|
||||
<!-- Top Bar -->
|
||||
<header class="flex justify-between items-center h-[72px] px-page-padding w-full sticky top-0 z-40 bg-white border-b border-slate-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary">Audit Logs</h2>
|
||||
<span class="text-slate-300 mx-2">/</span>
|
||||
<span class="text-slate-500 font-label-md">System Security</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Global Search -->
|
||||
<div class="relative w-80">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">search</span>
|
||||
<input class="w-full bg-slate-50 border-none rounded-lg py-2 pl-10 pr-4 text-body-md focus:ring-2 focus:ring-primary/20 placeholder:text-slate-400" placeholder="Search by Entity ID, User, or IP..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="w-10 h-10 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-full transition-colors relative">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-danger rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<img alt="Administrator Avatar" class="w-9 h-9 rounded-full bg-slate-100 border border-slate-200" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBIsOVKgRqgiod6bGWxQyDGQUWWzDfFxNrLh3RKcG7NMtzDN9wU5PZSQtPTzGuUueyYqzCkURd2rWz3yNelalYWw5DA_fmvE38P-w2mcjIPVRICWN-mM-vH-PEVpP64o4_xV42oXwhl2YPM_gPVfMAxgpNjhU2uIQg-5DcL8wlTOm-ZBeS9Fb4aDfXvrY-DsqRpT297-CzSthoRbrVd5_Lri6fxO8z7OnRzccmPp-AduLXLSy31_zrEkT7gb7Fe62Yn7lM9-04_IhE"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Operational Canvas -->
|
||||
<div class="p-page-padding space-y-6 flex-1">
|
||||
<!-- Filter Bar -->
|
||||
<section class="flex flex-wrap items-center gap-4 bg-white p-4 rounded-xl border border-slate-200">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span class="font-label-md text-slate-500">Action Type:</span>
|
||||
<select class="bg-transparent border-none p-0 text-label-md font-bold text-primary focus:ring-0">
|
||||
<option>All Actions</option>
|
||||
<option>Create</option>
|
||||
<option>Update</option>
|
||||
<option>Delete</option>
|
||||
<option>Login</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span class="font-label-md text-slate-500">User Role:</span>
|
||||
<select class="bg-transparent border-none p-0 text-label-md font-bold text-primary focus:ring-0">
|
||||
<option>All Roles</option>
|
||||
<option>Super Admin</option>
|
||||
<option>Operator</option>
|
||||
<option>Support</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span class="material-symbols-outlined text-slate-400 text-sm">calendar_today</span>
|
||||
<span class="font-label-md text-slate-500">Date Range:</span>
|
||||
<button class="text-label-md font-bold text-primary">Last 24 Hours</button>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 hover:bg-slate-50 rounded-lg text-label-md font-medium transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">filter_list</span>
|
||||
More Filters
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-label-md font-medium hover:bg-black transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
</section>
|
||||
<!-- Audit Table -->
|
||||
<section class="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||
<div class="overflow-x-auto data-table-container scrollbar-hide">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead class="sticky top-0 z-10 bg-white">
|
||||
<tr class="border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Timestamp</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">User & Role</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Action</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Resource ID</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">IP Address</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider text-right">Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-metric-sm text-slate-900">Oct 24, 2023</span>
|
||||
<span class="text-xs text-slate-500">14:22:45.002</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img alt="User" class="w-8 h-8 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAT7G2Zjmxb5coADrflGFZIwWN9riKJ4FfJYMicnfhYqCxMVqPLGf8GFKi_MTLd9WjPM6YnURSxDjvOFhF2wmnEy3skSxmwBBHRkivMuzFYJFuAy39E0sBKirLTbVOuXQ0Gx5xYQTZVkWs_shSkuW5StZT8zPCpRxQKlKtxAves_Peneg4pyn6KornQCenMinpb0fiD1j2ZqFUNAGoBN64DK9txaoo-w_I4lH5XJ-VfUbwGJ8sPyzXh5qatt6JW4OsbzKIzRmHNEPM"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-slate-900">Felix Chen</span>
|
||||
<span class="text-[10px] bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded-full w-fit font-bold uppercase">Super Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="text-body-md font-medium text-slate-900">Updated Merchant Fee</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<code class="font-mono text-xs text-primary bg-primary-fixed/30 px-2 py-1 rounded">MID-88219-X</code>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-slate-500 text-xs">
|
||||
192.168.1.104
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-success/10 text-success">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Success
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 text-right">
|
||||
<button class="text-primary hover:underline font-label-md" onclick="toggleDrawer('payload-1')">View JSON</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-metric-sm text-slate-900">Oct 24, 2023</span>
|
||||
<span class="text-xs text-slate-500">13:58:12.881</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img alt="User" class="w-8 h-8 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCXEfGIgENxAXyTqeCXABKrwKVvENV5YlyIBP8Q8LlYtFLS0jDj84rLwBSnQKtY-aKsAKyUlF-552T25AW-zbUuFI_rMTIm1BKyF5d3m0uMAJnFjPjsMWlu62fICZMjAbFi-ZzOJ58zjkzZCT4FySAr71nO3OtBMR_HHD0nVENVi5chys8qthCMch7ORsjd99BeGVR2xJOnVbReJMN-cFx66w3GHxJVj0Ym14aLofAbNI5VxgVmin7Hn3V7jSVVFdjF8iztn-2fpWs"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-slate-900">Sarah Miller</span>
|
||||
<span class="text-[10px] bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded-full w-fit font-bold uppercase">Operator</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="text-body-md font-medium text-slate-900">Failed Login Attempt</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 text-slate-400 text-xs">—</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-slate-500 text-xs">
|
||||
103.24.11.92
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-danger/10 text-danger">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger"></span> Failed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 text-right">
|
||||
<button class="text-primary hover:underline font-label-md">View JSON</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-metric-sm text-slate-900">Oct 24, 2023</span>
|
||||
<span class="text-xs text-slate-500">12:10:04.230</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img alt="User" class="w-8 h-8 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCmTGvw3lGcuiC_8sY1kMBG7O5iWz4Xg0Ppx8HP_T0RxlCvnScnjZphmuaDaURjnrJ52FXnR8pRTrLh1OE7cOoiO-p97YQRQ6coI8rAKWBCe7QZxj3ARR9rjk3NhaUeaGAMI7l_nGKfYW6XmGIwy5sALL1zamrSOy2XV2lpyCkb11ge3FRAVrz-EcDqPHTfohniT2UhUkMnkrXQyZ3YI6F7gP3FQSTAYkeaw-nTTMx7XtMBrSB0j_YkS_lf4qsFj2rgwlvMbo8-yLs"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-slate-900">Jordan Blake</span>
|
||||
<span class="text-[10px] bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded-full w-fit font-bold uppercase">Super Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="text-body-md font-medium text-slate-900">Deleted API Key</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<code class="font-mono text-xs text-primary bg-primary-fixed/30 px-2 py-1 rounded">KEY-TEST-992</code>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-slate-500 text-xs">
|
||||
45.12.88.21
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-success/10 text-success">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Success
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 text-right">
|
||||
<button class="text-primary hover:underline font-label-md">View JSON</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-metric-sm text-slate-900">Oct 24, 2023</span>
|
||||
<span class="text-xs text-slate-500">11:45:30.121</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<img alt="User" class="w-8 h-8 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBqg6S7qtOgdjYo8RqItZB-EV13d0Zyjsfif3UP_wBYeKrWDxjA3erAHPYKD_VJEeoyFyATqK7ww5Polle75Acq2HSQslPDgUbhMz5sKxLj9_ZK1srhVhWDMTo9bozujSsdbUb7tUtjNy92rYTBKUwVuif-i0JbzX8n0S20eMqNFlz3EqwqWyVFV1rbsDwh82bZgdbRWM9Hi00MAtMxAxITs07u5ZhkXriAnn382ym8pJZyRlUXZl657IaYdlN4j9TMuqp_cloiIow"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-slate-900">System Core</span>
|
||||
<span class="text-[10px] bg-orange-50 text-orange-700 px-1.5 py-0.5 rounded-full w-fit font-bold uppercase">Automated</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="text-body-md font-medium text-slate-900">Initiated Daily Settlement</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<code class="font-mono text-xs text-primary bg-primary-fixed/30 px-2 py-1 rounded">SETTLE-20231024</code>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-slate-500 text-xs">
|
||||
Internal
|
||||
</td>
|
||||
<td class="px-6 py-3.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-warning/10 text-warning">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span> Pending
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-3.5 text-right">
|
||||
<button class="text-primary hover:underline font-label-md">View JSON</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-between px-6 py-4 bg-white border-t border-slate-200">
|
||||
<span class="text-label-md text-slate-500">Showing 1 to 20 of 1,248 entries</span>
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg text-slate-400 hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined">chevron_left</span>
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-primary text-white font-label-md rounded-lg">1</button>
|
||||
<button class="px-4 py-2 text-slate-600 font-label-md hover:bg-slate-50 rounded-lg">2</button>
|
||||
<button class="px-4 py-2 text-slate-600 font-label-md hover:bg-slate-50 rounded-lg">3</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg text-slate-600 hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Detail Drawer (Hidden by Default) -->
|
||||
<div class="fixed inset-0 z-50 invisible" id="payload-drawer">
|
||||
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm opacity-0 transition-opacity duration-300" id="drawer-overlay"></div>
|
||||
<div class="absolute right-0 top-0 h-full w-[500px] bg-white shadow-2xl translate-x-full transition-transform duration-300 flex flex-col" id="drawer-content">
|
||||
<div class="p-6 border-b border-slate-200 flex justify-between items-center bg-slate-50">
|
||||
<div>
|
||||
<h3 class="font-headline-md text-slate-900">Audit Detail Payload</h3>
|
||||
<p class="text-xs text-slate-500 font-mono">ID: 550e8400-e29b-41d4-a716-446655440000</p>
|
||||
</div>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-200 text-slate-500 transition-colors" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 flex-1 overflow-y-auto space-y-6">
|
||||
<div class="space-y-3">
|
||||
<h4 class="font-label-md text-slate-400 uppercase tracking-wider">Operational Summary</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span class="text-xs text-slate-500">Method</span>
|
||||
<p class="font-bold text-primary">PATCH</p>
|
||||
</div>
|
||||
<div class="p-3 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span class="text-xs text-slate-500">Source Agent</span>
|
||||
<p class="font-bold text-primary">Web-Admin/2.4.1</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<h4 class="font-label-md text-slate-400 uppercase tracking-wider">Raw JSON Payload</h4>
|
||||
<button class="flex items-center gap-1.5 text-primary hover:text-primary-container text-xs font-bold transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">content_copy</span>
|
||||
Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-slate-900 rounded-xl p-6 overflow-hidden relative group">
|
||||
<pre class="font-mono text-[13px] text-green-400 overflow-x-auto scrollbar-hide">{
|
||||
"action": "UPDATE_MERCHANT_FEE",
|
||||
"metadata": {
|
||||
"merchant_id": "MID-88219-X",
|
||||
"initiated_by": "f.chen@soundbox.ops",
|
||||
"ip_address": "192.168.1.104"
|
||||
},
|
||||
"changes": {
|
||||
"transaction_fee_pct": {
|
||||
"old": 1.25,
|
||||
"new": 1.10
|
||||
},
|
||||
"fixed_charge": {
|
||||
"old": 0.50,
|
||||
"new": 0.50
|
||||
}
|
||||
},
|
||||
"timestamp": "2023-10-24T14:22:45.002Z",
|
||||
"auth_token_sig": "sha256:88a7b...12ff"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-info/10 rounded-xl border border-info/20 flex gap-3">
|
||||
<span class="material-symbols-outlined text-info">info</span>
|
||||
<p class="text-xs text-on-secondary-container/80 leading-relaxed">
|
||||
This action was performed via the Management Dashboard and required two-factor authentication. Verified by security group "SG-FIN-ADMIN".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-200 bg-white">
|
||||
<button class="w-full py-3 bg-slate-900 text-white rounded-xl font-bold hover:bg-black transition-all" onclick="toggleDrawer()">
|
||||
Close Inspection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('payload-drawer');
|
||||
const overlay = document.getElementById('drawer-overlay');
|
||||
const content = document.getElementById('drawer-content');
|
||||
|
||||
if (drawer.classList.contains('invisible')) {
|
||||
drawer.classList.remove('invisible');
|
||||
setTimeout(() => {
|
||||
overlay.classList.replace('opacity-0', 'opacity-100');
|
||||
content.classList.replace('translate-x-full', 'translate-x-0');
|
||||
}, 10);
|
||||
} else {
|
||||
overlay.classList.replace('opacity-100', 'opacity-0');
|
||||
content.classList.replace('translate-x-0', 'translate-x-full');
|
||||
setTimeout(() => {
|
||||
drawer.classList.add('invisible');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Close on overlay click
|
||||
document.getElementById('drawer-overlay').addEventListener('click', toggleDrawer);
|
||||
|
||||
// Escape key to close
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !document.getElementById('payload-drawer').classList.contains('invisible')) {
|
||||
toggleDrawer();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/admin_system_audit_logs/screen.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
592
design/device_registry_monitoring/code.html
Normal file
@ -0,0 +1,592 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Device Registry | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
[data-weight="fill"] .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.data-table-container {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"warning": "#F59E0B",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface": "#faf8ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"outline": "#737686",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"info": "#0EA5E9",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-bright": "#faf8ff",
|
||||
"surface-container": "#ededf9",
|
||||
"error-container": "#ffdad6",
|
||||
"slate-900": "#0F172A",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-700": "#334155",
|
||||
"slate-200": "#E2E8F0",
|
||||
"on-background": "#191b23",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"danger": "#DC2626",
|
||||
"on-primary-container": "#eeefff",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-surface-variant": "#434655",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"primary-container": "#2563eb",
|
||||
"background": "#F8FAFC",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-secondary-container": "#54647a",
|
||||
"slate-100": "#F1F5F9"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"gutter": "24px",
|
||||
"topbar-height": "72px",
|
||||
"card-padding": "20px",
|
||||
"row-height": "52px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", { "lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600" }],
|
||||
"label-md": ["12px", { "lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500" }],
|
||||
"headline-md": ["20px", { "lineHeight": "28px", "fontWeight": "600" }],
|
||||
"body-md": ["14px", { "lineHeight": "20px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "fontWeight": "600" }],
|
||||
"body-lg": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
|
||||
"metric-lg": ["32px", { "lineHeight": "40px", "fontWeight": "600" }],
|
||||
"metric-sm": ["14px", { "lineHeight": "20px", "fontWeight": "600" }]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background font-body-md text-on-background min-h-screen">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="text-label-md font-label-md text-slate-500">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-1 flex-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span class="font-body-md text-body-md">Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
<span class="font-body-md text-body-md">Merchant Management</span>
|
||||
</a>
|
||||
<!-- Active State: Device Registry -->
|
||||
<a class="flex items-center gap-3 px-3 py-2 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group" data-weight="fill" style="font-variation-settings: 'FILL' 1;">speaker_group</span>
|
||||
<span class="font-body-md text-body-md">Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span class="font-body-md text-body-md">Transactions</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
<span class="font-body-md text-body-md">Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
<span class="font-body-md text-body-md">Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto flex flex-col gap-1 pt-4 border-t border-slate-100">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span class="font-body-md text-body-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span class="font-body-md text-body-md">Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding bg-surface-container-lowest z-40">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="relative w-96">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">search</span>
|
||||
<input class="w-full bg-slate-50 border-none rounded-full pl-10 pr-4 py-2 text-body-md focus:ring-2 focus:ring-primary/20" placeholder="Search devices, merchants..." type="text"/>
|
||||
</div>
|
||||
<nav class="hidden md:flex gap-6">
|
||||
<a class="text-primary font-bold border-b-2 border-primary h-[72px] flex items-center" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant hover:text-primary transition-colors h-[72px] flex items-center" href="#">System Health</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-on-surface-variant relative">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-danger rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<img alt="Administrator Profile" class="w-8 h-8 rounded-full border border-slate-200" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAUwYwdc49M8Tip_Q5HmjRqGoXJNad0776g4k4xB27qMd7FLleWQSH8N1gke6Z6TiIPEBkLv5roSTK4A6M7VQVJn70_OxUWPXBdbShZifG7LpCjVfBESpHZfmsCtWATl6JUy9Cb8LbwBsahotAx-TO6rXW0uyxuskJecoWJnTymG38_sBZTSACEQveLAkKoJEZPJw6e53HokMROjScOdO-c3-2BwWuW7f05y-2ZPXthdv6ZcmR3ViwtYPiPbARRzL_nY5udIyoXvZA"/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 pt-[72px] p-page-padding">
|
||||
<!-- Page Header -->
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-1">Device Registry</h2>
|
||||
<p class="text-body-md text-on-surface-variant">Manage and monitor all IoT soundbox units across the network.</p>
|
||||
</div>
|
||||
<button class="bg-primary hover:bg-primary-container text-on-primary px-6 py-2.5 rounded-xl font-bold flex items-center gap-2 transition-all active:scale-95">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="add">add</span>
|
||||
Register New Device
|
||||
</button>
|
||||
</div>
|
||||
<!-- Summary KPI Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-gutter mb-8">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-primary/10 text-primary rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" data-icon="devices">devices</span>
|
||||
</div>
|
||||
<span class="text-success text-metric-sm font-metric-sm flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
+24
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-label-md font-label-md text-slate-500 uppercase tracking-wider">Total Registered</h3>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">1,200</p>
|
||||
<div class="mt-4 w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div class="bg-primary h-full w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-success/10 text-success rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" data-icon="router">router</span>
|
||||
</div>
|
||||
<span class="text-success text-metric-sm font-metric-sm flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">check_circle</span>
|
||||
75% Rate
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-label-md font-label-md text-slate-500 uppercase tracking-wider">Active Units</h3>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">900</p>
|
||||
<div class="mt-4 w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div class="bg-success h-full w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-warning/10 text-warning rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" data-icon="inventory_2">inventory_2</span>
|
||||
</div>
|
||||
<span class="text-slate-500 text-metric-sm font-metric-sm">Unassigned</span>
|
||||
</div>
|
||||
<h3 class="text-label-md font-label-md text-slate-500 uppercase tracking-wider">Stock Available</h3>
|
||||
<p class="text-metric-lg font-metric-lg text-on-surface">300</p>
|
||||
<div class="mt-4 w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div class="bg-warning h-full w-1/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filter Bar & Data Table -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl shadow-sm overflow-hidden">
|
||||
<!-- Filters -->
|
||||
<div class="p-4 border-b border-slate-200 flex flex-wrap items-center gap-4 bg-slate-50/50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-slate-400" data-icon="filter_list">filter_list</span>
|
||||
<span class="font-bold text-body-md">Filters</span>
|
||||
</div>
|
||||
<div class="h-6 w-[1px] bg-slate-300 mx-2"></div>
|
||||
<select class="bg-white border-slate-200 rounded-lg text-body-md py-1.5 px-3 focus:ring-primary focus:border-primary">
|
||||
<option>All Models</option>
|
||||
<option>Soundbox V2</option>
|
||||
<option>Soundbox V2 Pro</option>
|
||||
<option>Soundbox Mini</option>
|
||||
</select>
|
||||
<select class="bg-white border-slate-200 rounded-lg text-body-md py-1.5 px-3 focus:ring-primary focus:border-primary">
|
||||
<option>All Merchants</option>
|
||||
<option>Bakery A</option>
|
||||
<option>Supermarket X</option>
|
||||
<option>Coffee Shop B</option>
|
||||
</select>
|
||||
<select class="bg-white border-slate-200 rounded-lg text-body-md py-1.5 px-3 focus:ring-primary focus:border-primary">
|
||||
<option>All Connections</option>
|
||||
<option>4G LTE</option>
|
||||
<option>WiFi</option>
|
||||
</select>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button class="text-primary font-bold text-body-md hover:underline">Clear All</button>
|
||||
<button class="bg-white border border-slate-200 px-3 py-1.5 rounded-lg text-body-md flex items-center gap-2 hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="download">download</span>
|
||||
Export List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Table Container -->
|
||||
<div class="overflow-x-auto data-table-container">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Device ID</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Model</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Merchant Binding</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Connection</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider">Health</th>
|
||||
<th class="px-6 py-4 font-bold text-label-md text-slate-500 uppercase tracking-wider text-right">Last Seen</th>
|
||||
<th class="px-6 py-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="font-mono text-primary font-bold">SND-10293</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">Soundbox V2</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[16px]">store</span>
|
||||
</div>
|
||||
<span>Bakery A</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1.5 text-slate-600">
|
||||
<span class="material-symbols-outlined text-[18px]">cell_tower</span>
|
||||
4G
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success border border-success/20">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success mr-1.5"></span>
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1 text-success">
|
||||
<span class="material-symbols-outlined text-[18px]">favorite</span>
|
||||
Excellent
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height text-right font-mono text-slate-500">2 min ago</td>
|
||||
<td class="px-6 py-row-height text-right">
|
||||
<button class="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-slate-200 rounded-lg">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="font-mono text-primary font-bold">SND-10294</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">Soundbox V2</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<div class="flex items-center gap-2 text-slate-400 italic">
|
||||
<span>Unassigned</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1.5 text-slate-600">
|
||||
<span class="material-symbols-outlined text-[18px]">wifi</span>
|
||||
WiFi
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-500 border border-slate-200">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-400 mr-1.5"></span>
|
||||
Offline
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1 text-slate-400">
|
||||
<span class="material-symbols-outlined text-[18px]">heart_broken</span>
|
||||
N/A
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height text-right font-mono text-slate-500">14 hours ago</td>
|
||||
<td class="px-6 py-row-height text-right">
|
||||
<button class="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-slate-200 rounded-lg">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="font-mono text-primary font-bold">SND-10301</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">Soundbox V2 Pro</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[16px]">local_cafe</span>
|
||||
</div>
|
||||
<span>Coffee Shop B</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1.5 text-slate-600">
|
||||
<span class="material-symbols-outlined text-[18px]">cell_tower</span>
|
||||
4G
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success border border-success/20">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success mr-1.5"></span>
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1 text-warning">
|
||||
<span class="material-symbols-outlined text-[18px]">heart_minus</span>
|
||||
Good
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height text-right font-mono text-slate-500">Just now</td>
|
||||
<td class="px-6 py-row-height text-right">
|
||||
<button class="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-slate-200 rounded-lg">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="font-mono text-primary font-bold">SND-10315</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">Soundbox V2</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[16px]">shopping_basket</span>
|
||||
</div>
|
||||
<span>Supermarket X</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1.5 text-slate-600">
|
||||
<span class="material-symbols-outlined text-[18px]">wifi</span>
|
||||
WiFi
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-danger/10 text-danger border border-danger/20">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger mr-1.5"></span>
|
||||
Offline
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height">
|
||||
<span class="flex items-center gap-1 text-danger">
|
||||
<span class="material-symbols-outlined text-[18px]">heart_broken</span>
|
||||
Poor
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-row-height text-right font-mono text-slate-500">45 min ago</td>
|
||||
<td class="px-6 py-row-height text-right">
|
||||
<button class="opacity-0 group-hover:opacity-100 transition-opacity p-2 hover:bg-slate-200 rounded-lg">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="p-4 border-t border-slate-200 flex items-center justify-between bg-white">
|
||||
<span class="text-body-md text-slate-500">Showing <span class="font-bold text-on-surface">1-4</span> of <span class="font-bold text-on-surface">1,200</span> devices</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined">chevron_left</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary text-on-primary font-bold">1</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-slate-50 text-on-surface">2</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-slate-50 text-on-surface">3</button>
|
||||
<span class="px-2">...</span>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-slate-50 text-on-surface">300</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- System Maintenance Warning (Low-depth atmospheric card) -->
|
||||
<div class="mt-8 p-4 bg-tertiary-container/5 border border-tertiary-container/20 rounded-xl flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-full bg-tertiary-container/10 flex items-center justify-center text-tertiary">
|
||||
<span class="material-symbols-outlined">build</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-on-tertiary-fixed-variant">Scheduled Maintenance</h4>
|
||||
<p class="text-body-md text-on-tertiary-fixed-variant opacity-80">Device Heartbeat service will be offline for 15 minutes today at 02:00 UTC for firmware indexing updates.</p>
|
||||
</div>
|
||||
<button class="ml-auto text-on-tertiary-fixed-variant font-bold hover:underline">Dismiss</button>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Side Inspection Drawer (Initially hidden, triggered by row interaction) -->
|
||||
<div class="fixed inset-y-0 right-0 w-[450px] bg-white shadow-2xl z-[60] transform translate-x-full transition-transform duration-300 ease-in-out border-l border-slate-200" id="detailDrawer">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="p-6 border-b border-slate-200 flex justify-between items-center">
|
||||
<h3 class="font-headline-md text-headline-md">Device Detail</h3>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full" onclick="closeDrawer()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<img alt="Soundbox V2 Product" class="w-20 h-20 rounded-xl bg-slate-100" data-alt="A clean professional studio product shot of a minimalist electronic soundbox speaker device with a small LCD screen and premium matte plastic finish. The lighting is soft and corporate with subtle blue reflections on the surface consistent with a high-end fintech hardware brand. The background is a clean neutral white studio setting." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC-CSPTCnxQuDTN1XM0atRPM9hIcVzf3zpbuxEUGTIlC-c1BivDqPa9osmBscvoiUcJeMBwUaXbZ6Ut5FuG2a91sVtZjzWRTgLck34kJJJy3N2E9O3uVtZw6InOpX9Gkph2OJxu_Z-PkR_t3F56EVZY3u8o2iZO3iH8hj9_ajrku7g1r_l54uobcRoN3dRH3k_at6GTuGbMtSSD4ew24sX8nePUsVvILKJauQLcMKD14J6mtAGm0x5PfViQQKdJzf_pYMqKswr3Yz4"/>
|
||||
<div>
|
||||
<h4 class="font-bold text-headline-md mb-1">SND-10293</h4>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success">Soundbox V2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<section>
|
||||
<h5 class="text-label-md font-label-md text-slate-500 uppercase mb-3">Binding Info</h5>
|
||||
<div class="bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<div class="flex justify-between mb-2">
|
||||
<span class="text-on-surface-variant">Merchant Name</span>
|
||||
<span class="font-bold">Bakery A</span>
|
||||
</div>
|
||||
<div class="flex justify-between mb-2">
|
||||
<span class="text-on-surface-variant">MID</span>
|
||||
<span class="font-mono">MID-882910</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-on-surface-variant">Activated On</span>
|
||||
<span>12 Oct 2023</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h5 class="text-label-md font-label-md text-slate-500 uppercase mb-3">Live Payload (Audit Control)</h5>
|
||||
<div class="bg-slate-900 text-slate-300 p-4 rounded-xl font-mono text-[12px] relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 bg-slate-800 rounded opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
<pre>{
|
||||
"device_id": "SND-10293",
|
||||
"hb_state": "ACTIVE",
|
||||
"battery": "94%",
|
||||
"rssi": "-62dBm",
|
||||
"fw_ver": "2.4.1-stable",
|
||||
"last_event": "TXN_NOTIFY"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h5 class="text-label-md font-label-md text-slate-500 uppercase mb-3">Connectivity Health</h5>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
|
||||
<div class="bg-primary h-full w-[85%]"></div>
|
||||
</div>
|
||||
<span class="text-body-md font-bold whitespace-nowrap">85% Uptime</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-200 grid grid-cols-2 gap-4">
|
||||
<button class="w-full py-2.5 border border-slate-200 rounded-xl font-bold hover:bg-slate-50 transition-colors">Reboot Device</button>
|
||||
<button class="w-full py-2.5 bg-danger/10 text-danger rounded-xl font-bold hover:bg-danger/20 transition-colors">Unbind Merchant</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Simple micro-interaction for the detail drawer
|
||||
function openDrawer() {
|
||||
document.getElementById('detailDrawer').classList.remove('translate-x-full');
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
document.getElementById('detailDrawer').classList.add('translate-x-full');
|
||||
}
|
||||
|
||||
// Apply click event to all table rows (excluding the last column)
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('button')) {
|
||||
openDrawer();
|
||||
}
|
||||
});
|
||||
row.style.cursor = 'pointer';
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/device_registry_monitoring/screen.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
460
design/device_technical_detail/code.html
Normal file
@ -0,0 +1,460 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Device Detail | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"error-container": "#ffdad6",
|
||||
"warning": "#F59E0B",
|
||||
"slate-200": "#E2E8F0",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"inverse-surface": "#2e3039",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B",
|
||||
"secondary": "#505f76",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container": "#ededf9",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface": "#faf8ff",
|
||||
"slate-900": "#0F172A",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-container": "#54647a",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-background": "#191b23",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"info": "#0EA5E9",
|
||||
"danger": "#DC2626",
|
||||
"surface-bright": "#faf8ff",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#2563eb",
|
||||
"primary": "#004ac6",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-700": "#334155"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.code-font { font-family: 'JetBrains Mono', monospace; }
|
||||
.custom-scroll::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background: #E2E8F0; border-radius: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-md antialiased">
|
||||
<!-- Side Navigation Shell -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="text-label-md text-on-surface-variant">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">dashboard</span>
|
||||
<span class="font-body-md">Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">storefront</span>
|
||||
<span class="font-body-md">Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 bg-secondary-container text-on-secondary-container font-bold rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">speaker_group</span>
|
||||
<span class="font-body-md">Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">receipt_long</span>
|
||||
<span class="font-body-md">Transactions</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">account_balance</span>
|
||||
<span class="font-body-md">Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">history_edu</span>
|
||||
<span class="font-body-md">Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-slate-100 pt-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">settings</span>
|
||||
<span class="font-body-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">help</span>
|
||||
<span class="font-body-md">Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top Navigation Shell -->
|
||||
<header class="fixed top-0 right-0 h-[72px] flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding bg-surface-container-lowest border-b border-slate-200 z-40">
|
||||
<div class="flex items-center gap-4 bg-surface-container-low px-3 py-1.5 rounded-full w-96 border border-slate-200">
|
||||
<span class="material-symbols-outlined text-slate-500">search</span>
|
||||
<input class="bg-transparent border-none focus:ring-0 text-body-md w-full" placeholder="Search devices, merchants, or serials..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-4 text-on-surface-variant">
|
||||
<button class="hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
</button>
|
||||
<button class="hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span class="material-symbols-outlined">calendar_today</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-8 w-px bg-slate-200"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-right">
|
||||
<p class="text-label-md font-bold text-on-surface">Admin_User</p>
|
||||
<p class="text-[10px] text-slate-500 uppercase tracking-wider">Super Admin</p>
|
||||
</div>
|
||||
<img alt="Admin Avatar" class="w-10 h-10 rounded-full border-2 border-slate-100" data-alt="A professional headshot of a corporate technology administrator in a bright, modern office setting. The person has a friendly but authoritative expression. The lighting is crisp and natural, with a soft-focus background of a contemporary workstation and glass partitions, emphasizing a clean and efficient workspace." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBK5Bznv_N0fDPHYNOfdTITCcx2J24NhdTXOayYDW1Ve1g8JyTVuXORa1X-hvMSGk8VneyDK8kxQXLikdOs44c4a3-DanQUdKM7BqiizwA_m3MnULvWUBXydth4D6uac3UFSwU5Qx8ckVSYTrpoG0AvnxANAbvSv0CD2yCs21O8lpHokxv-vVCCaheQRGC5Wyw0TYY5o1V_D87PhtYXIVRL0yCMUz1e0pMoGhGY5g5BjNrQSg2gCTBVSJc9SHvTB9r_7EQPnBMV-0E"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 pt-[72px] min-h-screen">
|
||||
<div class="p-page-padding">
|
||||
<!-- Breadcrumb & Back Action -->
|
||||
<div class="flex items-center gap-2 mb-6 text-on-surface-variant">
|
||||
<a class="flex items-center hover:text-primary transition-colors" href="#">
|
||||
<span class="material-symbols-outlined text-sm mr-1">arrow_back</span>
|
||||
<span class="text-label-md">Back to Registry</span>
|
||||
</a>
|
||||
<span class="text-slate-300">/</span>
|
||||
<span class="text-label-md">SND-10293</span>
|
||||
</div>
|
||||
<!-- Device Header Summary -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding mb-gutter flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||
<div class="flex items-start gap-5">
|
||||
<div class="w-16 h-16 bg-primary-fixed rounded-xl flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined text-[40px]">speaker_group</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface">SND-10293</h2>
|
||||
<span class="bg-success/10 text-success text-[10px] font-bold px-2 py-0.5 rounded-full border border-success/20 uppercase tracking-wider flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-1">
|
||||
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-sm">settings_input_component</span>
|
||||
Soundbox V2 Pro
|
||||
</p>
|
||||
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-sm">schedule</span>
|
||||
Last seen 2 mins ago
|
||||
</p>
|
||||
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-sm">location_on</span>
|
||||
Mumbai, Central Region
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button class="px-4 py-2 border border-slate-200 text-on-surface-variant font-bold text-body-md rounded-lg hover:bg-slate-50 transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">download</span>
|
||||
Export Logs
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-primary text-on-primary font-bold text-body-md rounded-lg hover:opacity-90 active:scale-95 transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">sync</span>
|
||||
Refresh State
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab Navigation -->
|
||||
<div class="border-b border-slate-200 mb-8 flex gap-8">
|
||||
<button class="pb-4 text-body-md font-bold text-primary border-b-2 border-primary">Overview</button>
|
||||
<button class="pb-4 text-body-md font-medium text-on-surface-variant hover:text-primary transition-colors">Heartbeat</button>
|
||||
<button class="pb-4 text-body-md font-medium text-on-surface-variant hover:text-primary transition-colors">Configuration</button>
|
||||
<button class="pb-4 text-body-md font-medium text-on-surface-variant hover:text-primary transition-colors">Binding History</button>
|
||||
</div>
|
||||
<!-- Grid Layout -->
|
||||
<div class="grid grid-cols-12 gap-gutter">
|
||||
<!-- Left Column: Primary Content -->
|
||||
<div class="col-span-12 lg:col-span-8 space-y-gutter">
|
||||
<!-- KPI Metrics Bento Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-gutter">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Signal Strength (4G)</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">-78 dBm</h3>
|
||||
<span class="material-symbols-outlined text-success">signal_cellular_4_bar</span>
|
||||
</div>
|
||||
<p class="text-metric-sm text-success mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">check_circle</span>
|
||||
Excellent
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Battery Health</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">92%</h3>
|
||||
<span class="material-symbols-outlined text-primary">battery_5_bar</span>
|
||||
</div>
|
||||
<p class="text-metric-sm text-on-surface-variant mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">bolt</span>
|
||||
Discharging (External OFF)
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="text-label-md text-on-surface-variant mb-2">Firmware Version</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">v2.4.1</h3>
|
||||
<span class="material-symbols-outlined text-slate-400">verified</span>
|
||||
</div>
|
||||
<p class="text-metric-sm text-info mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">info</span>
|
||||
Latest version available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Merchant Binding Info -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div class="px-card-padding py-4 border-b border-slate-100 flex justify-between items-center bg-surface-container-low">
|
||||
<h4 class="font-headline-md text-headline-md">Current Merchant Binding</h4>
|
||||
<button class="text-primary text-label-md font-bold hover:underline">Change Merchant</button>
|
||||
</div>
|
||||
<div class="p-card-padding flex items-center gap-6">
|
||||
<div class="w-14 h-14 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center overflow-hidden">
|
||||
<img alt="Bakery A Logo" data-alt="A macro photograph of fresh, golden-brown sourdough bread and artisan pastries in a boutique bakery window. The lighting is warm and inviting, highlighting textures and powdered sugar. The overall aesthetic is rustic yet clean, using a natural color palette of ambers and creams to convey quality and craft." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBLL97IiG98RevWkfNoNu5bn4vTZ2kcq5CEVVFZhDo7FSSiL-KDp4dkGB2euEyy3qjWqJZcyEnIvEUF-zIgZAQb_fcnVzjHHCpHXhk3_iLjEuviUvlCULxDy3FSwdqNYnVWQI8hJlttUDVMHQWjGvmF5J-DU-1UpRll2tilFoGJsiRPOY9w077s67RaPXE2AQUDPnJqcUcXN-C8Vs-FndxWkqGGTnLLjPbGLJIVy4mQHd9PAT2uhjppmienzUToJxGVMX84pTk53WI"/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-headline-md font-bold text-on-surface">Bakery A - Mumbai Outlet</p>
|
||||
<p class="text-body-md text-on-surface-variant">Merchant ID: MID-99201-B02</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-label-md text-slate-500">Bound Since</p>
|
||||
<p class="text-body-md font-bold">12 Oct 2023, 11:45 AM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Live Payload Viewer -->
|
||||
<div class="bg-slate-900 rounded-xl overflow-hidden flex flex-col h-[400px]">
|
||||
<div class="px-4 py-3 bg-slate-800 flex justify-between items-center border-b border-slate-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-success animate-pulse"></span>
|
||||
<span class="text-label-md text-slate-100 font-bold uppercase tracking-widest">Live Payload Stream</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button class="text-slate-400 hover:text-white transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]">content_copy</span>
|
||||
</button>
|
||||
<button class="text-slate-400 hover:text-white transition-colors" id="clearConsole">
|
||||
<span class="material-symbols-outlined text-[18px]">block</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 flex-1 overflow-y-auto code-font text-[13px] text-green-400 space-y-1 custom-scroll" id="payload-stream">
|
||||
<p class="text-slate-500">[14:02:11] INITIALIZING WEBSOCKET CONNECTION...</p>
|
||||
<p class="text-slate-500">[14:02:12] CONNECTED TO SND-10293_GATEWAY_V4</p>
|
||||
<p class="text-success">[14:02:15] RECV: {"event": "heartbeat", "status": "online", "v_batt": 4.12, "rssi": -78, "ts": 1715421255}</p>
|
||||
<p class="text-blue-400">[14:03:01] SEND: {"cmd": "ack_config", "token": "fx-2291"}</p>
|
||||
<p class="text-success">[14:03:02] RECV: {"event": "tx_confirm", "tx_id": "QR-90112", "amount": 12.50, "currency": "INR"}</p>
|
||||
<p class="text-slate-500">[14:04:15] IDLE: STANDBY MODE ACTIVE</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column: Sidebar Panels -->
|
||||
<div class="col-span-12 lg:col-span-4 space-y-gutter">
|
||||
<!-- Remote Actions Panel -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-card-padding border-b border-slate-100">
|
||||
<h4 class="font-headline-md text-headline-md">Remote Actions</h4>
|
||||
</div>
|
||||
<div class="p-card-padding space-y-3">
|
||||
<button class="w-full flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-all group">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary">restart_alt</span>
|
||||
<span class="font-body-md font-bold">Reboot Device</span>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-slate-300">chevron_right</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-all group">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary">system_update</span>
|
||||
<span class="font-body-md font-bold">Update Firmware</span>
|
||||
</div>
|
||||
<span class="bg-primary/10 text-primary text-[10px] font-bold px-2 py-0.5 rounded-full">OTA Ready</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-all group">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary">lock_open</span>
|
||||
<span class="font-body-md font-bold">Unbind Merchant</span>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-slate-300">chevron_right</span>
|
||||
</button>
|
||||
<div class="pt-2">
|
||||
<button class="w-full py-2.5 bg-danger/10 text-danger border border-danger/20 font-bold text-body-md rounded-lg hover:bg-danger/20 transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">delete_forever</span>
|
||||
Decommission Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Health Timeline -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding shadow-sm">
|
||||
<h4 class="font-headline-md text-headline-md mb-6">Device Events</h4>
|
||||
<div class="space-y-6 relative before:content-[''] before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-[2px] before:bg-slate-100">
|
||||
<!-- Event 1 -->
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-success border-4 border-white shadow-sm flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[12px] text-white" style="font-variation-settings: 'FILL' 1;">check</span>
|
||||
</div>
|
||||
<p class="text-label-md font-bold">Successful Transaction</p>
|
||||
<p class="text-[12px] text-on-surface-variant">QR payment processed (₹45.00)</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Today, 02:03 PM</p>
|
||||
</div>
|
||||
<!-- Event 2 -->
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-blue-500 border-4 border-white shadow-sm flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[12px] text-white" style="font-variation-settings: 'FILL' 1;">sync</span>
|
||||
</div>
|
||||
<p class="text-label-md font-bold">Config Synchronized</p>
|
||||
<p class="text-[12px] text-on-surface-variant">Volume set to 80%</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Today, 11:20 AM</p>
|
||||
</div>
|
||||
<!-- Event 3 -->
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-warning border-4 border-white shadow-sm flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[12px] text-white" style="font-variation-settings: 'FILL' 1;">power_off</span>
|
||||
</div>
|
||||
<p class="text-label-md font-bold">AC Power Disconnected</p>
|
||||
<p class="text-[12px] text-on-surface-variant">Switched to battery backup</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Yesterday, 09:45 PM</p>
|
||||
</div>
|
||||
<!-- Event 4 -->
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-6 h-6 rounded-full bg-slate-200 border-4 border-white shadow-sm flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[12px] text-slate-500" style="font-variation-settings: 'FILL' 1;">bolt</span>
|
||||
</div>
|
||||
<p class="text-label-md font-bold text-slate-400">Firmware Update Scheduled</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Expected 15 May</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full mt-6 text-primary text-label-md font-bold hover:bg-slate-50 py-2 rounded-lg transition-colors">View All Events</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- FAB for Quick Config (Suppressed on detail pages as per mandate but kept for utility context if needed, hidden here) -->
|
||||
<div class="fixed bottom-6 right-6 z-50 hidden">
|
||||
<button class="w-14 h-14 bg-primary text-on-primary rounded-full shadow-lg flex items-center justify-center hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
// Micro-interaction: Live Payload Simulator
|
||||
const stream = document.getElementById('payload-stream');
|
||||
const clearBtn = document.getElementById('clearConsole');
|
||||
|
||||
const logs = [
|
||||
{ type: 'success', text: 'RECV: {"event": "heartbeat", "status": "online", "v_batt": 4.12, "rssi": -78}' },
|
||||
{ type: 'info', text: 'SEND: {"cmd": "ping", "id": 102}' },
|
||||
{ type: 'log', text: 'CONNECTION STABLE: latency 45ms' },
|
||||
{ type: 'warning', text: 'WARN: Battery voltage dipping below 3.9v threshold' },
|
||||
{ type: 'success', text: 'RECV: {"event": "audio_ack", "file_id": "notif_2"}' }
|
||||
];
|
||||
|
||||
setInterval(() => {
|
||||
const log = logs[Math.floor(Math.random() * logs.length)];
|
||||
const p = document.createElement('p');
|
||||
const time = new Date().toLocaleTimeString('en-GB', { hour12: false });
|
||||
|
||||
p.className = log.type === 'success' ? 'text-green-400' :
|
||||
log.type === 'info' ? 'text-blue-400' :
|
||||
log.type === 'warning' ? 'text-warning' : 'text-slate-500';
|
||||
|
||||
p.textContent = `[${time}] ${log.text}`;
|
||||
stream.appendChild(p);
|
||||
stream.scrollTop = stream.scrollHeight;
|
||||
|
||||
if (stream.children.length > 50) {
|
||||
stream.removeChild(stream.firstChild);
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
stream.innerHTML = '<p class="text-slate-500">--- CONSOLE CLEARED ---</p>';
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/device_technical_detail/screen.png
Normal file
|
After Width: | Height: | Size: 345 KiB |
252
design/device_ui_payment_success/code.html
Normal file
@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-container": "#54647a",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary": "#943700",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"on-surface": "#191b23",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"slate-900": "#0F172A",
|
||||
"surface-tint": "#0053db",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"inverse-surface": "#2e3039",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"mono": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.success-pulse {
|
||||
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(0.95); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.8; }
|
||||
100% { transform: scale(0.95); opacity: 1; }
|
||||
}
|
||||
.confetti {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 flex items-center justify-center min-h-screen p-4 overflow-hidden">
|
||||
<!-- Soundbox Device Simulation Frame -->
|
||||
<div class="relative w-full max-w-[400px] aspect-[3/4] bg-surface-container-lowest rounded-[48px] shadow-2xl border-[12px] border-slate-700 flex flex-col overflow-hidden">
|
||||
<!-- Physical Speaker Grille Top -->
|
||||
<div class="w-full h-12 flex items-center justify-center space-x-1.5 opacity-20 py-4">
|
||||
<div class="w-1.5 h-1.5 bg-slate-900 rounded-full"></div>
|
||||
<div class="w-1.5 h-1.5 bg-slate-900 rounded-full"></div>
|
||||
<div class="w-1.5 h-1.5 bg-slate-900 rounded-full"></div>
|
||||
<div class="w-1.5 h-1.5 bg-slate-900 rounded-full"></div>
|
||||
<div class="w-1.5 h-1.5 bg-slate-900 rounded-full"></div>
|
||||
</div>
|
||||
<!-- Success Content Canvas -->
|
||||
<main class="flex-1 flex flex-col items-center justify-center px-8 text-center relative">
|
||||
<!-- Confetti / Particles Effect -->
|
||||
<div class="absolute inset-0 pointer-events-none" id="confetti-container"></div>
|
||||
<!-- Success Checkmark Icon -->
|
||||
<div class="mb-8 relative">
|
||||
<div class="success-pulse absolute -inset-4 bg-success/10 rounded-full"></div>
|
||||
<div class="relative w-24 h-24 bg-success flex items-center justify-center rounded-full shadow-lg shadow-success/20">
|
||||
<span class="material-symbols-outlined text-white text-[64px]" style="font-variation-settings: 'wght' 700;">check</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Text -->
|
||||
<div class="space-y-2 mb-8">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-background tracking-tight">Payment Successful</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Merchant: Brew & Bean Cafe</p>
|
||||
</div>
|
||||
<!-- Transaction Amount Card -->
|
||||
<div class="w-full bg-surface-container-low rounded-xl p-6 border border-slate-200 mb-8">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-widest mb-1">Amount Paid</p>
|
||||
<div class="font-display-lg text-display-lg text-primary flex justify-center items-baseline gap-1">
|
||||
<span class="text-headline-md font-medium">Rp</span>
|
||||
<span>50,000</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Transaction Meta Details -->
|
||||
<div class="w-full space-y-3 mb-4">
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="font-label-md text-label-md text-slate-500">Reference No.</span>
|
||||
<span class="font-mono text-[13px] text-on-background font-medium">FTX-8829-0012</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="font-label-md text-label-md text-slate-500">Date & Time</span>
|
||||
<span class="font-body-md text-body-md text-on-background">Oct 24, 2023 · 14:32</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">Method</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-body-md text-body-md text-on-background">QRIS QuickPay</span>
|
||||
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Device Status Bar Bottom -->
|
||||
<footer class="h-16 border-t border-slate-100 bg-surface-container-lowest px-8 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-success text-[20px]">wifi</span>
|
||||
<span class="font-label-md text-label-md text-slate-500">Connected</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">88%</span>
|
||||
<span class="material-symbols-outlined text-slate-500 text-[20px]">battery_5_bar</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Physical Power Indicator Light -->
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-success shadow-[0_0_8px_rgba(22,163,74,0.6)]"></div>
|
||||
</div>
|
||||
<!-- Decorative Device Background Label -->
|
||||
<div class="absolute bottom-8 left-0 right-0 text-center pointer-events-none">
|
||||
<p class="font-headline-md text-headline-md text-slate-600 opacity-50">IOT SOUNDBOX GEN-3</p>
|
||||
</div>
|
||||
<script>
|
||||
// Simple confetti effect for the success state
|
||||
function createConfetti() {
|
||||
const container = document.getElementById('confetti-container');
|
||||
const colors = ['#16A34A', '#2563eb', '#F59E0B', '#004ac6'];
|
||||
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.classList.add('confetti');
|
||||
|
||||
const size = Math.random() * 6 + 4;
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
// Random position
|
||||
const startX = Math.random() * 100;
|
||||
const startY = Math.random() * 100;
|
||||
confetti.style.left = `${startX}%`;
|
||||
confetti.style.top = `${startY}%`;
|
||||
confetti.style.opacity = Math.random();
|
||||
|
||||
// Basic animation
|
||||
const duration = Math.random() * 3 + 2;
|
||||
const delay = Math.random() * 2;
|
||||
confetti.style.transition = `all ${duration}s ease-out ${delay}s`;
|
||||
|
||||
container.appendChild(confetti);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
confetti.style.transform = `translate(${(Math.random() - 0.5) * 200}px, ${(Math.random() - 0.5) * 200}px) rotate(${Math.random() * 360}deg)`;
|
||||
confetti.style.opacity = '0';
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
createConfetti();
|
||||
|
||||
// Re-run confetti occasionally
|
||||
setInterval(() => {
|
||||
const container = document.getElementById('confetti-container');
|
||||
container.innerHTML = '';
|
||||
createConfetti();
|
||||
}, 6000);
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/device_ui_payment_success/screen.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
469
design/device_ui_qr_payment_display/code.html
Normal file
@ -0,0 +1,469 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>FinOps Admin - Device QR State</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.device-screen-glare {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 50%);
|
||||
}
|
||||
@keyframes pulse-opacity {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-opacity 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-container": "#54647a",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary": "#943700",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"on-surface": "#191b23",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"slate-900": "#0F172A",
|
||||
"surface-tint": "#0053db",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"inverse-surface": "#2e3039",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-background min-h-screen flex">
|
||||
<!-- SideNavBar Shell -->
|
||||
<aside class="bg-surface-container-lowest border-r border-slate-200 w-64 h-screen fixed left-0 top-0 flex flex-col py-6 overflow-y-auto z-50">
|
||||
<div class="px-6 mb-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">FinOps Admin</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider mt-1">System Management</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center px-6 py-3 text-secondary hover:bg-slate-100 transition-colors group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">dashboard</span>
|
||||
<span class="font-body-md text-body-md">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center px-6 py-3 text-secondary hover:bg-slate-100 transition-colors group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">pending_actions</span>
|
||||
<span class="font-body-md text-body-md">Onboarding Queue</span>
|
||||
</a>
|
||||
<a class="flex items-center px-6 py-3 text-secondary hover:bg-slate-100 transition-colors group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">store</span>
|
||||
<span class="font-body-md text-body-md">Merchant Directory</span>
|
||||
</a>
|
||||
<!-- Active Tab: Device Fleet -->
|
||||
<a class="flex items-center px-6 py-3 text-primary font-bold border-r-4 border-primary bg-surface-container-low transition-colors group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 text-primary">speaker_group</span>
|
||||
<span class="font-body-md text-body-md">Device Fleet</span>
|
||||
</a>
|
||||
<a class="flex items-center px-6 py-3 text-secondary hover:bg-slate-100 transition-colors group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">assignment</span>
|
||||
<span class="font-body-md text-body-md">Audit Logs</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto px-6 pt-6 border-t border-slate-100">
|
||||
<button class="w-full bg-primary text-on-primary py-3 rounded-xl font-body-md text-body-md font-semibold hover:opacity-90 transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px]">add</span>
|
||||
New Application
|
||||
</button>
|
||||
<div class="mt-6 flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-slate-200 overflow-hidden">
|
||||
<img alt="Admin User Profile" data-alt="A professional headshot of a modern corporate administrator in a bright office environment. The person is smiling confidently, wearing a clean, white button-down shirt. The background is a soft-focus tech office with cool blue and white tones, matching a professional and precise fintech brand aesthetic. High-quality lighting highlights the facial features and creates a clean, corporate atmosphere." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA1Os3poCW14wa8nsVWtWvnG05uEk4Gb0rePLLwW4KguKbahuCdopeMNd2R7EB5MoKSQu7tLEiA7UG8XaIFIY6l9GsYl9EPxIOe2KiAI4nU7o5829TctzIBXO-bFSHG_b3b29aNSZqahFB3Y8BGHiAL2cKvYM0tAoKQY3P3ggSx_ZqGhA0RK3wqeARj0NwQH0O3Ktl11y3KfIwj_secOEJRjFctDskVyBW93sMKzCuwOFadiquE2fjsa1oF09aevHgPaZIsHPbC84U"/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface font-bold">Admin User</p>
|
||||
<p class="font-label-md text-slate-500">Super Admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Cluster -->
|
||||
<main class="flex-1 ml-64 min-h-screen">
|
||||
<!-- TopAppBar -->
|
||||
<header class="bg-surface-container-lowest border-b border-slate-200 h-topbar-height px-gutter flex justify-between items-center sticky top-0 z-40">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<div class="relative w-96">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border-none rounded-lg text-body-md focus:ring-2 focus:ring-primary/20" placeholder="Search devices, merchants, or transactions..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="font-headline-md text-headline-md font-black text-primary mr-8">Merchant Control Center</h2>
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="hover:bg-surface-container rounded-full p-2 transition-colors">
|
||||
<span class="material-symbols-outlined text-on-surface-variant">notifications</span>
|
||||
</button>
|
||||
<button class="hover:bg-surface-container rounded-full p-2 transition-colors">
|
||||
<span class="material-symbols-outlined text-on-surface-variant">help_outline</span>
|
||||
</button>
|
||||
<button class="hover:bg-surface-container rounded-full p-2 transition-colors">
|
||||
<span class="material-symbols-outlined text-on-surface-variant">account_circle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Content Canvas -->
|
||||
<div class="p-page-padding max-w-7xl mx-auto">
|
||||
<!-- Breadcrumbs & Actions -->
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-slate-500 mb-1">
|
||||
<span class="font-label-md">Device Fleet</span>
|
||||
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
||||
<span class="font-label-md">Soundbox SB-9021</span>
|
||||
</div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Live Device Simulation</h1>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button class="px-4 py-2 border border-slate-200 rounded-lg text-body-md font-medium hover:bg-white transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">refresh</span>
|
||||
Reset State
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-primary text-on-primary rounded-lg text-body-md font-medium hover:opacity-90 transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">screenshot_region</span>
|
||||
Remote Capture
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Layout: 12-column grid -->
|
||||
<div class="grid grid-cols-12 gap-gutter">
|
||||
<!-- LEFT: Physical Device Mockup (Central Focus) -->
|
||||
<div class="col-span-12 lg:col-span-7 flex justify-center items-start pt-10">
|
||||
<!-- Physical Device Frame -->
|
||||
<div class="relative w-[340px] h-[520px] bg-slate-900 rounded-[60px] p-4 shadow-2xl border-[8px] border-slate-800 ring-4 ring-slate-700/20">
|
||||
<!-- Speaker Mesh Top -->
|
||||
<div class="w-24 h-1.5 bg-slate-800 rounded-full mx-auto mt-4 mb-8 opacity-50"></div>
|
||||
<!-- Internal Screen Container -->
|
||||
<div class="relative w-full h-[400px] bg-white rounded-[32px] overflow-hidden flex flex-col shadow-inner">
|
||||
<!-- Screen Content: QR Generation State -->
|
||||
<div class="bg-primary px-6 py-6 text-white">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="font-label-md opacity-80 uppercase tracking-widest">Merchant</span>
|
||||
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">wifi</span>
|
||||
</div>
|
||||
<h3 class="font-headline-md text-headline-md leading-tight truncate">Central Coffee Roasters</h3>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center justify-center p-6 bg-white relative">
|
||||
<!-- QR Placeholder Area -->
|
||||
<div class="relative w-48 h-48 bg-white border-4 border-slate-100 p-2 rounded-2xl mb-6 flex items-center justify-center overflow-hidden">
|
||||
<!-- QR Code Mockup -->
|
||||
<div class="w-full h-full bg-slate-50 relative group">
|
||||
<div class="absolute inset-0 grid grid-cols-8 grid-rows-8 gap-0.5 opacity-20">
|
||||
<div class="bg-black"></div><div class="bg-black"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-black"></div>
|
||||
<div class="bg-black"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-black"></div>
|
||||
<!-- Just a pattern to look like QR -->
|
||||
<div class="bg-black"></div><div class="bg-black"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-black"></div><div class="bg-black"></div><div class="bg-black"></div>
|
||||
<div class="bg-white"></div><div class="bg-white"></div><div class="bg-white"></div><div class="bg-black"></div><div class="bg-white"></div><div class="bg-white"></div><div class="bg-white"></div><div class="bg-white"></div>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-10 h-10 bg-white rounded-lg p-1 shadow-sm border border-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-primary text-[24px]">qr_code_2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Corner accents -->
|
||||
<div class="absolute top-0 left-0 w-8 h-8 border-t-4 border-l-4 border-primary rounded-tl-xl"></div>
|
||||
<div class="absolute top-0 right-0 w-8 h-8 border-t-4 border-r-4 border-primary rounded-tr-xl"></div>
|
||||
<div class="absolute bottom-0 left-0 w-8 h-8 border-b-4 border-l-4 border-primary rounded-bl-xl"></div>
|
||||
<div class="absolute bottom-0 right-0 w-8 h-8 border-b-4 border-r-4 border-primary rounded-br-xl"></div>
|
||||
<!-- Scanning line animation -->
|
||||
<div class="absolute top-0 left-0 w-full h-[2px] bg-primary shadow-[0_0_15px_rgba(0,74,198,0.8)] animate-[bounce_3s_infinite] opacity-50"></div>
|
||||
</div>
|
||||
<!-- Amount Display -->
|
||||
<div class="text-center">
|
||||
<p class="font-label-md text-slate-500 uppercase tracking-widest mb-1">Total Payment</p>
|
||||
<h4 class="font-metric-lg text-metric-lg text-slate-900 tracking-tight">Rp 50.000</h4>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer Status -->
|
||||
<div class="bg-surface-container-low px-6 py-4 flex items-center justify-center gap-2 border-t border-slate-100">
|
||||
<span class="material-symbols-outlined text-warning animate-pulse-slow">pending</span>
|
||||
<span class="font-body-md text-body-md font-semibold text-on-surface-variant">Waiting for payment...</span>
|
||||
</div>
|
||||
<!-- Glare effect overlay -->
|
||||
<div class="absolute inset-0 device-screen-glare pointer-events-none"></div>
|
||||
</div>
|
||||
<!-- Physical Buttons -->
|
||||
<div class="absolute -right-2 top-24 w-2 h-16 bg-slate-700 rounded-l-md"></div>
|
||||
<div class="absolute -right-2 top-44 w-2 h-10 bg-slate-700 rounded-l-md"></div>
|
||||
<!-- Charging Indicator -->
|
||||
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
<div class="w-2 h-2 bg-success rounded-full shadow-[0_0_8px_rgba(22,163,74,0.6)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- RIGHT: Operational Context & Stats -->
|
||||
<div class="col-span-12 lg:col-span-5 space-y-6">
|
||||
<!-- Device Diagnostics Card -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-card-padding">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface mb-6 flex items-center justify-between">
|
||||
Device Diagnostics
|
||||
<span class="px-2 py-1 bg-success/10 text-success rounded text-[10px] uppercase font-bold tracking-widest">Active</span>
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center py-3 border-b border-slate-100">
|
||||
<span class="font-body-md text-slate-500">Signal Strength</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-end gap-0.5 h-4">
|
||||
<div class="w-1 h-2 bg-primary"></div>
|
||||
<div class="w-1 h-3 bg-primary"></div>
|
||||
<div class="w-1 h-4 bg-primary"></div>
|
||||
<div class="w-1 h-4 bg-slate-200"></div>
|
||||
</div>
|
||||
<span class="font-metric-sm text-metric-sm text-on-surface">-74 dBm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3 border-b border-slate-100">
|
||||
<span class="font-body-md text-slate-500">Battery Level</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-metric-sm text-metric-sm text-on-surface">94%</span>
|
||||
<span class="material-symbols-outlined text-success text-[20px]">battery_5_bar</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3 border-b border-slate-100">
|
||||
<span class="font-body-md text-slate-500">Firmware</span>
|
||||
<span class="font-metric-sm text-metric-sm text-slate-900 bg-slate-100 px-2 py-0.5 rounded">v2.4.1-stable</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3">
|
||||
<span class="font-body-md text-slate-500">Uptime</span>
|
||||
<span class="font-metric-sm text-metric-sm text-on-surface">14d 06h 22m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Transaction Event Log (Audit Block Style) -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-card-padding">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Recent Logs</h3>
|
||||
<button class="text-primary font-label-md hover:underline">View All</button>
|
||||
</div>
|
||||
<div class="bg-slate-900 text-slate-200 p-4 rounded-lg font-mono text-[12px] leading-relaxed overflow-hidden h-48 relative">
|
||||
<div class="absolute top-2 right-2">
|
||||
<button class="p-1 hover:bg-slate-800 rounded transition-colors" title="Copy Logs">
|
||||
<span class="material-symbols-outlined text-[16px] text-slate-400">content_copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p><span class="text-success">[14:22:01]</span> CMD_REQ_QR_GEN: amount=50000</p>
|
||||
<p><span class="text-primary">[14:22:02]</span> QR_API_RES: 200 OK - data_hash=7f2a1...</p>
|
||||
<p><span class="text-warning">[14:22:02]</span> UI_STATE_UPD: display_qr_waiting</p>
|
||||
<p><span class="text-slate-500">[14:22:05]</span> HEARTBEAT: latency=42ms</p>
|
||||
<p><span class="text-slate-500">[14:22:15]</span> HEARTBEAT: latency=38ms</p>
|
||||
<p><span class="text-info">[14:22:18]</span> SOCKET_EVT: client_polling_started</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Quick Bar -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center gap-4 hover:shadow-md transition-shadow cursor-pointer group">
|
||||
<div class="w-10 h-10 rounded-full bg-error-container flex items-center justify-center text-error group-hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-outlined">block</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface font-bold">Cancel Trans.</p>
|
||||
<p class="font-label-md text-slate-500">Void current QR</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center gap-4 hover:shadow-md transition-shadow cursor-pointer group">
|
||||
<div class="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center text-primary group-hover:scale-110 transition-transform">
|
||||
<span class="material-symbols-outlined">volume_up</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface font-bold">Test Speaker</p>
|
||||
<p class="font-label-md text-slate-500">Play chime</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Detailed History Table Preview -->
|
||||
<div class="mt-12">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Recent Merchant Transactions</h3>
|
||||
<div class="flex gap-2">
|
||||
<span class="px-3 py-1 bg-surface-container rounded-full text-label-md text-on-surface-variant font-medium">Daily Target: 82%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Transaction ID</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Timestamp</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Method</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider">Amount</th>
|
||||
<th class="px-6 py-4 font-label-md text-slate-500 uppercase tracking-wider text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height">
|
||||
<td class="px-6 py-2 font-body-md text-slate-900 font-medium">TXN-98421055</td>
|
||||
<td class="px-6 py-2 font-body-md text-slate-500">14:18:42</td>
|
||||
<td class="px-6 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">qr_code</span>
|
||||
<span class="font-body-md text-on-surface">QRIS Static</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 font-metric-sm text-on-surface">Rp 24,500</td>
|
||||
<td class="px-6 py-2 text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Settled
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height">
|
||||
<td class="px-6 py-2 font-body-md text-slate-900 font-medium">TXN-98421042</td>
|
||||
<td class="px-6 py-2 font-body-md text-slate-500">14:05:11</td>
|
||||
<td class="px-6 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">qr_code</span>
|
||||
<span class="font-body-md text-on-surface">QRIS Dynamic</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 font-metric-sm text-on-surface">Rp 120,000</td>
|
||||
<td class="px-6 py-2 text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Settled
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height">
|
||||
<td class="px-6 py-2 font-body-md text-slate-900 font-medium">TXN-98421021</td>
|
||||
<td class="px-6 py-2 font-body-md text-slate-500">13:58:30</td>
|
||||
<td class="px-6 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">qr_code</span>
|
||||
<span class="font-body-md text-on-surface">QRIS Dynamic</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 font-metric-sm text-on-surface">Rp 50,000</td>
|
||||
<td class="px-6 py-2 text-right">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-error/10 text-error">
|
||||
Failed
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Contextual FAB (Only relevant for device monitoring screen) -->
|
||||
<button class="fixed bottom-8 right-8 w-14 h-14 bg-primary text-on-primary rounded-full shadow-lg flex items-center justify-center hover:scale-110 transition-all z-50">
|
||||
<span class="material-symbols-outlined">support_agent</span>
|
||||
</button>
|
||||
<script>
|
||||
// Simple logic for simulating transaction status changes
|
||||
const statusText = document.querySelector('.bg-surface-container-low span:last-child');
|
||||
const statusIcon = document.querySelector('.bg-surface-container-low span:first-child');
|
||||
|
||||
// This simulates a transition after some time (purely visual for the demo)
|
||||
setTimeout(() => {
|
||||
statusText.textContent = "Payment successful!";
|
||||
statusText.classList.remove('text-on-surface-variant');
|
||||
statusText.classList.add('text-success');
|
||||
|
||||
statusIcon.textContent = "check_circle";
|
||||
statusIcon.classList.remove('text-warning', 'animate-pulse-slow');
|
||||
statusIcon.classList.add('text-success');
|
||||
}, 8000);
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/device_ui_qr_payment_display/screen.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
476
design/merchant_dashboard_portal/code.html
Normal file
@ -0,0 +1,476 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops | Merchant Portal</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.font-display { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
|
||||
/* Custom scrollbar for data-centric feel */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #E2E8F0; border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-surface-variant": "#434655",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-surface": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-900": "#0F172A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface">
|
||||
<!-- Sidebar (Shared Components Anchor) -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="px-4 mb-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-1">Merchant Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<!-- Navigation links based on JSON mapping and Merchant Priority -->
|
||||
<a class="bg-secondary-container text-on-secondary-container font-bold rounded-lg flex items-center gap-3 px-4 py-3 font-body-md text-body-md transition-all active:opacity-90 active:scale-95" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant hover:bg-slate-100 transition-colors flex items-center gap-3 px-4 py-3 font-body-md text-body-md rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span>Transactions</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant hover:bg-slate-100 transition-colors flex items-center gap-3 px-4 py-3 font-body-md text-body-md rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
<span>Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant hover:bg-slate-100 transition-colors flex items-center gap-3 px-4 py-3 font-body-md text-body-md rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span>Device Registry</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-slate-100 pt-4 space-y-1">
|
||||
<a class="text-on-surface-variant hover:bg-slate-100 transition-colors flex items-center gap-3 px-4 py-3 font-body-md text-body-md rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a class="text-on-surface-variant hover:bg-slate-100 transition-colors flex items-center gap-3 px-4 py-3 font-body-md text-body-md rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
<div class="mt-4 flex items-center gap-3 px-4 py-3">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-fixed flex items-center justify-center text-on-primary-fixed font-bold text-xs">JD</div>
|
||||
<div class="overflow-hidden">
|
||||
<p class="text-body-md font-bold truncate">John's Coffee</p>
|
||||
<p class="text-[10px] text-slate-500 truncate">MID: 98234102</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] bg-surface-container-lowest border-b border-slate-200 flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding z-40">
|
||||
<div class="flex items-center gap-6 flex-1">
|
||||
<div class="relative w-full max-w-md">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-lg">search</span>
|
||||
<input class="w-full bg-slate-50 border-none rounded-xl pl-10 pr-4 py-2 text-body-md focus:ring-2 focus:ring-primary/20 transition-all" placeholder="Search transactions, devices..." type="text"/>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
<a class="text-primary border-b-2 border-primary font-bold py-6 text-body-md" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant hover:text-primary transition-colors py-6 text-body-md" href="#">System Health</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="w-10 h-10 flex items-center justify-center text-on-surface-variant hover:bg-slate-100 rounded-full transition-colors relative">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center text-on-surface-variant hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<img class="w-10 h-10 rounded-full object-cover border-2 border-slate-100" data-alt="A professional headshot of a modern retail merchant in a high-key studio setting. The person is smiling confidently, wearing a clean, smart-casual outfit that reflects a contemporary business aesthetic. The background is a soft, neutral grey, ensuring the focus remains on the individual. The overall mood is approachable, professional, and reliable." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCBijoDwTMABf-Wk9DW7yUPFmhwoQfHS2HZ_4VFWZB3SgjvUR7YX07T0TFbG_ioe_-N-U3FL0zIUMbeiJij_YFx-StBbrnimmm2mvzRUPz3uPEfYoDuoIyJHcMxrj1MJRXN-QYsWB9Rsmfrb4VKGJ3qswyLQlvnTcV0sME6f0PfivL90VtJht40jQIq6HNLgKluzWqCaQsPspsig-A7Wp_tUlVOvAGkMF9vNy7rrsMqoojKIUt9rjJSK9Ai28UUs7pzuHwwdvfX2XQ"/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 pt-[72px] p-page-padding min-h-screen">
|
||||
<!-- Header Section -->
|
||||
<div class="mb-8 flex justify-between items-end">
|
||||
<div>
|
||||
<p class="text-primary font-bold text-label-md uppercase tracking-wider mb-1">Merchant Portal</p>
|
||||
<h2 class="font-headline-lg text-headline-lg">Good morning, John's Coffee</h2>
|
||||
</div>
|
||||
<button class="bg-primary text-white px-6 py-2.5 rounded-xl font-bold flex items-center gap-2 hover:bg-primary-container transition-all active:scale-95 shadow-lg shadow-primary/20">
|
||||
<span class="material-symbols-outlined text-lg" data-icon="add">add</span>
|
||||
Register New Device
|
||||
</button>
|
||||
</div>
|
||||
<!-- KPI Bento Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
|
||||
<!-- GMV Card -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding group hover:border-primary/30 transition-all">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-primary/5 rounded-lg flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
</div>
|
||||
<span class="text-success font-metric-sm text-metric-sm flex items-center">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="trending_up">trending_up</span>
|
||||
12.4%
|
||||
</span>
|
||||
</div>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-tight">Today's GMV</p>
|
||||
<h3 class="font-metric-lg text-metric-lg mt-1">₹42,850.50</h3>
|
||||
<p class="text-[11px] text-slate-400 mt-2">vs. ₹38,120.00 yesterday</p>
|
||||
</div>
|
||||
<!-- Transaction Count -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding group hover:border-primary/30 transition-all">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-info/5 rounded-lg flex items-center justify-center text-info">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
</div>
|
||||
<span class="text-success font-metric-sm text-metric-sm flex items-center">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="trending_up">trending_up</span>
|
||||
8%
|
||||
</span>
|
||||
</div>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-tight">Transaction Count</p>
|
||||
<h3 class="font-metric-lg text-metric-lg mt-1">184</h3>
|
||||
<p class="text-[11px] text-slate-400 mt-2">Avg. Ticket: ₹232.88</p>
|
||||
</div>
|
||||
<!-- Active Soundboxes -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding group hover:border-primary/30 transition-all">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-warning/5 rounded-lg flex items-center justify-center text-warning">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 bg-warning/10 px-2 py-0.5 rounded text-[10px] font-bold text-warning uppercase">
|
||||
<span class="w-1 h-1 rounded-full bg-warning animate-pulse"></span>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-tight">Active Soundboxes</p>
|
||||
<h3 class="font-metric-lg text-metric-lg mt-1">04 <span class="text-slate-300 font-normal">/ 05</span></h3>
|
||||
<p class="text-[11px] text-danger mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-xs" data-icon="error">error</span>
|
||||
1 Device Offline (Main Exit)
|
||||
</p>
|
||||
</div>
|
||||
<!-- Next Settlement -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding group hover:border-primary/30 transition-all">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-10 h-10 bg-success/5 rounded-lg flex items-center justify-center text-success">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
<span class="text-slate-400 font-label-md text-label-md uppercase">Pending</span>
|
||||
</div>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-tight">Next Settlement</p>
|
||||
<h3 class="font-metric-lg text-metric-lg mt-1">₹38,200</h3>
|
||||
<p class="text-[11px] text-slate-500 mt-2 font-bold italic">Scheduled: Oct 24, 06:00 AM</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Secondary Section: Transactions and Devices -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-gutter items-start">
|
||||
<!-- Recent Transactions Table -->
|
||||
<div class="lg:col-span-8 bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="px-6 py-5 border-b border-slate-200 flex justify-between items-center">
|
||||
<h4 class="font-headline-md text-headline-md">Recent Transactions</h4>
|
||||
<a class="text-primary font-bold text-body-md hover:underline" href="#">View All</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-slate-50 sticky top-0 border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase">Transaction ID</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase">Time</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase">Amount</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase">Status</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr class="hover:bg-slate-50 transition-colors group h-[52px]">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md font-bold text-slate-900">#TXN-88421</p>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-tighter">UPI • GPay</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-slate-500">10:24:12 AM</td>
|
||||
<td class="px-6 py-4 text-body-md font-bold text-slate-900 text-right tabular-nums">₹450.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success/10 text-success text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Settled
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="info">info</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group h-[52px]">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md font-bold text-slate-900">#TXN-88419</p>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-tighter">UPI • PhonePe</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-slate-500">10:22:05 AM</td>
|
||||
<td class="px-6 py-4 text-body-md font-bold text-slate-900 text-right tabular-nums">₹1,200.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success/10 text-success text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Settled
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="info">info</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group h-[52px]">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md font-bold text-slate-900">#TXN-88418</p>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-tighter">UPI • Paytm</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-slate-500">10:18:44 AM</td>
|
||||
<td class="px-6 py-4 text-body-md font-bold text-slate-900 text-right tabular-nums">₹85.50</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-warning/10 text-warning text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="info">info</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group h-[52px]">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md font-bold text-slate-900">#TXN-88415</p>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-tighter">UPI • BHIM</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-slate-500">09:55:12 AM</td>
|
||||
<td class="px-6 py-4 text-body-md font-bold text-slate-900 text-right tabular-nums">₹210.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-danger/10 text-danger text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger"></span>
|
||||
Failed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="info">info</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Status Widget -->
|
||||
<div class="lg:col-span-4 flex flex-col gap-gutter">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-card-padding shadow-sm">
|
||||
<h4 class="font-headline-md text-headline-md mb-6">Device Health</h4>
|
||||
<div class="space-y-4">
|
||||
<!-- Device 1 -->
|
||||
<div class="flex items-center justify-between p-3 rounded-lg border border-slate-100 hover:border-primary/20 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-success/10 flex items-center justify-center text-success">
|
||||
<span class="material-symbols-outlined text-lg" data-icon="speaker">speaker</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Main Counter</p>
|
||||
<p class="text-[10px] text-slate-500">SN: SB-2091-AX</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] font-bold text-success uppercase">Online</p>
|
||||
<p class="text-[10px] text-slate-400">88% Batt</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device 2 -->
|
||||
<div class="flex items-center justify-between p-3 rounded-lg border border-slate-100 hover:border-primary/20 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-success/10 flex items-center justify-center text-success">
|
||||
<span class="material-symbols-outlined text-lg" data-icon="speaker">speaker</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Patio Station</p>
|
||||
<p class="text-[10px] text-slate-500">SN: SB-2092-AX</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] font-bold text-success uppercase">Online</p>
|
||||
<p class="text-[10px] text-slate-400">42% Batt</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device 3 (Offline) -->
|
||||
<div class="flex items-center justify-between p-3 rounded-lg border border-danger/10 bg-danger/5 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-danger/10 flex items-center justify-center text-danger">
|
||||
<span class="material-symbols-outlined text-lg" data-icon="speaker_group">speaker_group</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Main Exit</p>
|
||||
<p class="text-[10px] text-slate-500">SN: SB-2104-CZ</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] font-bold text-danger uppercase">Offline</p>
|
||||
<p class="text-[10px] text-slate-400">Low Signal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full mt-6 py-2 border border-slate-200 rounded-lg text-body-md font-bold text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
Run Diagnostics
|
||||
</button>
|
||||
</div>
|
||||
<!-- Banner/Promotion Area -->
|
||||
<div class="relative bg-slate-900 rounded-xl p-card-padding overflow-hidden group">
|
||||
<div class="relative z-10">
|
||||
<span class="bg-primary/20 text-primary-fixed-dim text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded">Special Offer</span>
|
||||
<h5 class="text-white font-headline-md mt-2">Upgrade to Gen-2 Soundboxes</h5>
|
||||
<p class="text-slate-400 text-body-md mt-2 mb-4">Get 20% off and 5G connectivity for better reliability.</p>
|
||||
<button class="bg-white text-slate-900 px-4 py-2 rounded-lg font-bold text-body-md hover:bg-primary-fixed transition-colors">Learn More</button>
|
||||
</div>
|
||||
<!-- Decorative background element -->
|
||||
<div class="absolute -right-4 -bottom-4 w-32 h-32 bg-primary/20 rounded-full blur-3xl group-hover:bg-primary/40 transition-all duration-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Audit/Settlement Micro-widget (Optional Professional Detail) -->
|
||||
<div class="mt-8 bg-surface-container border border-slate-200 rounded-xl p-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-2 bg-white rounded border border-slate-200">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="history_edu">history_edu</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold text-on-surface">Compliance Status</p>
|
||||
<p class="text-[11px] text-slate-500">Your KYC verification is active and valid until Dec 2025.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="px-3 py-1 bg-success/20 text-success text-[11px] font-bold rounded uppercase">Verified</span>
|
||||
<span class="material-symbols-outlined text-slate-400 cursor-pointer hover:text-on-surface transition-colors" data-icon="more_vert">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
// Simple micro-interactions for the merchant portal
|
||||
document.querySelectorAll('button, a').forEach(elem => {
|
||||
elem.addEventListener('mousedown', () => {
|
||||
elem.style.transform = 'scale(0.97)';
|
||||
});
|
||||
elem.addEventListener('mouseup', () => {
|
||||
elem.style.transform = 'scale(1)';
|
||||
});
|
||||
elem.addEventListener('mouseleave', () => {
|
||||
elem.style.transform = 'scale(1)';
|
||||
});
|
||||
});
|
||||
|
||||
// Search Bar Focus Effect
|
||||
const searchInput = document.querySelector('input[type="text"]');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('focus', () => {
|
||||
searchInput.parentElement.classList.add('ring-2', 'ring-primary/20');
|
||||
});
|
||||
searchInput.addEventListener('blur', () => {
|
||||
searchInput.parentElement.classList.remove('ring-2', 'ring-primary/20');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/merchant_dashboard_portal/screen.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
427
design/merchant_detail_view/code.html
Normal file
@ -0,0 +1,427 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Detail | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Plus+Jakarta+Sans:wght@600;700;800&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind Config -->
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"warning": "#F59E0B",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface": "#faf8ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"outline": "#737686",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"info": "#0EA5E9",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-bright": "#faf8ff",
|
||||
"surface-container": "#ededf9",
|
||||
"error-container": "#ffdad6",
|
||||
"slate-900": "#0F172A",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-200": "#E2E8F0",
|
||||
"on-background": "#191b23",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"danger": "#DC2626",
|
||||
"on-primary-container": "#eeefff",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-surface-variant": "#434655",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"primary-container": "#2563eb",
|
||||
"background": "#F8FAFC",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-secondary-container": "#54647a",
|
||||
"slate-100": "#F1F5F9"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"gutter": "24px",
|
||||
"topbar-height": "72px",
|
||||
"card-padding": "20px",
|
||||
"row-height": "52px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"metric-sm": ["Inter"],
|
||||
"mono": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background font-body-md text-on-surface min-h-screen">
|
||||
<!-- Side Navigation Shell -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-body-md">Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">storefront</span>
|
||||
<span class="font-body-md">Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">speaker_group</span>
|
||||
<span class="font-body-md">Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">receipt_long</span>
|
||||
<span class="font-body-md">Transactions</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">account_balance</span>
|
||||
<span class="font-body-md">Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">history_edu</span>
|
||||
<span class="font-body-md">Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="pt-4 border-t border-slate-100 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-body-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="font-body-md">Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top Navigation Shell -->
|
||||
<header class="fixed top-0 right-0 h-[72px] flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding bg-surface-container-lowest border-b border-slate-200 z-40">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<div class="relative w-full max-w-md">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border-none rounded-full text-body-md focus:ring-2 focus:ring-primary/20" placeholder="Search merchants, devices, or IDs..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-4 text-on-surface-variant">
|
||||
<button class="hover:text-primary transition-colors"><span class="material-symbols-outlined">notifications</span></button>
|
||||
<button class="hover:text-primary transition-colors"><span class="material-symbols-outlined">calendar_today</span></button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 pl-6 border-l border-slate-200">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-on-surface font-bold">Admin User</p>
|
||||
<p class="text-[10px] text-slate-500 uppercase tracking-wider">Super Administrator</p>
|
||||
</div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAuZk5-C2Lq7_jeFw6ridlBQguJ_tmIKvNn7XY2MxpyRmXSuNPt2noIVsdmRItBwIrvUXnUoV1Qf9aJ3Rfe3cTHCc7qSIwqfRL7Ax-d4stkwZ93ihHPTlEd9Ig1FzJdxjGVfS5K5n7JexPl5KxZDBlzFH70iUKrqSuSx4KtxKHi1HkkLaLVhy3-VeXwJh6z_WA5WSV5tqUStG2CyEkHlqHbjiWb4vfZQ2MMlilUHlLjahS4htwQE_aZ6lu7FGhSo8wmWS4-6m6dtsg"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="ml-64 pt-[72px] min-h-screen p-page-padding">
|
||||
<!-- Breadcrumbs & Header -->
|
||||
<div class="mb-8">
|
||||
<nav class="flex items-center gap-2 text-slate-500 mb-2">
|
||||
<a class="font-label-md hover:text-primary" href="#">Merchant Management</a>
|
||||
<span class="material-symbols-outlined text-sm">chevron_right</span>
|
||||
<span class="font-label-md text-on-surface font-bold">Kopi Kenangan - GI Mall</span>
|
||||
</nav>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-2">Kopi Kenangan - GI Mall</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-3 py-1 bg-success/10 text-success rounded-full text-label-md font-bold flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 bg-success rounded-full"></span> Active
|
||||
</span>
|
||||
<span class="px-3 py-1 bg-primary/10 text-primary rounded-full text-label-md font-bold">
|
||||
Fee Profile: Flat 0.7%
|
||||
</span>
|
||||
<span class="text-slate-400 text-body-md">ID: MID-98234-KK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="px-4 py-2 bg-surface-container-lowest border border-slate-200 text-on-surface font-bold rounded-lg hover:bg-slate-50 transition-colors flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">edit</span> Edit
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-danger text-white font-bold rounded-lg hover:opacity-90 transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">block</span> Suspend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Summary Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-gutter mb-8">
|
||||
<div class="bg-surface-container-lowest p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">GMV Today</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<p class="font-metric-lg text-metric-lg text-on-surface">Rp12.500.000</p>
|
||||
<p class="font-metric-sm text-metric-sm text-success">+12.4%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Active Devices</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<p class="font-metric-lg text-metric-lg text-on-surface">4</p>
|
||||
<p class="font-metric-sm text-metric-sm text-slate-400">/ 5 Registered</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Success Rate</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<p class="font-metric-lg text-metric-lg text-on-surface">100%</p>
|
||||
<p class="font-metric-sm text-metric-sm text-success">Optimal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="flex items-center border-b border-slate-200 mb-8 overflow-x-auto no-scrollbar">
|
||||
<button class="px-6 py-4 text-primary font-bold border-b-2 border-primary whitespace-nowrap">Profile</button>
|
||||
<button class="px-6 py-4 text-slate-500 font-medium hover:text-on-surface whitespace-nowrap transition-colors">Outlets</button>
|
||||
<button class="px-6 py-4 text-slate-500 font-medium hover:text-on-surface whitespace-nowrap transition-colors">Devices</button>
|
||||
<button class="px-6 py-4 text-slate-500 font-medium hover:text-on-surface whitespace-nowrap transition-colors">Transactions</button>
|
||||
<button class="px-6 py-4 text-slate-500 font-medium hover:text-on-surface whitespace-nowrap transition-colors">Settlements</button>
|
||||
</div>
|
||||
<!-- Profile Content Grid (Bento Style) -->
|
||||
<div class="grid grid-cols-12 gap-6 mb-8">
|
||||
<!-- Business Info Card -->
|
||||
<div class="col-span-12 lg:col-span-8 space-y-6">
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
||||
<h3 class="font-headline-md text-[16px] text-on-surface">Business Information</h3>
|
||||
</div>
|
||||
<div class="p-6 grid grid-cols-2 gap-y-6 gap-x-12">
|
||||
<div>
|
||||
<p class="font-label-md text-slate-500 mb-1">Legal Entity Name</p>
|
||||
<p class="font-body-lg text-on-surface">PT Bumi Berkah Boga</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-slate-500 mb-1">Tax ID (NPWP)</p>
|
||||
<p class="font-body-lg text-on-surface font-mono">01.234.567.8-012.000</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="font-label-md text-slate-500 mb-1">Registered Business Address</p>
|
||||
<p class="font-body-lg text-on-surface">Grand Indonesia, East Mall, Level LG. Jl. M.H. Thamrin No.1, Jakarta Pusat, 10310</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-slate-500 mb-1">Business Category</p>
|
||||
<p class="font-body-lg text-on-surface">Food & Beverage (Coffee Shop)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-slate-500 mb-1">Onboarding Date</p>
|
||||
<p class="font-body-lg text-on-surface">Oct 12, 2023</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Settlement Info -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
||||
<h3 class="font-headline-md text-[16px] text-on-surface">Settlement Bank Account</h3>
|
||||
</div>
|
||||
<div class="p-6 flex items-center gap-6">
|
||||
<div class="w-16 h-16 bg-slate-100 rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-3xl text-slate-400">account_balance</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-body-lg text-on-surface font-bold">Bank Central Asia (BCA)</p>
|
||||
<p class="font-body-md text-slate-500">Account Number: <span class="text-on-surface font-mono">5020129481</span></p>
|
||||
<p class="font-body-md text-slate-500">Beneficiary: PT Bumi Berkah Boga</p>
|
||||
</div>
|
||||
<div class="px-3 py-1 bg-success/10 text-success rounded-lg text-label-md font-bold">
|
||||
Verified
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Side Cards: PIC & Location -->
|
||||
<div class="col-span-12 lg:col-span-4 space-y-6">
|
||||
<!-- PIC Contact Details -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
||||
<h3 class="font-headline-md text-[16px] text-on-surface">PIC Contact</h3>
|
||||
<span class="material-symbols-outlined text-slate-400 text-[20px]">contact_page</span>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold">JD</div>
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">John Doe</p>
|
||||
<p class="text-label-md text-slate-500">Operations Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 pt-2">
|
||||
<div class="flex items-center gap-3 text-body-md">
|
||||
<span class="material-symbols-outlined text-slate-400 text-[18px]">mail</span>
|
||||
<span class="text-on-surface">j.doe@kopikenangan.id</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-body-md">
|
||||
<span class="material-symbols-outlined text-slate-400 text-[18px]">phone</span>
|
||||
<span class="text-on-surface">+62 812-3456-7890</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Location Map Placeholder -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="h-48 bg-slate-200 relative">
|
||||
<img class="w-full h-full object-cover" data-alt="A stylized satellite view map showing a commercial district in Jakarta with a central pin pointing at a large shopping mall. The map colors are muted in shades of light gray and slate blue to match the corporate dashboard aesthetic. Sharp grid lines and minimal labels highlight the precise geolocation of the merchant." data-location="Jakarta" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDlt6GRr2iaPzTt0VsXntnT_1W2IOM8IHEZqrYEDlV4GK-AkuB97tjXHR0isXd1l9OXIaGTH_NvCxO14JYmB7WrO-ib0Cv4nAm7JhfvQFVqsrzqKHoof1c9Evj_XmyQ6pN3JMVEWCQtHWz2uuTscr3okdX99WpAgfHc-agtwflekGD4CxCKV17fjkdqn8JDr0FspXOsFSTblVfZviPFIjDf7SLdXjzpmKt-aMbgAwnK3lmpH6UsiodcYFG3o4Xemln28bhUDBvs8G8"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
</div>
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<p class="text-label-md text-slate-500">GPS: -6.1951, 106.8231</p>
|
||||
<button class="text-primary font-bold text-label-md flex items-center gap-1">
|
||||
Open in Maps <span class="material-symbols-outlined text-sm">open_in_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Audit History Block -->
|
||||
<div class="mt-12 bg-slate-900 rounded-xl overflow-hidden shadow-xl">
|
||||
<div class="px-6 py-4 border-b border-slate-800 flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-slate-400">terminal</span>
|
||||
<h3 class="text-white font-headline-md text-[14px] uppercase tracking-widest">Audit Logs & Raw Payloads</h3>
|
||||
</div>
|
||||
<button class="text-slate-400 hover:text-white transition-colors flex items-center gap-1 text-label-md">
|
||||
<span class="material-symbols-outlined text-[18px]">content_copy</span> Copy Log
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 font-mono text-[13px] leading-relaxed text-slate-300 max-h-64 overflow-y-auto custom-scrollbar">
|
||||
<div class="mb-3">
|
||||
<span class="text-slate-500">[2023-11-24 14:22:01]</span>
|
||||
<span class="text-primary-fixed">INFO:</span>
|
||||
Merchant record <span class="text-warning">MID-98234-KK</span> status changed from <span class="text-info">PENDING</span> to <span class="text-success">ACTIVE</span> by user <span class="font-bold">Admin_S7</span>.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="text-slate-500">[2023-11-24 10:15:44]</span>
|
||||
<span class="text-primary-fixed">DEBUG:</span>
|
||||
Bank verification payload received for BCA-5020129481. Response: {"status":"verified","match_score":1.0,"ref_id":"VB-9832"}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="text-slate-500">[2023-11-23 09:05:12]</span>
|
||||
<span class="text-primary-fixed">INFO:</span>
|
||||
Device provisioning started for Soundbox_SN:9877234. Associated to MID-98234-KK.
|
||||
</div>
|
||||
<div class="mb-3 opacity-60">
|
||||
<span class="text-slate-500">[2023-11-22 16:45:00]</span>
|
||||
<span class="text-primary-fixed">TRACE:</span>
|
||||
Internal system audit triggered. No anomalies detected for merchant cluster.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Interactive Layer: Notification Toast (Hidden by default) -->
|
||||
<div class="fixed bottom-8 right-8 bg-surface-container-highest border border-slate-200 shadow-2xl rounded-xl p-4 translate-y-24 opacity-0 transition-all duration-300 z-[100] flex items-center gap-4" id="toast">
|
||||
<div class="w-10 h-10 bg-success rounded-full flex items-center justify-center text-white">
|
||||
<span class="material-symbols-outlined">check_circle</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Update Successful</p>
|
||||
<p class="text-body-md text-slate-500">Merchant details have been synced.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Micro-interaction for buttons
|
||||
document.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('mousedown', () => {
|
||||
btn.style.transform = 'scale(0.96)';
|
||||
btn.style.opacity = '0.9';
|
||||
});
|
||||
btn.addEventListener('mouseup', () => {
|
||||
btn.style.transform = 'scale(1)';
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
btn.style.transform = 'scale(1)';
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
|
||||
// Simple Toast demo
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.classList.remove('translate-y-24', 'opacity-0');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-y-24', 'opacity-0');
|
||||
}, 5000);
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/merchant_detail_view/screen.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
539
design/merchant_list_management/code.html
Normal file
@ -0,0 +1,539 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Management | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"warning": "#F59E0B",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface": "#faf8ff",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"outline": "#737686",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"info": "#0EA5E9",
|
||||
"slate-500": "#64748B",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-bright": "#faf8ff",
|
||||
"surface-container": "#ededf9",
|
||||
"error-container": "#ffdad6",
|
||||
"slate-900": "#0F172A",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"slate-700": "#334155",
|
||||
"slate-200": "#E2E8F0",
|
||||
"on-background": "#191b23",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"danger": "#DC2626",
|
||||
"on-primary-container": "#eeefff",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-surface-variant": "#434655",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"primary-container": "#2563eb",
|
||||
"background": "#F8FAFC",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-secondary-container": "#54647a",
|
||||
"slate-100": "#F1F5F9"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"gutter": "24px",
|
||||
"topbar-height": "72px",
|
||||
"card-padding": "20px",
|
||||
"row-height": "52px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.data-table-container::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
.data-table-container::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.data-table-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-md antialiased min-h-screen">
|
||||
<!-- Side Navigation Bar -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="flex items-center gap-3 px-2 mb-8">
|
||||
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center text-on-primary">
|
||||
<span class="material-symbols-outlined">storefront</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">Admin Console</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-1 flex-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span class="font-body-md">Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
<span class="font-body-md">Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span class="font-body-md">Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span class="font-body-md">Transactions</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
<span class="font-body-md">Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
<span class="font-body-md">Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto flex flex-col gap-1 border-t border-slate-100 pt-4">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span class="font-body-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span class="font-body-md">Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] bg-surface-container-lowest border-b border-slate-200 flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding z-40">
|
||||
<div class="flex items-center flex-1 max-w-xl">
|
||||
<div class="relative w-full">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border-none rounded-full focus:ring-2 focus:ring-primary/20 font-body-md" placeholder="Search system resources..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<div class="flex items-center gap-3 pl-2">
|
||||
<div class="text-right">
|
||||
<p class="font-body-md font-semibold text-on-surface">Alex Rivera</p>
|
||||
<p class="font-label-md text-on-surface-variant">Senior Administrator</p>
|
||||
</div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full border border-slate-200" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAtHGoIBcar94rMuPWVlFHcN-UeUZqFQEA_6n8Bwewxqe80TjDQjWPaBTLNsg4S-gBxFpcIY0xWawaDEJfZ2xfpmPL1CW1eV38Mi5CsTPYygJfh2wHlBGu8oJ-K1525FjWYMBigTWZNn4wbyTlVVEB1EonSgphK2PUCtOH3uee5wkDF6kKIWDWjSZVel6TAmtuSf2py8PmIFK00Rrt01xUOMAzo1pEfLvQkG7DJ4eyAJJhWYBfqcpgWEAXGgBxRcyTuKtt9o6ETfHo"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 mt-[72px] p-page-padding">
|
||||
<!-- Page Header -->
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h2 class="font-display-lg text-display-lg text-on-surface">Merchant Management</h2>
|
||||
<p class="font-body-lg text-on-surface-variant mt-1">Oversee and manage the complete merchant lifecycle and compliance.</p>
|
||||
</div>
|
||||
<button class="bg-primary text-on-primary px-6 py-3 rounded-xl flex items-center gap-2 hover:opacity-90 active:scale-95 transition-all font-semibold">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
Register New Device
|
||||
</button>
|
||||
</div>
|
||||
<!-- Filter Bar -->
|
||||
<section class="bg-surface-container-lowest border border-slate-200 rounded-xl p-4 mb-6 flex flex-wrap items-center gap-4">
|
||||
<div class="flex-1 min-w-[240px] relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-body-md focus:ring-primary focus:border-primary" placeholder="Search by Merchant Name or ID..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<select class="border border-slate-200 rounded-lg py-2 pl-3 pr-8 text-body-md focus:ring-primary bg-white">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
<select class="border border-slate-200 rounded-lg py-2 pl-3 pr-8 text-body-md focus:ring-primary bg-white">
|
||||
<option value="">All Categories</option>
|
||||
<option value="fb">F&B</option>
|
||||
<option value="retail">Retail</option>
|
||||
<option value="services">Services</option>
|
||||
<option value="ecommerce">E-commerce</option>
|
||||
</select>
|
||||
<button class="flex items-center gap-2 border border-slate-200 rounded-lg py-2 px-4 text-on-surface hover:bg-slate-50 transition-colors font-semibold">
|
||||
<span class="material-symbols-outlined text-[20px]">download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Data Table Section -->
|
||||
<section class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div class="data-table-container overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Merchant Name</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Category</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Outlets</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Devices</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider">Last Transaction</th>
|
||||
<th class="px-6 py-4 font-semibold text-on-surface-variant text-label-md uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Merchant Row 1 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4 font-mono text-label-md text-on-surface-variant">MCH-88291</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-secondary-container flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A clean, minimalist logo for a coffee shop featuring a stylized steaming cup in a modern flat design. The background is a soft blue-grey, matching a fintech dashboard palette of reliable and professional tones." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBKfBUyrfZro5QVTE5ZCHKN39soarIHfzQFLprT4Y2uPMBLW-u0mWpegdCXdvKCnYE6rA0McX6GvMfR2Qyzz9WxW2ETHywyFDNqBBYo7kteOxuMIku9etvuLDx4nyLz7sPFnpbuUQY6ollY6OKKNJgmDya8_2YQI2ySdqQNNvOYFVoTH53wN4195W4pASxMCG0NnVAxwDIeX8CRvzXwhP8JcEkgJRLA2w5s_y8qaVqUqy5mjRmmn4HpjBgrAauWCTI_R0sjJzv7_JE"/>
|
||||
</div>
|
||||
<span class="font-semibold text-on-surface">Artisan Brew Co.</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-body-md text-on-surface-variant">F&B</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">12</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">45</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success">
|
||||
ACTIVE
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-on-surface-variant">Oct 24, 2023 14:22</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Merchant Row 2 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4 font-mono text-label-md text-on-surface-variant">MCH-44120</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-secondary-container flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A modern apparel brand logo featuring a stylized hanger icon in a sleek, minimalist geometric style. The color palette is professional slate and crisp white, designed for a corporate admin interface with high density and clarity." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBkmTU4JFWXV3Rit6K4fgcF7sGDXBXpntQ9fJsDAuahc9gDUe_HWHc46Zm-fTkbjd1mJHmhJsBxi2GyMqCZz8Y3gz1NY3ek5nJPgG5sPGM69X1d2u-UPRvogRgxCEnZly8s6rPGm_70NeYIPCo7KH-YMPM32yoPeWT5QSP1ihFq3Sn6L3zBJHrOEcYjMsBri9Nzr-NN505R9GSh4egxWehgbngkD60ylylOhcub3XmyFOndD8XBtQ0cKo-qZ96o-uZLqiRxtMmIR74"/>
|
||||
</div>
|
||||
<span class="font-semibold text-on-surface">Urban Threads</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-body-md text-on-surface-variant">Retail</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">04</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">08</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-danger/10 text-danger">
|
||||
INACTIVE
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-on-surface-variant">Oct 20, 2023 09:15</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Merchant Row 3 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4 font-mono text-label-md text-on-surface-variant">MCH-99012</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-secondary-container flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A logistics company logo showcasing a minimalist box icon with speed lines, rendered in professional navy and bright accent blue. The aesthetic is sharp, functional, and aligns with a high-performance data-centric operational dashboard." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAteFl10D32AZL0y8tuXOSnmnJ4tc58HNOXD2K2ImTWo4Nxsv5Np3ef99cG4E_PTxixQsDi96uykUjsg6p35VZwY5inZMRf_7Q4HBEScyHyI_TrnL4J6DymxcbidH77BRXV9y5ucOmUs0kQhGFh1cHiX4g_MXECvyykX97zVnauBJrMUr_4mPNQ9Fwemlim6s0_wcRjRajP-oQdreC4Nhi_F9_xsLPO0mbsEr7KX7Q1KGxy6xrNjIWTjTCo6HGpJxMK_tILcriVUN0"/>
|
||||
</div>
|
||||
<span class="font-semibold text-on-surface">Swift Logistics Ltd.</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-body-md text-on-surface-variant">Services</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">01</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">02</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-warning/10 text-warning">
|
||||
PENDING
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-on-surface-variant">--</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Merchant Row 4 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4 font-mono text-label-md text-on-surface-variant">MCH-23841</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-secondary-container flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A fresh produce market logo with a geometric broccoli icon in a vibrant yet professional green tone. The design is minimalist and high-contrast, optimized for rapid scanning within a dense corporate data table." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCCP8RJqNWQLE7Z-BIb3KRTDckaIjT7YjbE0OHl5fOFC561xfQf2IRDRzGal47AQb6QlrZ_p-rIZIXjfjcVvkE3zJe3RCTD5C0cen6M1F84bpKjfWO0-Mhj0pnZ_iLnTnSO4Wj_pdqwuiawXy9zIBD4y0UvnmXGvSKhWp9_FzHq781IUPGe7hWDtuWR8VoOampvdGw3fuEKLRIMXUJ1naRM5Vpv4BWX35JHkls9AUzwbdszHWMMeKkJHVGBwObTO8Ur_c-5NHJkpEo"/>
|
||||
</div>
|
||||
<span class="font-semibold text-on-surface">Green Garden Market</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-body-md text-on-surface-variant">Retail</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">25</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">110</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success">
|
||||
ACTIVE
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-on-surface-variant">Oct 24, 2023 16:45</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Merchant Row 5 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4 font-mono text-label-md text-on-surface-variant">MCH-11005</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-secondary-container flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A digital agency logo representing a minimalist smartphone silhouette with a small spark icon, using a sophisticated dark blue and slate palette. The style is clean, corporate, and focuses on professional reliability for fintech operations." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBBrKpwIAJIli0LBrGqV9D3Le2guSH2NHq8Y0cFb5jy1MWJTrTK3U0azfF4cTFBjxh90cdUL_RlsOjw2z77hcHmSoncAjOx90fFCnh11YbFnxHI_oDx2xd-rv3a0i9wpXXz7Jk5pWXnfc9jbEJvopMpXSwkHN8h475CjZ2UyJv3YhyLj0at7lVYDhnXKdcKXEPIGe3wUArGxiSmHlroSJYXJFglfswomZouovWyB_LAyTqXclNCCId3aIsOvdcZu0yAr0VxMDoyXpk"/>
|
||||
</div>
|
||||
<span class="font-semibold text-on-surface">Pixel Perfect Media</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-body-md text-on-surface-variant">E-commerce</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">02</td>
|
||||
<td class="px-6 py-4 font-mono text-body-md">02</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success">
|
||||
ACTIVE
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-body-md text-on-surface-variant">Oct 23, 2023 11:30</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 bg-white border-t border-slate-200 flex items-center justify-between">
|
||||
<p class="font-label-md text-on-surface-variant">
|
||||
Showing <span class="font-bold text-on-surface">1 - 5</span> of <span class="font-bold text-on-surface">124</span> merchants
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 text-slate-400 hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined text-[20px]">chevron_left</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded bg-primary text-on-primary text-label-md font-bold">1</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded hover:bg-slate-50 text-on-surface-variant text-label-md">2</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded hover:bg-slate-50 text-on-surface-variant text-label-md">3</button>
|
||||
<span class="text-slate-300">...</span>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded hover:bg-slate-50 text-on-surface-variant text-label-md">25</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 text-on-surface-variant hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-[20px]">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- KPI Cards Grid -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-4 gap-gutter mt-8">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Total Merchants</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">1,248</h3>
|
||||
<div class="flex items-center gap-1 mt-2 text-success font-metric-sm">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
<span>+12.5%</span>
|
||||
<span class="text-on-surface-variant ml-1">vs last month</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Active Devices</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">4,812</h3>
|
||||
<div class="flex items-center gap-1 mt-2 text-success font-metric-sm">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
<span>+4.2%</span>
|
||||
<span class="text-on-surface-variant ml-1">vs last month</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Pending KYM</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">24</h3>
|
||||
<div class="flex items-center gap-1 mt-2 text-warning font-metric-sm">
|
||||
<span class="material-symbols-outlined text-[16px]">priority_high</span>
|
||||
<span>Needs attention</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl shadow-sm">
|
||||
<p class="font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Total Volume</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-surface">$2.4M</h3>
|
||||
<div class="flex items-center gap-1 mt-2 text-success font-metric-sm">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
<span>+18.4%</span>
|
||||
<span class="text-on-surface-variant ml-1">vs last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- Detail Drawer Placeholder (Hidden by default) -->
|
||||
<div class="fixed top-0 right-0 h-full w-[480px] bg-white shadow-2xl z-[60] transform translate-x-full transition-transform duration-300 border-l border-slate-200" id="detail-drawer">
|
||||
<div class="p-6 h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="font-headline-lg text-headline-lg text-on-surface">Merchant Details</h3>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors" onclick="closeDrawer()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- Content will be injected here -->
|
||||
<div class="bg-slate-50 p-4 rounded-xl mb-6 flex items-center gap-4">
|
||||
<div class="w-16 h-16 rounded-xl bg-white border border-slate-200 flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="High-resolution, minimalist corporate logo mockup on a clean white background. Soft studio lighting creates a premium, modern tech aesthetic suitable for a detailed admin inspection panel." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCeTlL7iBlYwrDbVDHtYvDcce0uIyVd-9P6WnjYxruB94Q8dMXWwAFs6mv9sPK6Cf15cEm0dgG2nMeqBTXk0k0h6eh4laSjli21qrfsu8D2tT5M9oPy-Xonc1EgsgM24YUJl23Dydu2rmxDdoctzefQTtQm4EzUC4o2dBUQSRHp5KhgtL3qj-gegguvtEjH2cHIUKf40ZK9ew73QnvWr7fFYquaSAgMHt1vZgN6Ya9LJah4eRUlmklnZ_NeqxIPQN_AWOfvPCwQqo0"/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-headline-md text-on-surface">Artisan Brew Co.</h4>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-success/10 text-success">ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface-variant uppercase">Merchant ID</p>
|
||||
<p class="font-body-md font-semibold text-on-surface">MCH-88291</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface-variant uppercase">Tax ID</p>
|
||||
<p class="font-body-md font-semibold text-on-surface">TX-992-001</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface-variant uppercase mb-2">Operational Health</p>
|
||||
<div class="w-full bg-slate-100 rounded-full h-2">
|
||||
<div class="bg-success h-2 rounded-full w-[94%]"></div>
|
||||
</div>
|
||||
<p class="text-right text-xs mt-1 font-semibold text-success">94% Positive Uptime</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-on-surface-variant uppercase mb-3">Recent System Audit</p>
|
||||
<div class="bg-slate-900 rounded-lg p-4 font-mono text-xs text-slate-300 relative">
|
||||
<button class="absolute top-2 right-2 text-slate-500 hover:text-white">
|
||||
<span class="material-symbols-outlined text-[18px]">content_copy</span>
|
||||
</button>
|
||||
<pre>{
|
||||
"event": "SETTLEMENT_BATCH_CLOSE",
|
||||
"batch_id": "BT-7721",
|
||||
"total": 4500.22,
|
||||
"status": "SUCCESS",
|
||||
"timestamp": "2023-10-24T14:22:11Z"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto pt-6 border-t border-slate-200 grid grid-cols-2 gap-4">
|
||||
<button class="py-3 border border-slate-200 rounded-xl font-semibold text-on-surface hover:bg-slate-50 transition-all">Suspend Account</button>
|
||||
<button class="py-3 bg-primary text-on-primary rounded-xl font-semibold hover:opacity-90 transition-all">Edit Profiles</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Interaction Script -->
|
||||
<script>
|
||||
function openDrawer() {
|
||||
document.getElementById('detail-drawer').classList.remove('translate-x-full');
|
||||
}
|
||||
function closeDrawer() {
|
||||
document.getElementById('detail-drawer').classList.add('translate-x-full');
|
||||
}
|
||||
|
||||
// Add event listeners to table rows to simulate drawer opening
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', (e) => {
|
||||
if (e.target.closest('button')) return; // Don't trigger on action button
|
||||
openDrawer();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/merchant_list_management/screen.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
277
design/merchant_login_portal/code.html
Normal file
@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="id"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops - Merchant Login</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind Configuration -->
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-surface": "#191b23",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-primary": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"on-background": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"on-secondary-container": "#54647a",
|
||||
"secondary": "#505f76",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"danger": "#DC2626",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"outline": "#737686",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"slate-100": "#F1F5F9",
|
||||
"error-container": "#ffdad6",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"warning": "#F59E0B",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"info": "#0EA5E9",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-error": "#ffffff",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"metric-sm": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"metric-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.login-gradient {
|
||||
background: radial-gradient(circle at 10% 20%, rgba(37, 99, 235, 0.05) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background font-body-md text-on-surface selection:bg-primary-fixed selection:text-on-primary-fixed">
|
||||
<div class="min-h-screen flex flex-col md:flex-row">
|
||||
<!-- Left Side: Login Form -->
|
||||
<main class="w-full md:w-1/2 lg:w-2/5 flex flex-col justify-center items-center p-8 md:p-12 lg:p-24 bg-surface login-gradient relative overflow-hidden">
|
||||
<!-- Decorative Background Element -->
|
||||
<div class="absolute -top-24 -left-24 w-64 h-64 bg-primary-fixed opacity-10 rounded-full blur-3xl"></div>
|
||||
<div class="w-full max-w-md z-10">
|
||||
<!-- Brand Section -->
|
||||
<div class="mb-12 flex flex-col items-center md:items-start">
|
||||
<img alt="Soundbox Ops Logo" class="h-16 w-auto mb-6" src="https://lh3.googleusercontent.com/aida/ADBb0ug9HaB2AriUP4nVrKMOARoZTCm9CjAvMRfjT5wYuZ28c5NkPI6L46e_UFThUQPqPv-K4M6u1kyZquW5BDLZyRJSF9YLnwi_mI5uLm2wJEBGLDxQHiD_V9fmLgPaggsISesqP8Emgu71sqr05ARxSf4H2YOhq5u6sFUSvOIIL82WsxLajMZ-nih3D86dJlFa-trY0J3Rj5JyOW9El0dy9BF4-npXWTYUDjwfMVDeEGMw93XUNIHdxQmPkAg"/>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface tracking-tight mb-2">Merchant Control Center</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">Selamat datang kembali. Silakan masuk untuk mengelola transaksi Anda.</p>
|
||||
</div>
|
||||
<!-- Login Form -->
|
||||
<form class="space-y-6" onsubmit="event.preventDefault();">
|
||||
<div>
|
||||
<label class="block font-label-md text-label-md text-on-surface-variant mb-2 ml-1" for="email">Email Bisnis</label>
|
||||
<div class="relative group">
|
||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline group-focus-within:text-primary transition-colors">mail</span>
|
||||
<input class="w-full pl-12 pr-4 py-3.5 bg-surface-container-lowest border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-500 font-body-md text-on-surface" id="email" name="email" placeholder="nama@bisnisanda.com" type="email"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant ml-1" for="password">Kata Sandi</label>
|
||||
<a class="font-label-md text-label-md text-primary hover:underline transition-all" href="#">Lupa Kata Sandi?</a>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline group-focus-within:text-primary transition-colors">lock</span>
|
||||
<input class="w-full pl-12 pr-12 py-3.5 bg-surface-container-lowest border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-500 font-body-md text-on-surface" id="password" name="password" placeholder="••••••••" type="password"/>
|
||||
<button class="absolute right-4 top-1/2 -translate-y-1/2 text-outline hover:text-on-surface transition-colors" type="button">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input class="w-4 h-4 text-primary bg-surface-container-lowest border-slate-200 rounded focus:ring-primary" id="remember" type="checkbox"/>
|
||||
<label class="ml-2 font-body-md text-body-md text-on-surface-variant" for="remember">Ingat saya selama 30 hari</label>
|
||||
</div>
|
||||
<button class="w-full py-4 bg-primary hover:bg-surface-tint text-on-primary font-headline-md text-headline-md rounded-xl shadow-lg shadow-primary/20 active:scale-[0.98] transition-all flex justify-center items-center gap-2" type="submit">
|
||||
<span>Merchant Sign In</span>
|
||||
<span class="material-symbols-outlined">login</span>
|
||||
</button>
|
||||
</form>
|
||||
<!-- Footer Link -->
|
||||
<div class="mt-12 text-center">
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">
|
||||
Belum memiliki akun merchant?
|
||||
<a class="font-metric-sm text-metric-sm text-primary font-bold hover:underline underline-offset-4 decoration-2 transition-all" href="#">Register as Merchant</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Right Side: Illustrative Section -->
|
||||
<aside class="hidden md:flex md:w-1/2 lg:w-3/5 bg-slate-900 relative overflow-hidden items-center justify-center p-12 lg:p-24">
|
||||
<!-- Dynamic Background -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<div class="absolute top-0 right-0 w-[800px] h-[800px] bg-primary opacity-20 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div class="absolute bottom-0 left-0 w-[600px] h-[600px] bg-tertiary opacity-10 rounded-full blur-[100px] translate-y-1/2 -translate-x-1/4"></div>
|
||||
<div class="absolute inset-0 bg-[radial-gradient(#ffffff08_1px,transparent_1px)] [background-size:32px_32px]"></div>
|
||||
</div>
|
||||
<!-- Content Overlay -->
|
||||
<div class="relative z-10 w-full max-w-2xl">
|
||||
<!-- Bento Grid for Merchant Highlights -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="glass-card p-8 rounded-3xl col-span-2 md:col-span-1 transform -rotate-2 hover:rotate-0 transition-transform duration-500">
|
||||
<div class="w-12 h-12 rounded-2xl bg-primary-fixed flex items-center justify-center mb-6">
|
||||
<span class="material-symbols-outlined text-on-primary-fixed-variant" style="font-variation-settings: 'FILL' 1;">speed</span>
|
||||
</div>
|
||||
<h3 class="font-headline-md text-headline-md text-slate-900 mb-2">Settlement Instan</h3>
|
||||
<p class="font-body-md text-body-md text-slate-700">Dapatkan dana Anda langsung ke rekening segera setelah transaksi berhasil dilakukan.</p>
|
||||
</div>
|
||||
<div class="glass-card p-8 rounded-3xl col-span-2 md:col-span-1 translate-y-8 transform rotate-1 hover:rotate-0 transition-transform duration-500">
|
||||
<div class="w-12 h-12 rounded-2xl bg-secondary-fixed flex items-center justify-center mb-6">
|
||||
<span class="material-symbols-outlined text-on-secondary-fixed-variant" style="font-variation-settings: 'FILL' 1;">volume_up</span>
|
||||
</div>
|
||||
<h3 class="font-headline-md text-headline-md text-slate-900 mb-2">Suara Konfirmasi</h3>
|
||||
<p class="font-body-md text-body-md text-slate-700">Konfirmasi pembayaran melalui suara soundbox yang keras dan jernih, meminimalkan penipuan.</p>
|
||||
</div>
|
||||
<div class="glass-card p-8 rounded-3xl col-span-2 mt-12 bg-white/90 border-slate-100 shadow-xl">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-headline-md text-headline-md text-slate-900 mb-1">Modernisasi Bisnis Anda</h3>
|
||||
<p class="font-body-md text-body-md text-slate-600">Terpercaya oleh lebih dari 50,000 merchant di seluruh Indonesia untuk operasional harian yang lebih lancar.</p>
|
||||
</div>
|
||||
<div class="flex -space-x-4">
|
||||
<div class="w-12 h-12 rounded-full border-2 border-white bg-slate-200 overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A professional headshot of a smiling Indonesian business owner wearing a clean, modern shirt. The lighting is bright and professional, suggesting a high-end corporate or retail environment. The overall mood is confident and trustworthy, reflecting the brand's reliability." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDtE4L8mp1mkgI8dLB8Oq5CKyLD5Vzd4mvv7GzGRvSbfbRrWVgBSclHQY36Jzr9qP-mNw6IhIjrj_6yxMGSD3giFGHN0Pkte4riD4pLEjpLlgFyifKZ2hzvPfU7GAbLA5HzFSawA0AY6tyChhJGbrodOCTtJjRKmBlclSxzDlg_3vXls7B08vpDNC6xgEhjQESGmOZxWrDjGCKbW89nGKc82eosgGufit6cZQkLSLdIQ4ooBLl3uwJGQugGBlTWm61igh4QAQ5fDBg"/>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-full border-2 border-white bg-slate-200 overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="Close up portrait of a young female entrepreneur in a brightly lit, modern office setting. She has a friendly expression and is looking directly at the camera. The image uses soft, warm lighting and a shallow depth of field, emphasizing a personal yet professional connection." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBy6Dem2F98SCov_UZ_TxtsX_UZIURPsHIIkv9p_q8i15kLhTpxEiiNlpTSji4r61wqAlPiSiHopbeTru6Vl6RcLi4k7K15rJT8IjYyOmRQ_U6vdl87bFGBKOurEAMCbek7RKjMFPJUy-_pKmFU3t8iaxDqURyll45LORuUz3foHNvQXO7EIWu2F5QGZf1BQ6ZVlCWMOV8JGWlZXBofszdqkwS-yoiYMHVBWKK1-_ne9TYx3TVfY--Ltxpgw-PWgpssBE6sLqwL2oE"/>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-full border-2 border-white bg-primary flex items-center justify-center text-white font-bold text-xs">
|
||||
+50k
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Descriptive Image -->
|
||||
<div class="mt-16 relative">
|
||||
<img class="rounded-3xl shadow-2xl border border-white/20 w-full h-[300px] object-cover" data-alt="A sleek, modern soundbox device placed on a wooden countertop in a contemporary cafe. A customer is scanning a QR code next to it with a smartphone. The scene is bathed in natural daylight from a large window, creating a clean, efficient, and proactive atmosphere. The color palette features whites, light woods, and corporate blues." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCO3owYjs-q405IK5Aubxndby8PleePa1HDR4ggkxdNVa_QNlFyaGWrtSZ_Bcy0V_wUt9Wxy0WvA9ZF3L4AjSgqu8VnpbcdMUyEUoF4C_bQhL44ZLp4YqtOVAwhXsRcFB3uI6nM_CiTTTkCIWXykB6aBgvevGVtckOkYKliYvfrvTHwE6PhesFMbO9NktlCQ0waknzbeA02-CmJ32taCnTNb5lPQGWvkdLPG_yZy0Lqw69lbfNKsxBDYxtwyjfHL1sUUT4TymJoCro"/>
|
||||
<div class="absolute -bottom-6 -right-6 p-6 glass-card rounded-2xl flex items-center gap-4">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Status Sistem</p>
|
||||
<p class="font-metric-sm text-metric-sm text-success">Aktif & Stabil</p>
|
||||
</div>
|
||||
<div class="w-3 h-3 bg-success rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer for Desktop side -->
|
||||
<div class="absolute bottom-8 left-12 right-12 flex justify-between items-center z-10 border-t border-white/10 pt-6">
|
||||
<p class="font-label-md text-label-md text-slate-400">© 2024 Soundbox Ops. Precise. Reliable. Proactive.</p>
|
||||
<div class="flex gap-4">
|
||||
<a class="font-label-md text-label-md text-slate-400 hover:text-white transition-colors" href="#">Privasi</a>
|
||||
<a class="font-label-md text-label-md text-slate-400 hover:text-white transition-colors" href="#">Bantuan</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<!-- Mobile Footer -->
|
||||
<footer class="md:hidden p-8 bg-surface-container border-t border-slate-200 text-center">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-2">© 2024 Soundbox Ops. All rights reserved.</p>
|
||||
<div class="flex justify-center gap-6">
|
||||
<a class="font-label-md text-label-md text-primary" href="#">Kebijakan Privasi</a>
|
||||
<a class="font-label-md text-label-md text-primary" href="#">Syarat Layanan</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
// Simple micro-interactions
|
||||
document.querySelectorAll('input').forEach(input => {
|
||||
input.addEventListener('focus', () => {
|
||||
input.parentElement.classList.add('scale-[1.01]');
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
input.parentElement.classList.remove('scale-[1.01]');
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle Password visibility
|
||||
const toggleBtn = document.querySelector('button[type="button"]');
|
||||
const passInput = document.getElementById('password');
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const type = passInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
passInput.setAttribute('type', type);
|
||||
toggleBtn.querySelector('span').innerText = type === 'password' ? 'visibility' : 'visibility_off';
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/merchant_login_portal/screen.png
Normal file
|
After Width: | Height: | Size: 713 KiB |
624
design/merchant_settlement_history/code.html
Normal file
@ -0,0 +1,624 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Settlement History - Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.scroll-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scroll-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"danger": "#DC2626",
|
||||
"slate-900": "#0F172A",
|
||||
"on-surface-variant": "#434655",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"error-container": "#ffdad6",
|
||||
"outline": "#737686",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"info": "#0EA5E9",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"surface-container": "#ededf9",
|
||||
"primary-container": "#2563eb",
|
||||
"warning": "#F59E0B",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"error": "#ba1a1a",
|
||||
"surface": "#faf8ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-error": "#ffffff",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"tertiary": "#943700",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-surface": "#191b23",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"slate-200": "#E2E8F0",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface-tint": "#0053db",
|
||||
"on-error-container": "#93000a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"on-background": "#191b23",
|
||||
"slate-500": "#64748B",
|
||||
"primary": "#004ac6",
|
||||
"surface-bright": "#faf8ff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-primary": "#ffffff",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary": "#505f76",
|
||||
"on-secondary-container": "#54647a"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"page-padding": "24px",
|
||||
"card-padding": "20px",
|
||||
"gutter": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md min-h-screen flex">
|
||||
<!-- Side Navigation Bar -->
|
||||
<aside class="flex flex-col fixed left-0 top-0 h-full p-4 gap-2 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 w-64 z-50">
|
||||
<div class="mb-8 px-2 flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary-container rounded-xl flex items-center justify-center text-on-primary-container">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md text-primary leading-tight">Soundbox Admin</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500">Fintech Ops Suite</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 flex flex-col gap-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
<span>Reconciliation</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="security">security</span>
|
||||
<span>Audit Logs</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
<span>Fee Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container dark:bg-on-secondary-fixed-variant text-on-secondary-container dark:text-on-secondary-fixed rounded-xl font-bold font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span>Settlements</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="router">router</span>
|
||||
<span>Device Health</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="contact_support">contact_support</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto pt-4 border-t border-slate-100">
|
||||
<button class="w-full mb-4 bg-primary text-on-primary py-3 px-4 rounded-xl font-bold font-label-md text-label-md flex items-center justify-center gap-2 hover:opacity-90 transition-opacity">
|
||||
<span class="material-symbols-outlined" data-icon="add_chart">add_chart</span>
|
||||
Generate Report
|
||||
</button>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 ml-64 min-h-screen flex flex-col">
|
||||
<!-- Top Bar -->
|
||||
<header class="flex justify-between items-center h-[72px] px-page-padding w-full sticky top-0 z-50 bg-surface dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary dark:text-inverse-primary">Settlement History</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative">
|
||||
<input class="bg-slate-100 border-none rounded-full px-4 py-2 text-body-md w-64 focus:ring-2 focus:ring-primary/20" placeholder="Search transactions..." type="text"/>
|
||||
<span class="material-symbols-outlined absolute right-3 top-2 text-slate-500" data-icon="search">search</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors" data-icon="notifications">notifications</span>
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors" data-icon="settings">settings</span>
|
||||
<div class="h-10 w-10 rounded-full bg-slate-200 overflow-hidden border border-slate-300">
|
||||
<img alt="Administrator Avatar" class="w-full h-full object-cover" data-alt="A professional headshot of a smiling fintech administrator in a bright, modern corporate office. High-key lighting highlights the reliable and expert persona, matching a clean light-mode aesthetic with soft blue and grey tones." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAULoyxr37TXy6dAfagS4FSEQl7UpTG5XXKiqbc3Lt55-L4oWWcAjgJSHLfIRx3w_TYphBzQLrW9XdgtYvGMwDlsGslyaOunv0ANbViiiH0eSWWUrweqkulmIFSgKdqKoxSKdQ8L5ouHalFrIJtx0Lff4GQ-YmlbRwDJAfjaYzNFCucdQ4X7Dt7iIx83NWQ0Mf6PlAchGv7WoVm7K3TI8C6HkpMzpYhiphfN1LYtRDR4i-kW-Uk8Gcv2tPYheVSH_5-r9spt16EOl4"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Canvas -->
|
||||
<div class="p-page-padding space-y-8 max-w-7xl mx-auto w-full">
|
||||
<!-- KPI Summary Bento Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-gutter">
|
||||
<!-- Available Balance -->
|
||||
<div class="bg-white p-card-padding rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Available Balance</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-background">$12,480.50</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-primary-container/10 rounded-lg flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="text-success font-metric-sm text-metric-sm flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
8.2%
|
||||
</span>
|
||||
<span class="text-slate-400 font-label-md text-label-md">vs last week</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Next Payout -->
|
||||
<div class="bg-white p-card-padding rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Next Payout Date</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-background">Oct 24, 2023</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-warning/10 rounded-lg flex items-center justify-center text-warning">
|
||||
<span class="material-symbols-outlined" data-icon="event">event</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="font-label-md text-label-md text-slate-400">Estimated: <span class="text-on-surface font-semibold">$3,150.00</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Total Settled MTD -->
|
||||
<div class="bg-white p-card-padding rounded-xl border border-slate-200 shadow-sm flex flex-col justify-between">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Total Settled (MTD)</p>
|
||||
<h3 class="font-metric-lg text-metric-lg text-on-background">$45,210.00</h3>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-success/10 rounded-lg flex items-center justify-center text-success">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="text-success font-metric-sm text-metric-sm flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
12.5%
|
||||
</span>
|
||||
<span class="text-slate-400 font-label-md text-label-md">vs last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filters and Actions -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500 whitespace-nowrap">Period:</span>
|
||||
<select class="border-slate-200 rounded-lg text-body-md focus:ring-primary focus:border-primary py-1 px-3">
|
||||
<option>Last 30 Days</option>
|
||||
<option>Last 90 Days</option>
|
||||
<option>Year to Date</option>
|
||||
<option>Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500 whitespace-nowrap">Status:</span>
|
||||
<select class="border-slate-200 rounded-lg text-body-md focus:ring-primary focus:border-primary py-1 px-3">
|
||||
<option>All Statuses</option>
|
||||
<option>Processed</option>
|
||||
<option>Pending</option>
|
||||
<option>Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-2 px-4 py-2 text-primary bg-primary/5 hover:bg-primary/10 transition-colors rounded-lg font-label-md text-label-md">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="file_download">file_download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-primary text-on-primary hover:opacity-90 transition-opacity rounded-lg font-label-md text-label-md">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="filter_list">filter_list</span>
|
||||
Advanced Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Settlement History Table -->
|
||||
<div class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200 h-row-height">
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Settlement ID</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Bank Account</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider text-right">Gross Amount</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider text-right">Net Amount</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 font-label-md text-label-md text-slate-500 uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1: Processed -->
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height group">
|
||||
<td class="px-6 font-label-md text-label-md font-semibold text-primary">SET-902341</td>
|
||||
<td class="px-6 font-body-md text-body-md text-on-surface">Oct 21, 2023</td>
|
||||
<td class="px-6 font-body-md text-body-md text-slate-500">HDFC •••• 4492</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums">$2,450.00</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums font-semibold">$2,401.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Processed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<button class="text-primary hover:underline font-label-md text-label-md flex items-center gap-1 ml-auto group-hover:opacity-100 opacity-0 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="download">download</span>
|
||||
Download Proof
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2: Pending -->
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height group">
|
||||
<td class="px-6 font-label-md text-label-md font-semibold text-primary">SET-902339</td>
|
||||
<td class="px-6 font-body-md text-body-md text-on-surface">Oct 20, 2023</td>
|
||||
<td class="px-6 font-body-md text-body-md text-slate-500">HDFC •••• 4492</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums">$1,800.00</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums font-semibold">$1,764.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning">
|
||||
Pending
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="text-slate-400 font-label-md text-label-md italic">Awaiting Bank</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3: Processed -->
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height group">
|
||||
<td class="px-6 font-label-md text-label-md font-semibold text-primary">SET-902331</td>
|
||||
<td class="px-6 font-body-md text-body-md text-on-surface">Oct 19, 2023</td>
|
||||
<td class="px-6 font-body-md text-body-md text-slate-500">ICICI •••• 1102</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums">$4,120.00</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums font-semibold">$4,037.60</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Processed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<button class="text-primary hover:underline font-label-md text-label-md flex items-center gap-1 ml-auto group-hover:opacity-100 opacity-0 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="download">download</span>
|
||||
Download Proof
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4: Failed -->
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height group">
|
||||
<td class="px-6 font-label-md text-label-md font-semibold text-primary">SET-902325</td>
|
||||
<td class="px-6 font-body-md text-body-md text-on-surface">Oct 18, 2023</td>
|
||||
<td class="px-6 font-body-md text-body-md text-slate-500">HDFC •••• 4492</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums">$500.00</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums font-semibold">$490.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger/10 text-danger">
|
||||
Failed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<button class="text-danger hover:underline font-label-md text-label-md flex items-center gap-1 ml-auto">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="error_outline">error_outline</span>
|
||||
View Error
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5: Processed -->
|
||||
<tr class="hover:bg-slate-50 transition-colors h-row-height group">
|
||||
<td class="px-6 font-label-md text-label-md font-semibold text-primary">SET-902311</td>
|
||||
<td class="px-6 font-body-md text-body-md text-on-surface">Oct 17, 2023</td>
|
||||
<td class="px-6 font-body-md text-body-md text-slate-500">HDFC •••• 4492</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums">$3,600.00</td>
|
||||
<td class="px-6 font-body-md text-body-md text-right tabular-nums font-semibold">$3,528.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Processed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<button class="text-primary hover:underline font-label-md text-label-md flex items-center gap-1 ml-auto group-hover:opacity-100 opacity-0 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="download">download</span>
|
||||
Download Proof
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 flex items-center justify-between border-t border-slate-100 bg-white">
|
||||
<p class="font-label-md text-label-md text-slate-500">Showing <span class="font-semibold text-on-surface">1 - 5</span> of <span class="font-semibold text-on-surface">124</span> disbursements</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg text-slate-400 hover:text-primary hover:bg-slate-50 transition-all">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="chevron_left">chevron_left</span>
|
||||
</button>
|
||||
<button class="px-3 py-1 border border-primary bg-primary text-on-primary rounded-lg font-label-md text-label-md">1</button>
|
||||
<button class="px-3 py-1 border border-slate-200 hover:border-primary rounded-lg font-label-md text-label-md">2</button>
|
||||
<button class="px-3 py-1 border border-slate-200 hover:border-primary rounded-lg font-label-md text-label-md">3</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg text-slate-500 hover:text-primary hover:bg-slate-50 transition-all">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="chevron_right">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Transaction Trend & Detail Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
|
||||
<div class="lg:col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm p-card-padding">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h4 class="font-headline-md text-headline-md text-on-surface">Weekly Settlement Volume</h4>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex items-center gap-1 font-label-md text-label-md text-primary">
|
||||
<span class="w-3 h-3 rounded-full bg-primary"></span> Gross
|
||||
</span>
|
||||
<span class="flex items-center gap-1 font-label-md text-label-md text-secondary">
|
||||
<span class="w-3 h-3 rounded-full bg-secondary"></span> Net
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-64 flex items-end justify-between gap-4 px-4 pb-4">
|
||||
<!-- Bar Chart Mockup -->
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[40%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[60%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Mon</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[55%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[75%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Tue</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[30%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[45%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Wed</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[70%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[90%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Thu</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[45%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[65%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Fri</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[15%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[25%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Sat</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 flex-1 group">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute bottom-0 left-1/4 w-3 bg-secondary rounded-t-sm h-[10%]" title="Net"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-3 bg-primary rounded-t-sm h-[20%]" title="Gross"></div>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Sun</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Settlement Cycle Helper -->
|
||||
<div class="bg-primary p-card-padding rounded-xl border border-primary/20 shadow-sm text-on-primary">
|
||||
<h4 class="font-headline-md text-headline-md mb-4 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined" data-icon="info">info</span>
|
||||
Settlement Cycle
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-6 h-6 rounded-full bg-on-primary text-primary flex items-center justify-center font-bold text-xs">1</div>
|
||||
<div class="w-0.5 h-full bg-on-primary/30 my-1"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md font-bold">Daily Batching</p>
|
||||
<p class="text-body-md opacity-80">Transactions from 12:00 AM to 11:59 PM are batched for settlement.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-6 h-6 rounded-full bg-on-primary text-primary flex items-center justify-center font-bold text-xs">2</div>
|
||||
<div class="w-0.5 h-full bg-on-primary/30 my-1"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md font-bold">Fee Deduction</p>
|
||||
<p class="text-body-md opacity-80">Processing fees (approx. 2%) are automatically calculated and deducted.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-6 h-6 rounded-full bg-on-primary text-primary flex items-center justify-center font-bold text-xs">3</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md font-bold">Direct Deposit</p>
|
||||
<p class="text-body-md opacity-80">Funds reach your bank account within T+1 working days.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mt-8 w-full bg-white text-primary font-bold py-3 px-4 rounded-lg hover:bg-opacity-90 transition-all font-label-md text-label-md">
|
||||
Learn More About Payouts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Detail Drawer (Hidden by default) -->
|
||||
<div class="fixed inset-y-0 right-0 w-[450px] bg-white shadow-2xl z-[100] transform translate-x-full transition-transform duration-300 ease-in-out border-l border-slate-200 p-8 overflow-y-auto" id="detailDrawer">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h3 class="font-headline-md text-headline-md">Settlement Details</h3>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full transition-colors" onclick="toggleDrawer(false)">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="p-4 bg-slate-50 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500">Net Amount Paid</p>
|
||||
<p class="font-metric-lg text-metric-lg text-primary">$2,401.00</p>
|
||||
<span class="inline-flex items-center mt-2 px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
|
||||
Successful Transfer
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-label-md text-label-md font-bold uppercase text-slate-400">Breakdown</h4>
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-body-md text-slate-600">Gross Processing Volume</span>
|
||||
<span class="font-metric-sm text-metric-sm">$2,450.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-body-md text-slate-600">Platform Fees (2%)</span>
|
||||
<span class="font-metric-sm text-metric-sm text-danger">-$49.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||
<span class="text-body-md text-slate-600">Adjustment/Refunds</span>
|
||||
<span class="font-metric-sm text-metric-sm">$0.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<span class="text-body-md font-bold">Total Disbursed</span>
|
||||
<span class="font-metric-sm text-metric-sm font-bold text-primary">$2,401.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 pt-6">
|
||||
<h4 class="font-label-md text-label-md font-bold uppercase text-slate-400">Destination</h4>
|
||||
<div class="flex items-center gap-4 p-4 border border-slate-200 rounded-xl">
|
||||
<div class="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-slate-500" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md font-bold">HDFC Bank India</p>
|
||||
<p class="text-body-md text-slate-500">Checking Account •••• 4492</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 pt-6">
|
||||
<h4 class="font-label-md text-label-md font-bold uppercase text-slate-400">Transfer Log</h4>
|
||||
<div class="relative pl-6 space-y-6 before:absolute before:left-2 before:top-2 before:bottom-2 before:w-0.5 before:bg-slate-200">
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[22px] top-1 w-4 h-4 rounded-full bg-success ring-4 ring-white"></div>
|
||||
<p class="font-label-md text-label-md font-bold">Transfer Initiated</p>
|
||||
<p class="text-xs text-slate-400">Oct 21, 2023 • 09:12 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[22px] top-1 w-4 h-4 rounded-full bg-success ring-4 ring-white"></div>
|
||||
<p class="font-label-md text-label-md font-bold">Bank Processing</p>
|
||||
<p class="text-xs text-slate-400">Oct 21, 2023 • 11:45 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-[22px] top-1 w-4 h-4 rounded-full bg-success ring-4 ring-white"></div>
|
||||
<p class="font-label-md text-label-md font-bold">Funds Credited</p>
|
||||
<p class="text-xs text-slate-400">Oct 21, 2023 • 04:30 PM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8 flex flex-col gap-3">
|
||||
<button class="w-full bg-primary text-on-primary py-3 px-4 rounded-xl font-bold flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined" data-icon="download">download</span>
|
||||
Download Proof of Transfer
|
||||
</button>
|
||||
<button class="w-full bg-slate-100 text-slate-700 py-3 px-4 rounded-xl font-bold">
|
||||
Raise a Dispute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Overlay for drawer -->
|
||||
<div class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[90] hidden opacity-0 transition-opacity duration-300" id="drawerOverlay" onclick="toggleDrawer(false)"></div>
|
||||
</main>
|
||||
<script>
|
||||
function toggleDrawer(isOpen) {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
const overlay = document.getElementById('drawerOverlay');
|
||||
|
||||
if (isOpen) {
|
||||
drawer.classList.remove('translate-x-full');
|
||||
overlay.classList.remove('hidden');
|
||||
setTimeout(() => overlay.classList.add('opacity-100'), 10);
|
||||
} else {
|
||||
drawer.classList.add('translate-x-full');
|
||||
overlay.classList.remove('opacity-100');
|
||||
setTimeout(() => overlay.classList.add('hidden'), 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Simulating row click for detail view
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => toggleDrawer(true));
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/merchant_settlement_history/screen.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
324
design/onboarding_bank_account/code.html
Normal file
@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops - Merchant Onboarding</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* Custom scrollbar for a cleaner look */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"error-container": "#ffdad6",
|
||||
"warning": "#F59E0B",
|
||||
"slate-200": "#E2E8F0",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"inverse-surface": "#2e3039",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B",
|
||||
"secondary": "#505f76",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container": "#ededf9",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface": "#faf8ff",
|
||||
"slate-900": "#0F172A",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-container": "#54647a",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-background": "#191b23",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"info": "#0EA5E9",
|
||||
"danger": "#DC2626",
|
||||
"surface-bright": "#faf8ff",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#2563eb",
|
||||
"primary": "#004ac6",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-700": "#334155"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background font-body-md text-on-surface min-h-screen flex flex-col">
|
||||
<!-- Top Navigation Bar (Shell Suppressed for Flow, but following Brand Anchor for Identity) -->
|
||||
<header class="h-[72px] flex items-center justify-between px-page-padding bg-surface-container-lowest border-b border-slate-200 z-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-primary text-3xl" data-icon="account_balance">account_balance</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-headline-md text-headline-md text-primary font-bold">Soundbox Ops</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 tracking-wider">MERCHANT ONBOARDING</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="flex items-center gap-2 text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="help_outline">help_outline</span>
|
||||
<span class="font-label-md text-label-md">Support</span>
|
||||
</button>
|
||||
<div class="h-8 w-px bg-slate-200"></div>
|
||||
<button class="text-on-surface-variant font-label-md text-label-md hover:text-danger">Exit</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-grow flex items-center justify-center py-12 px-page-padding">
|
||||
<div class="w-full max-w-4xl">
|
||||
<!-- Progress Stepper -->
|
||||
<div class="mb-12 flex justify-between items-start relative px-4">
|
||||
<!-- Progress Line -->
|
||||
<div class="absolute top-5 left-8 right-8 h-1 bg-slate-200 z-0">
|
||||
<div class="h-full bg-primary w-2/3 transition-all duration-700 ease-out"></div>
|
||||
</div>
|
||||
<!-- Step 1: Complete -->
|
||||
<div class="relative z-10 flex flex-col items-center gap-3 group">
|
||||
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center shadow-md">
|
||||
<span class="material-symbols-outlined" data-icon="check" data-weight="fill">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant font-bold">Business Profile</span>
|
||||
</div>
|
||||
<!-- Step 2: Complete -->
|
||||
<div class="relative z-10 flex flex-col items-center gap-3 group">
|
||||
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center shadow-md">
|
||||
<span class="material-symbols-outlined" data-icon="check" data-weight="fill">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant font-bold">Verification Docs</span>
|
||||
</div>
|
||||
<!-- Step 3: Active -->
|
||||
<div class="relative z-10 flex flex-col items-center gap-3 group">
|
||||
<div class="w-10 h-10 rounded-full bg-primary text-on-primary flex items-center justify-center ring-4 ring-primary-fixed shadow-lg">
|
||||
<span class="font-bold">3</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">Settlement Account</span>
|
||||
</div>
|
||||
<!-- Step 4: Pending -->
|
||||
<div class="relative z-10 flex flex-col items-center gap-3 group">
|
||||
<div class="w-10 h-10 rounded-full bg-surface-container-highest text-on-surface-variant flex items-center justify-center border border-slate-200">
|
||||
<span class="font-bold">4</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-slate-500">Device Selection</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Onboarding Card Content -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col md:flex-row">
|
||||
<!-- Left Panel: Contextual Info -->
|
||||
<div class="w-full md:w-1/3 bg-slate-50 p-8 border-b md:border-b-0 md:border-r border-slate-200">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-4">Settlement Details</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant mb-8">
|
||||
Link the bank account where you wish to receive daily settlements from your Soundbox transactions.
|
||||
</p>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="material-symbols-outlined text-info" data-icon="verified_user">verified_user</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-md text-label-md font-bold text-on-surface">Secure Verification</span>
|
||||
<p class="text-[11px] leading-4 text-slate-500 uppercase tracking-tight">Real-time penny-drop validation via IMPS/NEFT networks.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="material-symbols-outlined text-warning" data-icon="schedule">schedule</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-md text-label-md font-bold text-on-surface">Daily T+1 Settlement</span>
|
||||
<p class="text-[11px] leading-4 text-slate-500 uppercase tracking-tight">Funds reach your account within 24 hours of merchant closure.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Panel: Form -->
|
||||
<div class="w-full md:w-2/3 p-8">
|
||||
<form class="space-y-8" id="settlement-form">
|
||||
<!-- Form Section Header -->
|
||||
<div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold block mb-1">ACCOUNT INFORMATION</span>
|
||||
<div class="h-0.5 w-12 bg-primary"></div>
|
||||
</div>
|
||||
<!-- Form Grid -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Bank Selection -->
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md font-bold text-on-surface-variant" for="bank-name">Bank Name</label>
|
||||
<div class="relative">
|
||||
<select class="w-full h-12 px-4 bg-white border border-slate-200 rounded-lg appearance-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all font-body-md text-body-md" id="bank-name">
|
||||
<option disabled="" selected="" value="">Select your bank</option>
|
||||
<option value="hdfc">HDFC Bank</option>
|
||||
<option value="icici">ICICI Bank</option>
|
||||
<option value="axis">Axis Bank</option>
|
||||
<option value="sbi">State Bank of India</option>
|
||||
<option value="kotak">Kotak Mahindra Bank</option>
|
||||
</select>
|
||||
<span class="material-symbols-outlined absolute right-4 top-3 text-slate-400 pointer-events-none" data-icon="expand_more">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Account Holder Name -->
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md font-bold text-on-surface-variant" for="account-holder">Account Holder Name</label>
|
||||
<input class="w-full h-12 px-4 bg-white border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all font-body-md text-body-md" id="account-holder" placeholder="Enter as per bank records" type="text"/>
|
||||
</div>
|
||||
<!-- Account Number -->
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md font-bold text-on-surface-variant" for="account-number">Account Number</label>
|
||||
<input class="tabular-nums w-full h-12 px-4 bg-white border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all font-body-md text-body-md" id="account-number" placeholder="Enter account number" type="password"/>
|
||||
<p class="text-[11px] text-slate-400">Account number is encrypted and processed via secure channels.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Verification Action -->
|
||||
<div class="pt-4 flex flex-col gap-4">
|
||||
<button class="w-full h-[52px] bg-primary-container text-on-primary-container font-bold rounded-lg flex items-center justify-center gap-2 hover:opacity-90 active:scale-[0.98] transition-all" id="verify-btn" type="button">
|
||||
<span class="material-symbols-outlined" data-icon="check_circle">check_circle</span>
|
||||
Verify Account Details
|
||||
</button>
|
||||
<!-- Success State (Hidden by default) -->
|
||||
<div class="hidden animate-in fade-in slide-in-from-top-4 duration-300 p-4 bg-success/10 border border-success/30 rounded-lg flex items-start gap-3" id="success-alert">
|
||||
<span class="material-symbols-outlined text-success" data-icon="verified" data-weight="fill">verified</span>
|
||||
<div>
|
||||
<span class="font-label-md text-label-md font-bold text-success block">Verification Successful</span>
|
||||
<p class="text-sm text-on-surface-variant">Bank account verified for merchant: <span class="font-bold" id="verified-name">John Doe Enterprises</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer Actions -->
|
||||
<div class="pt-8 flex items-center justify-between border-t border-slate-100 mt-8">
|
||||
<button class="px-6 h-10 text-on-surface-variant font-label-md text-label-md font-bold hover:bg-slate-100 rounded-lg transition-colors" type="button">
|
||||
Back to Step 2
|
||||
</button>
|
||||
<button class="px-8 h-10 bg-primary text-on-primary font-bold rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-opacity-90 transition-colors shadow-sm" disabled="" id="continue-btn" type="submit">
|
||||
Continue to Step 4
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Trust Footer -->
|
||||
<div class="mt-12 flex flex-col items-center gap-4 opacity-60">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="lock">lock</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest">PCI-DSS COMPLIANT</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="security">security</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest">AES-256 ENCRYPTION</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11px] text-center text-slate-500">© 2024 Soundbox Ops Infrastructure. All transaction data is processed through authorized banking gateways.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const verifyBtn = document.getElementById('verify-btn');
|
||||
const successAlert = document.getElementById('success-alert');
|
||||
const continueBtn = document.getElementById('continue-btn');
|
||||
const holderNameInput = document.getElementById('account-holder');
|
||||
const verifiedName = document.getElementById('verified-name');
|
||||
|
||||
verifyBtn.addEventListener('click', () => {
|
||||
// Simulate API verification
|
||||
verifyBtn.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-on-primary-container" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Verifying with Bank...
|
||||
`;
|
||||
verifyBtn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
verifyBtn.classList.add('hidden');
|
||||
successAlert.classList.remove('hidden');
|
||||
continueBtn.disabled = false;
|
||||
verifiedName.textContent = holderNameInput.value || "Retail Partner";
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
document.getElementById('settlement-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
// Handle form navigation
|
||||
console.log("Proceeding to next step...");
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/onboarding_bank_account/screen.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
301
design/onboarding_business_info/code.html
Normal file
@ -0,0 +1,301 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops - Merchant Onboarding</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"error-container": "#ffdad6",
|
||||
"warning": "#F59E0B",
|
||||
"slate-200": "#E2E8F0",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"inverse-surface": "#2e3039",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B",
|
||||
"secondary": "#505f76",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container": "#ededf9",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface": "#faf8ff",
|
||||
"slate-900": "#0F172A",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-container": "#54647a",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-background": "#191b23",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"info": "#0EA5E9",
|
||||
"danger": "#DC2626",
|
||||
"surface-bright": "#faf8ff",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#2563eb",
|
||||
"primary": "#004ac6",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-700": "#334155"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.step-active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #004ac6;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #004ac6 !important;
|
||||
box-shadow: 0 0 0 1px #004ac6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-md">
|
||||
<!-- Top Navigation Bar (Suppressed Shell version for focused task) -->
|
||||
<nav class="fixed top-0 left-0 right-0 h-[72px] bg-surface-container-lowest border-b border-slate-200 flex items-center px-page-padding z-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-white" style="font-variation-settings: 'FILL' 1;">account_balance_wallet</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md text-primary">Soundbox Ops</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Merchant Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<button class="flex items-center gap-2 px-4 py-2 text-on-surface-variant hover:bg-slate-100 rounded-lg transition-colors">
|
||||
<span class="material-symbols-outlined">help_outline</span>
|
||||
<span class="font-body-md text-body-md">Support</span>
|
||||
</button>
|
||||
<div class="w-px h-8 bg-slate-200"></div>
|
||||
<button class="flex items-center gap-2 px-2 py-1 text-on-surface-variant hover:bg-slate-100 rounded-lg transition-colors">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Main Content Area -->
|
||||
<main class="pt-[72px] min-h-screen flex">
|
||||
<!-- Left Column: Form (8 cols) -->
|
||||
<div class="flex-1 max-w-4xl mx-auto py-12 px-page-padding">
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between relative">
|
||||
<!-- Progress Line Background -->
|
||||
<div class="absolute top-1/2 left-0 w-full h-1 bg-slate-200 -translate-y-1/2 -z-10"></div>
|
||||
<!-- Active Progress Line -->
|
||||
<div class="absolute top-1/2 left-0 w-1/4 h-1 bg-primary -translate-y-1/2 -z-10"></div>
|
||||
<!-- Steps -->
|
||||
<div class="flex flex-col items-center gap-2 group">
|
||||
<div class="w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center font-bold ring-4 ring-white">1</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">Business Info</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-white border-2 border-slate-200 text-slate-400 flex items-center justify-center font-bold">2</div>
|
||||
<span class="font-label-md text-label-md text-slate-500">PIC Details</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-white border-2 border-slate-200 text-slate-400 flex items-center justify-center font-bold">3</div>
|
||||
<span class="font-label-md text-label-md text-slate-500">Bank Account</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-white border-2 border-slate-200 text-slate-400 flex items-center justify-center font-bold">4</div>
|
||||
<span class="font-label-md text-label-md text-slate-500">Documents</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Card -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-10">
|
||||
<header class="mb-8">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface">Business Information</h2>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant mt-2">Please provide the legal details of your business to begin the onboarding process.</p>
|
||||
</header>
|
||||
<form class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Legal Entity Name -->
|
||||
<div class="col-span-2">
|
||||
<label class="block font-label-md text-label-md text-on-surface-variant mb-2" for="legal_name">Legal Entity Name</label>
|
||||
<input class="w-full px-4 py-3 border border-slate-200 rounded-lg bg-surface-container-low font-body-md text-body-md transition-all" id="legal_name" name="legal_name" placeholder="e.g., PT. Maju Jaya Terang" type="text"/>
|
||||
<p class="mt-1.5 font-label-md text-label-md text-slate-500">Must match the name on your NIB or Akta Pendirian.</p>
|
||||
</div>
|
||||
<!-- Business Category -->
|
||||
<div>
|
||||
<label class="block font-label-md text-label-md text-on-surface-variant mb-2" for="category">Business Category</label>
|
||||
<select class="w-full px-4 py-3 border border-slate-200 rounded-lg bg-surface-container-low font-body-md text-body-md transition-all appearance-none" id="category" name="category">
|
||||
<option disabled="" selected="" value="">Select category</option>
|
||||
<option value="retail">Retail & FMCG</option>
|
||||
<option value="f&b">Food & Beverage</option>
|
||||
<option value="service">Services</option>
|
||||
<option value="tech">Technology</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Tax ID -->
|
||||
<div>
|
||||
<label class="block font-label-md text-label-md text-on-surface-variant mb-2" for="npwp">Tax ID (NPWP)</label>
|
||||
<input class="w-full px-4 py-3 border border-slate-200 rounded-lg bg-surface-container-low font-body-md text-body-md transition-all" id="npwp" name="npwp" placeholder="00.000.000.0-000.000" type="text"/>
|
||||
</div>
|
||||
<!-- Business Address -->
|
||||
<div class="col-span-2">
|
||||
<label class="block font-label-md text-label-md text-on-surface-variant mb-2" for="address">Business Address</label>
|
||||
<textarea class="w-full px-4 py-3 border border-slate-200 rounded-lg bg-surface-container-low font-body-md text-body-md transition-all" id="address" name="address" placeholder="Street name, building number, district..." rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6 border-t border-slate-100 flex items-center justify-between">
|
||||
<button class="px-6 py-2.5 font-label-md text-label-md text-on-surface-variant hover:bg-slate-100 rounded-lg transition-colors" type="button">Save Draft</button>
|
||||
<button class="px-8 py-2.5 bg-primary text-white font-label-md text-label-md rounded-lg hover:bg-blue-700 transition-all flex items-center gap-2" type="submit">
|
||||
Continue to PIC Details
|
||||
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Side Panel: Benefits & Support -->
|
||||
<aside class="w-[380px] bg-white border-l border-slate-200 p-10 flex flex-col gap-8 hidden lg:flex">
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface mb-6">Why Soundbox?</h3>
|
||||
<div class="space-y-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-secondary-container flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">bolt</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-semibold">Instant Verification</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mt-1">Get real-time audio confirmation for every QRIS transaction made at your shop.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-secondary-container flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">shield</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-semibold">Fraud Prevention</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mt-1">Eliminate fake payment screenshots with direct IoT device synchronization.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-secondary-container flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">leaderboard</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-semibold">Consolidated Dashboard</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mt-1">Manage multiple branches and devices from a single unified admin portal.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto pt-8 border-t border-slate-100">
|
||||
<div class="bg-surface-container-low rounded-xl p-6 border border-slate-100">
|
||||
<h4 class="font-label-md text-label-md text-primary font-bold uppercase mb-2">Need help?</h4>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant mb-4">Our onboarding specialists are ready to assist you via WhatsApp or Email.</p>
|
||||
<div class="space-y-2">
|
||||
<a class="flex items-center gap-3 text-on-surface hover:text-primary transition-colors" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">chat</span>
|
||||
<span class="font-label-md text-label-md">+62 812 3456 7890</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 text-on-surface hover:text-primary transition-colors" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">mail</span>
|
||||
<span class="font-label-md text-label-md">support@soundboxops.com</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contextual Image Placeholder -->
|
||||
<div class="relative w-full aspect-video rounded-xl overflow-hidden mt-4 group cursor-pointer shadow-sm">
|
||||
<img alt="Merchant using Soundbox" class="w-full h-full object-cover grayscale opacity-80 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500" data-alt="A clean and professional retail checkout counter featuring a modern IoT soundbox device next to a QR code payment stand. The environment is a bright, minimalist boutique store with soft sunlight streaming in. The image is captured in a high-key, modern corporate style using a neutral color palette with subtle blue accents, emphasizing efficiency and digital payment security." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBipLO1hcfKQu6ScpFFidOWl_Kh45ltpxM9HRovhKwc0EbZQfA9HB9ZMb_HqZ0lf585ZRlTVh3IIzER_HSuSOMu22IOdWIaHA1YhVZ170UeE3dnYsQIiFoIBSuAdCjUZbmLijzZF6QslYno-VHrHyuyZA3s15JVj9lzL1s6bdhbfl0r58iI4e4oA9MJCW6lf23oZVkmGlx3hKRWDNuy437g1v0b25V7kOAZxqXjWhubgDTVKVfOl9ERE4P7EaxLs89IXmuHKWA8R50"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent flex items-end p-4">
|
||||
<p class="text-white font-label-md text-label-md font-medium">Watch the Soundbox setup guide (2 min)</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<!-- Micro-interaction for form inputs -->
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
el.addEventListener('focus', () => {
|
||||
el.parentElement.querySelector('label')?.classList.add('text-primary');
|
||||
});
|
||||
el.addEventListener('blur', () => {
|
||||
el.parentElement.querySelector('label')?.classList.remove('text-primary');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/onboarding_business_info/screen.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
371
design/onboarding_document_upload/code.html
Normal file
@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Onboarding - KYC Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"error-container": "#ffdad6",
|
||||
"warning": "#F59E0B",
|
||||
"slate-200": "#E2E8F0",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"inverse-surface": "#2e3039",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B",
|
||||
"secondary": "#505f76",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container": "#ededf9",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface": "#faf8ff",
|
||||
"slate-900": "#0F172A",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-container": "#54647a",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-background": "#191b23",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"info": "#0EA5E9",
|
||||
"danger": "#DC2626",
|
||||
"surface-bright": "#faf8ff",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#2563eb",
|
||||
"primary": "#004ac6",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-700": "#334155"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.upload-dashed {
|
||||
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%23CBD5E1' stroke-width='2' stroke-dasharray='8%2c 8' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||
}
|
||||
.upload-dashed:hover {
|
||||
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='%23004ac6' stroke-width='2' stroke-dasharray='8%2c 8' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md min-h-screen">
|
||||
<!-- Stepper Navigation Header -->
|
||||
<header class="bg-surface-container-lowest border-b border-slate-200 sticky top-0 z-50">
|
||||
<div class="max-w-5xl mx-auto px-page-padding h-[80px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary text-3xl" data-icon="soundbox">soundpod</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-headline-md text-headline-md text-primary">Soundbox Ops</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Merchant Onboarding</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">Need help?</span>
|
||||
<a class="text-primary font-bold hover:underline" href="#">Contact Support</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress Bar Component -->
|
||||
<div class="bg-surface-container-low border-b border-slate-200">
|
||||
<div class="max-w-5xl mx-auto px-page-padding py-4">
|
||||
<div class="flex items-center justify-between relative">
|
||||
<!-- Progress Line -->
|
||||
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-slate-200 -translate-y-1/2 z-0"></div>
|
||||
<div class="absolute top-1/2 left-0 w-[75%] h-[2px] bg-primary -translate-y-1/2 z-0"></div>
|
||||
<!-- Steps -->
|
||||
<div class="relative z-10 flex flex-col items-center gap-2 group">
|
||||
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold text-xs ring-4 ring-white">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="check" data-weight="fill" style="font-variation-settings: 'FILL' 1;">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">Business info</span>
|
||||
</div>
|
||||
<div class="relative z-10 flex flex-col items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold text-xs ring-4 ring-white">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="check" data-weight="fill" style="font-variation-settings: 'FILL' 1;">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">Bank Account</span>
|
||||
</div>
|
||||
<div class="relative z-10 flex flex-col items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold text-xs ring-4 ring-white">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="check" data-weight="fill" style="font-variation-settings: 'FILL' 1;">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">Address</span>
|
||||
</div>
|
||||
<div class="relative z-10 flex flex-col items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center font-bold ring-4 ring-primary-fixed shadow-lg">4</div>
|
||||
<span class="font-label-md text-label-md text-primary font-bold">KYC Upload</span>
|
||||
</div>
|
||||
<div class="relative z-10 flex flex-col items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-white border-2 border-slate-200 text-slate-400 flex items-center justify-center font-bold text-xs">5</div>
|
||||
<span class="font-label-md text-label-md text-slate-400">Review</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="max-w-4xl mx-auto px-page-padding py-12">
|
||||
<!-- Header Content -->
|
||||
<div class="mb-10">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-2">Verify Your Business Identity</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">Please upload the required documents to comply with financial regulations and secure your merchant account. Ensure all photos are clear and legible.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-8">
|
||||
<!-- KYC Section: KTP -->
|
||||
<section class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-card-padding border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg bg-primary-fixed flex items-center justify-center flex-shrink-0">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="badge">badge</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md">Identity Card (KTP)</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Valid national ID card of the business owner or director.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start md:items-end gap-1">
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Formats: JPG, PNG, PDF</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Max size: 5MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-card-padding">
|
||||
<div class="upload-dashed h-48 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all hover:bg-surface-container-low group relative overflow-hidden" id="ktp-dropzone">
|
||||
<input accept=".jpg,.jpeg,.png,.pdf" class="absolute inset-0 opacity-0 cursor-pointer z-10" type="file"/>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<span class="material-symbols-outlined text-4xl text-slate-400 group-hover:text-primary transition-colors" data-icon="upload_file">upload_file</span>
|
||||
<div class="text-center">
|
||||
<p class="font-body-md text-body-md font-bold text-primary">Click to upload or drag and drop</p>
|
||||
<p class="font-label-md text-label-md text-slate-500">KTP Front View. Ensure name and photo are visible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-4 hidden" id="ktp-preview">
|
||||
<div class="flex items-center gap-3 bg-primary-fixed p-3 rounded-lg w-full">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="image">image</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-bold truncate">ktp_owner_final.jpg</p>
|
||||
<p class="text-xs text-on-primary-fixed-variant">1.2 MB • Ready to upload</p>
|
||||
</div>
|
||||
<button class="p-1 hover:bg-white rounded-full text-danger">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- KYC Section: NPWP -->
|
||||
<section class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-card-padding border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg bg-secondary-fixed flex items-center justify-center flex-shrink-0">
|
||||
<span class="material-symbols-outlined text-secondary" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md">Tax ID (NPWP)</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Company or individual tax identification card.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start md:items-end gap-1">
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Formats: JPG, PNG, PDF</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Max size: 5MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-card-padding">
|
||||
<div class="upload-dashed h-48 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all hover:bg-surface-container-low group relative" id="npwp-dropzone">
|
||||
<input accept=".jpg,.jpeg,.png,.pdf" class="absolute inset-0 opacity-0 cursor-pointer z-10" type="file"/>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<span class="material-symbols-outlined text-4xl text-slate-400 group-hover:text-primary transition-colors" data-icon="file_present">file_present</span>
|
||||
<div class="text-center">
|
||||
<p class="font-body-md text-body-md font-bold text-primary">Click to upload or drag and drop</p>
|
||||
<p class="font-label-md text-label-md text-slate-500">Scan or photo of the NPWP card.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- KYC Section: Business Photo -->
|
||||
<section class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-card-padding border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-lg bg-tertiary-fixed flex items-center justify-center flex-shrink-0">
|
||||
<span class="material-symbols-outlined text-tertiary" data-icon="add_a_photo">add_a_photo</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md">Business Location Photo</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Storefront photo showing business name and signage.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start md:items-end gap-1">
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Formats: JPG, PNG</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase">Max size: 10MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-card-padding">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="relative group aspect-video rounded-lg overflow-hidden border-2 border-slate-200 hover:border-primary transition-all">
|
||||
<img class="w-full h-full object-cover" data-alt="A brightly lit, professional modern retail storefront with a glass facade. The interior displays organized wooden shelving with various products. Warm atmospheric lighting illuminates the clean minimalist space, creating a welcoming and reliable corporate aesthetic. High resolution photography style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAExk-CpPooZnHdabVfhRzdZaMTbsKTyzwlSVgG0SqpxWiYvMKIu8xP03b-rul_7X2ShsBL0iNYXr-exJut1chAb514qjWcpsJvRdVgx3WOWxKt-e-pWyru6m3MrWdd12Bs2cLWvUvbaX9eeUDXWFBjF1cnd4Aq3wUz33rAdpavwi2S9bsN1IKbxFxxyErdIRkKf7cmz4EOCqYr60vKenmhm1c7vdS5ahi8ZaKmYuuQVbbF1s3Chs8Y-d7YKaFJQT9hAN7XI4luiro"/>
|
||||
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<button class="bg-white/90 hover:bg-white text-on-surface p-2 rounded-full flex items-center gap-2 text-sm font-bold shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform">
|
||||
<span class="material-symbols-outlined text-base" data-icon="sync">sync</span> Change
|
||||
</button>
|
||||
<button class="bg-danger/90 hover:bg-danger text-white p-2 rounded-full shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform">
|
||||
<span class="material-symbols-outlined text-base" data-icon="delete">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="absolute top-2 left-2">
|
||||
<span class="bg-success text-white text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wider">Validated</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-dashed aspect-video rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all hover:bg-surface-container-low group relative">
|
||||
<input accept=".jpg,.jpeg,.png" class="absolute inset-0 opacity-0 cursor-pointer z-10" type="file"/>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="material-symbols-outlined text-3xl text-slate-400 group-hover:text-primary transition-colors" data-icon="photo_camera">photo_camera</span>
|
||||
<p class="font-label-md text-label-md text-slate-500 text-center px-4">Upload interior or signage photo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Warning/Info Block -->
|
||||
<div class="mt-12 bg-primary-fixed/30 border border-primary/10 rounded-xl p-card-padding flex gap-4">
|
||||
<span class="material-symbols-outlined text-primary mt-0.5" data-icon="info">info</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-headline-md text-sm mb-1">Our Review Process</h4>
|
||||
<p class="font-body-md text-sm text-on-surface-variant">KYC documents are reviewed within 24-48 business hours. You will receive an email notification once your account status is updated. Providing high-quality, un-cropped photos will expedite this process.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Footer -->
|
||||
<div class="mt-12 flex flex-col md:flex-row items-center justify-between gap-6 border-t border-slate-200 pt-10">
|
||||
<button class="w-full md:w-auto px-8 py-3 rounded-full border-2 border-slate-200 font-bold text-on-surface-variant hover:bg-slate-50 transition-colors flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-xl" data-icon="arrow_back">arrow_back</span>
|
||||
Previous Step
|
||||
</button>
|
||||
<div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||
<button class="w-full md:w-auto px-8 py-3 rounded-full bg-slate-200 font-bold text-slate-500 cursor-not-allowed flex items-center justify-center gap-2" disabled="">
|
||||
Save Draft
|
||||
</button>
|
||||
<button class="w-full md:w-auto px-10 py-4 rounded-full bg-primary text-white font-bold shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2" onclick="handleSubmit()">
|
||||
Submit for Review
|
||||
<span class="material-symbols-outlined text-xl" data-icon="send">send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Simple Feedback Modal (Hidden) -->
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm hidden opacity-0 transition-opacity duration-300" id="success-modal">
|
||||
<div class="bg-white rounded-2xl p-10 max-w-md w-full mx-4 text-center transform scale-95 transition-transform duration-300 shadow-2xl">
|
||||
<div class="w-20 h-20 bg-success/10 text-success rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<span class="material-symbols-outlined text-5xl" data-icon="task_alt" data-weight="fill" style="font-variation-settings: 'FILL' 1;">task_alt</span>
|
||||
</div>
|
||||
<h2 class="font-headline-lg text-headline-lg mb-2">Documents Submitted!</h2>
|
||||
<p class="font-body-lg text-on-surface-variant mb-8">Thank you for providing your KYC details. Our verification team is now reviewing your application.</p>
|
||||
<button class="w-full py-4 bg-primary text-white rounded-full font-bold shadow-lg shadow-primary/20 hover:opacity-90 transition-all" onclick="closeModal()">
|
||||
Go to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function handleSubmit() {
|
||||
const modal = document.getElementById('success-modal');
|
||||
modal.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
modal.classList.remove('opacity-0');
|
||||
modal.children[0].classList.remove('scale-95');
|
||||
modal.children[0].classList.add('scale-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('success-modal');
|
||||
modal.classList.add('opacity-0');
|
||||
modal.children[0].classList.remove('scale-100');
|
||||
modal.children[0].classList.add('scale-95');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Mock interaction for file selection
|
||||
const dropzones = ['ktp-dropzone', 'npwp-dropzone'];
|
||||
dropzones.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('click', () => {
|
||||
const input = el.querySelector('input');
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/onboarding_document_upload/screen.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
359
design/onboarding_pic_details/code.html
Normal file
@ -0,0 +1,359 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Merchant Onboarding - PIC Details</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"error-container": "#ffdad6",
|
||||
"warning": "#F59E0B",
|
||||
"slate-200": "#E2E8F0",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"inverse-surface": "#2e3039",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"slate-100": "#F1F5F9",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B",
|
||||
"secondary": "#505f76",
|
||||
"on-primary-container": "#eeefff",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"surface-container": "#ededf9",
|
||||
"success": "#16A34A",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"surface": "#faf8ff",
|
||||
"slate-900": "#0F172A",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-container": "#54647a",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface-variant": "#434655",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"on-background": "#191b23",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"info": "#0EA5E9",
|
||||
"danger": "#DC2626",
|
||||
"surface-bright": "#faf8ff",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#2563eb",
|
||||
"primary": "#004ac6",
|
||||
"tertiary": "#943700",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"tertiary-container": "#bc4800",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-700": "#334155"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #F8FAFC;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.step-active {
|
||||
background-color: #004ac6;
|
||||
color: #ffffff;
|
||||
}
|
||||
.step-completed {
|
||||
background-color: #16A34A;
|
||||
color: #ffffff;
|
||||
}
|
||||
.step-inactive {
|
||||
background-color: #E2E8F0;
|
||||
color: #64748B;
|
||||
}
|
||||
.form-input-focus:focus {
|
||||
border-color: #004ac6;
|
||||
ring-color: #004ac6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface min-h-screen">
|
||||
<!-- Top Navigation Anchor -->
|
||||
<header class="fixed top-0 right-0 left-0 h-[72px] bg-surface-container-lowest border-b border-slate-200 z-50 flex items-center justify-between px-page-padding">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-primary flex items-center justify-center rounded-xl">
|
||||
<span class="material-symbols-outlined text-on-primary" data-icon="payments">payments</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-headline-md text-headline-md text-primary">Soundbox Ops</span>
|
||||
<span class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Merchant Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-2 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="help_outline">help_outline</span>
|
||||
<span class="font-body-md text-body-md">Support</span>
|
||||
</div>
|
||||
<div class="h-8 w-px bg-slate-200"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md font-bold">Session ID</p>
|
||||
<p class="font-label-md text-label-md text-slate-500">#OB-99281</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-[100px] pb-24 px-page-padding max-w-5xl mx-auto">
|
||||
<!-- Progress Stepper -->
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between relative">
|
||||
<!-- Connector Lines -->
|
||||
<div class="absolute top-1/2 left-0 w-full h-0.5 bg-slate-200 -z-10 -translate-y-1/2"></div>
|
||||
<div class="absolute top-1/2 left-0 w-1/3 h-0.5 bg-success -z-10 -translate-y-1/2 transition-all duration-500"></div>
|
||||
<!-- Step 1 -->
|
||||
<div class="flex flex-col items-center gap-2 bg-background px-4">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center step-completed shadow-sm">
|
||||
<span class="material-symbols-outlined" data-icon="check">check</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md font-bold text-success">Business Info</span>
|
||||
</div>
|
||||
<!-- Step 2 -->
|
||||
<div class="flex flex-col items-center gap-2 bg-background px-4">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center step-active shadow-md ring-4 ring-primary-fixed">
|
||||
<span class="font-metric-sm text-metric-sm">2</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md font-bold text-primary">PIC & Contact</span>
|
||||
</div>
|
||||
<!-- Step 3 -->
|
||||
<div class="flex flex-col items-center gap-2 bg-background px-4">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center step-inactive">
|
||||
<span class="font-metric-sm text-metric-sm">3</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md font-bold text-slate-400">Documents</span>
|
||||
</div>
|
||||
<!-- Step 4 -->
|
||||
<div class="flex flex-col items-center gap-2 bg-background px-4">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center step-inactive">
|
||||
<span class="font-metric-sm text-metric-sm">4</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md font-bold text-slate-400">Review</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-12 gap-8">
|
||||
<!-- Left: Form Content -->
|
||||
<div class="col-span-12 lg:col-span-8">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="p-8 border-b border-slate-100 bg-surface-container-low">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-2">Person in Charge (PIC)</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Please provide the contact details of the primary person responsible for operational decisions.</p>
|
||||
</div>
|
||||
<form class="p-8 space-y-6">
|
||||
<!-- Full Name -->
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block">Full Name (as per ID)</label>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" data-icon="person">person</span>
|
||||
<input class="w-full pl-12 pr-4 py-3 bg-white border border-slate-200 rounded-lg font-body-md text-body-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" placeholder="e.g. Alexander Graham" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Grid Row: Role & NIK -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block">Role / Designation</label>
|
||||
<select class="w-full px-4 py-3 bg-white border border-slate-200 rounded-lg font-body-md text-body-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none transition-all">
|
||||
<option value="">Select Role</option>
|
||||
<option value="owner">Owner / CEO</option>
|
||||
<option value="manager">Operations Manager</option>
|
||||
<option value="finance">Finance Head</option>
|
||||
<option value="it">IT/Technical Lead</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block">ID Card Number (NIK)</label>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" data-icon="badge">badge</span>
|
||||
<input class="w-full pl-12 pr-4 py-3 bg-white border border-slate-200 rounded-lg font-body-md text-body-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" maxlength="16" placeholder="16-digit ID number" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Email Address -->
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block">Professional Email Address</label>
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" data-icon="mail">mail</span>
|
||||
<input class="w-full pl-12 pr-4 py-3 bg-white border border-slate-200 rounded-lg font-body-md text-body-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" placeholder="alex@merchant.com" type="email"/>
|
||||
</div>
|
||||
<p class="font-label-md text-label-md text-slate-500 italic">This will be used for official settlements and system alerts.</p>
|
||||
</div>
|
||||
<!-- Phone Number & Verification Toggle -->
|
||||
<div class="space-y-4 p-5 bg-primary-fixed/30 rounded-xl border border-primary-fixed-dim">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block">Mobile Phone Number</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-white border border-slate-200 rounded-lg">
|
||||
<img alt="ID Flag" class="w-5 h-3" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCd5-agOQDxJNq3ljI2OleLexsB7UG4a06gY13NeDHd3atVIASpBCIQHGjp1BKAyCBIRZLYDlP5NJ4SaWT4pJYbRF2uDm6Udq74qVkGdLYrfma7ibVhXcBu4keW2X0fNlfAYWY1H4_p37xEGVey59ou7ty0xa9_wvHfNppbx1JVir-AmyN37Ay-sQkoYPep5h00ssSxsEDNbes1_wMAhy-35TvlV68txfKYdEQ_FpAOVEWBmgYrRducb0-e2HGtY-0VwWN8ST2Vfvo"/>
|
||||
<span class="font-body-md text-body-md text-on-surface-variant">+62</span>
|
||||
</div>
|
||||
<input class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg font-body-md text-body-md focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" placeholder="812-xxxx-xxxx" type="tel"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<label class="font-label-md text-label-md text-primary font-bold">Verify via WhatsApp</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input checked="" class="sr-only peer" type="checkbox" value=""/>
|
||||
<div class="w-11 h-6 bg-slate-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-slate-100">
|
||||
<button class="flex items-center gap-2 px-6 py-3 border border-slate-200 rounded-lg font-label-md text-label-md font-bold text-on-surface-variant hover:bg-slate-50 transition-colors" type="button">
|
||||
<span class="material-symbols-outlined" data-icon="arrow_back">arrow_back</span>
|
||||
Previous Step
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-8 py-3 bg-primary text-on-primary rounded-lg font-label-md text-label-md font-bold shadow-lg shadow-primary/20 hover:opacity-90 active:scale-95 transition-all" type="submit">
|
||||
Save & Continue
|
||||
<span class="material-symbols-outlined" data-icon="arrow_forward">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Contextual Help & Summary -->
|
||||
<div class="col-span-12 lg:col-span-4 space-y-6">
|
||||
<!-- Why this info matters -->
|
||||
<div class="bg-white border border-slate-200 p-6 rounded-xl space-y-4">
|
||||
<div class="w-12 h-12 bg-secondary-container rounded-full flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-on-secondary-container" data-icon="verified_user">verified_user</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface mb-2">Why we need this</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed">
|
||||
To maintain a secure financial network, we require a verified Point of Contact. This ensures operational alerts, settlement reports, and device updates reach the authorized personnel promptly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Trusted Badge -->
|
||||
<div class="bg-slate-900 p-6 rounded-xl text-white relative overflow-hidden group">
|
||||
<div class="relative z-10 flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-warning" data-icon="shield">shield</span>
|
||||
<span class="font-label-md text-label-md font-bold uppercase tracking-widest">Enterprise Security</span>
|
||||
</div>
|
||||
<p class="font-body-md text-body-md text-slate-300">
|
||||
Your data is encrypted using AES-256 standards. We never share personal contact details with third-party marketers.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Decorative background pattern -->
|
||||
<div class="absolute -right-4 -bottom-4 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
||||
<span class="material-symbols-outlined text-[120px]" data-icon="lock">lock</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Assistance Card -->
|
||||
<div class="p-6 border-2 border-dashed border-slate-200 rounded-xl flex flex-col items-center text-center gap-4">
|
||||
<p class="font-body-md text-body-md text-on-surface-variant italic">Need help with the onboarding process?</p>
|
||||
<button class="w-full py-2 px-4 bg-slate-100 rounded-lg font-label-md text-label-md font-bold text-on-surface-variant hover:bg-slate-200 transition-colors flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]" data-icon="support_agent">support_agent</span>
|
||||
Talk to an Onboarding Expert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Visual Accents: Blob background -->
|
||||
<div class="fixed top-[-10%] right-[-10%] w-[40%] h-[40%] bg-primary/5 blur-[120px] rounded-full -z-50"></div>
|
||||
<div class="fixed bottom-[-10%] left-[-10%] w-[30%] h-[30%] bg-success/5 blur-[100px] rounded-full -z-50"></div>
|
||||
<script>
|
||||
// Simple micro-interactions for inputs
|
||||
document.querySelectorAll('input, select').forEach(element => {
|
||||
element.addEventListener('focus', () => {
|
||||
element.parentElement.querySelector('.material-symbols-outlined')?.classList.add('text-primary');
|
||||
});
|
||||
element.addEventListener('blur', () => {
|
||||
element.parentElement.querySelector('.material-symbols-outlined')?.classList.remove('text-primary');
|
||||
});
|
||||
});
|
||||
|
||||
// Prevention of accidental form submission for demo
|
||||
document.querySelector('form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.innerHTML = `<span class="material-symbols-outlined animate-spin" data-icon="progress_activity">progress_activity</span> Processing...`;
|
||||
btn.classList.add('opacity-80', 'pointer-events-none');
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = `<span class="material-symbols-outlined" data-icon="check_circle">check_circle</span> Saved`;
|
||||
btn.classList.remove('bg-primary');
|
||||
btn.classList.add('bg-success');
|
||||
setTimeout(() => {
|
||||
alert('PIC details saved successfully! Proceeding to document upload...');
|
||||
btn.innerHTML = originalContent;
|
||||
btn.classList.remove('bg-success', 'opacity-80', 'pointer-events-none');
|
||||
btn.classList.add('bg-primary');
|
||||
}, 1000);
|
||||
}, 1500);
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/onboarding_pic_details/screen.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
655
design/outlet_branch_management/code.html
Normal file
@ -0,0 +1,655 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Outlet Management | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-surface-variant": "#434655",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-surface": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-900": "#0F172A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-md min-h-screen">
|
||||
<!-- Side Navigation -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 bg-surface-container-lowest border-r border-slate-200 flex flex-col py-6 px-4 gap-2 z-50">
|
||||
<div class="flex items-center gap-3 px-2 mb-8">
|
||||
<div class="w-10 h-10 bg-primary-container rounded-xl flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-white" style="font-variation-settings: 'FILL' 1;">sound_detection_dog_barking</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="text-[10px] uppercase tracking-wider text-slate-500 font-bold">Admin Console</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 flex flex-col gap-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-body-md">Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">storefront</span>
|
||||
<span class="font-body-md">Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">speaker_group</span>
|
||||
<span class="font-body-md">Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">receipt_long</span>
|
||||
<span class="font-body-md">Transactions</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">account_balance</span>
|
||||
<span class="font-body-md">Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">history_edu</span>
|
||||
<span class="font-body-md">Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<button class="mt-4 mb-8 mx-2 bg-primary text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 active:opacity-90 active:scale-95 transition-all shadow-lg shadow-primary/20">
|
||||
<span class="material-symbols-outlined text-[20px]">add_circle</span>
|
||||
Register New Device
|
||||
</button>
|
||||
<div class="flex flex-col gap-1 pt-4 border-t border-slate-100">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-body-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="font-body-md">Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Top App Bar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] bg-surface-container-lowest border-b border-slate-200 flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding z-40">
|
||||
<div class="flex items-center bg-slate-100 rounded-full px-4 py-2 w-96">
|
||||
<span class="material-symbols-outlined text-slate-500 mr-2">search</span>
|
||||
<input class="bg-transparent border-none focus:ring-0 text-body-md w-full placeholder:text-slate-400" placeholder="Search outlets, merchants or device IDs..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 text-on-surface-variant hover:text-primary transition-colors relative">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<button class="p-2 text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">calendar_today</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-8 w-[1px] bg-slate-200"></div>
|
||||
<div class="flex items-center gap-3 pl-2">
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-body-md text-on-surface">Alex Thompson</p>
|
||||
<p class="text-[11px] text-slate-500 font-medium">System Administrator</p>
|
||||
</div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full border-2 border-primary/10 object-cover" data-alt="A professional headshot of a middle-aged male administrator with a confident expression, wearing a navy blue blazer over a crisp white shirt. The background is a clean, out-of-focus corporate office environment with soft morning light streaming through windows, creating a bright and reliable fintech brand atmosphere." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCfm3dzHUMfLnejOKLG86RpDDoOIC_aJ0t45ABgJ5YaYjekFiCKZRbQFQBQWxfIPoLQwcr-fDLAuZitkNCuzNl6nmWhAPXh2VNBqTNprDonr9mihjslDaCp_mhjhpaGc4kMoZn9nCIDw-OAf1La-TUaW2Q9Atq25jAjaFbEPiccJ_kmTsxKMk6qZHry98H2eXDnsx9H5zVxWZ_b3p8TeapM2JORU7aqAaySzUQNJiTq5rdZdgPFvk82Dg3jCul06N4NWziRM6t7IaE"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64 pt-[72px] p-page-padding min-h-screen">
|
||||
<!-- Breadcrumbs & Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<nav class="flex items-center gap-2 text-label-md text-slate-500 mb-2">
|
||||
<a class="hover:text-primary transition-colors" href="#">Merchants</a>
|
||||
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
||||
<a class="hover:text-primary transition-colors" href="#">Global Retail Group</a>
|
||||
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
|
||||
<span class="text-primary font-bold">Outlets</span>
|
||||
</nav>
|
||||
<h2 class="font-display-lg text-display-lg text-on-surface">Outlet Registry</h2>
|
||||
<p class="text-on-surface-variant mt-1">Managing 14 operational nodes across 3 regions for Global Retail Group.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button class="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 rounded-xl font-bold text-on-surface-variant hover:bg-slate-50 transition-all active:scale-95">
|
||||
<span class="material-symbols-outlined text-[20px]">filter_list</span>
|
||||
Filter
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-4 py-2.5 bg-primary text-white rounded-xl font-bold hover:shadow-lg hover:shadow-primary/30 transition-all active:scale-95">
|
||||
<span class="material-symbols-outlined text-[20px]">add</span>
|
||||
Add New Outlet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- KPI Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider mb-1">Total Outlets</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-metric-lg text-metric-lg text-on-surface">14</span>
|
||||
<span class="font-metric-sm text-metric-sm text-success flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-400 mt-2">Active across 4 merchant accounts</p>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider mb-1">Active Devices</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-metric-lg text-metric-lg text-on-surface">142</span>
|
||||
<span class="font-metric-sm text-metric-sm text-success flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]">check_circle</span>
|
||||
98%
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-400 mt-2">3 currently in maintenance</p>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider mb-1">Daily GMV</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-metric-lg text-metric-lg text-on-surface">$124,502</span>
|
||||
<span class="font-metric-sm text-metric-sm text-success flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]">trending_up</span>
|
||||
12.4%
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-400 mt-2">Vs. previous 24h average</p>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider mb-1">Health Score</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-metric-lg text-metric-lg text-on-surface">A+</span>
|
||||
<span class="font-metric-sm text-metric-sm text-info flex items-center">
|
||||
<span class="material-symbols-outlined text-[16px]">verified</span>
|
||||
Stable
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-400 mt-2">Network latency: 142ms avg</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data Table Section -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-slate-200 flex justify-between items-center bg-slate-50/50">
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Outlet Fleet</h3>
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
</button>
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto custom-scrollbar">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-white border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white">Outlet Name</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white">Location</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white">Active Devices</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white text-right">Daily GMV</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white">Status</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider sticky top-0 bg-white text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-primary group-hover:bg-primary/10 transition-colors">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Downtown Flagship</p>
|
||||
<p class="text-[12px] text-slate-500 mono">OUT-49201-DF</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">location_on</span>
|
||||
<span class="text-body-md text-on-surface-variant">San Francisco, CA</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-lg font-bold text-on-surface">24</span>
|
||||
<div class="flex -space-x-2">
|
||||
<div class="w-6 h-6 rounded-full bg-success/20 border-2 border-white flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||
</div>
|
||||
<div class="w-6 h-6 rounded-full bg-success/20 border-2 border-white flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||
</div>
|
||||
<div class="w-6 h-6 rounded-full bg-slate-100 border-2 border-white flex items-center justify-center text-[8px] font-bold">+22</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="text-body-lg font-bold text-on-surface mono">$12,450.00</p>
|
||||
<p class="text-[11px] text-success flex items-center justify-end gap-1 font-bold">
|
||||
<span class="material-symbols-outlined text-[12px]">trending_up</span>
|
||||
+4.2%
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success rounded-full text-[12px] font-bold uppercase tracking-tight inline-flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Active
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-200 rounded-lg transition-colors text-slate-500" onclick="openDrawer('Downtown Flagship')">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-primary group-hover:bg-primary/10 transition-colors">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Eastside Mall Branch</p>
|
||||
<p class="text-[12px] text-slate-500 mono">OUT-49202-EM</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">location_on</span>
|
||||
<span class="text-body-md text-on-surface-variant">Austin, TX</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-lg font-bold text-on-surface">18</span>
|
||||
<div class="flex -space-x-2">
|
||||
<div class="w-6 h-6 rounded-full bg-warning/20 border-2 border-white flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-warning animate-pulse"></div>
|
||||
</div>
|
||||
<div class="w-6 h-6 rounded-full bg-slate-100 border-2 border-white flex items-center justify-center text-[8px] font-bold">+17</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="text-body-lg font-bold text-on-surface mono">$8,920.45</p>
|
||||
<p class="text-[11px] text-danger flex items-center justify-end gap-1 font-bold">
|
||||
<span class="material-symbols-outlined text-[12px]">trending_down</span>
|
||||
-2.1%
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning rounded-full text-[12px] font-bold uppercase tracking-tight inline-flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-200 rounded-lg transition-colors text-slate-500" onclick="openDrawer('Eastside Mall Branch')">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-primary group-hover:bg-primary/10 transition-colors">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Airport Terminal A</p>
|
||||
<p class="text-[12px] text-slate-500 mono">OUT-49203-AA</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">location_on</span>
|
||||
<span class="text-body-md text-on-surface-variant">Chicago, IL</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-lg font-bold text-on-surface">32</span>
|
||||
<div class="flex -space-x-2">
|
||||
<div class="w-6 h-6 rounded-full bg-success/20 border-2 border-white flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||
</div>
|
||||
<div class="w-6 h-6 rounded-full bg-slate-100 border-2 border-white flex items-center justify-center text-[8px] font-bold">+31</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="text-body-lg font-bold text-on-surface mono">$24,102.18</p>
|
||||
<p class="text-[11px] text-success flex items-center justify-end gap-1 font-bold">
|
||||
<span class="material-symbols-outlined text-[12px]">trending_up</span>
|
||||
+18.4%
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success rounded-full text-[12px] font-bold uppercase tracking-tight inline-flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Active
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-200 rounded-lg transition-colors text-slate-500" onclick="openDrawer('Airport Terminal A')">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-slate-50 transition-colors group opacity-75">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-slate-400 group-hover:bg-slate-200 transition-colors">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">Legacy Square Node</p>
|
||||
<p class="text-[12px] text-slate-500 mono">OUT-49105-LS</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-400">location_on</span>
|
||||
<span class="text-body-md text-on-surface-variant">Boston, MA</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-body-lg font-bold text-on-surface">0</span>
|
||||
<div class="flex">
|
||||
<div class="w-6 h-6 rounded-full bg-slate-200 border-2 border-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="text-body-lg font-bold text-slate-400 mono">$0.00</p>
|
||||
<p class="text-[11px] text-slate-400 flex items-center justify-end gap-1 font-bold">
|
||||
<span class="material-symbols-outlined text-[12px]">horizontal_rule</span>
|
||||
0%
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-slate-100 text-slate-500 rounded-full text-[12px] font-bold uppercase tracking-tight inline-flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span>
|
||||
Decommissioned
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-200 rounded-lg transition-colors text-slate-500" onclick="openDrawer('Legacy Square Node')">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 border-t border-slate-200 flex items-center justify-between bg-white">
|
||||
<p class="text-body-md text-slate-500">Showing <span class="font-bold text-on-surface">1-4</span> of <span class="font-bold text-on-surface">14</span> outlets</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined text-[20px]">chevron_left</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 bg-primary text-white font-bold rounded-lg flex items-center justify-center">1</button>
|
||||
<button class="w-10 h-10 border border-slate-200 text-on-surface-variant font-bold rounded-lg flex items-center justify-center hover:bg-slate-50">2</button>
|
||||
<button class="w-10 h-10 border border-slate-200 text-on-surface-variant font-bold rounded-lg flex items-center justify-center hover:bg-slate-50">3</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-[20px]">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Detail Drawer Overlay -->
|
||||
<div class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[60] hidden transition-opacity opacity-0 duration-300" id="drawer-overlay" onclick="closeDrawer()"></div>
|
||||
<!-- Detail Drawer -->
|
||||
<div class="fixed top-0 right-0 h-full w-[450px] bg-white z-[70] shadow-2xl transform translate-x-full transition-transform duration-300 ease-in-out border-l border-slate-200" id="detail-drawer">
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Drawer Header -->
|
||||
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface" id="drawer-title">Outlet Details</h2>
|
||||
<p class="text-on-surface-variant mono text-[12px] uppercase mt-1" id="drawer-subtitle">Registry Details & Performance</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full text-slate-500 transition-colors" onclick="closeDrawer()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Drawer Content -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
|
||||
<!-- Quick Map / Location Placeholder -->
|
||||
<div class="relative w-full h-40 bg-slate-100 rounded-xl overflow-hidden group">
|
||||
<img alt="Outlet Location Map" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" data-alt="A high-quality digital map visualization of a city grid with a glowing primary blue marker indicating a retail outlet's exact location. The map style is minimalist and professional, featuring clean lines, light slate roads, and subtle shadows. The atmosphere is data-driven and high-tech, perfect for a fintech admin console." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBgJJ_Pffdd01siqz80ATGDmpsX4M_Q9e8N61y1NBHxgqAi9FlNNaGbXx63jVU01gYYbGsWqpuRgd0ugP5iAWeezv7VUCS-nqsQ0_8jFidoh-nutfjRUZzCOF4PhgWpPMCiuGc9EeohMZrIWRj6ytjA6mDz0NRHQamTaSj98xXa1gaM9rszxzOpjq2Vr-RFuXsne7OYxFzzWw5JJF_MMASJN9rc5RxaWTcrXBbCqTbxYnlQpSuewUZ9U-wl-RadYy_DBWZ6OkRbPCo"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent flex items-end p-4">
|
||||
<p class="text-white font-bold flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px]">location_on</span>
|
||||
View on Live Map
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Metrics Bento Section -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-4 bg-slate-50 border border-slate-100 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Weekly Volume</p>
|
||||
<p class="font-headline-md text-headline-md text-on-surface">$84,203.44</p>
|
||||
<p class="text-[11px] text-success font-bold mt-1 flex items-center">
|
||||
<span class="material-symbols-outlined text-[14px]">arrow_upward</span>
|
||||
8.2%
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 bg-slate-50 border border-slate-100 rounded-xl">
|
||||
<p class="font-label-md text-label-md text-slate-500 mb-1">Avg. Transaction</p>
|
||||
<p class="font-headline-md text-headline-md text-on-surface">$42.10</p>
|
||||
<p class="text-[11px] text-slate-400 mt-1">Based on 2.4k txn</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Device Lifecycle Timeline -->
|
||||
<div>
|
||||
<h4 class="font-headline-md text-headline-md text-on-surface mb-4">Device Fleet Status</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="mt-1 flex flex-col items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-success"></div>
|
||||
<div class="w-0.5 h-12 bg-slate-200"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">22 Active Soundboxes</p>
|
||||
<p class="text-body-md text-on-surface-variant">Transmitting real-time payment alerts.</p>
|
||||
<p class="text-[11px] text-slate-400 mono mt-1">LATENCY: 114ms</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="mt-1 flex flex-col items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-warning"></div>
|
||||
<div class="w-0.5 h-12 border-l-2 border-dashed border-slate-200"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-on-surface">2 Provisioning</p>
|
||||
<p class="text-body-md text-on-surface-variant">Awaiting SIM activation & merchant link.</p>
|
||||
<p class="text-[11px] text-warning font-bold mt-1 uppercase tracking-tighter">Action Required</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="mt-1 flex flex-col items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-slate-400 text-on-surface">Next Scheduled Audit</p>
|
||||
<p class="text-body-md text-slate-400">Compliance check and hardware diagnostic.</p>
|
||||
<p class="text-[11px] text-slate-400 mono mt-1 italic">DUE IN: 14 DAYS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Raw Payload Viewer (Audit Block) -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h4 class="font-headline-md text-headline-md text-on-surface">Configuration Metadata</h4>
|
||||
<button class="text-primary text-[12px] font-bold flex items-center gap-1 hover:underline">
|
||||
<span class="material-symbols-outlined text-[14px]">content_copy</span>
|
||||
Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-slate-900 rounded-xl p-4 overflow-x-auto">
|
||||
<pre class="mono text-[12px] text-primary-fixed-dim leading-relaxed">{
|
||||
"outlet_id": "OUT-49201-DF",
|
||||
"merchant_group": "GLOBAL_RETAIL_001",
|
||||
"region": "US-WEST-1",
|
||||
"capabilities": ["SOUND_ALERTS", "QR_DYNAMIC"],
|
||||
"encryption_level": "AES-256",
|
||||
"last_heartbeat": "2023-11-24T10:42:12Z",
|
||||
"status": "OPERATIONAL"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Drawer Footer Actions -->
|
||||
<div class="p-6 border-t border-slate-200 flex gap-3 bg-white">
|
||||
<button class="flex-1 py-3 px-4 bg-primary text-white font-bold rounded-xl active:scale-95 transition-all">
|
||||
Edit Configuration
|
||||
</button>
|
||||
<button class="py-3 px-4 border border-slate-200 text-on-surface-variant font-bold rounded-xl active:scale-95 transition-all hover:bg-slate-50">
|
||||
Relocate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openDrawer(title) {
|
||||
const drawer = document.getElementById('detail-drawer');
|
||||
const overlay = document.getElementById('drawer-overlay');
|
||||
const drawerTitle = document.getElementById('drawer-title');
|
||||
|
||||
drawerTitle.innerText = title;
|
||||
|
||||
overlay.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
overlay.classList.remove('opacity-0');
|
||||
overlay.classList.add('opacity-100');
|
||||
drawer.classList.remove('translate-x-full');
|
||||
drawer.classList.add('translate-x-0');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
const drawer = document.getElementById('detail-drawer');
|
||||
const overlay = document.getElementById('drawer-overlay');
|
||||
|
||||
drawer.classList.remove('translate-x-0');
|
||||
drawer.classList.add('translate-x-full');
|
||||
overlay.classList.remove('opacity-100');
|
||||
overlay.classList.add('opacity-0');
|
||||
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/outlet_branch_management/screen.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
6
design/qris_soundbox_logo/code.html
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" rx="40" fill="#2563EB"/>
|
||||
<path d="M60 70C60 64.4772 64.4772 60 70 60H130C135.523 60 140 64.4772 140 70V130C140 135.523 135.523 140 130 140H70C64.4772 140 60 135.523 60 130V70Z" stroke="white" stroke-width="12"/>
|
||||
<rect x="80" y="80" width="40" height="40" rx="4" fill="white"/>
|
||||
<path d="M150 140L170 160" stroke="white" stroke-width="12" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
BIN
design/qris_soundbox_logo/screen.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
484
design/settlement_batch_management/code.html
Normal file
@ -0,0 +1,484 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Soundbox Ops - Disbursement Batches</title>
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet"/>
|
||||
<!-- Icons -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-surface-variant": "#434655",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-container": "#54647a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary": "#943700",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"on-surface": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-900": "#0F172A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #E2E8F0; border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #CBD5E1; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-md overflow-hidden">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 flex flex-col py-6 px-4 gap-2 bg-surface-container-lowest border-r border-slate-200 z-50">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Soundbox Ops</h1>
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
<span>Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span>Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span>Transactions</span>
|
||||
</a>
|
||||
<!-- Active Tab -->
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance" style="font-variation-settings: 'FILL' 1;">account_balance</span>
|
||||
<span>Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
<span>Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-slate-100 pt-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Wrapper -->
|
||||
<main class="ml-64 flex flex-col h-screen">
|
||||
<!-- TopNavBar -->
|
||||
<header class="flex justify-between items-center w-full px-page-padding h-[72px] bg-surface-container-lowest border-b border-slate-200 z-40 sticky top-0">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative w-64 group">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-primary transition-colors" data-icon="search">search</span>
|
||||
<input class="w-full bg-slate-50 border-none rounded-lg pl-10 pr-4 py-2 text-body-md focus:ring-2 focus:ring-primary/20 focus:bg-white transition-all" placeholder="Search batch or merchant..." type="text"/>
|
||||
</div>
|
||||
<nav class="hidden md:flex gap-6 items-center">
|
||||
<a class="font-body-md font-bold text-primary border-b-2 border-primary h-[72px] flex items-center" href="#">Dashboard</a>
|
||||
<a class="font-body-md text-on-surface-variant hover:text-primary transition-colors h-[72px] flex items-center" href="#">System Health</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full text-on-surface-variant hover:bg-slate-100 transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full text-on-surface-variant hover:bg-slate-100 transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full border border-slate-200" data-alt="A professional headshot of a software administrator in a modern office environment. The person is smiling warmly, conveying reliability and expertise. The background is a blurred high-tech workspace with soft bokeh of server lights and ergonomic furniture, maintaining a bright and corporate fintech aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDCXJdUBYnDdTG4e4iVIJeHJrev3W2zGYRLdM3emsKaeC8PvwIrFoadeyKJZZ7SIJabLa-H9EorlbbzSPmNWhWs0YsN05TP47OoT2YADkybLLRKe-kFBF5_RNbbwYXLsmQ9v46FgtXe_LJzWgJszNgEV8NKb44QLRMVEBkm-E440mJndyT1ozKuIYs4I5TgESnc5wcan9mtsaOMeE-2PE4ripuaqVI26dygzI4OH_k7m5KYG6BStH2NDH8nYorDhxlWWfwB8CUJQWU"/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Content Canvas -->
|
||||
<div class="flex-1 overflow-y-auto p-page-padding">
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h2 class="font-display-lg text-display-lg text-on-surface">Disbursement Batches</h2>
|
||||
<p class="font-body-lg text-slate-500 mt-1">Manage and track bulk merchant payouts across all bank partners.</p>
|
||||
</div>
|
||||
<button class="inline-flex items-center gap-2 bg-primary text-white px-6 py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all shadow-lg shadow-primary/20">
|
||||
<span class="material-symbols-outlined" data-icon="add_circle">add_circle</span>
|
||||
Generate New Batch
|
||||
</button>
|
||||
</div>
|
||||
<!-- Dashboard KPIs -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Pending Payouts</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">₹ 14.2M</h3>
|
||||
<span class="font-metric-sm text-success flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="trending_up">trending_up</span> 12%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Processing</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">24</h3>
|
||||
<span class="font-metric-sm text-warning flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="sync">sync</span> Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Avg. Success Rate</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">99.4%</h3>
|
||||
<span class="font-metric-sm text-success flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="check_circle">check_circle</span> Stable
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Total Fees (MTD)</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">₹ 420K</h3>
|
||||
<span class="font-metric-sm text-slate-500">vs ₹ 380K</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filters & Controls -->
|
||||
<div class="bg-white border border-slate-200 rounded-t-xl p-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg hover:border-primary cursor-pointer transition-colors">
|
||||
<span class="material-symbols-outlined text-slate-400" data-icon="account_balance">account_balance</span>
|
||||
<select class="bg-transparent border-none p-0 text-body-md focus:ring-0 cursor-pointer">
|
||||
<option>All Bank Partners</option>
|
||||
<option>HDFC Bank</option>
|
||||
<option>ICICI Bank</option>
|
||||
<option>Yes Bank</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg hover:border-primary cursor-pointer transition-colors">
|
||||
<span class="material-symbols-outlined text-slate-400" data-icon="calendar_month">calendar_month</span>
|
||||
<span class="text-body-md">Oct 01 - Oct 15, 2023</span>
|
||||
</div>
|
||||
<button class="text-primary font-bold text-body-md px-4 py-2 hover:bg-slate-50 rounded-lg transition-colors">Clear All</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-label-md text-slate-500">Sorted by Batch Date</span>
|
||||
<button class="p-2 text-slate-400 hover:text-on-surface transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="filter_list">filter_list</span>
|
||||
</button>
|
||||
<button class="p-2 text-slate-400 hover:text-on-surface transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="download">download</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data Table -->
|
||||
<div class="bg-white border-x border-b border-slate-200 rounded-b-xl overflow-hidden shadow-sm">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200 h-[52px]">
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider">Batch ID</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider">Period</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider text-right">Merchants</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider text-right">Gross Amount</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider text-right">Total Fees</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider text-right">Payout Amount</th>
|
||||
<th class="px-6 font-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231015-01</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 15, 08:00 - 12:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">1,240</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 4,24,500.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 1,240.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 4,23,260.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2: Processing -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231015-02</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 15, 12:00 - 16:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">850</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 2,12,000.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 850.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 2,11,150.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-warning/10 text-warning font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning animate-pulse"></span> Processing
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3: Failed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-42</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 20:00 - 23:59</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">412</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 95,400.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 412.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 94,988.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-danger/10 text-danger font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger"></span> Failed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-41</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 16:00 - 20:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">2,104</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 12,45,200.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 2,104.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 12,43,096.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-40</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 12:00 - 16:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">1,892</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 8,12,050.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 1,892.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 8,10,158.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination -->
|
||||
<div class="bg-white px-6 py-4 border-t border-slate-100 flex items-center justify-between">
|
||||
<p class="text-body-md text-slate-500">Showing <span class="font-bold text-on-surface">1 - 5</span> of 248 batches</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined" data-icon="chevron_left">chevron_left</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 bg-primary text-white font-bold rounded-lg">1</button>
|
||||
<button class="w-10 h-10 hover:bg-slate-50 rounded-lg text-body-md">2</button>
|
||||
<button class="w-10 h-10 hover:bg-slate-50 rounded-lg text-body-md">3</button>
|
||||
<span class="px-2 text-slate-400">...</span>
|
||||
<button class="w-10 h-10 hover:bg-slate-50 rounded-lg text-body-md">48</button>
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined" data-icon="chevron_right">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Detailed Drawer (Hidden by Default) -->
|
||||
<div class="fixed inset-y-0 right-0 w-[480px] bg-white shadow-2xl z-[60] translate-x-full transition-transform duration-300 ease-in-out border-l border-slate-200 flex flex-col" id="detailDrawer">
|
||||
<div class="p-6 border-b border-slate-100 flex items-center justify-between">
|
||||
<h3 class="font-headline-md text-headline-md">Batch Details</h3>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 transition-colors" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="font-label-md text-slate-500 uppercase">Batch ID</span>
|
||||
<span class="font-body-md font-bold" id="drawerBatchId">#BAT-20231015-01</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="font-label-md text-slate-500 uppercase">Settlement Bank</span>
|
||||
<span class="flex items-center gap-2 font-body-md">
|
||||
<span class="w-6 h-6 bg-blue-100 rounded flex items-center justify-center text-[10px] font-bold text-blue-700">HDFC</span>
|
||||
HDFC Bank Ltd.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-xl p-4 mb-8">
|
||||
<p class="font-label-md text-slate-500 mb-3 uppercase">Verification Progress</p>
|
||||
<div class="relative h-2 w-full bg-slate-200 rounded-full overflow-hidden mb-2">
|
||||
<div class="absolute inset-y-0 left-0 bg-success w-[100%] transition-all duration-1000"></div>
|
||||
</div>
|
||||
<p class="text-label-md text-success font-bold">100% KYC Verified & Cleaned</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<h4 class="font-body-md font-bold text-on-surface">Timeline</h4>
|
||||
<div class="relative pl-6 border-l-2 border-slate-100 space-y-8">
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Batch Initialized</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 08:00 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Merchant Ledger Locked</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 08:15 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Bank File Uploaded (SFTP)</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 09:30 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Batch Settled</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 11:45 AM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase">Raw API Response</p>
|
||||
<div class="bg-slate-900 rounded-lg p-4 font-mono text-[12px] text-primary-fixed-dim relative group">
|
||||
<button class="absolute top-2 right-2 text-slate-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="content_copy">content_copy</span>
|
||||
</button>
|
||||
<pre>{
|
||||
"batch_id": "BAT-20231015-01",
|
||||
"status": "COMPLETED",
|
||||
"merchant_count": 1240,
|
||||
"net_payout": 423260.00,
|
||||
"currency": "INR",
|
||||
"bank_ref": "HDFC_91230491_SET"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-100 bg-slate-50 flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 bg-white border border-slate-200 rounded-xl font-bold hover:bg-slate-100 transition-colors">Download CSV</button>
|
||||
<button class="flex-1 px-4 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors">Re-run Reconciliation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
if (drawer.classList.contains('translate-x-full')) {
|
||||
drawer.classList.remove('translate-x-full');
|
||||
} else {
|
||||
drawer.classList.add('translate-x-full');
|
||||
}
|
||||
}
|
||||
|
||||
// Simulating row click to open drawer
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const batchId = row.querySelector('td:first-child').textContent;
|
||||
document.getElementById('drawerBatchId').textContent = batchId;
|
||||
toggleDrawer();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/settlement_batch_management/screen.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
201
design/soundbox_ops/DESIGN.md
Normal file
@ -0,0 +1,201 @@
|
||||
---
|
||||
name: Soundbox Ops
|
||||
colors:
|
||||
surface: '#faf8ff'
|
||||
surface-dim: '#d9d9e5'
|
||||
surface-bright: '#faf8ff'
|
||||
surface-container-lowest: '#ffffff'
|
||||
surface-container-low: '#f3f3fe'
|
||||
surface-container: '#ededf9'
|
||||
surface-container-high: '#e7e7f3'
|
||||
surface-container-highest: '#e1e2ed'
|
||||
on-surface: '#191b23'
|
||||
on-surface-variant: '#434655'
|
||||
inverse-surface: '#2e3039'
|
||||
inverse-on-surface: '#f0f0fb'
|
||||
outline: '#737686'
|
||||
outline-variant: '#c3c6d7'
|
||||
surface-tint: '#0053db'
|
||||
primary: '#004ac6'
|
||||
on-primary: '#ffffff'
|
||||
primary-container: '#2563eb'
|
||||
on-primary-container: '#eeefff'
|
||||
inverse-primary: '#b4c5ff'
|
||||
secondary: '#505f76'
|
||||
on-secondary: '#ffffff'
|
||||
secondary-container: '#d0e1fb'
|
||||
on-secondary-container: '#54647a'
|
||||
tertiary: '#943700'
|
||||
on-tertiary: '#ffffff'
|
||||
tertiary-container: '#bc4800'
|
||||
on-tertiary-container: '#ffede6'
|
||||
error: '#ba1a1a'
|
||||
on-error: '#ffffff'
|
||||
error-container: '#ffdad6'
|
||||
on-error-container: '#93000a'
|
||||
primary-fixed: '#dbe1ff'
|
||||
primary-fixed-dim: '#b4c5ff'
|
||||
on-primary-fixed: '#00174b'
|
||||
on-primary-fixed-variant: '#003ea8'
|
||||
secondary-fixed: '#d3e4fe'
|
||||
secondary-fixed-dim: '#b7c8e1'
|
||||
on-secondary-fixed: '#0b1c30'
|
||||
on-secondary-fixed-variant: '#38485d'
|
||||
tertiary-fixed: '#ffdbcd'
|
||||
tertiary-fixed-dim: '#ffb596'
|
||||
on-tertiary-fixed: '#360f00'
|
||||
on-tertiary-fixed-variant: '#7d2d00'
|
||||
background: '#F8FAFC'
|
||||
on-background: '#191b23'
|
||||
surface-variant: '#e1e2ed'
|
||||
success: '#16A34A'
|
||||
warning: '#F59E0B'
|
||||
danger: '#DC2626'
|
||||
info: '#0EA5E9'
|
||||
slate-900: '#0F172A'
|
||||
slate-700: '#334155'
|
||||
slate-500: '#64748B'
|
||||
slate-200: '#E2E8F0'
|
||||
slate-100: '#F1F5F9'
|
||||
typography:
|
||||
display-lg:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 36px
|
||||
fontWeight: '600'
|
||||
lineHeight: 44px
|
||||
letterSpacing: -0.02em
|
||||
headline-lg:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 28px
|
||||
fontWeight: '600'
|
||||
lineHeight: 36px
|
||||
headline-md:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 20px
|
||||
fontWeight: '600'
|
||||
lineHeight: 28px
|
||||
body-lg:
|
||||
fontFamily: Inter
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: 24px
|
||||
body-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 14px
|
||||
fontWeight: '400'
|
||||
lineHeight: 20px
|
||||
label-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 12px
|
||||
fontWeight: '500'
|
||||
lineHeight: 16px
|
||||
letterSpacing: 0.01em
|
||||
metric-lg:
|
||||
fontFamily: Inter
|
||||
fontSize: 32px
|
||||
fontWeight: '600'
|
||||
lineHeight: 40px
|
||||
metric-sm:
|
||||
fontFamily: Inter
|
||||
fontSize: 14px
|
||||
fontWeight: '600'
|
||||
lineHeight: 20px
|
||||
rounded:
|
||||
sm: 0.125rem
|
||||
DEFAULT: 0.25rem
|
||||
md: 0.375rem
|
||||
lg: 0.5rem
|
||||
xl: 0.75rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
topbar-height: 72px
|
||||
page-padding: 24px
|
||||
card-padding: 20px
|
||||
row-height: 52px
|
||||
gutter: 24px
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
|
||||
The design system is engineered for the high-stakes environment of fintech operations. The brand personality is **Precise, Reliable, and Proactive**, focusing on the seamless bridge between digital ledgers and physical IoT soundboxes.
|
||||
|
||||
The chosen style is **Corporate / Modern** with a heavy emphasis on **Data-Centricity**. This is achieved through a structured information hierarchy that prioritizes rapid scanning of transaction states and device health. The interface utilizes high-density layouts, subtle tonal layering to separate operational controls from data displays, and a functional aesthetic that avoids unnecessary decoration in favor of utility and speed.
|
||||
|
||||
**Key visual principles:**
|
||||
- **Clarity over Flourish:** Every pixel must serve a functional purpose in the monitoring workflow.
|
||||
- **Immediate Feedback:** Use of distinct semantic colors to highlight system anomalies or successful settlements.
|
||||
- **Operational Efficiency:** Minimizing clicks through the use of drawers and context-aware action bars.
|
||||
|
||||
## Colors
|
||||
|
||||
The palette is anchored by **Primary Blue (#2563EB)** to evoke trust and established financial authority. The neutral foundation is built exclusively on the **Slate** scale, providing a cool, professional backdrop that allows semantic alerts to stand out.
|
||||
|
||||
**Usage Guidance:**
|
||||
- **Primary:** Actionable elements, active navigation states, and primary buttons.
|
||||
- **Semantic Colors:** Reserved strictly for status indicators (Success for 'Settled', Warning for 'Degraded', Danger for 'Offline/Failed').
|
||||
- **Neutrals:** `Slate-900` for primary headings, `Slate-700` for body text, and `Slate-100/200` for borders and subtle backgrounds.
|
||||
- **Background:** The `F8FAFC` base provides a low-strain environment for long-duration operational shifts.
|
||||
|
||||
## Typography
|
||||
|
||||
This design system employs a dual-font strategy. **Plus Jakarta Sans** is used for headings to provide a modern, clean fintech feel, while **Inter** is used for all body and functional text for maximum legibility at small sizes.
|
||||
|
||||
**Tabular Numbers:**
|
||||
Crucial for a platform dealing with currency (IDR) and device IDs, all metric-related typography must have `font-variant-numeric: tabular-nums` enabled. This ensures that columns of numbers align perfectly in data tables and KPI cards, facilitating easier comparison.
|
||||
|
||||
**Scale and Hierarchy:**
|
||||
- Headlines use Semibold (`600`) to anchor page sections.
|
||||
- Body text remains Regular (`400`) for extended reading in audit logs.
|
||||
- Labels use Medium (`500`) with slight letter spacing for specialized metadata.
|
||||
|
||||
## Layout & Spacing
|
||||
|
||||
The system follows a **12-column fluid grid** for the main content area, allowing the platform to scale from large monitoring displays to standard laptops.
|
||||
|
||||
**Key Layout Rules:**
|
||||
- **Fixed Infrastructure:** The topbar is locked at `72px` and the sidebar remains visible for Admin/Merchant portals to provide persistent global context.
|
||||
- **Density:** We utilize a "Comfortable-Compact" rhythm. Page margins are set to `24px`, but internal card components and data tables use tighter spacing to maximize information density without sacrificing touch/click targets.
|
||||
- **Vertical Rhythm:** A base `4px` / `8px` grid governs all spacing. Data tables utilize a consistent `52px` row height to accommodate status chips and action buttons comfortably.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
To maintain a "fast and clean" feel, the system avoids heavy drop shadows. Instead, it utilizes **Tonal Layers** and **Low-Contrast Outlines**.
|
||||
|
||||
- **Level 0 (Surface):** `#F8FAFC` (Background) - The lowest layer.
|
||||
- **Level 1 (Cards/Container):** `#FFFFFF` with a `1px` border of `Slate-200`. This is the primary surface for all data widgets.
|
||||
- **Level 2 (Interactive):** Elements that require focus (like drawers or modals) use a very soft, diffused shadow (`0 10px 15px -3px rgb(0 0 0 / 0.1)`) to lift them above the data grid.
|
||||
- **Depth via Border:** Sticky headers in tables use a `1px` bottom border in `Slate-200` rather than a shadow to indicate they are "above" the scrolling content while maintaining a flat, professional profile.
|
||||
|
||||
## Shapes
|
||||
|
||||
The design system adopts a **Soft** shape language. A `0.25rem` (4px) base radius is used for most UI elements, which maintains a professional, structured look while appearing modern.
|
||||
|
||||
- **Components:** Buttons, Input fields, and KPI cards use the base `rounded` (4px) or `rounded-lg` (8px).
|
||||
- **Status Chips:** Use `rounded-full` (pill-shaped) to distinguish them from interactive buttons.
|
||||
- **Icons:** Should follow the same geometric principles—no sharp 90-degree corners, but not overly rounded or "bubbly."
|
||||
|
||||
## Components
|
||||
|
||||
### KPI Cards
|
||||
Standardized containers for top-level metrics. They include a `label-md` for title, a `metric-lg` for the value, and a bottom section for `metric-sm` trend indicators (e.g., +12% vs last month).
|
||||
|
||||
### Data Tables
|
||||
- **Sticky Header:** Background remains white with a `Slate-200` bottom border during scroll.
|
||||
- **Numeric Alignment:** Amounts and percentages are right-aligned using tabular numbers.
|
||||
- **Row Hover:** Use `Slate-50` background on hover to facilitate line scanning.
|
||||
|
||||
### Status Chips
|
||||
Pill-shaped badges with low-opacity backgrounds and high-contrast text:
|
||||
- **Active/Online/Settled:** Green (Success)
|
||||
- **Pending/Retrying:** Orange (Warning)
|
||||
- **Failed/Offline:** Red (Danger)
|
||||
- **Default/Inactive:** Slate-500 (Neutral)
|
||||
|
||||
### Vertical Timelines
|
||||
Used for transaction history and device lifecycle. Dots represent states; solid lines represent completed paths, and dashed lines represent pending/future steps.
|
||||
|
||||
### Audit Blocks (Raw Payload Viewer)
|
||||
Monospaced font (`Courier Prime` or `JetBrains Mono` as an alternative), contained in a `Slate-900` box with a 'Copy' action always present in the top-right corner.
|
||||
|
||||
### Detail Drawers
|
||||
Standardized side-panel that slides from the right. It allows for "quick inspection" of a merchant or device without losing the context of the main list.
|
||||
657
design/transaction_history_monitoring/code.html
Normal file
@ -0,0 +1,657 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Transactions | Soundbox Ops</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #F8FAFC;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #E2E8F0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.mono-text {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"slate-200": "#E2E8F0",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ededf9",
|
||||
"on-background": "#191b23",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-primary": "#ffffff",
|
||||
"background": "#F8FAFC",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"info": "#0EA5E9",
|
||||
"on-surface-variant": "#434655",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"on-error": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#38485d",
|
||||
"on-primary-container": "#eeefff",
|
||||
"error": "#ba1a1a",
|
||||
"on-secondary-container": "#54647a",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"surface-variant": "#e1e2ed",
|
||||
"tertiary": "#943700",
|
||||
"surface": "#faf8ff",
|
||||
"tertiary-container": "#bc4800",
|
||||
"secondary-fixed": "#d3e4fe",
|
||||
"primary": "#004ac6",
|
||||
"on-error-container": "#93000a",
|
||||
"on-surface": "#191b23",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"slate-900": "#0F172A",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"outline-variant": "#c3c6d7",
|
||||
"on-secondary-fixed": "#0b1c30",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"secondary-container": "#d0e1fb",
|
||||
"secondary": "#505f76",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"danger": "#DC2626",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed-dim": "#b7c8e1",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"slate-100": "#F1F5F9",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"success": "#16A34A",
|
||||
"primary-container": "#2563eb",
|
||||
"surface-tint": "#0053db",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"slate-700": "#334155",
|
||||
"inverse-surface": "#2e3039",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"warning": "#F59E0B",
|
||||
"outline": "#737686",
|
||||
"slate-500": "#64748B"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"page-padding": "24px",
|
||||
"topbar-height": "72px",
|
||||
"row-height": "52px",
|
||||
"gutter": "24px",
|
||||
"card-padding": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Plus Jakarta Sans"],
|
||||
"label-md": ["Inter"],
|
||||
"metric-lg": ["Inter"],
|
||||
"headline-md": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"metric-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
||||
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
||||
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
||||
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="w-64 h-full fixed left-0 top-0 flex flex-col py-6 px-4 gap-2 bg-surface-container-lowest dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 z-50">
|
||||
<div class="px-4 mb-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">Soundbox Ops</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
Overview
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
Merchant Management
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
Device Registry
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container dark:bg-secondary text-on-secondary-container dark:text-on-secondary font-bold rounded-lg font-body-md text-body-md" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
Transactions
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
|
||||
Ledger & Settlement
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
Audit Control
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto pt-6 border-t border-slate-100">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
Settings
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant dark:text-slate-400 font-body-md text-body-md hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- TopNavBar -->
|
||||
<header class="fixed top-0 right-0 h-[72px] flex justify-between items-center w-[calc(100%-256px)] ml-64 px-page-padding bg-surface-container-lowest dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 z-40">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative w-96">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]" data-icon="search">search</span>
|
||||
<input class="w-full bg-slate-100 border-none rounded-xl py-2 pl-10 pr-4 text-body-md focus:ring-2 focus:ring-primary/20" placeholder="Search TxID, Merchant, or RRN..." type="text"/>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<a class="text-primary dark:text-primary-fixed border-b-2 border-primary h-[72px] flex items-center font-body-md" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 hover:text-primary h-[72px] flex items-center font-body-md" href="#">System Health</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="p-2 text-on-surface-variant hover:bg-slate-100 rounded-full transition-colors relative">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<button class="p-2 text-on-surface-variant hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-slate-200 mx-2"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md font-bold">Admin User</p>
|
||||
<p class="font-label-md text-[10px] text-slate-500 uppercase tracking-wider">Super Admin</p>
|
||||
</div>
|
||||
<img alt="Administrator Profile" class="w-10 h-10 rounded-full bg-slate-100" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDSeJvTDs1TfYLIgdJ99lOXHT5MY8X9SROFFT_ZKrdyO71EDMx1uVpWWLSdzowrHAbCMudUvLgfWEXLTF554Zm4jU_9PUPfPHUfEgp7sOGPDLWT_nlc2MQWH5CuyWmIpmtnQr6CBb8pL7491sl7kx1fZteImOaTsRYroTGvHLzuUH6BDseXkEq10bJw9YhHKQLpQiy3jTo_pMVRnxI1lwYXOShYCmA9uh9LQv4KArnlqQmJEHpBRghfePXKC6JHWnre2hxKUc0Wyow"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="ml-64 pt-[72px] min-h-screen p-page-padding">
|
||||
<!-- Summary Bar (KPI Cards) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1 uppercase tracking-tight">Total Volume (24h)</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">Rp 1.42B</h3>
|
||||
<span class="font-metric-sm text-metric-sm text-success flex items-center mb-1">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
+12.4%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1 uppercase tracking-tight">Success Rate</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">99.92%</h3>
|
||||
<span class="font-metric-sm text-metric-sm text-success flex items-center mb-1">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="check_circle">check_circle</span>
|
||||
Stable
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1 uppercase tracking-tight">Pending Settlements</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">142</h3>
|
||||
<span class="font-metric-sm text-metric-sm text-warning flex items-center mb-1">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="schedule">schedule</span>
|
||||
-5.2%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant mb-1 uppercase tracking-tight">Active QRIS Soundboxes</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">1,894</h3>
|
||||
<span class="font-metric-sm text-metric-sm text-info flex items-center mb-1">
|
||||
<span class="material-symbols-outlined text-[16px]" data-icon="sensors">sensors</span>
|
||||
98% Online
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filters & Tools Bar -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-4 mb-6 flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">Status:</span>
|
||||
<select class="bg-slate-50 border-slate-200 rounded-lg text-body-md py-1.5 focus:ring-primary/20">
|
||||
<option>All Statuses</option>
|
||||
<option>Success</option>
|
||||
<option>Pending</option>
|
||||
<option>Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">Method:</span>
|
||||
<select class="bg-slate-50 border-slate-200 rounded-lg text-body-md py-1.5 focus:ring-primary/20">
|
||||
<option>QRIS & VA</option>
|
||||
<option>QRIS Only</option>
|
||||
<option>VA Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-md text-label-md text-slate-500">Range:</span>
|
||||
<button class="bg-slate-50 border border-slate-200 px-4 py-1.5 rounded-lg text-body-md flex items-center gap-2 hover:bg-slate-100 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="calendar_month">calendar_month</span>
|
||||
Oct 24, 2023 - Oct 31, 2023
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button class="px-4 py-2 border border-slate-200 rounded-lg text-body-md font-bold flex items-center gap-2 hover:bg-slate-50 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="download">download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-primary text-on-primary rounded-lg text-body-md font-bold flex items-center gap-2 hover:opacity-90 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="add">add</span>
|
||||
New Transaction
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Transactions Table -->
|
||||
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto custom-scrollbar">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider">Timestamp</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider">Transaction ID</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider">Merchant Name</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider">Amount</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider text-right">Fee</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider text-right">Net</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant font-bold uppercase tracking-wider text-center">Status</th>
|
||||
<th class="px-6 py-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Oct 31, 2023</p>
|
||||
<p class="text-[12px] text-slate-500">14:22:05</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="mono-text text-body-md text-primary font-medium">TXN-98421054</span>
|
||||
<p class="text-[11px] text-slate-400">RRN: 310542918420</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-500" data-icon="coffee">coffee</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold">Brew & Co. Central</p>
|
||||
<p class="text-[12px] text-slate-500">QRIS Dynamic</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Rp 45.000</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md text-slate-500">Rp 315</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Rp 44.685</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full bg-success/10 text-success text-[11px] font-bold uppercase flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Settled
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Oct 31, 2023</p>
|
||||
<p class="text-[12px] text-slate-500">14:18:12</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="mono-text text-body-md text-primary font-medium">TXN-98421053</span>
|
||||
<p class="text-[11px] text-slate-400">RRN: 310542918421</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-500" data-icon="shopping_basket">shopping_basket</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold">Mart Express #42</p>
|
||||
<p class="text-[12px] text-slate-500">Static QRIS</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Rp 128.400</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md text-slate-500">Rp 899</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Rp 127.501</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full bg-warning/10 text-warning text-[11px] font-bold uppercase flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Oct 31, 2023</p>
|
||||
<p class="text-[12px] text-slate-500">14:05:44</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="mono-text text-body-md text-primary font-medium">TXN-98421052</span>
|
||||
<p class="text-[11px] text-slate-400">RRN: 310542918422</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-500" data-icon="local_gas_station">local_gas_station</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold">Shell Kemang</p>
|
||||
<p class="text-[12px] text-slate-500">Virtual Account</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Rp 350.000</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md text-slate-500">Rp 4.500</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Rp 345.500</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full bg-danger/10 text-danger text-[11px] font-bold uppercase flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger"></span>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-slate-50 transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Oct 31, 2023</p>
|
||||
<p class="text-[12px] text-slate-500">13:58:31</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="mono-text text-body-md text-primary font-medium">TXN-98421051</span>
|
||||
<p class="text-[11px] text-slate-400">RRN: 310542918423</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px] text-slate-500" data-icon="restaurant">restaurant</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-md text-body-md font-bold">Sushi Tei GI</p>
|
||||
<p class="text-[12px] text-slate-500">QRIS Dynamic</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-body-md text-body-md font-bold">Rp 742.800</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md text-slate-500">Rp 5.200</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<p class="font-body-md text-body-md font-bold text-on-surface">Rp 737.600</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex justify-center">
|
||||
<span class="px-2.5 py-1 rounded-full bg-success/10 text-success text-[11px] font-bold uppercase flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span>
|
||||
Settled
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 text-slate-400 hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex items-center justify-between">
|
||||
<p class="text-body-md text-on-surface-variant">Showing <span class="font-bold text-on-surface">1 - 10</span> of 1,284 transactions</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-3 py-1.5 border border-slate-200 rounded-lg bg-white text-slate-400 hover:text-on-surface transition-colors cursor-not-allowed">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_left">chevron_left</span>
|
||||
</button>
|
||||
<button class="px-3 py-1.5 border border-slate-200 rounded-lg bg-white text-on-surface hover:bg-slate-50 transition-colors">
|
||||
1
|
||||
</button>
|
||||
<button class="px-3 py-1.5 border border-primary bg-primary-container/10 text-primary font-bold rounded-lg">
|
||||
2
|
||||
</button>
|
||||
<button class="px-3 py-1.5 border border-slate-200 rounded-lg bg-white text-on-surface hover:bg-slate-50 transition-colors">
|
||||
3
|
||||
</button>
|
||||
<button class="px-3 py-1.5 border border-slate-200 rounded-lg bg-white text-on-surface hover:bg-slate-50 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_right">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Detail Drawer Overlay -->
|
||||
<div class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[60] opacity-0 pointer-events-none transition-opacity duration-300" id="drawerOverlay" onclick="toggleDrawer()"></div>
|
||||
<!-- Transaction Detail Drawer -->
|
||||
<aside class="fixed top-0 right-0 h-full w-[480px] bg-surface-container-lowest z-[70] translate-x-full transition-transform duration-300 shadow-2xl overflow-y-auto custom-scrollbar" id="detailDrawer">
|
||||
<div class="p-6 border-b border-slate-200 flex items-center justify-between sticky top-0 bg-surface-container-lowest z-10">
|
||||
<div>
|
||||
<h2 class="font-headline-md text-headline-md">Transaction Details</h2>
|
||||
<p class="text-body-md text-on-surface-variant">TXN-98421054</p>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-slate-100 rounded-full transition-colors" onclick="toggleDrawer()">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-8">
|
||||
<!-- Status Card -->
|
||||
<div class="bg-success/5 border border-success/20 rounded-xl p-6 text-center">
|
||||
<div class="w-12 h-12 bg-success text-white rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<span class="material-symbols-outlined text-[28px]" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
</div>
|
||||
<h3 class="text-headline-md text-success mb-1">Rp 45.000</h3>
|
||||
<p class="text-body-md font-bold text-success uppercase tracking-widest">Successfully Settled</p>
|
||||
<p class="text-label-md text-slate-500 mt-2">Processed via QRIS National Pool</p>
|
||||
</div>
|
||||
<!-- Detail Grid -->
|
||||
<div class="grid grid-cols-2 gap-y-6">
|
||||
<div>
|
||||
<p class="text-label-md text-slate-500 uppercase">Merchant</p>
|
||||
<p class="text-body-md font-bold">Brew & Co. Central</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-label-md text-slate-500 uppercase">Merchant ID</p>
|
||||
<p class="text-body-md font-bold mono-text">MID-99201</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-label-md text-slate-500 uppercase">Terminal / Device</p>
|
||||
<p class="text-body-md font-bold">Soundbox v2 (#SB-4402)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-label-md text-slate-500 uppercase">Acquirer</p>
|
||||
<p class="text-body-md font-bold">Bank Indonesia / ASPI</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="text-label-md text-slate-500 uppercase">Retrieval Reference Number (RRN)</p>
|
||||
<p class="text-body-md font-bold mono-text">310542918420</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vertical Timeline -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-bold text-body-md">Transaction Lifecycle</h4>
|
||||
<div class="relative pl-8 space-y-6">
|
||||
<div class="absolute left-3.5 top-2 bottom-2 w-[1px] bg-slate-200"></div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-8 w-7 h-7 bg-primary text-white rounded-full flex items-center justify-center border-4 border-white">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="bolt">bolt</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">QR Generated</p>
|
||||
<p class="text-label-md text-slate-500">Oct 31, 2023 • 14:21:40</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-8 w-7 h-7 bg-primary text-white rounded-full flex items-center justify-center border-4 border-white">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="payments">payments</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Payment Received</p>
|
||||
<p class="text-label-md text-slate-500">Oct 31, 2023 • 14:22:01</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-8 w-7 h-7 bg-success text-white rounded-full flex items-center justify-center border-4 border-white">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-body-md font-bold">Funds Settled to Ledger</p>
|
||||
<p class="text-label-md text-slate-500">Oct 31, 2023 • 14:22:05</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Raw Payload Viewer (Audit Block) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-bold text-body-md">Audit Trail (Raw JSON)</h4>
|
||||
<button class="text-primary text-[12px] font-bold hover:underline flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[14px]" data-icon="content_copy">content_copy</span>
|
||||
Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-slate-900 rounded-lg p-4 text-slate-300 text-[12px] mono-text h-40 overflow-y-auto custom-scrollbar">
|
||||
<pre>{
|
||||
"tx_id": "TXN-98421054",
|
||||
"rrn": "310542918420",
|
||||
"merchant": {
|
||||
"name": "Brew & Co. Central",
|
||||
"mcc": "5812",
|
||||
"postal": "10110"
|
||||
},
|
||||
"qris_data": {
|
||||
"type": "DYNAMIC",
|
||||
"payload": "00020101021226590014ID...",
|
||||
"mdr_rate": 0.007
|
||||
},
|
||||
"ledger": {
|
||||
"gross": 45000,
|
||||
"fee": 315,
|
||||
"net": 44685
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="grid grid-cols-2 gap-4 pt-4">
|
||||
<button class="py-3 border border-slate-200 rounded-lg font-bold text-body-md flex items-center justify-center gap-2 hover:bg-slate-50 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="print">print</span>
|
||||
Print Receipt
|
||||
</button>
|
||||
<button class="py-3 border border-danger/20 text-danger rounded-lg font-bold text-body-md flex items-center justify-center gap-2 hover:bg-danger/5 transition-colors">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="undo">undo</span>
|
||||
Void/Refund
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<script>
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
const overlay = document.getElementById('drawerOverlay');
|
||||
|
||||
if (drawer.classList.contains('translate-x-full')) {
|
||||
drawer.classList.remove('translate-x-full');
|
||||
overlay.classList.remove('opacity-0', 'pointer-events-none');
|
||||
} else {
|
||||
drawer.classList.add('translate-x-full');
|
||||
overlay.classList.add('opacity-0', 'pointer-events-none');
|
||||
}
|
||||
}
|
||||
|
||||
// Atmospheric effect: Pulse the "Pending" dots subtly
|
||||
setInterval(() => {
|
||||
const pendingDots = document.querySelectorAll('.bg-warning');
|
||||
pendingDots.forEach(dot => {
|
||||
dot.style.opacity = '0.5';
|
||||
setTimeout(() => dot.style.opacity = '1', 500);
|
||||
});
|
||||
}, 1500);
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/transaction_history_monitoring/screen.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
65
dist/app.js
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import morgan from "morgan";
|
||||
import { requestContext } from "./shared/middleware/requestContext";
|
||||
import { handleErrors, successResponse } from "./shared/middleware/errorMiddleware";
|
||||
import adminRoutes from "./routes/admin";
|
||||
import integrationRoutes from "./routes/integrations";
|
||||
import deviceRoutes from "./routes/device";
|
||||
import { startNotificationOrchestrator } from "./shared/orchestrators/notificationOrchestrator";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
const app = express();
|
||||
startNotificationOrchestrator();
|
||||
app.use(helmet());
|
||||
app.use(express.json());
|
||||
app.use(morgan("dev"));
|
||||
app.use(requestContext);
|
||||
app.get("/", (_req, res) => {
|
||||
res.json(successResponse(_req, { status: "ok" }));
|
||||
});
|
||||
function resolveUiPageFile(slug) {
|
||||
const workspaceRoot = process.cwd();
|
||||
const candidates = [
|
||||
path.resolve(workspaceRoot, "ui", slug, "index.html"),
|
||||
path.resolve(workspaceRoot, "ui", slug.replace(/_/g, "-"), "index.html"),
|
||||
path.resolve(workspaceRoot, "ui", slug.replace(/-/g, "_"), "index.html")
|
||||
];
|
||||
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
||||
}
|
||||
app.get("/ui", (_req, res) => {
|
||||
const filePath = path.resolve(process.cwd(), "ui/index.html");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
app.get("/ui/hub", (_req, res) => {
|
||||
const filePath = path.resolve(process.cwd(), "ui/hub.html");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
app.get("/ui/:page", (req, res, next) => {
|
||||
const filePath = resolveUiPageFile(req.params.page);
|
||||
if (!filePath) {
|
||||
return next();
|
||||
}
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
app.use("/admin", adminRoutes);
|
||||
app.use("/integrations", integrationRoutes);
|
||||
app.use("/device", deviceRoutes);
|
||||
app.use((err, _req, res, next) => {
|
||||
handleErrors(err, _req, res, next);
|
||||
});
|
||||
app.get("/health", (req, res) => {
|
||||
res.status(200).json(successResponse(req, {
|
||||
status: "healthy",
|
||||
time: new Date().toISOString()
|
||||
}));
|
||||
});
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
code: "NOT_FOUND",
|
||||
message: `Route ${req.path} not found`,
|
||||
request_id: req.requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
export default app;
|
||||
17
dist/config/env.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
export const env = {
|
||||
PORT: Number(process.env.PORT || 3000),
|
||||
ADMIN_TOKEN: process.env.ADMIN_TOKEN || "admin-dev-token",
|
||||
DEVICE_TOKEN: process.env.DEVICE_TOKEN || "device-dev-token",
|
||||
TRACE_HEADER: process.env.TRACE_HEADER || "x-request-id",
|
||||
IDEMPOTENCY_TTL_MS: Number(process.env.IDEMPOTENCY_TTL_MS || 300000),
|
||||
INTEGRATION_WEBHOOK_SECRET: process.env.INTEGRATION_WEBHOOK_SECRET || "dev-callback-secret",
|
||||
MQTT_PUBLISH_FORCE_FAIL_ALL: process.env.MQTT_PUBLISH_FORCE_FAIL_ALL || "false",
|
||||
MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS: process.env.MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS || "",
|
||||
MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS: Number(process.env.MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS || 15000),
|
||||
DATABASE_URL: process.env.DATABASE_URL || "",
|
||||
PGHOST: process.env.PGHOST || "127.0.0.1",
|
||||
PGPORT: Number(process.env.PGPORT || 5432),
|
||||
PGUSER: process.env.PGUSER || "postgres",
|
||||
PGPASSWORD: process.env.PGPASSWORD || "postgres",
|
||||
PGDATABASE: process.env.PGDATABASE || "qris_soundbox_platform"
|
||||
};
|
||||
13
dist/index.js
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import { createServer } from "node:http";
|
||||
import app from "./app";
|
||||
import { ensureSchema } from "./shared/db/pool";
|
||||
import { env } from "./config/env";
|
||||
const port = env.PORT;
|
||||
const server = createServer(app);
|
||||
async function bootstrap() {
|
||||
await ensureSchema();
|
||||
server.listen(port, () => {
|
||||
console.log(`QRIS Soundbox Platform bootstrap ready on :${port}`);
|
||||
});
|
||||
}
|
||||
void bootstrap();
|
||||
1107
dist/routes/admin.js
vendored
Normal file
126
dist/routes/device.js
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
import { Router } from "express";
|
||||
import { ApiError } from "../shared/errors";
|
||||
import { requireDeviceToken } from "../shared/middleware/auth";
|
||||
import { successResponse } from "../shared/middleware/errorMiddleware";
|
||||
import { getDeviceById, patchDevice } from "../shared/store/deviceStore";
|
||||
import { createDeviceHeartbeat } from "../shared/store/heartbeatStore";
|
||||
import { acknowledgeDeviceCommand } from "../shared/store/deviceCommandStore";
|
||||
const router = Router();
|
||||
function normalizeNumberOrNull(value) {
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function normalizeSignalStrength(value) {
|
||||
const normalized = normalizeNumberOrNull(value);
|
||||
if (normalized === null) {
|
||||
return null;
|
||||
}
|
||||
if (normalized < 0 || normalized > 100) {
|
||||
throw new Error("NETWORK_STRENGTH_OUT_OF_RANGE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function normalizeBatteryLevel(value) {
|
||||
const normalized = normalizeNumberOrNull(value);
|
||||
if (normalized === null) {
|
||||
return null;
|
||||
}
|
||||
if (normalized < 0 || normalized > 100) {
|
||||
throw new Error("BATTERY_LEVEL_OUT_OF_RANGE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
router.post("/heartbeat", requireDeviceToken, async (req, res, next) => {
|
||||
const payload = req.body;
|
||||
if (!payload || !payload.device_id) {
|
||||
return next(new ApiError("BAD_REQUEST", "device_id is required", 400));
|
||||
}
|
||||
const device = await getDeviceById(payload.device_id);
|
||||
if (!device) {
|
||||
return next(new ApiError("NOT_FOUND", "device not found", 404));
|
||||
}
|
||||
const eventTs = payload.timestamp ? new Date(payload.timestamp) : new Date();
|
||||
if (Number.isNaN(eventTs.getTime())) {
|
||||
return next(new ApiError("BAD_REQUEST", "timestamp must be valid ISO datetime", 400));
|
||||
}
|
||||
let payloadNetworkStrength;
|
||||
let payloadBattery;
|
||||
try {
|
||||
payloadNetworkStrength = normalizeSignalStrength(payload.network_strength);
|
||||
payloadBattery = normalizeBatteryLevel(payload.battery_level);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === "NETWORK_STRENGTH_OUT_OF_RANGE") {
|
||||
return next(new ApiError("BAD_REQUEST", "network_strength must be between 0 and 100", 400));
|
||||
}
|
||||
if (error instanceof Error && error.message === "BATTERY_LEVEL_OUT_OF_RANGE") {
|
||||
return next(new ApiError("BAD_REQUEST", "battery_level must be between 0 and 100", 400));
|
||||
}
|
||||
return next(error);
|
||||
}
|
||||
const heartbeat = await createDeviceHeartbeat({
|
||||
device_id: payload.device_id,
|
||||
timestamp: eventTs.toISOString(),
|
||||
firmware_version: payload.firmware_version,
|
||||
network_strength: payloadNetworkStrength,
|
||||
battery_level: payloadBattery,
|
||||
state: payload.state,
|
||||
payload_json: {
|
||||
network_strength_raw: payload.network_strength,
|
||||
battery_level_raw: payload.battery_level,
|
||||
state: payload.state,
|
||||
firmware_version: payload.firmware_version,
|
||||
timestamp: payload.timestamp,
|
||||
request_id: req.requestId
|
||||
}
|
||||
});
|
||||
await patchDevice(payload.device_id, {
|
||||
last_seen_at: heartbeat.timestamp,
|
||||
firmware_version: payload.firmware_version || device.firmware_version
|
||||
});
|
||||
res.json(successResponse(req, {
|
||||
heartbeat_id: heartbeat.id,
|
||||
device_id: heartbeat.device_id,
|
||||
request_id: req.requestId,
|
||||
server_time: heartbeat.received_at
|
||||
}));
|
||||
});
|
||||
router.post("/commands/ack", requireDeviceToken, async (req, res, next) => {
|
||||
const payload = req.body;
|
||||
if (!payload || !payload.command_id || !payload.device_id || !payload.status) {
|
||||
return next(new ApiError("BAD_REQUEST", "command_id, device_id, status are required", 400));
|
||||
}
|
||||
if (!["delivered", "failed", "timeout"].includes(payload.status)) {
|
||||
return next(new ApiError("BAD_REQUEST", "status must be delivered, failed, or timeout", 400));
|
||||
}
|
||||
const device = await getDeviceById(payload.device_id);
|
||||
if (!device) {
|
||||
return next(new ApiError("NOT_FOUND", "device not found", 404));
|
||||
}
|
||||
const updated = await acknowledgeDeviceCommand({
|
||||
device_id: device.id,
|
||||
command_id: payload.command_id,
|
||||
status: payload.status,
|
||||
reason: payload.reason,
|
||||
result_payload: payload.result_payload
|
||||
});
|
||||
if (!updated) {
|
||||
return next(new ApiError("NOT_FOUND", "command not found", 404));
|
||||
}
|
||||
res.json(successResponse(req, {
|
||||
command_id: updated.id,
|
||||
device_id: updated.device_id,
|
||||
status: updated.status,
|
||||
acknowledged_at: updated.acknowledged_at
|
||||
}));
|
||||
});
|
||||
export default router;
|
||||
242
dist/routes/integrations.js
vendored
Normal file
@ -0,0 +1,242 @@
|
||||
import { Router } from "express";
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import { ApiError } from "../shared/errors";
|
||||
import { successResponse } from "../shared/middleware/errorMiddleware";
|
||||
import { readIdempotency, writeIdempotency } from "../shared/idempotency/idempotencyStore";
|
||||
import { addTransactionEvent, findTransactionByPartnerReference, getTransactionEvents, updateTransactionStatus } from "../shared/store/transactionStore";
|
||||
import { emitTransactionPaid } from "../shared/events/transactionEvents";
|
||||
import { env } from "../config/env";
|
||||
const router = Router();
|
||||
function parsePaymentStatus(rawStatus) {
|
||||
const normalized = String(rawStatus || "").toLowerCase();
|
||||
if (["paid", "success", "successful", "settled", "completed"].includes(normalized)) {
|
||||
return { status: "paid" };
|
||||
}
|
||||
if (["failed", "declined", "rejected", "error", "cancelled"].includes(normalized)) {
|
||||
return { status: "failed" };
|
||||
}
|
||||
if (["expired", "timeout", "stale"].includes(normalized)) {
|
||||
return { status: "expired" };
|
||||
}
|
||||
return { status: "failed", reason: "UNKNOWN_STATUS" };
|
||||
}
|
||||
function verifySignature(payload, signature) {
|
||||
if (!signature) {
|
||||
throw new ApiError("WEBHOOK_SIGNATURE_INVALID", "missing X-Partner-Signature", 401);
|
||||
}
|
||||
const { signature: _ignoredSignature, ...signaturePayload } = payload;
|
||||
const secret = env.INTEGRATION_WEBHOOK_SECRET;
|
||||
const expected = createHmac("sha256", secret)
|
||||
.update(JSON.stringify(signaturePayload))
|
||||
.digest("hex");
|
||||
const a = Buffer.from(signature.toLowerCase(), "utf8");
|
||||
const b = Buffer.from(expected.toLowerCase(), "utf8");
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
throw new ApiError("WEBHOOK_SIGNATURE_INVALID", "invalid X-Partner-Signature", 401);
|
||||
}
|
||||
}
|
||||
function parseCallback(payload) {
|
||||
const partnerReference = payload.partner_reference;
|
||||
if (!partnerReference) {
|
||||
throw new ApiError("CALLBACK_PARTNER_DATA_INVALID", "partner_reference is required", 400);
|
||||
}
|
||||
const amount = Number(payload.amount || 0);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
throw new ApiError("CALLBACK_PARTNER_DATA_INVALID", "amount must be > 0", 400);
|
||||
}
|
||||
const parsed = {
|
||||
partnerReference,
|
||||
partnerTxnId: payload.partner_txn_id,
|
||||
status: parsePaymentStatus(payload.payment_status || payload.status),
|
||||
amount,
|
||||
paidAt: payload.paid_at,
|
||||
currency: typeof payload.currency === "string" && payload.currency.length > 0 ? payload.currency.toUpperCase() : "IDR"
|
||||
};
|
||||
return parsed;
|
||||
}
|
||||
function makeIdempotencyKey(payload, headerValue) {
|
||||
if (headerValue) {
|
||||
return headerValue;
|
||||
}
|
||||
return `${payload.partnerReference}:${payload.partnerTxnId || ""}:${payload.status.status}`;
|
||||
}
|
||||
function buildCallbackResponse(req, transactionId, eventId, note, reason) {
|
||||
const response = {
|
||||
status: "accepted",
|
||||
event_id: eventId,
|
||||
transaction_id: transactionId,
|
||||
request_id: req.requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
if (note) {
|
||||
response.note = note;
|
||||
}
|
||||
if (reason) {
|
||||
response.reason = reason;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
function writeCallbackResult(idempotencyKey, response, transactionId) {
|
||||
writeIdempotency("callback.processing", idempotencyKey, { response, transaction_id: transactionId }, env.IDEMPOTENCY_TTL_MS);
|
||||
}
|
||||
async function makeResponseEventId(txId, fallbackTag) {
|
||||
const events = await getTransactionEvents(txId);
|
||||
return events.at(-1)?.id || `${fallbackTag}_${Date.now()}`;
|
||||
}
|
||||
router.post("/qris/callback", async (req, res, next) => {
|
||||
const incoming = req.body;
|
||||
const signature = req.header("X-Partner-Signature");
|
||||
let parsed;
|
||||
try {
|
||||
verifySignature(incoming, signature);
|
||||
parsed = parseCallback(incoming);
|
||||
}
|
||||
catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
const idempotencyKey = makeIdempotencyKey(parsed, req.header("Idempotency-Key") || undefined);
|
||||
const cache = readIdempotency("callback.processing", idempotencyKey);
|
||||
if (cache && typeof cache === "object" && "response" in cache) {
|
||||
const cached = cache;
|
||||
const transactionId = typeof cached.transaction_id === "string" ? cached.transaction_id : null;
|
||||
if (transactionId) {
|
||||
await addTransactionEvent({
|
||||
transaction_id: transactionId,
|
||||
event_type: "CALLBACK_DUPLICATE",
|
||||
source: "webhook",
|
||||
payload_json: {
|
||||
idempotency_key: idempotencyKey,
|
||||
source_payload: incoming
|
||||
}
|
||||
});
|
||||
}
|
||||
return res.json(successResponse(req, cached.response));
|
||||
}
|
||||
const tx = await findTransactionByPartnerReference(parsed.partnerReference);
|
||||
if (!tx) {
|
||||
const response = buildCallbackResponse(req, null, `callback_no_tx_${Date.now()}`, "TRANSACTION_NOT_FOUND");
|
||||
writeCallbackResult(idempotencyKey, response, null);
|
||||
return res.json(successResponse(req, response));
|
||||
}
|
||||
if (parsed.amount !== tx.amount) {
|
||||
const response = buildCallbackResponse(req, tx.id, `callback_amount_mismatch_${Date.now()}`, "AMOUNT_MISMATCH");
|
||||
await addTransactionEvent({
|
||||
transaction_id: tx.id,
|
||||
event_type: "CALLBACK_REJECTED",
|
||||
source: "webhook",
|
||||
payload_json: {
|
||||
idempotency_key: idempotencyKey,
|
||||
received_amount: parsed.amount,
|
||||
expected_amount: tx.amount
|
||||
}
|
||||
});
|
||||
writeCallbackResult(idempotencyKey, response, tx.id);
|
||||
return res.status(409).json(successResponse(req, response));
|
||||
}
|
||||
await addTransactionEvent({
|
||||
transaction_id: tx.id,
|
||||
event_type: "CALLBACK_RECEIVED",
|
||||
source: "webhook",
|
||||
payload_json: {
|
||||
idempotency_key: idempotencyKey,
|
||||
partner_reference: parsed.partnerReference,
|
||||
partner_txn_id: parsed.partnerTxnId,
|
||||
candidate_status: parsed.status.status,
|
||||
status_reason: parsed.status.reason,
|
||||
partner_payload: incoming
|
||||
}
|
||||
});
|
||||
const eventContext = {
|
||||
partner_reference: parsed.partnerReference,
|
||||
partner_txn_id: parsed.partnerTxnId,
|
||||
idempotency_key: idempotencyKey,
|
||||
candidate_currency: parsed.currency,
|
||||
reason: parsed.status.reason
|
||||
};
|
||||
if (parsed.status.status === "paid") {
|
||||
const wasPaid = tx.status === "paid";
|
||||
let updated;
|
||||
try {
|
||||
updated = await updateTransactionStatus(tx.id, "paid", {
|
||||
source: "webhook",
|
||||
eventContext,
|
||||
paid_at: parsed.paidAt
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
|
||||
const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
|
||||
writeCallbackResult(idempotencyKey, response, tx.id);
|
||||
return res.status(409).json(successResponse(req, response));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!wasPaid) {
|
||||
emitTransactionPaid({
|
||||
transaction_id: updated.id,
|
||||
merchant_id: updated.merchant_id,
|
||||
outlet_id: updated.outlet_id,
|
||||
terminal_id: updated.terminal_id,
|
||||
device_id: updated.device_id,
|
||||
amount: updated.amount,
|
||||
currency: updated.currency,
|
||||
partner_reference: updated.partner_reference,
|
||||
paid_at: updated.paid_at
|
||||
});
|
||||
await addTransactionEvent({
|
||||
transaction_id: updated.id,
|
||||
event_type: "PUSH_QUEUED",
|
||||
source: "system",
|
||||
payload_json: {
|
||||
event_type: "transaction.paid",
|
||||
transaction_id: updated.id,
|
||||
partner_reference: updated.partner_reference,
|
||||
device_id: updated.device_id
|
||||
}
|
||||
});
|
||||
}
|
||||
const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"));
|
||||
writeCallbackResult(idempotencyKey, response, updated.id);
|
||||
return res.json(successResponse(req, response));
|
||||
}
|
||||
if (parsed.status.status === "expired") {
|
||||
let updated;
|
||||
try {
|
||||
updated = await updateTransactionStatus(tx.id, "expired", {
|
||||
source: "webhook",
|
||||
eventContext,
|
||||
expired_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
|
||||
const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
|
||||
writeCallbackResult(idempotencyKey, response, tx.id);
|
||||
return res.status(409).json(successResponse(req, response));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"));
|
||||
writeCallbackResult(idempotencyKey, response, updated.id);
|
||||
return res.json(successResponse(req, response));
|
||||
}
|
||||
let updated;
|
||||
try {
|
||||
updated = await updateTransactionStatus(tx.id, "failed", {
|
||||
source: "webhook",
|
||||
eventContext
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("INVALID_TRANSACTION_STATE_TRANSITION")) {
|
||||
const response = buildCallbackResponse(req, tx.id, `callback_transition_${Date.now()}`, "transaction state transition rejected");
|
||||
writeCallbackResult(idempotencyKey, response, tx.id);
|
||||
return res.status(409).json(successResponse(req, response));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const response = buildCallbackResponse(req, updated.id, await makeResponseEventId(updated.id, "tx_event"), undefined, parsed.status.reason);
|
||||
writeCallbackResult(idempotencyKey, response, updated.id);
|
||||
return res.json(successResponse(req, response));
|
||||
});
|
||||
export default router;
|
||||
195
dist/shared/db/pool.js
vendored
Normal file
@ -0,0 +1,195 @@
|
||||
import { Pool } from "pg";
|
||||
import { env } from "../../config/env";
|
||||
let pool = null;
|
||||
function buildPoolConfig() {
|
||||
if (env.DATABASE_URL) {
|
||||
return {
|
||||
connectionString: env.DATABASE_URL
|
||||
};
|
||||
}
|
||||
return {
|
||||
host: env.PGHOST,
|
||||
port: env.PGPORT,
|
||||
user: env.PGUSER,
|
||||
password: env.PGPASSWORD,
|
||||
database: env.PGDATABASE
|
||||
};
|
||||
}
|
||||
export function getPool() {
|
||||
if (!pool) {
|
||||
const config = buildPoolConfig();
|
||||
pool = new Pool(config);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
export async function withClient(work) {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
return await work(client);
|
||||
}
|
||||
finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
export async function ensureSchema() {
|
||||
const pool = getPool();
|
||||
await pool.query(MIGRATIONS_SQL);
|
||||
}
|
||||
const MIGRATIONS_SQL = `
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS merchants (
|
||||
id TEXT PRIMARY KEY,
|
||||
merchant_code TEXT NOT NULL UNIQUE,
|
||||
legal_name TEXT NOT NULL,
|
||||
brand_name TEXT,
|
||||
settlement_account_reference TEXT,
|
||||
settlement_account_type TEXT,
|
||||
payout_mode TEXT NOT NULL DEFAULT 'merchant_direct' CHECK (payout_mode IN ('merchant_direct', 'manual')),
|
||||
fee_profile_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
onboarding_status TEXT NOT NULL DEFAULT 'pending' CHECK (onboarding_status IN ('pending', 'approved', 'rejected')),
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS outlets (
|
||||
id TEXT PRIMARY KEY,
|
||||
merchant_id TEXT NOT NULL REFERENCES merchants (id) ON DELETE CASCADE,
|
||||
outlet_code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
address TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS terminals (
|
||||
id TEXT PRIMARY KEY,
|
||||
outlet_id TEXT NOT NULL REFERENCES outlets (id) ON DELETE CASCADE,
|
||||
terminal_code TEXT NOT NULL UNIQUE,
|
||||
qr_mode TEXT NOT NULL DEFAULT 'static' CHECK (qr_mode IN ('static', 'dynamic_mqtt', 'dynamic_api')),
|
||||
partner_reference TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id TEXT PRIMARY KEY,
|
||||
device_code TEXT NOT NULL UNIQUE,
|
||||
serial_number TEXT,
|
||||
vendor TEXT,
|
||||
model TEXT,
|
||||
communication_mode TEXT NOT NULL DEFAULT 'static' CHECK (communication_mode IN ('static', 'mqtt', 'api')),
|
||||
capability_profile_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
auth_method TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
firmware_version TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_status_last_seen ON devices (status, last_seen_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_bindings (
|
||||
id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL REFERENCES devices (id) ON DELETE CASCADE,
|
||||
merchant_id TEXT NOT NULL REFERENCES merchants (id) ON DELETE CASCADE,
|
||||
outlet_id TEXT NOT NULL REFERENCES outlets (id) ON DELETE CASCADE,
|
||||
terminal_id TEXT NOT NULL REFERENCES terminals (id) ON DELETE CASCADE,
|
||||
active_flag BOOLEAN NOT NULL DEFAULT false,
|
||||
bound_at TIMESTAMPTZ NOT NULL,
|
||||
unbound_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_device_active_binding ON device_bindings (device_id) WHERE active_flag = TRUE;
|
||||
CREATE INDEX IF NOT EXISTS idx_device_bindings_terminal_active ON device_bindings (terminal_id, active_flag);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_heartbeats (
|
||||
id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL REFERENCES devices (id) ON DELETE CASCADE,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
received_at TIMESTAMPTZ NOT NULL,
|
||||
firmware_version TEXT,
|
||||
network_strength INTEGER,
|
||||
battery_level INTEGER,
|
||||
state TEXT,
|
||||
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_device_heartbeats_device ON device_heartbeats (device_id, received_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_commands (
|
||||
id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL REFERENCES devices (id) ON DELETE CASCADE,
|
||||
command TEXT NOT NULL,
|
||||
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
status TEXT NOT NULL DEFAULT 'accepted' CHECK (status IN ('accepted', 'delivered', 'failed', 'timeout')),
|
||||
requested_at TIMESTAMPTZ NOT NULL,
|
||||
acknowledged_at TIMESTAMPTZ,
|
||||
result_payload_json JSONB,
|
||||
reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_device_commands_device_request ON device_commands (device_id, requested_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
transaction_code TEXT NOT NULL UNIQUE,
|
||||
merchant_id TEXT NOT NULL REFERENCES merchants (id) ON DELETE CASCADE,
|
||||
outlet_id TEXT NOT NULL REFERENCES outlets (id) ON DELETE CASCADE,
|
||||
terminal_id TEXT NOT NULL REFERENCES terminals (id) ON DELETE CASCADE,
|
||||
device_id TEXT REFERENCES devices (id) ON DELETE SET NULL,
|
||||
qr_mode TEXT NOT NULL DEFAULT 'static' CHECK (qr_mode IN ('static', 'dynamic')),
|
||||
initiation_mode TEXT NOT NULL DEFAULT 'static' CHECK (initiation_mode IN ('static', 'manual', 'dynamic_api', 'dynamic_mqtt')),
|
||||
partner_reference TEXT NOT NULL UNIQUE,
|
||||
amount NUMERIC(20,2) NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'IDR',
|
||||
status TEXT NOT NULL DEFAULT 'initiated' CHECK (status IN ('initiated', 'awaiting_payment', 'paid', 'failed', 'expired', 'reversed')),
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
paid_at TIMESTAMPTZ,
|
||||
expired_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_partner_ref ON transactions (partner_reference);
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_merchant_created ON transactions (merchant_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_status_created ON transactions (status, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS transaction_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
transaction_id TEXT NOT NULL REFERENCES transactions (id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transaction_events_tx ON transaction_events (transaction_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
transaction_id TEXT NOT NULL REFERENCES transactions (id) ON DELETE CASCADE,
|
||||
device_id TEXT REFERENCES devices (id) ON DELETE SET NULL,
|
||||
delivery_channel TEXT NOT NULL DEFAULT 'mqtt' CHECK (delivery_channel IN ('mqtt')),
|
||||
payload_type TEXT NOT NULL DEFAULT 'payment_success' CHECK (payload_type IN ('payment_success')),
|
||||
delivery_status TEXT NOT NULL CHECK (delivery_status IN ('queued', 'sent', 'acknowledged', 'failed', 'retrying')),
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
ack_status TEXT NOT NULL DEFAULT 'not_needed' CHECK (ack_status IN ('pending', 'received', 'not_supported', 'not_needed')),
|
||||
event_id TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
sent_at TIMESTAMPTZ,
|
||||
ack_at TIMESTAMPTZ,
|
||||
next_retry_at TIMESTAMPTZ,
|
||||
CONSTRAINT notifications_unique_tx_event UNIQUE (transaction_id, event_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_device_status ON notifications (device_id, delivery_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_status_created ON notifications (delivery_status, created_at DESC);
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
21
dist/shared/errors/index.js
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
export class ApiError extends Error {
|
||||
statusCode;
|
||||
code;
|
||||
details;
|
||||
constructor(code, message, statusCode = 400, details) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.statusCode = statusCode;
|
||||
this.details = details;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
export function errorEnvelope(error, requestId) {
|
||||
return {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
details: error.details,
|
||||
request_id: requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
53
dist/shared/events/transactionEvents.js
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
const transactionPaidEvents = new Map();
|
||||
const transactionPaidIndex = new Map();
|
||||
const transactionPaidSubscribers = new Set();
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function cloneInternalEvent(event) {
|
||||
return {
|
||||
...event,
|
||||
payload_json: { ...event.payload_json }
|
||||
};
|
||||
}
|
||||
export function subscribeTransactionPaid(handler) {
|
||||
transactionPaidSubscribers.add(handler);
|
||||
return () => transactionPaidSubscribers.delete(handler);
|
||||
}
|
||||
export function emitTransactionPaid(payload) {
|
||||
const existingId = transactionPaidIndex.get(payload.transaction_id);
|
||||
if (existingId) {
|
||||
const events = transactionPaidEvents.get(payload.transaction_id) || [];
|
||||
const existing = events.find((event) => event.id === existingId);
|
||||
if (existing) {
|
||||
return cloneInternalEvent(existing);
|
||||
}
|
||||
}
|
||||
const event = {
|
||||
id: randomUUID(),
|
||||
event_type: "transaction.paid",
|
||||
transaction_id: payload.transaction_id,
|
||||
payload_json: payload,
|
||||
created_at: nowIso()
|
||||
};
|
||||
const bucket = transactionPaidEvents.get(payload.transaction_id) || [];
|
||||
bucket.push(event);
|
||||
transactionPaidEvents.set(payload.transaction_id, bucket);
|
||||
transactionPaidIndex.set(payload.transaction_id, event.id);
|
||||
const frozen = cloneInternalEvent(event);
|
||||
for (const listener of Array.from(transactionPaidSubscribers)) {
|
||||
listener(frozen);
|
||||
}
|
||||
return frozen;
|
||||
}
|
||||
export function getTransactionPaidEvents(transactionId) {
|
||||
if (transactionId) {
|
||||
return (transactionPaidEvents.get(transactionId) || []).map(cloneInternalEvent);
|
||||
}
|
||||
return Array.from(transactionPaidEvents.values()).flatMap((bucket) => bucket.map(cloneInternalEvent));
|
||||
}
|
||||
export function getTransactionPaidEventByTransactionId(transactionId) {
|
||||
const events = transactionPaidEvents.get(transactionId) || [];
|
||||
return events.length > 0 ? cloneInternalEvent(events[events.length - 1]) : null;
|
||||
}
|
||||
26
dist/shared/idempotency/idempotencyStore.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
const store = new Map();
|
||||
export function makeIdempotencyKey(scope, key) {
|
||||
return `${scope}:${key}`;
|
||||
}
|
||||
export function readIdempotency(scope, key) {
|
||||
const entry = store.get(makeIdempotencyKey(scope, key));
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
if (entry.expiresAt < Date.now()) {
|
||||
store.delete(makeIdempotencyKey(scope, key));
|
||||
return null;
|
||||
}
|
||||
return entry.value;
|
||||
}
|
||||
export function writeIdempotency(scope, key, value, ttlMs) {
|
||||
store.set(makeIdempotencyKey(scope, key), {
|
||||
key,
|
||||
scope,
|
||||
value,
|
||||
expiresAt: Date.now() + ttlMs
|
||||
});
|
||||
}
|
||||
export function clearIdempotency(scope, key) {
|
||||
store.delete(makeIdempotencyKey(scope, key));
|
||||
}
|
||||
30
dist/shared/middleware/auth.js
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { ApiError } from "../errors";
|
||||
import { env } from "../../config/env";
|
||||
function extractAdminToken(req) {
|
||||
const raw = req.header("authorization") || "";
|
||||
if (raw.startsWith("Bearer ")) {
|
||||
return raw.slice(7);
|
||||
}
|
||||
return raw || req.header("x-admin-token") || "";
|
||||
}
|
||||
export function requireAdminToken(req, _res, next) {
|
||||
const token = extractAdminToken(req);
|
||||
if (!token) {
|
||||
return next(new ApiError("UNAUTHORIZED", "Missing admin bearer token", 401));
|
||||
}
|
||||
if (token !== env.ADMIN_TOKEN) {
|
||||
return next(new ApiError("UNAUTHORIZED", "Invalid admin token", 401));
|
||||
}
|
||||
return next();
|
||||
}
|
||||
export function requireDeviceToken(req, _res, next) {
|
||||
const raw = req.header("authorization") || "";
|
||||
const token = raw.startsWith("Bearer ") ? raw.slice(7) : raw;
|
||||
if (!token) {
|
||||
return next(new ApiError("UNAUTHORIZED", "Missing device bearer token", 401));
|
||||
}
|
||||
if (token !== env.DEVICE_TOKEN) {
|
||||
return next(new ApiError("UNAUTHORIZED", "Invalid device token", 401));
|
||||
}
|
||||
return next();
|
||||
}
|
||||
22
dist/shared/middleware/errorMiddleware.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import { ApiError, errorEnvelope } from "../errors";
|
||||
export function successResponse(req, data) {
|
||||
return {
|
||||
data,
|
||||
request_id: req.requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
export function handleErrors(err, req, res, _next) {
|
||||
if (err instanceof ApiError) {
|
||||
res.status(err.statusCode).json(errorEnvelope({
|
||||
...err
|
||||
}, req.requestId));
|
||||
return;
|
||||
}
|
||||
res.status(500).json({
|
||||
code: "INTERNAL_ERROR",
|
||||
message: err.message || "Unexpected server error",
|
||||
request_id: req.requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
37
dist/shared/middleware/idempotency.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
import { ApiError } from "../errors";
|
||||
import { readIdempotency, writeIdempotency } from "../idempotency/idempotencyStore";
|
||||
import { env } from "../../config/env";
|
||||
export function idempotency(options) {
|
||||
return function idempotencyMiddleware(req, _res, next) {
|
||||
const idempotencyKey = req.header("idempotency-key");
|
||||
if (!idempotencyKey) {
|
||||
if (options.required === false) {
|
||||
return next();
|
||||
}
|
||||
return next(new ApiError("DUPLICATE_REQUEST", "Missing Idempotency-Key", 400));
|
||||
}
|
||||
const cached = readIdempotency(options.scope, idempotencyKey);
|
||||
if (cached) {
|
||||
const cachedPayload = cached.response ?? cached;
|
||||
const cachedStatus = cached.statusCode || 200;
|
||||
return _res.status(cachedStatus).json(cachedPayload);
|
||||
}
|
||||
req.body = { ...(req.body || {}), __idempotencyKey: idempotencyKey };
|
||||
const originalJson = _res.json.bind(_res);
|
||||
const originalStatus = _res.status.bind(_res);
|
||||
let statusCode = 200;
|
||||
_res.status = function statusWithStore(code) {
|
||||
statusCode = code;
|
||||
return originalStatus(code);
|
||||
};
|
||||
_res.json = function jsonWithStore(payload) {
|
||||
writeIdempotency(options.scope, idempotencyKey, {
|
||||
response: payload,
|
||||
statusCode,
|
||||
at: Date.now()
|
||||
}, options.ttlMs || env.IDEMPOTENCY_TTL_MS);
|
||||
return originalJson(payload);
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
11
dist/shared/middleware/requestContext.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { env } from "../../config/env";
|
||||
export function requestContext(req, _res, next) {
|
||||
const requestId = req.header(env.TRACE_HEADER) ||
|
||||
req.header("x-trace-id") ||
|
||||
randomUUID();
|
||||
const traceId = req.header("x-trace-id") || requestId;
|
||||
req.requestId = requestId;
|
||||
req.traceId = traceId;
|
||||
next();
|
||||
}
|
||||
274
dist/shared/orchestrators/notificationOrchestrator.js
vendored
Normal file
@ -0,0 +1,274 @@
|
||||
import { getActiveBindingByDevice, getActiveBindingByTerminal } from "../store/bindingStore";
|
||||
import { createNotification, getNotificationByTransactionAndEvent, getNotificationByTransactionId, listNotifications, toNotificationPayload, updateNotification } from "../store/notificationStore";
|
||||
import { getMerchantById } from "../store/merchantStore";
|
||||
import { getTransactionById, listTransactions, toTransactionPayload } from "../store/transactionStore";
|
||||
import { buildPaymentSuccessPayload, publishPaymentSuccess } from "../services/mqttPublisher";
|
||||
import { subscribeTransactionPaid } from "../events/transactionEvents";
|
||||
import { env } from "../../config/env";
|
||||
const RETRY_INTERVAL_MS = [
|
||||
env.MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS || 15000,
|
||||
(env.MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS || 15000) * 2,
|
||||
(env.MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS || 15000) * 4
|
||||
];
|
||||
const RETRY_POLL_INTERVAL_MS = 5000;
|
||||
const MAX_RETRY = 3;
|
||||
async function resolveNotificationDevice(payload) {
|
||||
if (payload.device_id) {
|
||||
const binding = await getActiveBindingByDevice(payload.device_id);
|
||||
if (binding && binding.terminal_id === payload.terminal_id) {
|
||||
return {
|
||||
deviceId: payload.device_id,
|
||||
source: "tx_device"
|
||||
};
|
||||
}
|
||||
}
|
||||
const terminalBinding = await getActiveBindingByTerminal(payload.terminal_id);
|
||||
if (!terminalBinding) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
deviceId: terminalBinding.device_id,
|
||||
source: "binding_device"
|
||||
};
|
||||
}
|
||||
function buildNotificationPayload(txPayload, deviceId) {
|
||||
return {
|
||||
message_type: "payment_success",
|
||||
transaction_id: txPayload.transaction_id,
|
||||
merchant_id: txPayload.merchant_id,
|
||||
terminal_id: txPayload.terminal_id,
|
||||
amount: txPayload.amount,
|
||||
currency: txPayload.currency,
|
||||
paid_at: txPayload.paid_at,
|
||||
partner_reference: txPayload.partner_reference,
|
||||
target_device_id: deviceId
|
||||
};
|
||||
}
|
||||
function resolveDeliveryStatus(bindingResolved) {
|
||||
return bindingResolved ? "queued" : "failed";
|
||||
}
|
||||
function resolveFailureReason(bindingResolved) {
|
||||
if (!bindingResolved) {
|
||||
return "NOTIFICATION_NO_ACTIVE_BINDING";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function makeNextRetryDate(retryCount) {
|
||||
const intervalMs = RETRY_INTERVAL_MS[Math.min(retryCount, RETRY_INTERVAL_MS.length - 1)] || 60000;
|
||||
return new Date(Date.now() + intervalMs).toISOString();
|
||||
}
|
||||
async function getNotificationMerchantName(merchantId) {
|
||||
const merchant = await getMerchantById(merchantId);
|
||||
return merchant?.brand_name || merchant?.legal_name || merchantId;
|
||||
}
|
||||
function mapMqttFailureState(retryCount, reason) {
|
||||
const nextRetryCount = retryCount + 1;
|
||||
if (nextRetryCount >= MAX_RETRY) {
|
||||
return {
|
||||
status: "failed",
|
||||
retry_count: nextRetryCount,
|
||||
reason: reason || "NOTIFICATION_RETRY_EXHAUSTED"
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "retrying",
|
||||
retry_count: nextRetryCount,
|
||||
next_retry_at: makeNextRetryDate(retryCount),
|
||||
reason
|
||||
};
|
||||
}
|
||||
async function markNotificationSent(notification, publishResult) {
|
||||
await updateNotification(notification.id, {
|
||||
delivery_status: "sent",
|
||||
retry_count: notification.retry_count,
|
||||
ack_status: "not_supported",
|
||||
sent_at: publishResult.publishedAt
|
||||
});
|
||||
}
|
||||
async function markNotificationFailed(notification, publishResult) {
|
||||
const next = mapMqttFailureState(notification.retry_count, publishResult.reason);
|
||||
await updateNotification(notification.id, {
|
||||
delivery_status: next.status,
|
||||
retry_count: next.retry_count,
|
||||
reason: next.reason,
|
||||
ack_status: "not_supported",
|
||||
next_retry_at: next.next_retry_at
|
||||
});
|
||||
}
|
||||
async function markNoDeviceFailure(notification) {
|
||||
await updateNotification(notification.id, {
|
||||
delivery_status: "failed",
|
||||
retry_count: 0,
|
||||
reason: "NOTIFICATION_NO_ACTIVE_BINDING",
|
||||
ack_status: "not_needed"
|
||||
});
|
||||
}
|
||||
async function resolveNotificationFromTransaction(notification) {
|
||||
const tx = await getTransactionById(notification.transaction_id);
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
transaction_id: tx.id,
|
||||
merchant_id: tx.merchant_id,
|
||||
outlet_id: tx.outlet_id,
|
||||
terminal_id: tx.terminal_id,
|
||||
device_id: tx.device_id,
|
||||
amount: tx.amount,
|
||||
currency: tx.currency,
|
||||
paid_at: tx.paid_at,
|
||||
partner_reference: tx.partner_reference
|
||||
};
|
||||
}
|
||||
async function getMqttPayloadFromNotification(notification, paidAt) {
|
||||
return buildPaymentSuccessPayload({
|
||||
transaction_id: String(notification.payload_json.transaction_id || ""),
|
||||
merchant_id: String(notification.payload_json.merchant_id || ""),
|
||||
merchant_name: await getNotificationMerchantName(String(notification.payload_json.merchant_id || "")),
|
||||
device_id: String(notification.device_id || ""),
|
||||
amount: Number(notification.payload_json.amount || 0),
|
||||
currency: String(notification.payload_json.currency || "IDR"),
|
||||
paid_at: paidAt,
|
||||
partner_reference: String(notification.payload_json.partner_reference || ""),
|
||||
event_id: notification.event_id
|
||||
});
|
||||
}
|
||||
async function publishNotificationNow(notification, eventPayload) {
|
||||
if (!notification.device_id) {
|
||||
const resolvedFromTransaction = await resolveNotificationFromTransaction(notification);
|
||||
if (!resolvedFromTransaction) {
|
||||
await markNoDeviceFailure(notification);
|
||||
return;
|
||||
}
|
||||
const resolved = await resolveNotificationDevice(resolvedFromTransaction);
|
||||
if (!resolved) {
|
||||
await markNoDeviceFailure(notification);
|
||||
return;
|
||||
}
|
||||
notification = await updateNotification(notification.id, {
|
||||
device_id: resolved.deviceId,
|
||||
delivery_status: "queued",
|
||||
reason: undefined
|
||||
});
|
||||
}
|
||||
const effectivePayload = eventPayload ?? (await resolveNotificationFromTransaction(notification));
|
||||
if (!effectivePayload) {
|
||||
await markNoDeviceFailure(notification);
|
||||
return;
|
||||
}
|
||||
const mqttPayload = await buildPaymentSuccessPayload({
|
||||
transaction_id: effectivePayload.transaction_id,
|
||||
merchant_id: effectivePayload.merchant_id,
|
||||
merchant_name: await getNotificationMerchantName(effectivePayload.merchant_id),
|
||||
device_id: notification.device_id || String(effectivePayload.device_id || ""),
|
||||
amount: effectivePayload.amount,
|
||||
currency: effectivePayload.currency,
|
||||
paid_at: effectivePayload.paid_at,
|
||||
partner_reference: effectivePayload.partner_reference,
|
||||
event_id: notification.event_id
|
||||
});
|
||||
const result = await publishPaymentSuccess(mqttPayload);
|
||||
if (!result.ok) {
|
||||
await markNotificationFailed(notification, result);
|
||||
return;
|
||||
}
|
||||
await markNotificationSent(notification, result);
|
||||
}
|
||||
async function onTransactionPaid(event) {
|
||||
const payload = event.payload_json;
|
||||
const existing = await getNotificationByTransactionAndEvent(payload.transaction_id, event.id);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
const resolved = await resolveNotificationDevice(payload);
|
||||
const deliveryStatus = resolveDeliveryStatus(resolved);
|
||||
const created = await createNotification({
|
||||
transaction_id: payload.transaction_id,
|
||||
device_id: resolved?.deviceId || null,
|
||||
event_id: event.id,
|
||||
delivery_status: deliveryStatus,
|
||||
reason: resolveFailureReason(resolved),
|
||||
payload_json: buildNotificationPayload(payload, resolved?.deviceId || ""),
|
||||
ack_status: resolved ? "not_supported" : "not_needed"
|
||||
});
|
||||
await publishNotificationNow(created, payload);
|
||||
}
|
||||
async function bootstrapNotificationForPaidTransaction(transaction) {
|
||||
const existing = await getNotificationByTransactionId(transaction.id);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
transaction_id: transaction.id,
|
||||
merchant_id: transaction.merchant_id,
|
||||
outlet_id: transaction.outlet_id,
|
||||
terminal_id: transaction.terminal_id,
|
||||
device_id: transaction.device_id,
|
||||
amount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
partner_reference: transaction.partner_reference,
|
||||
paid_at: transaction.paid_at
|
||||
};
|
||||
const eventId = `bootstrap_${transaction.id}`;
|
||||
const resolved = await resolveNotificationDevice(payload);
|
||||
const deliveryStatus = resolveDeliveryStatus(resolved);
|
||||
const created = await createNotification({
|
||||
transaction_id: transaction.id,
|
||||
device_id: resolved?.deviceId || null,
|
||||
event_id: eventId,
|
||||
delivery_status: deliveryStatus,
|
||||
reason: resolveFailureReason(resolved),
|
||||
payload_json: buildNotificationPayload(payload, resolved?.deviceId || ""),
|
||||
ack_status: resolved ? "not_supported" : "not_needed"
|
||||
});
|
||||
await publishNotificationNow(created, payload);
|
||||
}
|
||||
async function seedPaidTransactions() {
|
||||
const paidTransactions = await listTransactions({ status: "paid" });
|
||||
for (const tx of paidTransactions) {
|
||||
await bootstrapNotificationForPaidTransaction(toTransactionPayload(tx));
|
||||
}
|
||||
}
|
||||
async function processRetryCycle() {
|
||||
const now = new Date().toISOString();
|
||||
const retrying = await listNotifications({
|
||||
delivery_status: "retrying"
|
||||
});
|
||||
const due = retrying.filter((notification) => {
|
||||
if (!notification.next_retry_at) {
|
||||
return true;
|
||||
}
|
||||
return notification.next_retry_at <= now;
|
||||
});
|
||||
for (const notification of due) {
|
||||
await publishNotificationNow(notification, null);
|
||||
}
|
||||
}
|
||||
export async function retryNotificationByTransactionId(transactionId) {
|
||||
const tx = await getTransactionById(transactionId);
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
if (tx.status !== "paid") {
|
||||
throw new Error("NOTIFICATION_PUBLISH_CONDITION");
|
||||
}
|
||||
const notification = await getNotificationByTransactionId(transactionId);
|
||||
if (!notification) {
|
||||
return null;
|
||||
}
|
||||
if (notification.delivery_status === "acknowledged") {
|
||||
return toNotificationPayload(notification);
|
||||
}
|
||||
await publishNotificationNow(notification, null);
|
||||
const updated = await getNotificationByTransactionId(transactionId);
|
||||
return updated ? toNotificationPayload(updated) : null;
|
||||
}
|
||||
export function startNotificationOrchestrator() {
|
||||
void seedPaidTransactions();
|
||||
void subscribeTransactionPaid((event) => {
|
||||
void onTransactionPaid(event);
|
||||
});
|
||||
setInterval(() => {
|
||||
void processRetryCycle();
|
||||
}, RETRY_POLL_INTERVAL_MS);
|
||||
}
|
||||
52
dist/shared/services/mqttPublisher.js
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
import { env } from "../../config/env";
|
||||
const forcedFailAll = String(env.MQTT_PUBLISH_FORCE_FAIL_ALL).toLowerCase() === "true";
|
||||
const forcedFailDevices = new Set(String(env.MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS)
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean));
|
||||
function shouldForceFail(deviceId) {
|
||||
return forcedFailAll || forcedFailDevices.has(deviceId);
|
||||
}
|
||||
export function buildPaymentSuccessPayload(input) {
|
||||
const displayAmount = `${input.amount.toLocaleString("id-ID")}`;
|
||||
return {
|
||||
message_type: "payment_success",
|
||||
device_id: input.device_id,
|
||||
event_id: input.event_id,
|
||||
transaction_id: input.transaction_id,
|
||||
merchant_id: input.merchant_id,
|
||||
merchant_name: input.merchant_name,
|
||||
amount: input.amount,
|
||||
currency: input.currency,
|
||||
paid_at: input.paid_at,
|
||||
partner_reference: input.partner_reference,
|
||||
audio_text: `Pembayaran diterima ${input.currency} ${displayAmount}`,
|
||||
display_text: `Pembayaran diterima Rp${displayAmount}`
|
||||
};
|
||||
}
|
||||
export function makePaymentSuccessTopic(deviceId) {
|
||||
return `devices/${deviceId}/downlink/payment/success`;
|
||||
}
|
||||
export async function publishPaymentSuccess(payload) {
|
||||
const publishedAt = new Date().toISOString();
|
||||
const topic = makePaymentSuccessTopic(payload.device_id);
|
||||
if (shouldForceFail(payload.device_id)) {
|
||||
return {
|
||||
ok: false,
|
||||
topic,
|
||||
qos: 1,
|
||||
retained: false,
|
||||
publishedAt,
|
||||
reason: "MQTT_PUBLISH_SIMULATED_FAILURE",
|
||||
payload
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
topic,
|
||||
qos: 1,
|
||||
retained: false,
|
||||
publishedAt,
|
||||
payload
|
||||
};
|
||||
}
|
||||
97
dist/shared/store/bindingStore.js
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool, withClient } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapBinding(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
device_id: row.device_id,
|
||||
merchant_id: row.merchant_id,
|
||||
outlet_id: row.outlet_id,
|
||||
terminal_id: row.terminal_id,
|
||||
active_flag: row.active_flag,
|
||||
bound_at: row.bound_at,
|
||||
unbound_at: row.unbound_at || undefined
|
||||
};
|
||||
}
|
||||
export async function getActiveBindingByDevice(deviceId) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_bindings
|
||||
WHERE device_id = $1 AND active_flag = TRUE
|
||||
ORDER BY bound_at DESC
|
||||
LIMIT 1`, [deviceId]);
|
||||
return rows[0] ? mapBinding(rows[0]) : null;
|
||||
}
|
||||
export async function getActiveBindingByTerminal(terminalId) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_bindings
|
||||
WHERE terminal_id = $1 AND active_flag = TRUE
|
||||
ORDER BY bound_at DESC
|
||||
LIMIT 1`, [terminalId]);
|
||||
return rows[0] ? mapBinding(rows[0]) : null;
|
||||
}
|
||||
export async function getBindingsByDeviceId(deviceId) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_bindings
|
||||
WHERE device_id = $1
|
||||
ORDER BY bound_at DESC`, [deviceId]);
|
||||
return rows.map(mapBinding);
|
||||
}
|
||||
export async function bindDevice(payload) {
|
||||
const now = nowIso();
|
||||
const result = await withClient(async (client) => {
|
||||
const existing = await client.query(`SELECT * FROM device_bindings
|
||||
WHERE device_id = $1 AND active_flag = TRUE
|
||||
ORDER BY bound_at DESC
|
||||
LIMIT 1`, [payload.device_id]);
|
||||
const same = existing.rows[0]
|
||||
? mapBinding(existing.rows[0])
|
||||
: null;
|
||||
if (same &&
|
||||
same.merchant_id === payload.merchant_id &&
|
||||
same.outlet_id === payload.outlet_id &&
|
||||
same.terminal_id === payload.terminal_id) {
|
||||
return same;
|
||||
}
|
||||
await client.query("BEGIN");
|
||||
try {
|
||||
if (existing.rows[0]) {
|
||||
await client.query(`UPDATE device_bindings
|
||||
SET active_flag = FALSE, unbound_at = $2
|
||||
WHERE id = $1`, [same.id, now]);
|
||||
}
|
||||
const id = randomUUID();
|
||||
const inserted = await client.query(`INSERT INTO device_bindings (
|
||||
id,
|
||||
device_id,
|
||||
merchant_id,
|
||||
outlet_id,
|
||||
terminal_id,
|
||||
active_flag,
|
||||
bound_at
|
||||
) VALUES ($1,$2,$3,$4,$5,TRUE,$6)
|
||||
RETURNING *`, [id, payload.device_id, payload.merchant_id, payload.outlet_id, payload.terminal_id, now]);
|
||||
await client.query("COMMIT");
|
||||
return mapBinding(inserted.rows[0]);
|
||||
}
|
||||
catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
export async function unbindDevice(deviceId) {
|
||||
const now = nowIso();
|
||||
const { rows } = await getPool().query(`UPDATE device_bindings
|
||||
SET active_flag = FALSE,
|
||||
unbound_at = $2
|
||||
WHERE device_id = $1 AND active_flag = TRUE
|
||||
RETURNING *`, [deviceId, now]);
|
||||
return rows[0] ? mapBinding(rows[0]) : null;
|
||||
}
|
||||
export async function getBindingById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM device_bindings WHERE id = $1", [id]);
|
||||
return rows[0] ? mapBinding(rows[0]) : null;
|
||||
}
|
||||
export function toBindingPayload(binding) {
|
||||
return { ...binding };
|
||||
}
|
||||
92
dist/shared/store/deviceCommandStore.js
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function normalizeStatus(status) {
|
||||
if (status === "accepted" || status === "delivered" || status === "failed" || status === "timeout") {
|
||||
return status;
|
||||
}
|
||||
return "accepted";
|
||||
}
|
||||
function mapCommand(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
device_id: row.device_id,
|
||||
command: row.command,
|
||||
payload: row.payload_json || {},
|
||||
status: row.status,
|
||||
requested_at: row.requested_at,
|
||||
acknowledged_at: row.acknowledged_at || null,
|
||||
result_payload: row.result_payload_json || null,
|
||||
reason: row.reason || null
|
||||
};
|
||||
}
|
||||
export async function createDeviceCommand(payload) {
|
||||
const entity = {
|
||||
id: `cmd_${randomUUID()}`,
|
||||
device_id: payload.device_id,
|
||||
command: payload.command,
|
||||
payload: payload.payload || {},
|
||||
status: normalizeStatus(payload.status || "accepted"),
|
||||
requested_at: nowIso(),
|
||||
acknowledged_at: null,
|
||||
result_payload: null,
|
||||
reason: null
|
||||
};
|
||||
const { rows } = await getPool().query(`INSERT INTO device_commands (
|
||||
id,
|
||||
device_id,
|
||||
command,
|
||||
payload_json,
|
||||
status,
|
||||
requested_at,
|
||||
acknowledged_at,
|
||||
result_payload_json,
|
||||
reason
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
RETURNING *`, [
|
||||
entity.id,
|
||||
entity.device_id,
|
||||
entity.command,
|
||||
entity.payload,
|
||||
entity.status,
|
||||
entity.requested_at,
|
||||
entity.acknowledged_at,
|
||||
entity.result_payload,
|
||||
entity.reason
|
||||
]);
|
||||
return mapCommand(rows[0]);
|
||||
}
|
||||
export async function listDeviceCommands(deviceId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM device_commands WHERE device_id = $1 ORDER BY requested_at DESC", [deviceId]);
|
||||
return rows.map(mapCommand);
|
||||
}
|
||||
export async function getDeviceCommandById(deviceId, commandId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM device_commands WHERE device_id = $1 AND id = $2", [deviceId, commandId]);
|
||||
return rows[0] ? mapCommand(rows[0]) : null;
|
||||
}
|
||||
export function toDeviceCommandPayload(command) {
|
||||
return mapCommand(command);
|
||||
}
|
||||
export function toDeviceCommandPayloadBrief(command) {
|
||||
return {
|
||||
command_id: command.id,
|
||||
device_id: command.device_id,
|
||||
command: command.command,
|
||||
status: command.status,
|
||||
requested_at: command.requested_at,
|
||||
acknowledged_at: command.acknowledged_at
|
||||
};
|
||||
}
|
||||
export async function acknowledgeDeviceCommand(payload) {
|
||||
const now = nowIso();
|
||||
const { rows } = await getPool().query(`UPDATE device_commands
|
||||
SET status = $3,
|
||||
acknowledged_at = $4,
|
||||
result_payload_json = $5,
|
||||
reason = $6
|
||||
WHERE device_id = $1 AND id = $2
|
||||
RETURNING *`, [payload.device_id, payload.command_id, payload.status, now, payload.result_payload || null, payload.reason || null]);
|
||||
return rows[0] ? mapCommand(rows[0]) : null;
|
||||
}
|
||||
106
dist/shared/store/deviceStore.js
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function makeCode(id) {
|
||||
return `d_${id.slice(0, 6)}`;
|
||||
}
|
||||
function mapDevice(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
device_code: row.device_code,
|
||||
serial_number: row.serial_number || undefined,
|
||||
vendor: row.vendor || undefined,
|
||||
model: row.model || undefined,
|
||||
communication_mode: row.communication_mode,
|
||||
capability_profile_json: row.capability_profile_json || {},
|
||||
auth_method: row.auth_method || undefined,
|
||||
status: row.status,
|
||||
last_seen_at: row.last_seen_at || undefined,
|
||||
firmware_version: row.firmware_version || undefined,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
export async function createDevice(payload) {
|
||||
const id = randomUUID();
|
||||
const now = nowIso();
|
||||
const { rows } = await getPool().query(`INSERT INTO devices (
|
||||
id,
|
||||
device_code,
|
||||
serial_number,
|
||||
vendor,
|
||||
model,
|
||||
communication_mode,
|
||||
capability_profile_json,
|
||||
auth_method,
|
||||
status,
|
||||
last_seen_at,
|
||||
firmware_version,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||
RETURNING *`, [
|
||||
id,
|
||||
payload.device_code || makeCode(id),
|
||||
payload.serial_number,
|
||||
payload.vendor,
|
||||
payload.model,
|
||||
payload.communication_mode || "static",
|
||||
payload.capability_profile_json || {},
|
||||
payload.auth_method || "token",
|
||||
payload.status || "active",
|
||||
payload.last_seen_at || null,
|
||||
payload.firmware_version,
|
||||
now,
|
||||
now
|
||||
]);
|
||||
return mapDevice(rows[0]);
|
||||
}
|
||||
export async function listDevices() {
|
||||
const { rows } = await getPool().query("SELECT * FROM devices ORDER BY created_at DESC");
|
||||
return rows.map(mapDevice);
|
||||
}
|
||||
export async function getDeviceById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM devices WHERE id = $1", [id]);
|
||||
return rows[0] ? mapDevice(rows[0]) : null;
|
||||
}
|
||||
export async function patchDevice(id, patch) {
|
||||
const existing = await getDeviceById(id);
|
||||
if (!existing) {
|
||||
throw new Error("DEVICE_NOT_FOUND");
|
||||
}
|
||||
const merged = { ...existing, ...patch, updated_at: nowIso() };
|
||||
const { rows } = await getPool().query(`UPDATE devices
|
||||
SET device_code = $2,
|
||||
serial_number = $3,
|
||||
vendor = $4,
|
||||
model = $5,
|
||||
communication_mode = $6,
|
||||
capability_profile_json = $7,
|
||||
auth_method = $8,
|
||||
status = $9,
|
||||
firmware_version = $10,
|
||||
last_seen_at = $11,
|
||||
updated_at = $12
|
||||
WHERE id = $1
|
||||
RETURNING *`, [
|
||||
id,
|
||||
merged.device_code,
|
||||
merged.serial_number,
|
||||
merged.vendor,
|
||||
merged.model,
|
||||
merged.communication_mode || "static",
|
||||
merged.capability_profile_json || {},
|
||||
merged.auth_method,
|
||||
merged.status,
|
||||
merged.firmware_version,
|
||||
merged.last_seen_at || null,
|
||||
merged.updated_at
|
||||
]);
|
||||
return mapDevice(rows[0]);
|
||||
}
|
||||
export function toDevicePayload(device) {
|
||||
return { ...device };
|
||||
}
|
||||
102
dist/shared/store/heartbeatStore.js
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function mapHeartbeat(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
device_id: row.device_id,
|
||||
timestamp: row.timestamp,
|
||||
received_at: row.received_at,
|
||||
firmware_version: row.firmware_version || undefined,
|
||||
network_strength: row.network_strength,
|
||||
battery_level: row.battery_level,
|
||||
state: row.state || undefined
|
||||
};
|
||||
}
|
||||
export async function createDeviceHeartbeat(payload) {
|
||||
const now = nowIso();
|
||||
const id = randomUUID();
|
||||
const { rows } = await getPool().query(`INSERT INTO device_heartbeats (
|
||||
id,
|
||||
device_id,
|
||||
timestamp,
|
||||
received_at,
|
||||
firmware_version,
|
||||
network_strength,
|
||||
battery_level,
|
||||
state,
|
||||
payload_json
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
RETURNING *`, [
|
||||
id,
|
||||
payload.device_id,
|
||||
payload.timestamp,
|
||||
now,
|
||||
payload.firmware_version,
|
||||
payload.network_strength,
|
||||
payload.battery_level,
|
||||
payload.state,
|
||||
payload.payload_json || {}
|
||||
]);
|
||||
return mapHeartbeat(rows[0]);
|
||||
}
|
||||
export async function getLatestHeartbeatByDeviceId(deviceId) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_heartbeats
|
||||
WHERE device_id = $1
|
||||
ORDER BY received_at DESC
|
||||
LIMIT 1`, [deviceId]);
|
||||
return rows[0] ? mapHeartbeat(rows[0]) : null;
|
||||
}
|
||||
export async function getHeartbeatCountForDeviceLastHours(deviceId, hours = 24) {
|
||||
const { rows } = await getPool().query(`SELECT COUNT(*)::INT AS cnt
|
||||
FROM device_heartbeats
|
||||
WHERE device_id = $1
|
||||
AND (NOW() AT TIME ZONE 'utc' - timestamp) <= ($2 || ' hours')::interval`, [deviceId, hours]);
|
||||
return Number(rows[0]?.cnt || 0);
|
||||
}
|
||||
export async function listHeartbeats(filter) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
let i = 1;
|
||||
if (filter?.device_id) {
|
||||
clauses.push(`device_id = $${i++}`);
|
||||
params.push(filter.device_id);
|
||||
}
|
||||
if (filter?.state) {
|
||||
clauses.push(`state = $${i++}`);
|
||||
params.push(filter.state);
|
||||
}
|
||||
if (filter?.from) {
|
||||
clauses.push(`timestamp >= $${i++}`);
|
||||
params.push(filter.from);
|
||||
}
|
||||
if (filter?.to) {
|
||||
clauses.push(`timestamp <= $${i++}`);
|
||||
params.push(filter.to);
|
||||
}
|
||||
const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
const limitSql = filter?.limit ? `LIMIT ${Number(filter.limit)}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM device_heartbeats ${whereSql} ORDER BY received_at DESC ${limitSql}`, params);
|
||||
return rows.map(mapHeartbeat);
|
||||
}
|
||||
export function deriveDeviceStatus(input) {
|
||||
const now = Date.now();
|
||||
const lastSeen = Date.parse(input?.last_seen_at || "");
|
||||
if (!Number.isFinite(lastSeen)) {
|
||||
return "offline";
|
||||
}
|
||||
const ageSeconds = (now - lastSeen) / 1000;
|
||||
if (ageSeconds > 900) {
|
||||
return "offline";
|
||||
}
|
||||
if (ageSeconds > 90) {
|
||||
return "stale";
|
||||
}
|
||||
if ((typeof input?.network_strength === "number" && input.network_strength < 40) ||
|
||||
(typeof input?.battery_level === "number" && input.battery_level < 20)) {
|
||||
return "degraded";
|
||||
}
|
||||
return "online";
|
||||
}
|
||||
146
dist/shared/store/locationStore.js
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function makeCode(prefix, id) {
|
||||
return `${prefix}_${id.slice(0, 6)}`;
|
||||
}
|
||||
function mapOutlet(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
merchant_id: row.merchant_id,
|
||||
outlet_code: row.outlet_code,
|
||||
name: row.name,
|
||||
address: row.address || undefined,
|
||||
status: row.status,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
function mapTerminal(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
outlet_id: row.outlet_id,
|
||||
terminal_code: row.terminal_code,
|
||||
qr_mode: row.qr_mode,
|
||||
partner_reference: row.partner_reference || undefined,
|
||||
status: row.status,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
export async function createOutlet(payload) {
|
||||
const id = randomUUID();
|
||||
const now = nowIso();
|
||||
const { rows } = await getPool().query(`INSERT INTO outlets (id, merchant_id, outlet_code, name, address, status, created_at, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
RETURNING *`, [
|
||||
id,
|
||||
payload.merchant_id,
|
||||
payload.outlet_code || makeCode("out", id),
|
||||
payload.name,
|
||||
payload.address,
|
||||
payload.status || "active",
|
||||
now,
|
||||
now
|
||||
]);
|
||||
return mapOutlet(rows[0]);
|
||||
}
|
||||
export async function createTerminal(payload) {
|
||||
const id = randomUUID();
|
||||
const now = nowIso();
|
||||
const { rows } = await getPool().query(`INSERT INTO terminals (id, outlet_id, terminal_code, qr_mode, partner_reference, status, created_at, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
RETURNING *`, [
|
||||
id,
|
||||
payload.outlet_id,
|
||||
payload.terminal_code || makeCode("term", id),
|
||||
payload.qr_mode || "static",
|
||||
payload.partner_reference || null,
|
||||
payload.status || "active",
|
||||
now,
|
||||
now
|
||||
]);
|
||||
return mapTerminal(rows[0]);
|
||||
}
|
||||
export async function listOutlets(filter) {
|
||||
if (filter?.merchant_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM outlets WHERE merchant_id = $1 ORDER BY created_at DESC", [filter.merchant_id]);
|
||||
return rows.map(mapOutlet);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM outlets ORDER BY created_at DESC");
|
||||
return rows.map(mapOutlet);
|
||||
}
|
||||
export async function listTerminals(filter) {
|
||||
if (filter?.outlet_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM terminals WHERE outlet_id = $1 ORDER BY created_at DESC", [filter.outlet_id]);
|
||||
return rows.map(mapTerminal);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM terminals ORDER BY created_at DESC");
|
||||
return rows.map(mapTerminal);
|
||||
}
|
||||
export async function getOutletById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM outlets WHERE id = $1", [id]);
|
||||
return rows[0] ? mapOutlet(rows[0]) : null;
|
||||
}
|
||||
export async function getTerminalById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM terminals WHERE id = $1", [id]);
|
||||
return rows[0] ? mapTerminal(rows[0]) : null;
|
||||
}
|
||||
export async function patchOutlet(id, patch) {
|
||||
const existing = await getOutletById(id);
|
||||
if (!existing) {
|
||||
throw new Error("OUTLET_NOT_FOUND");
|
||||
}
|
||||
const merged = { ...existing, ...patch, updated_at: nowIso() };
|
||||
const { rows } = await getPool().query(`UPDATE outlets
|
||||
SET merchant_id = $2,
|
||||
outlet_code = $3,
|
||||
name = $4,
|
||||
address = $5,
|
||||
status = $6,
|
||||
updated_at = $7
|
||||
WHERE id = $1
|
||||
RETURNING *`, [
|
||||
id,
|
||||
merged.merchant_id,
|
||||
merged.outlet_code,
|
||||
merged.name,
|
||||
merged.address || null,
|
||||
merged.status,
|
||||
merged.updated_at
|
||||
]);
|
||||
return mapOutlet(rows[0]);
|
||||
}
|
||||
export async function patchTerminal(id, patch) {
|
||||
const existing = await getTerminalById(id);
|
||||
if (!existing) {
|
||||
throw new Error("TERMINAL_NOT_FOUND");
|
||||
}
|
||||
const merged = { ...existing, ...patch, updated_at: nowIso() };
|
||||
const { rows } = await getPool().query(`UPDATE terminals
|
||||
SET outlet_id = $2,
|
||||
terminal_code = $3,
|
||||
qr_mode = $4,
|
||||
partner_reference = $5,
|
||||
status = $6,
|
||||
updated_at = $7
|
||||
WHERE id = $1
|
||||
RETURNING *`, [
|
||||
id,
|
||||
merged.outlet_id,
|
||||
merged.terminal_code,
|
||||
merged.qr_mode,
|
||||
merged.partner_reference || null,
|
||||
merged.status,
|
||||
merged.updated_at
|
||||
]);
|
||||
return mapTerminal(rows[0]);
|
||||
}
|
||||
export function toOutletPayload(outlet) {
|
||||
return { ...outlet };
|
||||
}
|
||||
export function toTerminalPayload(terminal) {
|
||||
return { ...terminal };
|
||||
}
|
||||
119
dist/shared/store/merchantStore.js
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function makeCode(id) {
|
||||
return `m_${id.slice(0, 6)}`;
|
||||
}
|
||||
function toPublic(entity) {
|
||||
return entity;
|
||||
}
|
||||
function mapRowToMerchant(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
merchant_code: row.merchant_code,
|
||||
legal_name: row.legal_name,
|
||||
brand_name: row.brand_name || undefined,
|
||||
settlement_account_reference: row.settlement_account_reference || undefined,
|
||||
settlement_account_type: row.settlement_account_type || undefined,
|
||||
payout_mode: row.payout_mode,
|
||||
fee_profile_id: row.fee_profile_id || undefined,
|
||||
status: row.status,
|
||||
onboarding_status: row.onboarding_status,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
export async function createMerchant(payload) {
|
||||
const id = randomUUID();
|
||||
const now = nowIso();
|
||||
const payoutMode = payload.payout_mode || "merchant_direct";
|
||||
const { rows } = await getPool().query(`INSERT INTO merchants (
|
||||
id,
|
||||
merchant_code,
|
||||
legal_name,
|
||||
brand_name,
|
||||
settlement_account_reference,
|
||||
settlement_account_type,
|
||||
payout_mode,
|
||||
fee_profile_id,
|
||||
status,
|
||||
onboarding_status,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
||||
RETURNING *`, [
|
||||
id,
|
||||
makeCode(id),
|
||||
payload.legal_name,
|
||||
payload.brand_name,
|
||||
payload.settlement_account_reference,
|
||||
payload.settlement_account_type,
|
||||
payoutMode,
|
||||
payload.fee_profile_id,
|
||||
payload.status || "active",
|
||||
payload.onboarding_status || "pending",
|
||||
now,
|
||||
now
|
||||
]);
|
||||
return toPublic(mapRowToMerchant(rows[0]));
|
||||
}
|
||||
export async function getMerchantById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM merchants WHERE id = $1", [id]);
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return mapRowToMerchant(rows[0]);
|
||||
}
|
||||
export async function listMerchants() {
|
||||
const { rows } = await getPool().query("SELECT * FROM merchants ORDER BY created_at DESC");
|
||||
return rows.map(mapRowToMerchant);
|
||||
}
|
||||
export async function patchMerchant(id, patch) {
|
||||
const existing = await getMerchantById(id);
|
||||
if (!existing) {
|
||||
throw new Error("MERCHANT_NOT_FOUND");
|
||||
}
|
||||
const merged = { ...existing, ...patch, updated_at: nowIso() };
|
||||
const { rows } = await getPool().query(`UPDATE merchants
|
||||
SET legal_name = $2,
|
||||
brand_name = $3,
|
||||
settlement_account_reference = $4,
|
||||
settlement_account_type = $5,
|
||||
payout_mode = $6,
|
||||
fee_profile_id = $7,
|
||||
status = $8,
|
||||
onboarding_status = $9,
|
||||
updated_at = $10
|
||||
WHERE id = $1
|
||||
RETURNING *`, [
|
||||
id,
|
||||
merged.legal_name,
|
||||
merged.brand_name,
|
||||
merged.settlement_account_reference,
|
||||
merged.settlement_account_type,
|
||||
merged.payout_mode,
|
||||
merged.fee_profile_id,
|
||||
merged.status,
|
||||
merged.onboarding_status,
|
||||
merged.updated_at
|
||||
]);
|
||||
return mapRowToMerchant(rows[0]);
|
||||
}
|
||||
export function toMerchantPayload(m) {
|
||||
return {
|
||||
id: m.id,
|
||||
merchant_code: m.merchant_code,
|
||||
legal_name: m.legal_name,
|
||||
brand_name: m.brand_name,
|
||||
settlement_account_reference: m.settlement_account_reference,
|
||||
settlement_account_type: m.settlement_account_type,
|
||||
payout_mode: m.payout_mode,
|
||||
fee_profile_id: m.fee_profile_id,
|
||||
status: m.status,
|
||||
onboarding_status: m.onboarding_status,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at
|
||||
};
|
||||
}
|
||||
151
dist/shared/store/notificationStore.js
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function cloneNotification(notification) {
|
||||
return {
|
||||
...notification,
|
||||
payload_json: { ...notification.payload_json }
|
||||
};
|
||||
}
|
||||
function mapNotification(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
transaction_id: row.transaction_id,
|
||||
device_id: row.device_id || null,
|
||||
delivery_channel: "mqtt",
|
||||
payload_type: "payment_success",
|
||||
delivery_status: row.delivery_status,
|
||||
retry_count: row.retry_count,
|
||||
ack_status: row.ack_status,
|
||||
event_id: row.event_id,
|
||||
reason: row.reason || undefined,
|
||||
payload_json: row.payload_json || {},
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
sent_at: row.sent_at || undefined,
|
||||
ack_at: row.ack_at || undefined,
|
||||
next_retry_at: row.next_retry_at || undefined
|
||||
};
|
||||
}
|
||||
export async function createNotification(payload) {
|
||||
const now = nowIso();
|
||||
const insert = await getPool().query(`INSERT INTO notifications (
|
||||
id,
|
||||
transaction_id,
|
||||
device_id,
|
||||
delivery_status,
|
||||
retry_count,
|
||||
ack_status,
|
||||
event_id,
|
||||
reason,
|
||||
payload_json,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||||
ON CONFLICT (transaction_id, event_id) DO UPDATE
|
||||
SET updated_at = EXCLUDED.updated_at
|
||||
RETURNING *`, [
|
||||
randomUUID(),
|
||||
payload.transaction_id,
|
||||
payload.device_id,
|
||||
payload.delivery_status,
|
||||
0,
|
||||
payload.ack_status || "not_needed",
|
||||
payload.event_id,
|
||||
payload.reason || null,
|
||||
payload.payload_json || {},
|
||||
now,
|
||||
now
|
||||
]);
|
||||
if (insert.rowCount && insert.rowCount > 0) {
|
||||
return mapNotification(insert.rows[0]);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM notifications WHERE transaction_id = $1 AND event_id = $2", [payload.transaction_id, payload.event_id]);
|
||||
return mapNotification(rows[0]);
|
||||
}
|
||||
export async function getNotificationById(notificationId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM notifications WHERE id = $1", [notificationId]);
|
||||
return rows[0] ? cloneNotification(mapNotification(rows[0])) : null;
|
||||
}
|
||||
export async function updateNotification(notificationId, patch) {
|
||||
const existing = await getNotificationById(notificationId);
|
||||
if (!existing) {
|
||||
throw new Error("NOTIFICATION_NOT_FOUND");
|
||||
}
|
||||
const next = {
|
||||
...existing,
|
||||
...patch,
|
||||
id: existing.id,
|
||||
transaction_id: existing.transaction_id,
|
||||
device_id: existing.device_id,
|
||||
delivery_channel: existing.delivery_channel,
|
||||
payload_type: existing.payload_type,
|
||||
event_id: existing.event_id,
|
||||
payload_json: existing.payload_json,
|
||||
created_at: existing.created_at,
|
||||
updated_at: nowIso()
|
||||
};
|
||||
const { rows } = await getPool().query(`UPDATE notifications
|
||||
SET delivery_status = $2,
|
||||
retry_count = $3,
|
||||
ack_status = $4,
|
||||
device_id = COALESCE($5, device_id),
|
||||
reason = $6,
|
||||
sent_at = $7,
|
||||
ack_at = $8,
|
||||
next_retry_at = $9,
|
||||
updated_at = $10
|
||||
WHERE id = $1
|
||||
RETURNING *`, [
|
||||
notificationId,
|
||||
next.delivery_status,
|
||||
next.retry_count,
|
||||
next.ack_status,
|
||||
next.device_id ?? null,
|
||||
next.reason || null,
|
||||
next.sent_at || null,
|
||||
next.ack_at || null,
|
||||
next.next_retry_at || null,
|
||||
next.updated_at
|
||||
]);
|
||||
return cloneNotification(mapNotification(rows[0]));
|
||||
}
|
||||
export async function getNotificationByTransactionId(transactionId) {
|
||||
const { rows } = await getPool().query(`SELECT * FROM notifications
|
||||
WHERE transaction_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`, [transactionId]);
|
||||
return rows[0] ? mapNotification(rows[0]) : null;
|
||||
}
|
||||
export async function getNotificationByTransactionAndEvent(transactionId, eventId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM notifications WHERE transaction_id = $1 AND event_id = $2", [transactionId, eventId]);
|
||||
return rows[0] ? mapNotification(rows[0]) : null;
|
||||
}
|
||||
export async function listNotificationsByDevice(deviceId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM notifications WHERE device_id = $1 ORDER BY created_at DESC", [deviceId]);
|
||||
return rows.map(mapNotification);
|
||||
}
|
||||
export async function listNotifications(filter) {
|
||||
const filters = [];
|
||||
const params = [];
|
||||
if (filter?.transaction_id) {
|
||||
params.push(filter.transaction_id);
|
||||
filters.push(`transaction_id = $${params.length}`);
|
||||
}
|
||||
if (filter?.device_id) {
|
||||
params.push(filter.device_id);
|
||||
filters.push(`device_id = $${params.length}`);
|
||||
}
|
||||
if (filter?.delivery_status) {
|
||||
params.push(filter.delivery_status);
|
||||
filters.push(`delivery_status = $${params.length}`);
|
||||
}
|
||||
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
||||
const { rows } = await getPool().query(`SELECT * FROM notifications ${where} ORDER BY created_at DESC`, params);
|
||||
return rows.map(mapNotification);
|
||||
}
|
||||
export function toNotificationPayload(notification) {
|
||||
return cloneNotification(notification);
|
||||
}
|
||||
199
dist/shared/store/transactionStore.js
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPool } from "../db/pool";
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
function makeCode(id) {
|
||||
return `tx_${id.slice(0, 8)}`;
|
||||
}
|
||||
function mapTransaction(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
transaction_code: row.transaction_code,
|
||||
merchant_id: row.merchant_id,
|
||||
outlet_id: row.outlet_id,
|
||||
terminal_id: row.terminal_id,
|
||||
device_id: row.device_id || undefined,
|
||||
qr_mode: row.qr_mode,
|
||||
initiation_mode: row.initiation_mode,
|
||||
partner_reference: row.partner_reference,
|
||||
amount: Number(row.amount),
|
||||
currency: row.currency,
|
||||
status: row.status,
|
||||
created_at: row.created_at,
|
||||
paid_at: row.paid_at || undefined,
|
||||
expired_at: row.expired_at || undefined,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
function mapEvent(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
transaction_id: row.transaction_id,
|
||||
event_type: row.event_type,
|
||||
source: row.source,
|
||||
payload_json: row.payload_json || {},
|
||||
created_at: row.created_at
|
||||
};
|
||||
}
|
||||
const TRANSACTION_STATE_TRANSITIONS = {
|
||||
initiated: ["initiated", "awaiting_payment", "paid", "failed", "expired", "reversed"],
|
||||
awaiting_payment: ["awaiting_payment", "paid", "failed", "expired", "reversed"],
|
||||
paid: ["paid", "reversed"],
|
||||
failed: ["failed", "reversed"],
|
||||
expired: ["expired", "reversed"],
|
||||
reversed: ["reversed"]
|
||||
};
|
||||
function isValidTransactionTransition(from, to) {
|
||||
return TRANSACTION_STATE_TRANSITIONS[from]?.includes(to) ?? false;
|
||||
}
|
||||
export async function createTransaction(payload) {
|
||||
const id = randomUUID();
|
||||
const now = nowIso();
|
||||
const entity = {
|
||||
id,
|
||||
transaction_code: makeCode(id),
|
||||
merchant_id: payload.merchant_id,
|
||||
outlet_id: payload.outlet_id,
|
||||
terminal_id: payload.terminal_id,
|
||||
device_id: payload.device_id,
|
||||
qr_mode: payload.qr_mode || "static",
|
||||
initiation_mode: payload.initiation_mode || "static",
|
||||
partner_reference: payload.partner_reference,
|
||||
amount: payload.amount,
|
||||
currency: payload.currency || "IDR",
|
||||
status: payload.status || "initiated",
|
||||
created_at: now,
|
||||
paid_at: payload.paid_at,
|
||||
expired_at: payload.expired_at,
|
||||
updated_at: now
|
||||
};
|
||||
const txResult = await getPool().query(`INSERT INTO transactions (
|
||||
id,
|
||||
transaction_code,
|
||||
merchant_id,
|
||||
outlet_id,
|
||||
terminal_id,
|
||||
device_id,
|
||||
qr_mode,
|
||||
initiation_mode,
|
||||
partner_reference,
|
||||
amount,
|
||||
currency,
|
||||
status,
|
||||
created_at,
|
||||
paid_at,
|
||||
expired_at,
|
||||
updated_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
||||
RETURNING *`, [
|
||||
entity.id,
|
||||
entity.transaction_code,
|
||||
entity.merchant_id,
|
||||
entity.outlet_id,
|
||||
entity.terminal_id,
|
||||
entity.device_id || null,
|
||||
entity.qr_mode,
|
||||
entity.initiation_mode,
|
||||
entity.partner_reference,
|
||||
entity.amount,
|
||||
entity.currency,
|
||||
entity.status,
|
||||
entity.created_at,
|
||||
entity.paid_at || null,
|
||||
entity.expired_at || null,
|
||||
entity.updated_at
|
||||
]);
|
||||
await addTransactionEvent({
|
||||
transaction_id: txResult.rows[0].id,
|
||||
event_type: "INITIATED",
|
||||
source: "system",
|
||||
payload_json: { status: txResult.rows[0].status, partner_reference: payload.partner_reference }
|
||||
});
|
||||
return mapTransaction(txResult.rows[0]);
|
||||
}
|
||||
export async function addTransactionEvent(payload) {
|
||||
const id = randomUUID();
|
||||
const { rows } = await getPool().query(`INSERT INTO transaction_events (id, transaction_id, event_type, source, payload_json, created_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)
|
||||
RETURNING *`, [id, payload.transaction_id, payload.event_type, payload.source, payload.payload_json || {}, nowIso()]);
|
||||
return mapEvent(rows[0]);
|
||||
}
|
||||
export async function updateTransactionStatus(id, to, options) {
|
||||
const entity = await getTransactionById(id);
|
||||
if (!entity) {
|
||||
throw new Error("TRANSACTION_NOT_FOUND");
|
||||
}
|
||||
if (!isValidTransactionTransition(entity.status, to)) {
|
||||
throw new Error(`INVALID_TRANSACTION_STATE_TRANSITION:${entity.status}->${to}`);
|
||||
}
|
||||
if (entity.status === to) {
|
||||
return entity;
|
||||
}
|
||||
const now = nowIso();
|
||||
const next = {
|
||||
...entity,
|
||||
status: to,
|
||||
paid_at: options.paid_at || entity.paid_at,
|
||||
expired_at: options.expired_at || entity.expired_at,
|
||||
updated_at: now
|
||||
};
|
||||
if (to === "paid" && !next.paid_at) {
|
||||
next.paid_at = now;
|
||||
}
|
||||
if (to === "expired" && !next.expired_at) {
|
||||
next.expired_at = now;
|
||||
}
|
||||
const { rows } = await getPool().query(`UPDATE transactions
|
||||
SET status = $2,
|
||||
paid_at = $3,
|
||||
expired_at = $4,
|
||||
updated_at = $5
|
||||
WHERE id = $1
|
||||
RETURNING *`, [id, next.status, next.paid_at || null, next.expired_at || null, next.updated_at]);
|
||||
await addTransactionEvent({
|
||||
transaction_id: id,
|
||||
event_type: "STATE_CHANGED",
|
||||
source: options.source,
|
||||
payload_json: {
|
||||
from: entity.status,
|
||||
to,
|
||||
...options.eventContext
|
||||
}
|
||||
});
|
||||
return mapTransaction(rows[0]);
|
||||
}
|
||||
export async function getTransactionById(id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE id = $1", [id]);
|
||||
return rows[0] ? mapTransaction(rows[0]) : null;
|
||||
}
|
||||
export async function findTransactionByPartnerReference(partnerReference) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE partner_reference = $1", [partnerReference]);
|
||||
return rows[0] ? mapTransaction(rows[0]) : null;
|
||||
}
|
||||
export async function listTransactions(filter) {
|
||||
if (filter?.status && filter?.merchant_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE status = $1 AND merchant_id = $2 ORDER BY created_at DESC", [filter.status, filter.merchant_id]);
|
||||
return rows.map(mapTransaction);
|
||||
}
|
||||
if (filter?.status) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE status = $1 ORDER BY created_at DESC", [filter.status]);
|
||||
return rows.map(mapTransaction);
|
||||
}
|
||||
if (filter?.merchant_id) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC", [filter.merchant_id]);
|
||||
return rows.map(mapTransaction);
|
||||
}
|
||||
const { rows } = await getPool().query("SELECT * FROM transactions ORDER BY created_at DESC");
|
||||
return rows.map(mapTransaction);
|
||||
}
|
||||
export async function getTransactionEvents(transactionId) {
|
||||
const { rows } = await getPool().query("SELECT * FROM transaction_events WHERE transaction_id = $1 ORDER BY created_at ASC", [transactionId]);
|
||||
return rows.map(mapEvent);
|
||||
}
|
||||
export function toTransactionPayload(transaction) {
|
||||
return { ...transaction };
|
||||
}
|
||||
export function toTransactionEventPayload(event) {
|
||||
return { ...event, payload_json: { ...event.payload_json } };
|
||||
}
|
||||
3415
package-lock.json
generated
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "qris-soundbox-platform",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"start:dist": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"smoke:cleanup": "node scripts/smoke-cleanup.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"smoke:flow": "node scripts/smoke.mjs",
|
||||
"smoke:e2e": "bash -c 'export PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=qris_soundbox_platform; npm run smoke:cleanup; PORT=3100 ADMIN_TOKEN=admin-dev-token DEVICE_TOKEN=device-dev-token INTEGRATION_WEBHOOK_SECRET=dev-callback-secret npm start > /tmp/qris-smoke-e2e-server.log 2>&1 & echo $! >/tmp/qris-smoke-e2e.pid; for i in $(seq 1 120); do if curl -s -f http://127.0.0.1:3100/health >/dev/null; then break; fi; sleep 0.2; done; npm run smoke:flow; status=$?; kill $(cat /tmp/qris-smoke-e2e.pid) >/dev/null 2>&1 || true; exit $status'"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.19.2",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.21.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^22.7.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
48
scripts/smoke-cleanup.mjs
Normal file
@ -0,0 +1,48 @@
|
||||
import { env as processEnv } from "node:process";
|
||||
import { Pool } from "pg";
|
||||
|
||||
function required(value, fallback) {
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
const pool = processEnv.DATABASE_URL
|
||||
? new Pool({ connectionString: processEnv.DATABASE_URL })
|
||||
: new Pool({
|
||||
host: required(processEnv.PGHOST, "127.0.0.1"),
|
||||
port: Number(required(processEnv.PGPORT, "5432")),
|
||||
user: required(processEnv.PGUSER, "postgres"),
|
||||
password: processEnv.PGPASSWORD || "",
|
||||
database: required(processEnv.PGDATABASE, "qris_soundbox_platform")
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const txResult = await client.query("DELETE FROM transactions WHERE partner_reference LIKE 'PR-%' RETURNING id");
|
||||
const devResult = await client.query("DELETE FROM devices WHERE device_code LIKE 'DEV-%' RETURNING id");
|
||||
const merchantResult = await client.query("DELETE FROM merchants WHERE legal_name LIKE 'Smoke Merchant %' RETURNING id");
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
transactions_deleted: txResult.rowCount,
|
||||
devices_deleted: devResult.rowCount,
|
||||
merchants_deleted: merchantResult.rowCount,
|
||||
note: "outlets/terminals are removed via merchant cascade"
|
||||
}));
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("cleanup failed", error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("cleanup failed", error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
167
scripts/smoke.mjs
Normal file
@ -0,0 +1,167 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
|
||||
const PORT = process.env.PORT || "3100";
|
||||
const BASE = process.env.BASE_URL || `http://127.0.0.1:${PORT}`;
|
||||
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "admin-dev-token";
|
||||
const DEVICE_TOKEN = process.env.DEVICE_TOKEN || "device-dev-token";
|
||||
const SECRET = process.env.INTEGRATION_WEBHOOK_SECRET || "dev-callback-secret";
|
||||
|
||||
function short(data) {
|
||||
const json = typeof data === 'string' ? data : JSON.stringify(data || {});
|
||||
return json.length > 180 ? `${json.slice(0, 180)}...` : json;
|
||||
}
|
||||
|
||||
async function req(path, options = {}) {
|
||||
const response = await fetch(`${BASE}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {})
|
||||
},
|
||||
body: Object.prototype.hasOwnProperty.call(options, 'body') ? JSON.stringify(options.body) : undefined
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let body = null;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${options._label || path} failed: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log(`${options._label || `${options.method || 'GET'} ${path}`} => ${response.status} ${short(body)}`);
|
||||
return body;
|
||||
}
|
||||
|
||||
async function reqAdmin(path, opts = {}) {
|
||||
return req(path, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${ADMIN_TOKEN}` } });
|
||||
}
|
||||
|
||||
async function reqDevice(path, opts = {}) {
|
||||
return req(path, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${DEVICE_TOKEN}` } });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await req('/health', { _label: 'GET /health' });
|
||||
await req('/admin/login', { method: 'POST', body: { username: 'admin', password: 'admin' }, _label: 'POST /admin/login' });
|
||||
|
||||
await reqAdmin('/admin/seed/status', { _label: 'GET /admin/seed/status' });
|
||||
const ts = Date.now();
|
||||
|
||||
const merchant = await reqAdmin('/admin/merchants', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
legal_name: `Smoke Merchant ${ts}`,
|
||||
brand_name: `SMK-${ts}`,
|
||||
settlement_account_reference: `bank:${ts}`,
|
||||
settlement_account_type: 'merchant_bank_account',
|
||||
payout_mode: 'merchant_direct'
|
||||
},
|
||||
_label: 'POST /admin/merchants'
|
||||
});
|
||||
|
||||
const merchantId = merchant?.data?.id;
|
||||
const outlet = await reqAdmin(`/admin/merchants/${merchantId}/outlets`, {
|
||||
method: 'POST',
|
||||
body: { name: `Outlet ${ts}` },
|
||||
_label: 'POST /admin/merchants/:id/outlets'
|
||||
});
|
||||
|
||||
const outletId = outlet?.data?.id;
|
||||
const terminal = await reqAdmin(`/admin/outlets/${outletId}/terminals`, {
|
||||
method: 'POST',
|
||||
body: { terminal_code: `TERM-${ts}`, qr_mode: 'static' },
|
||||
_label: 'POST /admin/outlets/:id/terminals'
|
||||
});
|
||||
|
||||
const terminalId = terminal?.data?.id;
|
||||
const device = await reqAdmin('/admin/devices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
device_code: `DEV-${ts}`,
|
||||
vendor: 'acme',
|
||||
model: 'v1',
|
||||
communication_mode: 'mqtt',
|
||||
status: 'active'
|
||||
},
|
||||
_label: 'POST /admin/devices'
|
||||
});
|
||||
|
||||
const deviceId = device?.data?.id;
|
||||
await reqAdmin(`/admin/devices/${deviceId}/bind`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
merchant_id: merchantId,
|
||||
outlet_id: outletId,
|
||||
terminal_id: terminalId
|
||||
},
|
||||
_label: 'POST /admin/devices/:id/bind'
|
||||
});
|
||||
|
||||
await reqDevice('/device/heartbeat', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
device_id: deviceId,
|
||||
timestamp: new Date().toISOString(),
|
||||
firmware_version: '1.2.3',
|
||||
network_strength: 88,
|
||||
battery_level: 77,
|
||||
state: 'idle'
|
||||
},
|
||||
_label: 'POST /device/heartbeat'
|
||||
});
|
||||
|
||||
const tx = await reqAdmin('/admin/transactions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
partner_reference: `PR-${ts}`,
|
||||
merchant_id: merchantId,
|
||||
outlet_id: outletId,
|
||||
terminal_id: terminalId,
|
||||
device_id: deviceId,
|
||||
amount: 19900,
|
||||
currency: 'IDR',
|
||||
qr_mode: 'static',
|
||||
initiation_mode: 'static',
|
||||
status: 'initiated'
|
||||
},
|
||||
_label: 'POST /admin/transactions'
|
||||
});
|
||||
|
||||
const txId = tx?.data?.id;
|
||||
const callback = {
|
||||
partner_reference: `PR-${ts}`,
|
||||
partner_txn_id: `PTX-${ts}`,
|
||||
amount: 19900,
|
||||
currency: 'IDR',
|
||||
payment_status: 'paid',
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString()
|
||||
};
|
||||
const signature = createHmac('sha256', SECRET).update(JSON.stringify(callback)).digest('hex');
|
||||
|
||||
await req('/integrations/qris/callback', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Partner-Signature': signature },
|
||||
body: { ...callback, signature },
|
||||
_label: 'POST /integrations/qris/callback'
|
||||
});
|
||||
|
||||
await reqAdmin(`/admin/transactions/${txId}`, { _label: 'GET /admin/transactions/:id' });
|
||||
await reqAdmin(`/admin/transactions/${txId}/events`, { _label: 'GET /admin/transactions/:id/events' });
|
||||
await reqAdmin(`/admin/transactions/${txId}/heartbeats`, { _label: 'GET /admin/transactions/:id/heartbeats' });
|
||||
await reqAdmin(`/admin/devices/${deviceId}/heartbeats`, { _label: 'GET /admin/devices/:id/heartbeats' });
|
||||
await reqAdmin('/admin/notifications/failed', { _label: 'GET /admin/notifications/failed' });
|
||||
await reqAdmin(`/admin/transactions/${txId}/retry-notification`, {
|
||||
method: 'POST',
|
||||
body: {},
|
||||
_label: 'POST /admin/transactions/:id/retry-notification'
|
||||
});
|
||||
await reqAdmin('/admin/dashboard/summary', { _label: 'GET /admin/dashboard/summary' });
|
||||
|
||||
console.log(`Smoke point 4 flow done. tx=${txId} device=${deviceId}`);
|
||||
})();
|
||||
84
src/app.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import morgan from "morgan";
|
||||
import { env } from "./config/env";
|
||||
import { requestContext } from "./shared/middleware/requestContext";
|
||||
import { handleErrors, successResponse } from "./shared/middleware/errorMiddleware";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import adminRoutes from "./routes/admin";
|
||||
import integrationRoutes from "./routes/integrations";
|
||||
import deviceRoutes from "./routes/device";
|
||||
import { startNotificationOrchestrator } from "./shared/orchestrators/notificationOrchestrator";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
const app = express();
|
||||
startNotificationOrchestrator();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(express.json());
|
||||
app.use(morgan("dev"));
|
||||
app.use(requestContext);
|
||||
|
||||
app.get("/", (_req, res) => {
|
||||
res.json(successResponse(_req, { status: "ok" }));
|
||||
});
|
||||
|
||||
function resolveUiPageFile(slug: string) {
|
||||
const workspaceRoot = process.cwd();
|
||||
const candidates = [
|
||||
path.resolve(workspaceRoot, "ui", slug, "index.html"),
|
||||
path.resolve(workspaceRoot, "ui", slug.replace(/_/g, "-"), "index.html"),
|
||||
path.resolve(workspaceRoot, "ui", slug.replace(/-/g, "_"), "index.html")
|
||||
];
|
||||
|
||||
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
||||
}
|
||||
|
||||
app.get("/ui", (_req, res) => {
|
||||
const filePath = path.resolve(process.cwd(), "ui/index.html");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.get("/ui/hub", (_req, res) => {
|
||||
const filePath = path.resolve(process.cwd(), "ui/hub.html");
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.use("/ui/shared", express.static(path.resolve(process.cwd(), "ui", "shared")));
|
||||
|
||||
app.get("/ui/:page", (req, res, next) => {
|
||||
const filePath = resolveUiPageFile(req.params.page);
|
||||
if (!filePath) {
|
||||
return next();
|
||||
}
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.use("/admin", adminRoutes);
|
||||
app.use("/integrations", integrationRoutes);
|
||||
app.use("/device", deviceRoutes);
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {
|
||||
handleErrors(err, _req, res, next);
|
||||
});
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.status(200).json(
|
||||
successResponse(req, {
|
||||
status: "healthy",
|
||||
time: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
code: "NOT_FOUND",
|
||||
message: `Route ${req.path} not found`,
|
||||
request_id: req.requestId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||