Files
boc/iom/quixzoom-app/screens/EarningsScreen.tsx
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

264 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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' },
})