Files
ladill-mini/apps/ios/LadillMini/Features/Home/HomeTabs.swift
T
isaaccladandCursor ea6afb80a4
Deploy Ladill Mini / deploy (push) Successful in 35s
Add Ladill Mini native Android and iOS apps.
Ship full-featured mobile clients with auth, payments, QRs, push notifications, and iOS polish including Figtree typography, app icons, and QR previews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 22:30:35 +00:00

243 lines
10 KiB
Swift

import SwiftUI
struct OverviewTab: View {
@ObservedObject var vm: HomeViewModel
let onCreateQr: () -> Void
let onSeePayments: () -> Void
let onOpenWallet: () -> Void
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
let firstName = SessionStore.shared.name?
.trimmingCharacters(in: .whitespaces)
.split(separator: " ").first.map(String.init)
Text(firstName.map { "Hello, \($0)" } ?? "Hello")
.font(.ladill(size: 22, weight: .semibold))
.foregroundStyle(LadillColors.ink)
Text("Here is today's takings, your payment QRs, and recent payments.")
.font(.ladill(size: 14))
.foregroundStyle(LadillColors.muted)
if vm.hasPayout == false {
payoutNote
}
switch vm.overviewState {
case .idle, .loading:
LoadingView()
.task { await vm.loadOverview() }
case .failure(let message):
ErrorView(message: message) { Task { await vm.loadOverview() } }
case .success(let data):
overviewContent(data)
}
}
.padding(20)
}
.refreshable { await vm.refreshOverview() }
}
private var payoutNote: some View {
Button(action: onOpenWallet) {
HStack {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(LadillColors.indigo)
Text("Set up your payout account to withdraw takings.")
.font(.ladill(size: 13))
.foregroundStyle(LadillColors.indigoText)
Spacer()
}
.padding(14)
.background(LadillColors.indigoSoft)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.buttonStyle(.plain)
}
@ViewBuilder
private func overviewContent(_ data: Overview) -> some View {
let currency = data.currency ?? "GHS"
HStack(spacing: 12) {
StatCard(label: "Today", value: Formatters.money(minor: data.todayTakingsMinor ?? 0, currency: currency), accent: true)
StatCard(label: "Payments", value: "\(data.todayCount ?? 0)")
}
HStack(spacing: 12) {
StatCard(label: "Payment QRs", value: "\(data.paymentQrCount ?? 0)")
StatCard(label: "Wallet", value: Formatters.money(minor: data.walletBalanceMinor ?? 0, currency: currency))
}
LadillButton(title: "Create payment QR", action: onCreateQr)
.padding(.top, 4)
if let payments = data.recentPayments, !payments.isEmpty {
HStack {
Text("Recent payments")
.font(.ladill(size: 16, weight: .semibold))
Spacer()
Button("See all", action: onSeePayments)
.font(.ladill(size: 13, weight: .semibold))
.foregroundStyle(LadillColors.indigo)
}
.padding(.top, 8)
ForEach(payments) { payment in
paymentRow(payment)
}
}
}
private func paymentRow(_ payment: Payment) -> some View {
LadillCard {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(payment.payerName ?? payment.qrLabel ?? "Payment")
.font(.ladill(size: 15, weight: .semibold))
Text(Formatters.date(iso: payment.paidAt ?? payment.createdAt))
.font(.ladill(size: 12))
.foregroundStyle(LadillColors.muted)
}
Spacer()
Text(Formatters.money(minor: payment.amountMinor ?? 0, currency: payment.currency ?? "GHS"))
.font(.ladill(size: 15, weight: .semibold))
.foregroundStyle(LadillColors.money)
}
}
}
}
struct PaymentQrsTab: View {
@ObservedObject var vm: HomeViewModel
let onCreateQr: () -> Void
let onOpenQr: (Int64) -> Void
var body: some View {
ScrollView {
VStack(spacing: 12) {
LadillButton(title: "Create payment QR", action: onCreateQr)
switch vm.qrsState {
case .idle, .loading:
LoadingView().task { await vm.loadQrs() }
case .failure(let message):
ErrorView(message: message) { Task { await vm.loadQrs() } }
case .success(let qrs):
if qrs.isEmpty {
Text("No payment QRs yet. Create one to start collecting.")
.font(.ladill(size: 14))
.foregroundStyle(LadillColors.muted)
.multilineTextAlignment(.center)
.padding(.vertical, 40)
} else {
ForEach(qrs) { qr in
Button { onOpenQr(qr.id) } label: {
LadillCard {
HStack {
Image(systemName: "qrcode")
.font(.system(size: 28))
.foregroundStyle(LadillColors.indigo)
VStack(alignment: .leading, spacing: 4) {
Text(qr.label)
.font(.ladill(size: 15, weight: .semibold))
.foregroundStyle(LadillColors.ink)
Text(qr.businessName ?? "")
.font(.ladill(size: 12))
.foregroundStyle(LadillColors.muted)
}
Spacer()
if qr.isActive == false {
Text("Paused")
.font(.ladill(size: 11, weight: .medium))
.foregroundStyle(LadillColors.muted)
}
}
}
}
.buttonStyle(.plain)
}
}
}
}
.padding(20)
}
.refreshable { await vm.loadQrs() }
}
}
struct PaymentsTab: View {
@ObservedObject var vm: HomeViewModel
var body: some View {
ScrollView {
VStack(spacing: 12) {
switch vm.paymentsState {
case .idle, .loading:
LoadingView().task { await vm.loadPayments() }
case .failure(let message):
ErrorView(message: message) { Task { await vm.loadPayments() } }
case .success(let payments):
if payments.isEmpty {
Text("No payments yet.")
.font(.ladill(size: 14))
.foregroundStyle(LadillColors.muted)
.padding(.vertical, 40)
} else {
ForEach(payments) { payment in
LadillCard {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(payment.payerName ?? payment.qrLabel ?? "Payment")
.font(.ladill(size: 15, weight: .semibold))
Text(Formatters.date(iso: payment.paidAt ?? payment.createdAt))
.font(.ladill(size: 12))
.foregroundStyle(LadillColors.muted)
}
Spacer()
Text(Formatters.money(minor: payment.amountMinor ?? 0, currency: payment.currency ?? "GHS"))
.font(.ladill(size: 15, weight: .semibold))
.foregroundStyle(LadillColors.money)
}
}
}
}
}
}
.padding(20)
}
.refreshable { await vm.loadPayments() }
}
}
struct PayoutsTab: View {
@ObservedObject var vm: HomeViewModel
let onOpenWallet: () -> Void
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Payouts")
.font(.ladill(size: 22, weight: .semibold))
Text("Takings settle into your Ladill wallet (5% fee), then withdraw to bank or MoMo.")
.font(.ladill(size: 14))
.foregroundStyle(LadillColors.muted)
switch vm.payoutsState {
case .idle, .loading:
LoadingView().task { await vm.loadPayouts() }
case .failure(let message):
ErrorView(message: message) { Task { await vm.loadPayouts() } }
case .success(let data):
let currency = data.currency ?? "GHS"
StatCard(label: "Total revenue", value: Formatters.money(minor: data.totalRevenueMinor ?? 0, currency: currency), accent: true)
StatCard(label: "Wallet balance", value: Formatters.money(minor: data.walletBalanceMinor ?? 0, currency: currency))
}
LadillButton(title: "Open wallet", action: onOpenWallet)
}
.padding(20)
}
.refreshable { await vm.loadPayouts() }
}
}