Update CBS Mobile App codebase

This commit is contained in:
Abin Toppo
2026-09-21 13:21:25 +05:30
parent 58af541536
commit 2dcb10775c
339 changed files with 26750 additions and 16767 deletions
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInRight, FadeOutLeft } from 'react-native-reanimated';
import { Input, Button, DatePicker, KeyboardAwareScrollView } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import type { AgentLoansStackParamList } from '../navigation/agentNavigationTypes';
import { useAccountOpeningForm } from '../hooks/useAccountOpeningForm';
@@ -33,7 +33,7 @@ const StepIndicator = ({ steps, currentStepIndex }: { steps: any[], currentStepI
{i < currentStepIndex ? (
<Ionicons name="checkmark" size={14} color={theme.colors.white} />
) : (
<Text style={{ color: isPassed ? theme.colors.white : theme.colors.textMuted, fontSize: 12, fontWeight: '700' }}>{i + 1}</Text>
<Text style={{ color: isPassed ? theme.colors.white : theme.colors.textMuted, fontSize: 12, fontFamily: fontFamily.bold }}>{i + 1}</Text>
)}
</View>
{i < steps.length - 1 && <View style={[styles.stepLine, { backgroundColor: i < currentStepIndex ? theme.colors.primary : theme.colors.cardBorder }]} />}
@@ -48,7 +48,7 @@ const StepIndicator = ({ steps, currentStepIndex }: { steps: any[], currentStepI
style={[
styles.stepLabel,
{ color: i <= currentStepIndex ? theme.colors.primary : theme.colors.textMuted },
i === currentStepIndex && { fontWeight: '700' },
i === currentStepIndex && { fontFamily: fontFamily.bold },
]}
>
{s.label}
@@ -231,7 +231,7 @@ export default function AccountOpeningScreen({ navigation }: { navigation: Nativ
value={draft.initialDeposit}
onChangeText={v => updateField('initialDeposit', v)}
keyboardType="number-pad"
leftAccessory={<Text style={{ fontSize: 16, fontWeight: '700', color: theme.colors.textPrimary }}>₹</Text>}
leftAccessory={<Text style={{ fontSize: 16, fontFamily: fontFamily.bold, color: theme.colors.textPrimary }}>₹</Text>}
/>
{(draft.accountType === 'RD' || draft.accountType === 'DDS') && (
<Input
@@ -240,7 +240,7 @@ export default function AccountOpeningScreen({ navigation }: { navigation: Nativ
value={draft.monthlyInstallment}
onChangeText={v => updateField('monthlyInstallment', v)}
keyboardType="number-pad"
leftAccessory={<Text style={{ fontSize: 16, fontWeight: '700', color: theme.colors.textPrimary }}>₹</Text>}
leftAccessory={<Text style={{ fontSize: 16, fontFamily: fontFamily.bold, color: theme.colors.textPrimary }}>₹</Text>}
/>
)}
</View>
@@ -295,7 +295,7 @@ export default function AccountOpeningScreen({ navigation }: { navigation: Nativ
<View style={styles.reviewRow}>
<Text style={[styles.reviewLabel, { color: theme.colors.textMuted }]}>Initial Deposit</Text>
<Text style={[styles.reviewValue, { color: theme.colors.success, fontWeight: '800', fontSize: 16 }]}>
<Text style={[styles.reviewValue, { color: theme.colors.success, fontFamily: fontFamily.bold, fontSize: 16 }]}>
{formatAgentCurrency(Number(draft.initialDeposit))}
</Text>
</View>
@@ -361,7 +361,7 @@ const styles = StyleSheet.create({
},
localHeaderTitle: {
fontSize: 18,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: 0.5,
},
// ── Step indicator ─────────────────────────
@@ -401,7 +401,7 @@ const styles = StyleSheet.create({
},
stepLabel: {
fontSize: 10,
fontWeight: '600',
fontFamily: fontFamily.semibold,
textTransform: 'uppercase',
letterSpacing: 0.3,
width: 54,
@@ -418,7 +418,7 @@ const styles = StyleSheet.create({
},
stepTitle: {
fontSize: 20,
fontWeight: '800',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
stepSubtitle: {
@@ -448,7 +448,7 @@ const styles = StyleSheet.create({
},
typeLabel: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
typeDesc: {
@@ -477,7 +477,7 @@ const styles = StyleSheet.create({
},
sectionDividerText: {
fontSize: 12,
fontWeight: '700',
fontFamily: fontFamily.bold,
textTransform: 'uppercase',
letterSpacing: 0.5,
paddingHorizontal: 12,
@@ -502,7 +502,7 @@ const styles = StyleSheet.create({
flex: 1,
fontSize: 13,
lineHeight: 18,
fontWeight: '500',
fontFamily: fontFamily.medium,
},
// ── Review card ───────────────────────────
@@ -519,11 +519,11 @@ const styles = StyleSheet.create({
},
reviewLabel: {
fontSize: 13,
fontWeight: '500',
fontFamily: fontFamily.medium,
},
reviewValue: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
textAlign: 'right',
flexShrink: 1,
marginLeft: 12,
@@ -5,12 +5,14 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell, Input, Button, KeyboardAwareScrollView } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppSelector } from '../../../app/hooks';
import { useToast } from '../../../hooks/useToast';
import { selectCustomerById } from '../store/agentSelectors';
import type { AgentCustomersStackParamList } from '../navigation/agentNavigationTypes';
import { useCustomerForm } from '../hooks/useCustomerForm';
import { useLazyGetCustomerByClientIdQuery } from '../services/agentApi';
type Props = NativeStackScreenProps<AgentCustomersStackParamList, 'AddEditCustomer'>;
@@ -18,7 +20,7 @@ const FormSection = ({ title, icon, children }: { title: string, icon: string, c
const theme = useTheme();
return (
<Animated.View entering={FadeInUp} style={[styles.section, { backgroundColor: theme.colors.surface, borderColor: theme.colors.cardBorder }]}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionHeader, { borderBottomColor: theme.colors.cardBorder }]}>
<View style={[styles.sectionIcon, { backgroundColor: `${theme.colors.primary}15` }]}>
<Ionicons name={icon as any} size={18} color={theme.colors.primary} />
</View>
@@ -39,7 +41,9 @@ export default function AddEditCustomerScreen({ route, navigation }: Props) {
customerId ? selectCustomerById(customerId)(state) : null
);
const { data, errors, updateField, submit, isSubmitting } = useCustomerForm(existingCustomer || undefined);
const { data, errors, updateField, prefill, submit, isSubmitting } = useCustomerForm(existingCustomer || undefined);
const toast = useToast();
const [lookupClient, { isFetching: isLookingUp }] = useLazyGetCustomerByClientIdQuery();
const isEdit = !!existingCustomer;
@@ -50,6 +54,31 @@ export default function AddEditCustomerScreen({ route, navigation }: Props) {
}
};
const handleFetchClient = async () => {
const clientId = data.clientId.trim();
if (!clientId) {
toast('Enter a Client ID first', 'error');
return;
}
try {
const client = await lookupClient(clientId).unwrap();
prefill({
name: client.name,
phone: client.phone,
email: client.email,
address: client.address,
city: client.city,
state: client.state,
pincode: client.pincode,
aadhaarNumber: client.aadhaarNumber,
panNumber: client.panNumber,
});
toast('Details loaded', 'success', `${client.name}'s profile has been auto-filled.`);
} catch {
toast('Client not found', 'error', 'No client matches this Client ID.');
}
};
return (
@@ -57,6 +86,27 @@ export default function AddEditCustomerScreen({ route, navigation }: Props) {
<View style={styles.flexContainer}>
<KeyboardAwareScrollView contentContainerStyle={styles.scrollContent} style={styles.flex}>
<FormSection title="Client ID" icon="card-outline">
<Text style={[styles.helperText, { color: theme.colors.textSecondary }]}>
Have an existing Client ID? Enter it to auto-fill the customer's details.
</Text>
<Input
label="Client ID (Optional)"
placeholder="e.g. CL-100245"
value={data.clientId}
onChangeText={(v) => updateField('clientId', v)}
autoCapitalize="characters"
error={errors.clientId}
/>
<Button
title="Fetch Details"
variant="secondary"
onPress={handleFetchClient}
loading={isLookingUp}
leftAccessory={<Ionicons name="search-outline" size={18} color={theme.colors.textPrimary} />}
/>
</FormSection>
<FormSection title="Personal Information" icon="person-outline">
<Input
label="Full Name"
@@ -73,7 +123,7 @@ export default function AddEditCustomerScreen({ route, navigation }: Props) {
keyboardType="number-pad"
maxLength={10}
error={errors.phone}
leftAccessory={<Text style={{ color: theme.colors.textPrimary, fontWeight: 'bold' }}>+91</Text>}
leftAccessory={<Text style={{ color: theme.colors.textPrimary, fontFamily: fontFamily.bold }}>+91</Text>}
/>
<Input
label="Email Address (Optional)"
@@ -183,7 +233,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.05)',
},
sectionIcon: {
width: 32,
@@ -195,13 +244,17 @@ const styles = StyleSheet.create({
},
sectionTitle: {
fontSize: 15,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
sectionContent: {
padding: 16,
paddingBottom: 16,
gap: 16,
},
helperText: {
fontSize: 13,
lineHeight: 18,
},
row: {
flexDirection: 'row',
},
@@ -7,7 +7,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeIn } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppSelector, useAppDispatch } from '../../../app/hooks';
import type { AgentTabParamList, AgentDashboardStackParamList } from '../navigation/agentNavigationTypes';
import { useAgentDashboard } from '../hooks/useAgentDashboard';
@@ -166,7 +166,7 @@ export default function AgentDashboardScreen({ navigation }: { navigation: Agent
title="Today's Collection"
value={formatAgentCurrency(stats.todayCollection)}
icon="wallet-outline"
gradient={['#2563EB', '#3B82F6']}
gradient={['#2563EB', theme.colors.info]}
onPress={goToCollections}
/>
<View style={{ width: 12 }} />
@@ -175,7 +175,7 @@ export default function AgentDashboardScreen({ navigation }: { navigation: Agent
title="This Week"
value={formatAgentCurrency(stats.weeklyCollection)}
icon="trending-up-outline"
gradient={['#059669', '#10B981']}
gradient={[theme.colors.success, theme.colors.success]}
onPress={goToCollectionHistory}
/>
</View>
@@ -196,7 +196,7 @@ export default function AgentDashboardScreen({ navigation }: { navigation: Agent
/>
</View>
<View style={styles.chartDivider} />
<View style={[styles.chartDivider, { backgroundColor: theme.colors.cardBorder }]} />
<Text style={[styles.chartSubtitle, { color: theme.colors.textMuted }]}>Last 7 Days Trend</Text>
<AgentMiniChart
@@ -253,12 +253,12 @@ const styles = StyleSheet.create({
},
greeting: {
fontSize: 14,
fontWeight: '500',
fontFamily: fontFamily.medium,
marginBottom: 4,
},
name: {
fontSize: 24,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: -0.5,
},
avatarFrame: {
@@ -292,22 +292,21 @@ const styles = StyleSheet.create({
},
chartTitle: {
fontSize: 12,
fontWeight: '600',
fontFamily: fontFamily.semibold,
marginBottom: 4,
},
chartValue: {
fontSize: 22,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: -0.5,
},
chartDivider: {
height: 1,
backgroundColor: 'rgba(0,0,0,0.05)',
marginVertical: 16,
},
chartSubtitle: {
fontSize: 11,
fontWeight: '500',
fontFamily: fontFamily.medium,
marginBottom: 12,
},
footerSpacer: {
@@ -1,7 +1,7 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ScreenShell, Card } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppDispatch } from '../../../app/hooks';
import { logoutSession } from '../../auth/store/authThunk';
import { Button } from '../../../components/Button';
@@ -48,7 +48,7 @@ const styles = StyleSheet.create({
},
title: {
fontSize: 24,
fontWeight: 'bold',
fontFamily: fontFamily.bold,
},
subtitle: {
fontSize: 16,
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeIn, FadeInRight, FadeInLeft } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import type { AgentLoansStackParamList } from '../navigation/agentNavigationTypes';
import LoanRequestListScreen from './LoanRequestListScreen';
@@ -94,7 +94,7 @@ const styles = StyleSheet.create({
},
headerTitle: {
fontSize: 22,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: -0.5,
},
tabContainer: {
@@ -115,7 +115,7 @@ const styles = StyleSheet.create({
},
tabLabel: {
fontSize: 12,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
contentArea: {
flex: 1,
@@ -7,7 +7,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell, Button } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentTabParamList, AgentProfileStackParamList } from '../navigation/agentNavigationTypes';
@@ -25,8 +25,8 @@ export default function AgentProfileScreen({ navigation }: { navigation: Navigat
const handleLogout = () => {
Alert.alert(
'Log Out',
'Are you sure you want to log out of your agent account?',
'Log out of this device?',
'Signing out clears everything this app has stored on this phone, not just your session.',
[
{ text: 'Cancel', style: 'cancel' },
{
@@ -153,11 +153,11 @@ const styles = StyleSheet.create({
},
avatarText: {
fontSize: 32,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
name: {
fontSize: 20,
fontWeight: '800',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
role: {
@@ -179,11 +179,11 @@ const styles = StyleSheet.create({
},
badgeText: {
fontSize: 12,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
fontFamily: fontFamily.semibold,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
@@ -214,7 +214,7 @@ const styles = StyleSheet.create({
},
settingsLabel: {
fontSize: 15,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
divider: {
height: 1,
+121 -64
View File
@@ -1,100 +1,157 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { View, Text, StyleSheet, Share } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell, Button } from '../../../components';
import { useTheme } from '../../../theme';
import { useToast } from '../../../hooks/useToast';
import { useTheme, fontFamily } from '../../../theme';
import type { AgentDashboardStackParamList } from '../navigation/agentNavigationTypes';
type Props = NativeStackScreenProps<AgentDashboardStackParamList, 'AgentRoute'>;
export default function AgentRouteScreen({ navigation }: Props) {
type Stop = {
id: string;
name: string;
area: string;
time: string;
status: 'done' | 'next' | 'pending';
};
const STOPS: Stop[] = [
{ id: '1', name: 'Ramesh Kumar', area: 'MG Road, Shop 14', time: '9:30 AM', status: 'done' },
{ id: '2', name: 'Sunita Devi', area: 'Gandhi Nagar, H-22', time: '10:15 AM', status: 'done' },
{ id: '3', name: 'Imran Shaikh', area: 'Market Lane, 7B', time: '11:00 AM', status: 'next' },
{ id: '4', name: 'Lakshmi Bai', area: 'Station Road, 41', time: '11:45 AM', status: 'pending' },
{ id: '5', name: 'Vikram Patel', area: 'Old Town, 9', time: '12:30 PM', status: 'pending' },
];
export default function AgentRouteScreen(_props: Props) {
const theme = useTheme();
const toast = useToast();
const statusColor = (status: Stop['status']) =>
status === 'done' ? theme.colors.success : status === 'next' ? theme.colors.primary : theme.colors.textMuted;
const handleShare = async () => {
const summary = STOPS.map((stop, index) => `${index + 1}. ${stop.name} — ${stop.area} (${stop.time})`).join('\n');
try {
await Share.share({
title: "Today's collection route",
message: `Today's collection route (${STOPS.length} stops):\n\n${summary}`,
});
} catch {
toast('Unable to share', 'error', 'Could not open the share sheet. Please try again.');
}
};
return (
<ScreenShell title="My Route" scrollable={false}>
<View style={styles.container}>
<View style={styles.mapPlaceholder}>
<Ionicons name="map-outline" size={64} color={theme.colors.textMuted} />
<Text style={[styles.mapText, { color: theme.colors.textMuted }]}>Map Integration Coming Soon</Text>
<ScreenShell title="My Route" subtitle="Today's collection route" contentContainerStyle={styles.content}>
{/* Map preview */}
<View style={[styles.mapPlaceholder, { borderColor: theme.colors.cardBorder, backgroundColor: theme.colors.surfaceMuted }]}>
<Ionicons name="map-outline" size={48} color={theme.colors.primary} />
<Text style={[styles.mapText, { color: theme.colors.textSecondary }]}>Route overview · 5 stops · 8.4 km</Text>
<View style={styles.pinRow}>
{STOPS.map((stop) => (
<View key={stop.id} style={[styles.pin, { backgroundColor: statusColor(stop.status) }]} />
))}
</View>
</View>
<View style={[styles.infoCard, { backgroundColor: theme.colors.surface, borderColor: theme.colors.cardBorder }]}>
<Text style={[styles.infoTitle, { color: theme.colors.textPrimary }]}>Route Summary</Text>
<View style={[styles.divider, { backgroundColor: theme.colors.cardBorder }]} />
<View style={styles.statRow}>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Total Stops</Text>
<Text style={[styles.statValue, { color: theme.colors.textPrimary }]}>12</Text>
</View>
<View style={styles.statRow}>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Completed</Text>
<Text style={[styles.statValue, { color: theme.colors.success }]}>5</Text>
</View>
<View style={styles.statRow}>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Remaining</Text>
<Text style={[styles.statValue, { color: theme.colors.warning }]}>7</Text>
</View>
{/* Summary */}
<View style={[styles.infoCard, { backgroundColor: theme.colors.surface, borderColor: theme.colors.cardBorder }]}>
<View style={styles.statCol}>
<Text style={[styles.statValue, { color: theme.colors.textPrimary }]}>12</Text>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Total stops</Text>
</View>
<View style={[styles.statDivider, { backgroundColor: theme.colors.cardBorder }]} />
<View style={styles.statCol}>
<Text style={[styles.statValue, { color: theme.colors.success }]}>5</Text>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Completed</Text>
</View>
<View style={[styles.statDivider, { backgroundColor: theme.colors.cardBorder }]} />
<View style={styles.statCol}>
<Text style={[styles.statValue, { color: theme.colors.warning }]}>7</Text>
<Text style={[styles.statLabel, { color: theme.colors.textMuted }]}>Remaining</Text>
</View>
</View>
<Button
title="Start Navigation"
onPress={() => {}}
style={{ marginTop: 'auto', marginBottom: 24 }}
{/* Stops list */}
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Stops</Text>
<View style={[styles.stopsCard, { backgroundColor: theme.colors.surface, borderColor: theme.colors.cardBorder }]}>
{STOPS.map((stop, index) => (
<View key={stop.id} style={[styles.stopRow, index !== STOPS.length - 1 && { borderBottomWidth: 1, borderBottomColor: theme.colors.cardBorder }]}>
<View style={[styles.stopDot, { backgroundColor: `${statusColor(stop.status)}20` }]}>
<Ionicons
name={stop.status === 'done' ? 'checkmark' : 'location'}
size={14}
color={statusColor(stop.status)}
/>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.stopName, { color: theme.colors.textPrimary }]}>{stop.name}</Text>
<Text style={[styles.stopArea, { color: theme.colors.textSecondary }]}>{stop.area}</Text>
</View>
<Text style={[styles.stopTime, { color: theme.colors.textMuted }]}>{stop.time}</Text>
</View>
))}
</View>
{/* Actions */}
<View style={styles.actionRow}>
<Button
title="Start Navigation"
fullWidth={false}
style={styles.actionBtn}
onPress={() => toast('Opening maps', 'info', 'Launching turn-by-turn navigation for your route.')}
leftAccessory={<Ionicons name="navigate-outline" size={18} color={theme.colors.white} />}
/>
<Button
title="Share Route"
variant="secondary"
fullWidth={false}
style={styles.actionBtn}
onPress={handleShare}
leftAccessory={<Ionicons name="share-social-outline" size={18} color={theme.colors.textPrimary} />}
/>
</View>
<View style={{ height: 32 }} />
</ScreenShell>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
content: { gap: 16 },
mapPlaceholder: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.02)',
height: 160,
borderRadius: 16,
borderWidth: 1,
borderStyle: 'dashed',
borderColor: 'rgba(0,0,0,0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
},
mapText: {
marginTop: 16,
fontSize: 16,
fontWeight: '600',
gap: 10,
},
mapText: { fontSize: 14, fontFamily: fontFamily.semibold },
pinRow: { flexDirection: 'row', gap: 8 },
pin: { width: 10, height: 10, borderRadius: 5 },
infoCard: {
flexDirection: 'row',
borderRadius: 16,
borderWidth: 1,
padding: 16,
alignItems: 'center',
},
infoTitle: {
fontSize: 16,
fontWeight: '700',
marginBottom: 12,
},
divider: {
height: 1,
width: '100%',
marginBottom: 12,
},
statRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
},
statLabel: {
fontSize: 14,
},
statValue: {
fontSize: 16,
fontWeight: '700',
},
statCol: { flex: 1, alignItems: 'center', gap: 4 },
statDivider: { width: 1, height: 32 },
statValue: { fontSize: 20, fontFamily: fontFamily.bold },
statLabel: { fontSize: 12 },
sectionTitle: { fontSize: 16, fontFamily: fontFamily.bold, marginLeft: 4 },
stopsCard: { borderRadius: 16, borderWidth: 1, paddingHorizontal: 16 },
stopRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 14 },
stopDot: { width: 30, height: 30, borderRadius: 15, alignItems: 'center', justifyContent: 'center' },
stopName: { fontSize: 14, fontFamily: fontFamily.bold },
stopArea: { fontSize: 12, fontFamily: fontFamily.medium },
stopTime: { fontSize: 12, fontFamily: fontFamily.semibold },
actionRow: { flexDirection: 'row', gap: 12 },
actionBtn: { flex: 1 },
});
@@ -4,7 +4,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentCollectionsStackParamList } from '../navigation/agentNavigationTypes';
import { useGetCollectionHistoryQuery } from '../services/agentApi';
@@ -93,12 +93,12 @@ const styles = StyleSheet.create({
},
summaryLabel: {
fontSize: 12,
fontWeight: '500',
fontFamily: fontFamily.medium,
marginBottom: 4,
},
summaryValue: {
fontSize: 20,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
vDivider: {
width: 1,
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeIn, SlideInUp } from 'react-native-reanimated';
import { ScreenShell, Button } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import type { AgentCollectionsStackParamList } from '../navigation/agentNavigationTypes';
import { formatFullCurrency, formatDateTime } from '../utils/agentFormatters';
@@ -36,7 +36,7 @@ export default function CollectionReceiptScreen({ route, navigation }: Props) {
styles.infoValue,
{
color: strong ? theme.colors.textPrimary : theme.colors.textSecondary,
fontWeight: strong ? '700' : '500'
fontFamily: strong ? fontFamily.bold : fontFamily.medium
}
]}>
{value}
@@ -130,12 +130,12 @@ const styles = StyleSheet.create({
},
successTitle: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 8,
},
successAmount: {
fontSize: 32,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
dividerContainer: {
flexDirection: 'row',
@@ -199,6 +199,6 @@ const styles = StyleSheet.create({
},
actionBtnText: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
});
@@ -3,7 +3,7 @@ import { View, StyleSheet, FlatList, RefreshControl, Text } from 'react-native';
import Animated, { FadeInUp } from 'react-native-reanimated';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import { useGetCommissionSummaryQuery } from '../services/agentApi';
@@ -132,13 +132,13 @@ const styles = StyleSheet.create({
totalLabel: {
color: 'rgba(255,255,255,0.8)',
fontSize: 13,
fontWeight: '500',
fontFamily: fontFamily.medium,
marginBottom: 4,
},
totalValue: {
color: '#fff',
fontSize: 28,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: -0.5,
},
iconCircle: {
@@ -161,11 +161,11 @@ const styles = StyleSheet.create({
lastPaidValue: {
color: '#fff',
fontSize: 14,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
historyTitle: {
fontSize: 18,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 12,
},
historyCard: {
@@ -181,7 +181,7 @@ const styles = StyleSheet.create({
},
historyMonth: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
statusBadge: {
paddingHorizontal: 8,
@@ -190,7 +190,7 @@ const styles = StyleSheet.create({
},
statusText: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
divider: {
height: 1,
@@ -210,6 +210,6 @@ const styles = StyleSheet.create({
},
value: {
fontSize: 15,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
});
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInRight, FadeOutLeft, FadeIn } from 'react-native-reanimated';
import { ScreenShell, Input, Button, KeyboardAwareScrollView } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import type { AgentLoansStackParamList } from '../navigation/agentNavigationTypes';
import { useLoanRequestForm } from '../hooks/useLoanRequestForm';
@@ -46,7 +46,7 @@ const StepIndicator = ({ step }: { step: number }) => {
<Text style={{
color: isActive ? theme.colors.white : theme.colors.textMuted,
fontSize: 11,
fontWeight: '800',
fontFamily: fontFamily.bold,
}}>{s.id}</Text>
)}
</View>
@@ -54,7 +54,7 @@ const StepIndicator = ({ step }: { step: number }) => {
styles.stepLabel,
{
color: isActive ? theme.colors.primary : isCompleted ? theme.colors.success : theme.colors.textMuted,
fontWeight: isActive ? '700' : '500',
fontFamily: isActive ? fontFamily.bold : fontFamily.medium,
},
]}>{s.label}</Text>
</View>
@@ -103,6 +103,12 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
const [step, setStep] = useState(1);
const selectedCategory = LOAN_CATEGORIES.find(c => c.id === data.category);
// The chosen type's categorical pair — a legible tone and a measured tint —
// for whichever theme is live. Falls back to the brand tint before a type is
// picked, so the banner is never drawn with an undefined colour.
const selectedTone = selectedCategory
? theme.colors.category[selectedCategory.hue]
: { tone: theme.colors.primary, surface: theme.colors.primarySurface };
const handleNext = () => {
if (step === 1 && !data.category) return;
@@ -185,29 +191,40 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
<View style={styles.categoryList}>
{LOAN_CATEGORIES.map(cat => {
const isSelected = data.category === cat.id;
// The hue resolves to a tone and a measured tint here,
// where the theme exists. The tint replaces the old
// `${hex}12` / `${hex}20` alpha suffixes, which composited
// the raw hue over the card and so worked on white only.
const { tone, surface } = theme.colors.category[cat.hue];
return (
<Pressable
key={cat.id}
style={[
styles.catCard,
{
backgroundColor: isSelected ? `${cat.color}12` : theme.colors.surfaceMuted,
borderColor: isSelected ? cat.color : 'transparent',
backgroundColor: isSelected ? surface : theme.colors.surfaceMuted,
borderColor: isSelected ? tone : 'transparent',
borderWidth: isSelected ? 1.5 : 1,
},
]}
onPress={() => updateField('category', cat.id as any)}
>
<View style={[styles.catIcon, { backgroundColor: `${cat.color}20` }]}>
<Ionicons name={cat.icon as any} size={22} color={cat.color} />
<View style={[styles.catIcon, { backgroundColor: surface }]}>
<Ionicons name={cat.icon as any} size={22} color={tone} />
</View>
<Text style={[
styles.catLabel,
{ color: isSelected ? cat.color : theme.colors.textPrimary },
{ color: isSelected ? tone : theme.colors.textPrimary },
]}>{cat.label}</Text>
{isSelected && (
<View style={[styles.catCheck, { backgroundColor: cat.color }]}>
<Ionicons name="checkmark" size={12} color={theme.colors.white} />
<View style={[styles.catCheck, { backgroundColor: tone }]}>
{/* On a saturated fill, so this takes the on-fill
tone — white in light mode, dark ink in dark. */}
<Ionicons
name="checkmark"
size={12}
color={theme.colors.textOnAccentFill}
/>
</View>
)}
</Pressable>
@@ -234,7 +251,7 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
onChangeText={v => updateField('customerPhone', v)}
keyboardType="number-pad"
maxLength={10}
leftAccessory={<Text style={{ color: theme.colors.textMuted, fontWeight: '600', fontSize: 13 }}>+91</Text>}
leftAccessory={<Text style={{ color: theme.colors.textMuted, fontFamily: fontFamily.semibold, fontSize: 13 }}>+91</Text>}
/>
</SectionCard>
</Animated.View>
@@ -244,12 +261,18 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
{step === 2 && (
<Animated.View entering={FadeInRight} exiting={FadeOutLeft}>
{selectedCategory && (
<Animated.View entering={FadeIn} style={[styles.selectedBanner, { backgroundColor: `${selectedCategory.color}12`, borderColor: `${selectedCategory.color}30` }]}>
<View style={[styles.bannerIcon, { backgroundColor: `${selectedCategory.color}20` }]}>
<Ionicons name={selectedCategory.icon as any} size={20} color={selectedCategory.color} />
<Animated.View
entering={FadeIn}
style={[
styles.selectedBanner,
{ backgroundColor: selectedTone.surface, borderColor: selectedTone.tone },
]}
>
<View style={[styles.bannerIcon, { backgroundColor: selectedTone.surface }]}>
<Ionicons name={selectedCategory.icon as any} size={20} color={selectedTone.tone} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.bannerTitle, { color: selectedCategory.color }]}>{selectedCategory.label}</Text>
<Text style={[styles.bannerTitle, { color: selectedTone.tone }]}>{selectedCategory.label}</Text>
<Text style={[styles.bannerSub, { color: theme.colors.textMuted }]}>for {data.customerName}</Text>
</View>
</Animated.View>
@@ -263,7 +286,7 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
onChangeText={v => updateField('amount', v)}
keyboardType="number-pad"
error={errors.amount}
leftAccessory={<Text style={{ fontSize: 16, fontWeight: '800', color: theme.colors.primary }}>₹</Text>}
leftAccessory={<Text style={{ fontSize: 16, fontFamily: fontFamily.bold, color: theme.colors.primary }}>₹</Text>}
/>
<View style={styles.row}>
<View style={styles.col}>
@@ -274,7 +297,7 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
onChangeText={v => updateField('tenureMonths', v)}
keyboardType="number-pad"
error={errors.tenureMonths}
rightAccessory={<Text style={{ fontSize: 12, color: theme.colors.textMuted, fontWeight: '600' }}>months</Text>}
rightAccessory={<Text style={{ fontSize: 12, color: theme.colors.textMuted, fontFamily: fontFamily.semibold }}>months</Text>}
/>
</View>
<View style={{ width: 12 }} />
@@ -286,7 +309,7 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
onChangeText={v => updateField('monthlyIncome', v)}
keyboardType="number-pad"
error={errors.monthlyIncome}
leftAccessory={<Text style={{ fontSize: 14, fontWeight: '700', color: theme.colors.textMuted }}>₹</Text>}
leftAccessory={<Text style={{ fontSize: 14, fontFamily: fontFamily.bold, color: theme.colors.textMuted }}>₹</Text>}
/>
</View>
</View>
@@ -311,9 +334,11 @@ export default function CreateLoanRequestScreen({ route, navigation }: Props) {
<View style={[styles.reviewCard, { backgroundColor: theme.colors.surface, borderColor: theme.colors.cardBorder }]}>
{/* Header badge */}
{selectedCategory && (
<View style={[styles.reviewBadge, { backgroundColor: `${selectedCategory.color}12` }]}>
<Ionicons name={selectedCategory.icon as any} size={18} color={selectedCategory.color} />
<Text style={[styles.reviewBadgeText, { color: selectedCategory.color }]}>{selectedCategory.label}</Text>
<View style={[styles.reviewBadge, { backgroundColor: selectedTone.surface }]}>
<Ionicons name={selectedCategory.icon as any} size={18} color={selectedTone.tone} />
<Text style={[styles.reviewBadgeText, { color: selectedTone.tone }]}>
{selectedCategory.label}
</Text>
</View>
)}
@@ -416,7 +441,7 @@ const styles = StyleSheet.create({
},
sectionTitle: {
fontSize: 17,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 2,
},
sectionSubtitle: {
@@ -453,7 +478,7 @@ const styles = StyleSheet.create({
},
catLabel: {
fontSize: 11,
fontWeight: '600',
fontFamily: fontFamily.semibold,
textAlign: 'center',
lineHeight: 14,
},
@@ -486,7 +511,7 @@ const styles = StyleSheet.create({
},
bannerTitle: {
fontSize: 14,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
bannerSub: {
fontSize: 12,
@@ -503,7 +528,7 @@ const styles = StyleSheet.create({
errorText: {
fontSize: 12,
marginTop: 6,
fontWeight: '500',
fontFamily: fontFamily.medium,
},
// ── Review Card ───────────────────────────
@@ -524,7 +549,7 @@ const styles = StyleSheet.create({
},
reviewBadgeText: {
fontSize: 13,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
reviewDivider: {
height: 1,
@@ -538,12 +563,12 @@ const styles = StyleSheet.create({
},
reviewLabel: {
fontSize: 13,
fontWeight: '500',
fontFamily: fontFamily.medium,
minWidth: 80,
},
reviewValue: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
textAlign: 'right',
flexShrink: 1,
marginLeft: 16,
@@ -560,7 +585,7 @@ const styles = StyleSheet.create({
},
docsTitle: {
fontSize: 13,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 3,
},
docsText: {
@@ -597,7 +622,7 @@ const styles = StyleSheet.create({
},
progressText: {
fontSize: 11,
fontWeight: '600',
fontFamily: fontFamily.semibold,
minWidth: 60,
},
progressTrack: {
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell, Button } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppSelector } from '../../../app/hooks';
import { selectCustomerById } from '../store/agentSelectors';
import { useDeleteAgentCustomerMutation } from '../services/agentApi';
@@ -13,6 +13,8 @@ import type { AgentCustomersStackParamList } from '../navigation/agentNavigation
import { formatAgentCurrency, formatDate, maskAadhaar, maskPhone } from '../utils/agentFormatters';
import { AgentSectionHeader } from '../components/AgentSectionHeader';
import { FeatureKey } from '../../../constants/featureKeys';
import { FeatureGate, useFeatureAction } from '../../../features';
type Props = NativeStackScreenProps<AgentCustomersStackParamList, 'CustomerDetail'>;
@@ -55,6 +57,14 @@ export default function CustomerDetailScreen({ route, navigation }: Props) {
);
};
// Second line of defence: even if some path reached this handler while
// `agent.customers.delete` is off, it returns before the confirm dialog and
// before any delete request is issued.
const handleDeleteCustomer = useFeatureAction(
FeatureKey.AGENT_CUSTOMERS_DELETE,
handleDelete,
);
const InfoRow = ({ label, value, icon, masked = false }: { label: string, value: string, icon?: string, masked?: boolean }) => (
<View style={styles.infoRow}>
<View style={styles.infoLabelArea}>
@@ -105,12 +115,14 @@ export default function CustomerDetailScreen({ route, navigation }: Props) {
<Text style={[styles.iconLabel, { color: theme.colors.success }]}>SMS</Text>
</Pressable>
<Pressable style={styles.iconBtn} onPress={() => navigation.navigate('AddEditCustomer', { customerId: customer.id })}>
<View style={[styles.iconFrame, { backgroundColor: `${theme.colors.primary}15` }]}>
<Ionicons name="create" size={20} color={theme.colors.primary} />
</View>
<Text style={[styles.iconLabel, { color: theme.colors.primary }]}>Edit</Text>
</Pressable>
<FeatureGate feature={FeatureKey.AGENT_CUSTOMERS_EDIT}>
<Pressable style={styles.iconBtn} onPress={() => navigation.navigate('AddEditCustomer', { customerId: customer.id })}>
<View style={[styles.iconFrame, { backgroundColor: `${theme.colors.primary}15` }]}>
<Ionicons name="create" size={20} color={theme.colors.primary} />
</View>
<Text style={[styles.iconLabel, { color: theme.colors.primary }]}>Edit</Text>
</Pressable>
</FeatureGate>
</View>
</Animated.View>
@@ -170,16 +182,18 @@ export default function CustomerDetailScreen({ route, navigation }: Props) {
</Animated.View>
{/* Delete Area */}
<View style={styles.deleteArea}>
<Button
title="Remove Customer"
variant="ghost"
loading={isDeleting}
onPress={handleDelete}
leftAccessory={<Ionicons name="trash-outline" size={18} color={theme.colors.danger} />}
style={{ borderColor: theme.colors.danger, borderWidth: 1 }}
/>
</View>
<FeatureGate feature={FeatureKey.AGENT_CUSTOMERS_DELETE}>
<View style={styles.deleteArea}>
<Button
title="Remove Customer"
variant="ghost"
loading={isDeleting}
onPress={handleDeleteCustomer}
leftAccessory={<Ionicons name="trash-outline" size={18} color={theme.colors.danger} />}
style={{ borderColor: theme.colors.danger, borderWidth: 1 }}
/>
</View>
</FeatureGate>
<View style={styles.footerSpacer} />
</ScrollView>
@@ -209,14 +223,14 @@ const styles = StyleSheet.create({
},
avatarBigText: {
fontSize: 32,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
headerInfo: {
alignItems: 'center',
},
headerName: {
fontSize: 22,
fontWeight: '800',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
headerPhone: {
@@ -230,7 +244,7 @@ const styles = StyleSheet.create({
},
statusText: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
actionButtonsRow: {
flexDirection: 'row',
@@ -250,7 +264,7 @@ const styles = StyleSheet.create({
},
iconLabel: {
fontSize: 12,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
section: {
marginBottom: 24,
@@ -267,11 +281,11 @@ const styles = StyleSheet.create({
},
accType: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
accNumber: {
fontSize: 14,
fontFamily: 'monospace',
fontFamily: fontFamily.mono,
},
divider: {
height: 1,
@@ -292,7 +306,7 @@ const styles = StyleSheet.create({
},
finValue: {
fontSize: 18,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
vDivider: {
width: 1,
@@ -314,7 +328,7 @@ const styles = StyleSheet.create({
},
infoValue: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
flex: 1,
textAlign: 'right',
},
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppSelector } from '../../../app/hooks';
import { selectCustomerById } from '../store/agentSelectors';
import type { AgentCustomersStackParamList } from '../navigation/agentNavigationTypes';
@@ -73,7 +73,7 @@ export default function CustomerInstallmentHistoryScreen({ route, navigation }:
return (
<ScreenShell title="Installment History" scrollable={false}>
<View style={styles.header}>
<View style={[styles.header, { borderBottomColor: theme.colors.cardBorder }]}>
<Text style={[styles.customerName, { color: theme.colors.textPrimary }]}>{customer.name}</Text>
<Text style={[styles.accNumber, { color: theme.colors.textMuted }]}>
{customer.accounts[0]?.type} A/C: {customer.accounts[0]?.accountNumber}
@@ -96,16 +96,15 @@ const styles = StyleSheet.create({
padding: 16,
paddingTop: 8,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.05)',
},
customerName: {
fontSize: 18,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
accNumber: {
fontSize: 14,
fontFamily: 'monospace',
fontFamily: fontFamily.mono,
},
listContent: {
padding: 16,
@@ -133,7 +132,7 @@ const styles = StyleSheet.create({
},
date: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
marginBottom: 2,
},
receipt: {
@@ -144,11 +143,11 @@ const styles = StyleSheet.create({
},
amount: {
fontSize: 15,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 2,
},
status: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
});
@@ -4,10 +4,10 @@ import { CompositeNavigationProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import Animated, { FadeIn } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentTabParamList, AgentCustomersStackParamList } from '../navigation/agentNavigationTypes';
@@ -20,6 +20,8 @@ import type { CustomerFilter } from '../types/agentTypes';
import { CustomerCard } from '../components/CustomerCard';
import { AgentSkeletonLoader } from '../components/AgentSkeletonLoader';
import { AgentEmptyState } from '../components/AgentEmptyState';
import { FeatureKey } from '../../../constants/featureKeys';
import { FeatureGate } from '../../../features';
type NavigationProp = CompositeNavigationProp<
NativeStackNavigationProp<AgentCustomersStackParamList, 'CustomerList'>,
@@ -143,8 +145,12 @@ export default function CustomerListScreen({ navigation }: { navigation: Navigat
onAction={searchQuery ? () => handleSearch('') : undefined}
/>
)}
// No `exiting` here: FlatList unmounts cells for virtualisation as well
// as for deletion, and an exit animation makes Reanimated hold the
// outgoing native view while Fabric is recycling that cell — which
// crashes with "addViewAt: the specified child already has a parent".
renderItem={({ item, index }) => (
<Animated.View entering={FadeIn.delay(Math.min(index * 50, 500))} exiting={FadeOut}>
<Animated.View entering={FadeIn.delay(Math.min(index * 50, 500))}>
<CustomerCard
customer={item}
onPress={() => navigation.navigate('CustomerDetail', { customerId: item.id })}
@@ -156,12 +162,14 @@ export default function CustomerListScreen({ navigation }: { navigation: Navigat
</View>
{/* FAB to add new customer */}
<Pressable
style={[styles.fab, { backgroundColor: theme.colors.primary }]}
onPress={() => navigation.navigate('AddEditCustomer', {})}
>
<Ionicons name="add" size={24} color={theme.colors.white} />
</Pressable>
<FeatureGate feature={FeatureKey.AGENT_CUSTOMERS_CREATE}>
<Pressable
style={[styles.fab, { backgroundColor: theme.colors.primary }]}
onPress={() => navigation.navigate('AddEditCustomer', {})}
>
<Ionicons name="add" size={24} color={theme.colors.white} />
</Pressable>
</FeatureGate>
</ScreenShell>
);
}
@@ -205,7 +213,7 @@ const styles = StyleSheet.create({
},
filterText: {
fontSize: 13,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
listArea: {
flex: 1,
@@ -8,7 +8,7 @@ import Animated, { FadeIn, SlideInDown } from 'react-native-reanimated';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
import { ScreenShell, Button, Input } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentTabParamList, AgentCollectionsStackParamList } from '../navigation/agentNavigationTypes';
@@ -64,7 +64,7 @@ const CollectionForm = ({ selectedEntry, navigation, bottomSheetRef }: { selecte
onChangeText={setAmount}
keyboardType="number-pad"
error={formError || undefined}
leftAccessory={<Text style={{ fontSize: 18, fontWeight: '700', color: theme.colors.textPrimary }}>₹</Text>}
leftAccessory={<Text style={{ fontSize: 18, fontFamily: fontFamily.bold, color: theme.colors.textPrimary }}>₹</Text>}
/>
<Text style={[styles.modeLabel, { color: theme.colors.textMuted }]}>Payment Mode</Text>
@@ -314,13 +314,13 @@ const styles = StyleSheet.create({
summaryLabel: {
color: 'rgba(255,255,255,0.8)',
fontSize: 12,
fontWeight: '500',
fontFamily: fontFamily.medium,
marginBottom: 4,
},
summaryValue: {
color: '#fff',
fontSize: 24,
fontWeight: '800',
fontFamily: fontFamily.bold,
letterSpacing: -0.5,
},
progressTrack: {
@@ -346,7 +346,7 @@ const styles = StyleSheet.create({
offlineText: {
color: '#fff',
fontSize: 12,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
listArea: {
flex: 1,
@@ -361,11 +361,11 @@ const styles = StyleSheet.create({
},
listTitle: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
historyLink: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
filterRow: {
flexDirection: 'row',
@@ -381,7 +381,7 @@ const styles = StyleSheet.create({
},
filterText: {
fontSize: 12,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
listContent: {
padding: 16,
@@ -411,11 +411,11 @@ const styles = StyleSheet.create({
},
avatarText: {
fontSize: 18,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
entryName: {
fontSize: 15,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 2,
},
entryMeta: {
@@ -433,11 +433,11 @@ const styles = StyleSheet.create({
collectBtnText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
entryAmount: {
fontSize: 15,
fontWeight: '800',
fontFamily: fontFamily.bold,
},
statusBadge: {
paddingHorizontal: 8,
@@ -446,7 +446,7 @@ const styles = StyleSheet.create({
},
statusText: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
sheetContainer: {
flex: 1,
@@ -457,7 +457,7 @@ const styles = StyleSheet.create({
},
sheetTitle: {
fontSize: 20,
fontWeight: '800',
fontFamily: fontFamily.bold,
marginBottom: 24,
},
sheetCustomerInfo: {
@@ -465,7 +465,7 @@ const styles = StyleSheet.create({
},
sheetCustomerName: {
fontSize: 18,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 4,
},
sheetCustomerMeta: {
@@ -473,7 +473,7 @@ const styles = StyleSheet.create({
},
modeLabel: {
fontSize: 13,
fontWeight: '600',
fontFamily: fontFamily.semibold,
marginBottom: 12,
marginTop: 16,
},
@@ -493,6 +493,6 @@ const styles = StyleSheet.create({
},
modeText: {
fontSize: 13,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
});
@@ -4,7 +4,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { SlideInRight } from 'react-native-reanimated';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentLoansStackParamList } from '../navigation/agentNavigationTypes';
@@ -114,7 +114,7 @@ const styles = StyleSheet.create({
},
title: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
newBtn: {
flexDirection: 'row',
@@ -126,7 +126,7 @@ const styles = StyleSheet.create({
},
newBtnText: {
fontSize: 12,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
listContent: {
padding: 16,
@@ -148,7 +148,7 @@ const styles = StyleSheet.create({
},
customerName: {
fontSize: 16,
fontWeight: '700',
fontFamily: fontFamily.bold,
marginBottom: 2,
},
category: {
@@ -161,7 +161,7 @@ const styles = StyleSheet.create({
},
statusText: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
divider: {
height: 1,
@@ -178,6 +178,6 @@ const styles = StyleSheet.create({
},
value: {
fontSize: 14,
fontWeight: '600',
fontFamily: fontFamily.semibold,
},
});
@@ -5,7 +5,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { ScreenShell } from '../../../components';
import { useTheme } from '../../../theme';
import { useTheme, fontFamily } from '../../../theme';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import type { AgentProfileStackParamList } from '../navigation/agentNavigationTypes';
import { useGetSmsLogsQuery } from '../services/agentApi';
@@ -106,7 +106,7 @@ const styles = StyleSheet.create({
},
phone: {
fontSize: 14,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
headerRight: {
flexDirection: 'row',
@@ -115,7 +115,7 @@ const styles = StyleSheet.create({
},
statusText: {
fontSize: 10,
fontWeight: '700',
fontFamily: fontFamily.bold,
},
divider: {
height: 1,