Files
boc/iom/quixzoom-app/screens/EarningsScreen.tsx
T

264 lines
9.7 KiB
TypeScript
Raw Normal View History

/**
* EarningsScreen — saldo + utbetalningshistorik
*/
import { useCallback, useEffect, useState } from 'react'
import {
View, Text, ScrollView, TouchableOpacity,
StyleSheet, ActivityIndicator, RefreshControl, Alert,
} from 'react-native'
import { wallet, payouts, type WalletBalance, type Payout } from '../lib/api'
type TabKey = 'oversikt' | 'utbetalningar'
const PAYOUT_STATUS: Record<string, { label: string; color: string }> = {
pending: { label: 'Väntar', color: '#f59e0b' },
approved: { label: 'Godkänd', color: '#10b981' },
paid: { label: 'Utbetald', color: '#6366f1' },
rejected: { label: 'Nekad', color: '#ef4444' },
}
export function EarningsScreen() {
const [tab, setTab] = useState<TabKey>('oversikt')
const [balance, setBalance] = useState<WalletBalance | null>(null)
const [payoutList, setPayoutList] = useState<Payout[]>([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [requesting, setRequesting] = useState(false)
const load = useCallback(async () => {
try {
const [b, p] = await Promise.all([
wallet.balance(),
payouts.history(),
])
setBalance(b)
setPayoutList(p)
} catch {
// silent
} finally {
setLoading(false)
setRefreshing(false)
}
}, [])
useEffect(() => { load() }, [])
const onRefresh = useCallback(() => { setRefreshing(true); load() }, [load])
const handleRequestPayout = () => {
if (!balance) return
const available = balance.balance - balance.pending_payouts
if (available < 10000) { // 100 SEK minimum
Alert.alert('Minsta utbetalning 100 kr', `Du har ${(available / 100).toFixed(0)} kr tillgängligt.`)
return
}
Alert.alert(
'Begär utbetalning',
`Vill du ta ut ${(available / 100).toFixed(0)} SEK till ditt bankkonto?`,
[
{ text: 'Avbryt', style: 'cancel' },
{
text: 'Ta ut',
onPress: async () => {
setRequesting(true)
try {
await payouts.request(available)
Alert.alert('✅ Utbetalning begärd!', 'Pengarna är hos dig inom 13 bankdagar.')
load()
} catch (e: any) {
Alert.alert('Fel', e?.message ?? 'Kunde inte begära utbetalning.')
} finally {
setRequesting(false)
}
},
},
],
)
}
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator color="#6366f1" size="large" />
</View>
)
}
return (
<ScrollView
style={styles.container}
contentContainerStyle={{ paddingBottom: 40 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#6366f1" />}
>
{/* Big balance card */}
<View style={styles.balanceCard}>
<Text style={styles.balanceLabel}>Tillgängligt</Text>
<Text style={styles.balanceAmount}>
{balance ? (balance.balance / 100).toFixed(0) : '0'} SEK
</Text>
{balance && balance.pending_payouts > 0 && (
<Text style={styles.pendingNote}>
+ {(balance.pending_payouts / 100).toFixed(0)} SEK under behandling
</Text>
)}
<TouchableOpacity
style={[styles.payoutBtn, requesting && styles.payoutBtnDisabled]}
onPress={handleRequestPayout}
disabled={requesting}
>
{requesting
? <ActivityIndicator color="#fff" size="small" />
: <Text style={styles.payoutBtnText}>💳 Begär utbetalning</Text>
}
</TouchableOpacity>
</View>
{/* Tab bar */}
<View style={styles.tabBar}>
{([['oversikt', 'Översikt'], ['utbetalningar', 'Utbetalningar']] as [TabKey, string][]).map(([key, label]) => (
<TouchableOpacity
key={key}
style={[styles.tab, tab === key && styles.tabActive]}
onPress={() => setTab(key)}
>
<Text style={[styles.tabText, tab === key && styles.tabTextActive]}>{label}</Text>
</TouchableOpacity>
))}
</View>
{/* Översikt */}
{tab === 'oversikt' && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Din ekonomi</Text>
<View style={styles.infoCard}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Valuta</Text>
<Text style={styles.infoValue}>{balance?.currency ?? 'SEK'}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Tillgängligt</Text>
<Text style={styles.infoValue}>{balance ? (balance.balance / 100).toFixed(0) : '0'} kr</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Väntande utbetalningar</Text>
<Text style={styles.infoValue}>{balance ? (balance.pending_payouts / 100).toFixed(0) : '0'} kr</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Totalt utbetalt</Text>
<Text style={styles.infoValue}>
{payoutList
.filter(p => p.status === 'paid')
.reduce((sum, p) => sum + p.amount, 0) / 100} kr
</Text>
</View>
</View>
</View>
)}
{/* Utbetalningar */}
{tab === 'utbetalningar' && (
<View style={styles.section}>
{payoutList.length === 0
? (
<View style={styles.emptyWrap}>
<Text style={styles.emptyEmoji}>💳</Text>
<Text style={styles.emptyTitle}>Inga utbetalningar ännu</Text>
<Text style={styles.empty}>Begär din första utbetalning ovan.</Text>
</View>
)
: payoutList.map(p => {
const s = PAYOUT_STATUS[p.status] ?? { label: p.status, color: '#fff' }
return (
<View key={p.id} style={styles.payoutRow}>
<View>
<Text style={styles.payoutDate}>
{new Date(p.created_at).toLocaleDateString('sv-SE', { month: 'short', day: 'numeric', year: 'numeric' })}
</Text>
{p.paid_at && (
<Text style={styles.payoutPaidAt}>
Betald {new Date(p.paid_at).toLocaleDateString('sv-SE')}
</Text>
)}
</View>
<Text style={styles.payoutAmount}>
{(p.amount / 100).toFixed(0)} {p.currency}
</Text>
<View style={[styles.payoutStatusBadge, { backgroundColor: s.color + '22' }]}>
<Text style={[styles.payoutStatusText, { color: s.color }]}>{s.label}</Text>
</View>
</View>
)
})
}
</View>
)}
</ScrollView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0A1B' },
center: { flex: 1, backgroundColor: '#0A0A1B', alignItems: 'center', justifyContent: 'center' },
balanceCard: {
margin: 16,
marginTop: 56,
backgroundColor: '#6366f1',
borderRadius: 24, padding: 28,
alignItems: 'center',
},
balanceLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 14, marginBottom: 6 },
balanceAmount: { color: '#fff', fontSize: 48, fontWeight: '900', letterSpacing: -2 },
pendingNote: { color: 'rgba(255,255,255,0.6)', fontSize: 13, marginTop: 4 },
payoutBtn: {
marginTop: 20,
backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: 14, paddingHorizontal: 28, paddingVertical: 12,
borderWidth: 1, borderColor: 'rgba(255,255,255,0.25)',
},
payoutBtnDisabled: { opacity: 0.6 },
payoutBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 },
tabBar: {
flexDirection: 'row', marginHorizontal: 16, marginTop: 16, marginBottom: 4,
backgroundColor: 'rgba(255,255,255,0.04)',
borderRadius: 14, padding: 4,
},
tab: { flex: 1, paddingVertical: 8, alignItems: 'center', borderRadius: 10 },
tabActive: { backgroundColor: '#6366f1' },
tabText: { color: 'rgba(255,255,255,0.4)', fontSize: 12, fontWeight: '600' },
tabTextActive: { color: '#fff' },
section: { paddingHorizontal: 16, paddingTop: 16 },
sectionTitle: { color: 'rgba(255,255,255,0.35)', fontSize: 12, fontWeight: '700', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 16 },
infoCard: {
backgroundColor: 'rgba(255,255,255,0.04)',
borderRadius: 16, padding: 20,
borderWidth: 1, borderColor: 'rgba(255,255,255,0.07)',
},
infoRow: {
flexDirection: 'row', justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)',
},
infoLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 14 },
infoValue: { color: '#fff', fontSize: 14, fontWeight: '700' },
emptyWrap: { alignItems: 'center', paddingTop: 40 },
emptyEmoji: { fontSize: 40, marginBottom: 12 },
emptyTitle: { color: '#fff', fontSize: 16, fontWeight: '700', marginBottom: 6 },
empty: { color: 'rgba(255,255,255,0.3)', textAlign: 'center', fontSize: 14 },
payoutRow: {
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between',
paddingVertical: 14,
borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)',
},
payoutDate: { color: '#fff', fontSize: 13, fontWeight: '500' },
payoutPaidAt: { color: 'rgba(255,255,255,0.3)', fontSize: 11 },
payoutAmount: { color: '#fff', fontWeight: '700', fontSize: 14 },
payoutStatusBadge: { borderRadius: 20, paddingHorizontal: 10, paddingVertical: 4 },
payoutStatusText: { fontSize: 12, fontWeight: '600' },
})