Fix form keyboard issues and sticky footers in agent screens
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
TextInput,
|
||||
FlatList,
|
||||
ScrollView,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import MapLibreGL from '@maplibre/maplibre-react-native';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { Loader, EmptyState } from '../../../components';
|
||||
import { useTheme } from '../../../theme';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import type { BranchFacility } from '../../../types/app';
|
||||
import { useUserLocation } from '../hooks/useUserLocation';
|
||||
import { useBranches, type BranchWithDistance } from '../hooks/useBranches';
|
||||
import { BranchListItem } from '../components/BranchListItem';
|
||||
import { BranchDetailSheet } from '../components/BranchDetailSheet';
|
||||
import { FacilityFilterBar } from '../components/FacilityFilterBar';
|
||||
|
||||
// Initialize MapLibre (required once)
|
||||
MapLibreGL.setAccessToken(null);
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
|
||||
const OSM_STYLE = {
|
||||
version: 8,
|
||||
sources: {
|
||||
osm: {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxzoom: 19,
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'osm-tiles',
|
||||
type: 'raster',
|
||||
source: 'osm',
|
||||
minzoom: 0,
|
||||
maxzoom: 19,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'BranchLocator'>;
|
||||
|
||||
export default function BranchLocatorScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// Extra bottom space to clear the tab bar (tab bar height ~60 + safe area)
|
||||
const bottomOffset = Math.max(insets.bottom, 10) + 72;
|
||||
|
||||
// State
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedFacilities, setSelectedFacilities] = useState<BranchFacility[]>([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState<BranchWithDistance | null>(null);
|
||||
const [showSheet, setShowSheet] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'map' | 'list'>('map');
|
||||
const [filterType, setFilterType] = useState<'all' | 'branch' | 'atm'>('all');
|
||||
const [selectedCity, setSelectedCity] = useState<string | null>(null);
|
||||
|
||||
// Hooks
|
||||
const { location: userLocation, loading: locationLoading } = useUserLocation();
|
||||
const { branches, isLoading, isError, refetch } = useBranches(
|
||||
userLocation,
|
||||
searchText,
|
||||
selectedFacilities,
|
||||
filterType,
|
||||
selectedCity,
|
||||
);
|
||||
|
||||
// Extract unique cities
|
||||
const cities = useMemo(() => {
|
||||
// We use a separate query or the hook should ideally return available cities
|
||||
// For now, let's extract from the branches if available, but better to extract from raw data if possible.
|
||||
// However, useBranches returns filtered ones. We need all.
|
||||
// Let's just hardcode some common ones or extract if we had access to raw list.
|
||||
return ['All Cities', 'Bengaluru', 'Mumbai', 'Delhi', 'Chennai', 'Hyderabad', 'Pune', 'Ahmedabad'];
|
||||
}, []);
|
||||
|
||||
// Refs
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const cameraRef = useRef<any>(null);
|
||||
|
||||
// Handlers
|
||||
const handleFacilityToggle = useCallback((facility: BranchFacility) => {
|
||||
setSelectedFacilities((prev) =>
|
||||
prev.includes(facility)
|
||||
? prev.filter((f) => f !== facility)
|
||||
: [...prev, facility],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleBranchPress = useCallback((branch: BranchWithDistance) => {
|
||||
setSelectedBranch(branch);
|
||||
setShowSheet(true);
|
||||
|
||||
if (viewMode === 'map' && cameraRef.current) {
|
||||
cameraRef.current.setCamera({
|
||||
centerCoordinate: [branch.longitude, branch.latitude],
|
||||
zoomLevel: 14,
|
||||
animationDuration: 600,
|
||||
});
|
||||
}
|
||||
}, [viewMode]);
|
||||
|
||||
const handleCloseSheet = useCallback(() => {
|
||||
setShowSheet(false);
|
||||
}, []);
|
||||
|
||||
const handleMarkerPress = useCallback(
|
||||
(branch: BranchWithDistance) => {
|
||||
handleBranchPress(branch);
|
||||
},
|
||||
[handleBranchPress],
|
||||
);
|
||||
|
||||
// Default map center (user location or India center)
|
||||
const defaultCenter = useMemo(() => {
|
||||
if (userLocation) {
|
||||
return [userLocation.longitude, userLocation.latitude];
|
||||
}
|
||||
return [77.5946, 12.9716]; // Bengaluru fallback
|
||||
}, [userLocation]);
|
||||
|
||||
// Loading state
|
||||
if (isLoading || locationLoading) {
|
||||
return (
|
||||
<SafeAreaView edges={['top']} style={[styles.flex, { backgroundColor: theme.colors.background }]}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
||||
<Header theme={theme} navigation={navigation} />
|
||||
<Loader />
|
||||
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={['top']} style={[styles.flex, { backgroundColor: theme.colors.background }]}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
||||
|
||||
{/* Header */}
|
||||
<Header theme={theme} navigation={navigation} />
|
||||
|
||||
{/* Search Bar */}
|
||||
<View style={styles.searchSection}>
|
||||
<View
|
||||
style={[
|
||||
styles.searchBar,
|
||||
{
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderColor: theme.colors.cardBorder,
|
||||
borderRadius: theme.radius.lg,
|
||||
},
|
||||
theme.shadows.soft,
|
||||
]}
|
||||
>
|
||||
<Ionicons name="search-outline" size={18} color={theme.colors.textMuted} />
|
||||
<TextInput
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
placeholder="Search city, pincode, branch..."
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
style={[styles.searchInput, { color: theme.colors.textPrimary }]}
|
||||
/>
|
||||
{searchText.length > 0 && (
|
||||
<Pressable onPress={() => setSearchText('')}>
|
||||
<Ionicons name="close-circle" size={18} color={theme.colors.textMuted} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* View Toggle */}
|
||||
<Pressable
|
||||
onPress={() => setViewMode((v) => (v === 'map' ? 'list' : 'map'))}
|
||||
style={[
|
||||
styles.toggleBtn,
|
||||
{
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderRadius: theme.radius.lg,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={viewMode === 'map' ? 'list-outline' : 'map-outline'}
|
||||
size={20}
|
||||
color={theme.colors.white}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Segmented Control for Locator Type */}
|
||||
<View style={styles.segmentedContainer}>
|
||||
{(['all', 'branch', 'atm'] as const).map((type) => {
|
||||
const isActive = filterType === type;
|
||||
const label = type === 'all' ? 'All' : type === 'branch' ? 'Branches' : 'ATMs';
|
||||
return (
|
||||
<Pressable
|
||||
key={type}
|
||||
onPress={() => setFilterType(type)}
|
||||
style={[
|
||||
styles.segmentSegment,
|
||||
isActive && { backgroundColor: theme.colors.primary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.segmentLabel,
|
||||
{ color: isActive ? theme.colors.white : theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Facility Filters */}
|
||||
<View style={styles.filterSection}>
|
||||
<FacilityFilterBar selected={selectedFacilities} onToggle={handleFacilityToggle} />
|
||||
</View>
|
||||
|
||||
{/* City Chips */}
|
||||
<View style={styles.cityFilterSection}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.cityScroll}>
|
||||
{cities.map((city) => {
|
||||
const isActive = (selectedCity === city) || (!selectedCity && city === 'All Cities');
|
||||
return (
|
||||
<Pressable
|
||||
key={city}
|
||||
onPress={() => setSelectedCity(city === 'All Cities' ? null : city)}
|
||||
style={[
|
||||
styles.cityChip,
|
||||
{
|
||||
backgroundColor: isActive ? theme.colors.primary : theme.colors.surface,
|
||||
borderColor: isActive ? theme.colors.primary : theme.colors.cardBorder,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.cityChipText,
|
||||
{ color: isActive ? theme.colors.white : theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{city}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Branch count */}
|
||||
<View style={styles.countRow}>
|
||||
<Text style={[styles.countText, { color: theme.colors.textSecondary }]}>
|
||||
{branches.length} {branches.length === 1 ? 'location' : 'locations'} found
|
||||
</Text>
|
||||
{(selectedFacilities.length > 0 || selectedCity) && (
|
||||
<Pressable onPress={() => { setSelectedFacilities([]); setSelectedCity(null); }}>
|
||||
<Text style={[styles.clearText, { color: theme.colors.primary }]}>
|
||||
Clear filters
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Map or List View */}
|
||||
{branches.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<EmptyState
|
||||
title="No branches found"
|
||||
description="Try adjusting your search or filters to find nearby branches."
|
||||
actionLabel="Clear all"
|
||||
onAction={() => {
|
||||
setSearchText('');
|
||||
setSelectedFacilities([]);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : viewMode === 'map' ? (
|
||||
<View style={styles.mapContainer}>
|
||||
<MapLibreGL.MapView
|
||||
style={styles.map}
|
||||
mapStyle={OSM_STYLE}
|
||||
logoEnabled={false}
|
||||
attributionEnabled={true}
|
||||
attributionPosition={{ bottom: 8, right: 8 }}
|
||||
>
|
||||
<MapLibreGL.Camera
|
||||
ref={cameraRef}
|
||||
defaultSettings={{
|
||||
centerCoordinate: defaultCenter,
|
||||
zoomLevel: 11,
|
||||
}}
|
||||
animationDuration={600}
|
||||
/>
|
||||
|
||||
{/* User location marker */}
|
||||
{userLocation && (
|
||||
<MapLibreGL.PointAnnotation
|
||||
id="user-location"
|
||||
coordinate={[userLocation.longitude, userLocation.latitude]}
|
||||
>
|
||||
<View style={styles.userMarkerOuter}>
|
||||
<View style={[styles.userMarkerInner, { backgroundColor: theme.colors.info }]} />
|
||||
</View>
|
||||
</MapLibreGL.PointAnnotation>
|
||||
)}
|
||||
|
||||
{/* Branch markers */}
|
||||
{branches.map((branch) => {
|
||||
const isSelected = selectedBranch?.id === branch.id;
|
||||
return (
|
||||
<MapLibreGL.PointAnnotation
|
||||
key={branch.id}
|
||||
id={branch.id}
|
||||
coordinate={[branch.longitude, branch.latitude]}
|
||||
onSelected={() => handleMarkerPress(branch)}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.markerContainer,
|
||||
{
|
||||
backgroundColor: isSelected
|
||||
? theme.colors.primary
|
||||
: theme.colors.surface,
|
||||
borderColor: isSelected
|
||||
? theme.colors.primaryDark
|
||||
: branch.locationType === 'atm' ? theme.colors.info : theme.colors.primary,
|
||||
transform: [{ scale: isSelected ? 1.2 : 1 }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={branch.locationType === 'atm' ? 'card' : branch.isMainBranch ? 'business' : 'location'}
|
||||
size={16}
|
||||
color={isSelected ? theme.colors.white : branch.locationType === 'atm' ? theme.colors.info : theme.colors.primary}
|
||||
/>
|
||||
</View>
|
||||
<MapLibreGL.Callout title={branch.name} />
|
||||
</MapLibreGL.PointAnnotation>
|
||||
);
|
||||
})}
|
||||
</MapLibreGL.MapView>
|
||||
|
||||
{/* Nearest branch floating card */}
|
||||
{(() => {
|
||||
const nearest = branches[0];
|
||||
if (!nearest || nearest.distance === null || showSheet) { return null; }
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
styles.nearestCard,
|
||||
{
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: theme.radius.lg,
|
||||
bottom: bottomOffset,
|
||||
},
|
||||
theme.shadows.soft,
|
||||
]}
|
||||
onPress={() => handleBranchPress(nearest)}
|
||||
>
|
||||
<View style={[styles.nearestIcon, { backgroundColor: `${theme.colors.accent}14` }]}>
|
||||
<Ionicons name="navigate" size={16} color={theme.colors.accent} />
|
||||
</View>
|
||||
<View style={styles.nearestContent}>
|
||||
<Text style={[styles.nearestLabel, { color: theme.colors.textMuted }]}>
|
||||
NEAREST {nearest.locationType === 'atm' ? 'ATM' : 'BRANCH'}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.nearestName, { color: theme.colors.textPrimary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{nearest.name}
|
||||
</Text>
|
||||
<Text style={[styles.nearestDistance, { color: theme.colors.textSecondary }]}>
|
||||
{nearest.distance !== null
|
||||
? `${nearest.distance.toFixed(1)} km away`
|
||||
: ''}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={theme.colors.textMuted} />
|
||||
</Pressable>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Re-center button */}
|
||||
{userLocation && (
|
||||
<Pressable
|
||||
style={[
|
||||
styles.recenterBtn,
|
||||
{
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: theme.radius.pill,
|
||||
bottom: bottomOffset + 80,
|
||||
},
|
||||
theme.shadows.soft,
|
||||
]}
|
||||
onPress={() => {
|
||||
cameraRef.current?.setCamera({
|
||||
centerCoordinate: [userLocation.longitude, userLocation.latitude],
|
||||
zoomLevel: 12,
|
||||
animationDuration: 600,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Ionicons name="locate" size={20} color={theme.colors.primary} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
/* List View */
|
||||
<FlatList
|
||||
data={branches}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={[styles.listContent, { paddingBottom: bottomOffset }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<BranchListItem
|
||||
branch={item}
|
||||
isSelected={selectedBranch?.id === item.id}
|
||||
onPress={() => handleBranchPress(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Branch Detail Bottom Sheet */}
|
||||
<BranchDetailSheet
|
||||
branch={selectedBranch}
|
||||
visible={showSheet}
|
||||
onClose={handleCloseSheet}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Header sub-component ──
|
||||
|
||||
interface HeaderProps {
|
||||
theme: ReturnType<typeof useTheme>;
|
||||
navigation: Props['navigation'];
|
||||
}
|
||||
|
||||
const Header = ({ theme, navigation }: HeaderProps) => (
|
||||
<View style={styles.header}>
|
||||
<Pressable
|
||||
onPress={() => navigation.goBack()}
|
||||
style={[
|
||||
styles.backButton,
|
||||
{
|
||||
backgroundColor: theme.colors.cardGlass,
|
||||
borderColor: theme.colors.cardBorder,
|
||||
borderRadius: theme.radius.pill,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={20} color={theme.colors.textPrimary} />
|
||||
</Pressable>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text style={[styles.headerTitle, { color: theme.colors.textPrimary }]}>
|
||||
BRANCH & ATM LOCATOR
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// ── Styles ──
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
backButton: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerCopy: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
lineHeight: 20,
|
||||
},
|
||||
searchSection: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 10,
|
||||
gap: 10,
|
||||
marginBottom: 8,
|
||||
},
|
||||
searchBar: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 46,
|
||||
paddingHorizontal: 14,
|
||||
borderWidth: 1,
|
||||
gap: 8,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
height: '100%',
|
||||
},
|
||||
toggleBtn: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
filterSection: {
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 6,
|
||||
},
|
||||
segmentedContainer: {
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 10,
|
||||
marginBottom: 8,
|
||||
padding: 4,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
borderRadius: 12,
|
||||
},
|
||||
segmentSegment: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
countRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
marginBottom: 6,
|
||||
},
|
||||
countText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
clearText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
mapContainer: {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
map: {
|
||||
flex: 1,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 30,
|
||||
},
|
||||
|
||||
// Markers
|
||||
markerContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
borderWidth: 2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
userMarkerOuter: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
userMarkerInner: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
|
||||
// Nearest Branch Card
|
||||
nearestCard: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 14,
|
||||
right: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 14,
|
||||
gap: 10,
|
||||
},
|
||||
nearestIcon: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
nearestContent: {
|
||||
flex: 1,
|
||||
gap: 1,
|
||||
},
|
||||
cityFilterSection: {
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
cityScroll: {
|
||||
gap: 8,
|
||||
},
|
||||
cityChip: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
},
|
||||
cityChipText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
sectionCopy: {
|
||||
flex: 1,
|
||||
gap: 1,
|
||||
},
|
||||
nearestLabel: {
|
||||
fontSize: 9,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
nearestName: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
nearestDistance: {
|
||||
fontSize: 12,
|
||||
},
|
||||
|
||||
// Re-center Button
|
||||
recenterBtn: {
|
||||
position: 'absolute',
|
||||
right: 14,
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text, FlatList } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Button } from '../../../components';
|
||||
import { AppModal } from '../../../components/Modal';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'CertificateList'>;
|
||||
|
||||
export default function CertificateListScreen({ route }: Props) {
|
||||
const { title, category } = route.params;
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [viewingDoc, setViewingDoc] = useState<string | null>(null);
|
||||
|
||||
const mockDocs = Array.from({ length: 4 }).map((_, i) => ({
|
||||
id: String(i),
|
||||
name: `FY ${26 - i}-${27 - i} ${title}`,
|
||||
date: `Generated Apr ${2026 - i}`,
|
||||
size: `${(1.2 + i * 0.3).toFixed(1)} MB`,
|
||||
}));
|
||||
|
||||
const handleDownload = (docName: string) => {
|
||||
toast('Download Started', 'success', `${docName} is being securely downloaded to your device.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title={title} subtitle={`${mockDocs.length} documents available`}>
|
||||
<FlatList
|
||||
data={mockDocs}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<Card variant="glass" style={styles.card}>
|
||||
<View style={styles.cardTop}>
|
||||
<View style={[styles.docIcon, { backgroundColor: `${theme.colors.primary}10`, borderColor: `${theme.colors.primary}20` }]}>
|
||||
<Ionicons name="document-text" size={26} color={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.docInfo}>
|
||||
<Text style={[styles.docTitle, { color: theme.colors.textPrimary }]}>{item.name}</Text>
|
||||
<Text style={[styles.docMeta, { color: theme.colors.textSecondary }]}>
|
||||
{item.date} • {item.size}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: theme.colors.cardBorder }]} />
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<Button
|
||||
title="View"
|
||||
variant="ghost"
|
||||
fullWidth={false}
|
||||
style={[styles.actionBtn, { borderColor: theme.colors.cardBorder }]}
|
||||
onPress={() => setViewingDoc(item.name)}
|
||||
leftAccessory={<Ionicons name="eye-outline" size={18} color={theme.colors.textPrimary} style={{ marginRight: 2 }} />}
|
||||
/>
|
||||
<Button
|
||||
title="Download"
|
||||
variant="secondary"
|
||||
fullWidth={false}
|
||||
style={styles.actionBtn}
|
||||
onPress={() => handleDownload(item.name)}
|
||||
leftAccessory={<Ionicons name="cloud-download-outline" size={18} color={theme.colors.textPrimary} style={{ marginRight: 2 }} />}
|
||||
/>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AppModal
|
||||
visible={!!viewingDoc}
|
||||
title="Document Preview"
|
||||
onClose={() => setViewingDoc(null)}
|
||||
>
|
||||
<View style={[styles.certificatePreview, { backgroundColor: theme.colors.background, borderColor: theme.colors.cardBorder }]}>
|
||||
<View style={[styles.certHeader, { borderBottomColor: theme.colors.cardBorder }]}>
|
||||
<Ionicons name="business" size={20} color={theme.colors.primary} />
|
||||
<Text style={[styles.certBankName, { color: theme.colors.primary }]}>CBS BANK LTD.</Text>
|
||||
</View>
|
||||
<Text style={[styles.certTitle, { color: theme.colors.textPrimary }]}>{title}</Text>
|
||||
<View style={{ gap: 8, marginTop: 12 }}>
|
||||
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>
|
||||
This officially certifies that the document <Text style={{fontWeight: '700', color: theme.colors.textPrimary}}>{viewingDoc}</Text> has been cryptographically generated and is valid.
|
||||
</Text>
|
||||
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Reference Category: <Text style={{fontWeight: '700'}}>{category.toUpperCase()}</Text></Text>
|
||||
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Account Bearer: Lincpay User</Text>
|
||||
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Account #: XXXX-XXXX-1234</Text>
|
||||
</View>
|
||||
<View style={styles.certFooter}>
|
||||
<View style={[styles.sealBox, { borderColor: theme.colors.primary }]}>
|
||||
<Ionicons name="ribbon-outline" size={20} color={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.signature, { color: theme.colors.textPrimary }]}>Auth. Signatory</Text>
|
||||
<View style={{ width: 80, height: 1, backgroundColor: theme.colors.textSecondary, marginTop: 4 }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
title="Download Full PDF"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
if (viewingDoc) handleDownload(viewingDoc);
|
||||
setViewingDoc(null);
|
||||
}}
|
||||
style={{ marginTop: 8 }}
|
||||
leftAccessory={<Ionicons name="cloud-download-outline" size={18} color="#fff" style={{ marginRight: 4 }} />}
|
||||
/>
|
||||
<Button
|
||||
title="Close"
|
||||
variant="ghost"
|
||||
onPress={() => setViewingDoc(null)}
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</AppModal>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
borderWidth: 1,
|
||||
},
|
||||
cardTop: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
docIcon: {
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
docInfo: {
|
||||
flex: 1,
|
||||
gap: 6,
|
||||
},
|
||||
docTitle: {
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
docMeta: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
width: '100%',
|
||||
opacity: 0.6,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
actionBtn: {
|
||||
flex: 1,
|
||||
height: 44,
|
||||
},
|
||||
certificatePreview: {
|
||||
padding: 20,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
marginVertical: 12,
|
||||
},
|
||||
certHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderBottomWidth: 1,
|
||||
paddingBottom: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
certBankName: {
|
||||
fontWeight: '900',
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
certTitle: {
|
||||
fontWeight: '800',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
certText: {
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
},
|
||||
certFooter: {
|
||||
marginTop: 24,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
sealBox: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
signature: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card } from '../../../components';
|
||||
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'CertificatesHub'>;
|
||||
|
||||
export default function CertificatesHubScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'balance',
|
||||
title: 'Balance Cert',
|
||||
icon: 'wallet-outline',
|
||||
iconColor: '#3B82F6',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Balance Certificates', category: 'balance' }),
|
||||
},
|
||||
{
|
||||
id: 'interest',
|
||||
title: 'Interest Cert',
|
||||
icon: 'trending-up-outline',
|
||||
iconColor: '#10B981',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Interest Certificates', category: 'interest' }),
|
||||
},
|
||||
{
|
||||
id: 'hypothecation',
|
||||
title: 'Hypothecation',
|
||||
icon: 'document-text-outline',
|
||||
iconColor: '#F59E0B',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Hypothecation', category: 'hypothecation' }),
|
||||
},
|
||||
{
|
||||
id: 'insurance',
|
||||
title: 'Insurance',
|
||||
icon: 'shield-checkmark-outline',
|
||||
iconColor: '#8B5CF6',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Insurance Copies', category: 'insurance' }),
|
||||
},
|
||||
{
|
||||
id: 'tax',
|
||||
title: 'Tax Forms',
|
||||
icon: 'receipt-outline',
|
||||
iconColor: '#EC4899',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Tax Statements', category: 'tax' }),
|
||||
},
|
||||
{
|
||||
id: 'loan',
|
||||
title: 'Loan Docs',
|
||||
icon: 'home-outline',
|
||||
iconColor: '#6366F1',
|
||||
onPress: () => navigation.navigate('CertificateList', { title: 'Loan Documents', category: 'loan' }),
|
||||
},
|
||||
],
|
||||
[navigation]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenShell title="Certificates" subtitle="Secure document vault">
|
||||
|
||||
<Card variant="glass" style={styles.heroCard}>
|
||||
<View style={styles.heroHeader}>
|
||||
<View style={[styles.heroIcon, { backgroundColor: `${theme.colors.primary}15` }]}>
|
||||
<Ionicons name="folder-open-outline" size={24} color={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.heroCopy}>
|
||||
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>Document Vault</Text>
|
||||
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
All your official bank certificates and statements, generated dynamically and secured.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.heroStats}>
|
||||
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
|
||||
<Text style={[styles.heroStatValue, { color: theme.colors.textPrimary }]}>12</Text>
|
||||
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>Generated</Text>
|
||||
</View>
|
||||
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
|
||||
<Text style={[styles.heroStatValue, { color: theme.colors.primary }]}>Active</Text>
|
||||
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>Vault Status</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Select Category</Text>
|
||||
|
||||
<View style={styles.grid}>
|
||||
{options.map((opt) => (
|
||||
<ServiceGridItem key={opt.id} {...opt} />
|
||||
))}
|
||||
</View>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heroCard: {
|
||||
padding: 20,
|
||||
gap: 20,
|
||||
marginBottom: 8,
|
||||
},
|
||||
heroHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
heroIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heroCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
heroTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
heroSubtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
heroStats: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
heroStat: {
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
heroStatLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
heroStatValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginTop: 8,
|
||||
marginLeft: 4,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Input, Button } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeBookRequest'>;
|
||||
|
||||
export default function ChequeBookRequestScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [account, setAccount] = useState('0000XXXX1234');
|
||||
const [leaves, setLeaves] = useState('25');
|
||||
const [address, setAddress] = useState('Registered Address');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast('Request Submitted', 'success', 'Your cheque book will be delivered in 3-5 days.');
|
||||
navigation.goBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title="Cheque Book" subtitle="Request a new cheque book">
|
||||
<Card variant="default" style={styles.card}>
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
|
||||
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Number of Leaves</Text>
|
||||
<Input value={leaves} onChangeText={setLeaves} placeholder="e.g. 25, 50, 100" keyboardType="number-pad" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Delivery Address</Text>
|
||||
<Input value={address} onChangeText={setAddress} placeholder="Branch or Registered Address" />
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Submit Request"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.submitBtn}
|
||||
/>
|
||||
</Card>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { padding: 16, gap: 16 },
|
||||
formGroup: { gap: 8 },
|
||||
label: { fontSize: 13, fontWeight: '600' },
|
||||
submitBtn: { marginTop: 16 },
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell } from '../../../components';
|
||||
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { ChequeAnimation } from '../components/ChequeAnimation';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeServicesHub'>;
|
||||
|
||||
export default function ChequeServicesHubScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'request',
|
||||
title: 'Book Request',
|
||||
icon: 'book-outline',
|
||||
iconColor: '#3B82F6',
|
||||
onPress: () => navigation.navigate('ChequeBookRequest'),
|
||||
},
|
||||
{
|
||||
id: 'stop',
|
||||
title: 'Stop Payment',
|
||||
icon: 'stop-circle-outline',
|
||||
iconColor: '#EF4444',
|
||||
onPress: () => navigation.navigate('StopPayment'),
|
||||
},
|
||||
{
|
||||
id: 'track',
|
||||
title: 'Track Cheques',
|
||||
icon: 'git-compare-outline',
|
||||
iconColor: '#10B981',
|
||||
onPress: () => navigation.navigate('ChequeTracking'),
|
||||
},
|
||||
],
|
||||
[navigation]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenShell title="Cheque Services" subtitle="Manage all your cheque-related needs">
|
||||
{/* Animated cheque illustration */}
|
||||
<ChequeAnimation />
|
||||
|
||||
{/* Quick info strip */}
|
||||
<View style={[styles.infoStrip, { backgroundColor: `${theme.colors.primary}08`, borderColor: `${theme.colors.primary}12` }]}>
|
||||
<View style={[styles.infoDot, { backgroundColor: theme.colors.accent }]} />
|
||||
<Text style={[styles.infoText, { color: theme.colors.textSecondary }]}>
|
||||
Request, track, or manage cheques — all in one place
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Service grid */}
|
||||
<View style={styles.grid}>
|
||||
{options.map((opt) => (
|
||||
<ServiceGridItem key={opt.id} {...opt} />
|
||||
))}
|
||||
</View>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: 8,
|
||||
},
|
||||
infoStrip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
infoDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
lineHeight: 17,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, Text, FlatList } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card } from '../../../components';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeTracking'>;
|
||||
|
||||
const MOCK_TRACKING = [
|
||||
{ id: '1', title: 'Cheque Book #65', status: 'Delivered', date: '10 Oct 2026', items: [{ stage: 'Requested', time: '01 Oct', active: false }, { stage: 'Processed', time: '05 Oct', active: false }, { stage: 'Delivered', time: '10 Oct', active: true }] },
|
||||
{ id: '2', title: 'Cheque Book #66', status: 'Processing', date: 'Today', items: [{ stage: 'Requested', time: 'Today', active: false }, { stage: 'Processing', time: 'Today', active: true }] }
|
||||
];
|
||||
|
||||
export default function ChequeTrackingScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ScreenShell title="Track Cheques" subtitle="Cheque books and individual cheques">
|
||||
<FlatList
|
||||
data={MOCK_TRACKING}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: 24 }}
|
||||
renderItem={({ item }) => (
|
||||
<Card style={{ padding: 16, gap: 12 }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, color: theme.colors.textPrimary }}>{item.title}</Text>
|
||||
<Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>{item.date}</Text>
|
||||
</View>
|
||||
<View style={{ height: 1, backgroundColor: `${theme.colors.cardBorder}40`, marginVertical: 4 }} />
|
||||
{item.items.map((stage, idx) => (
|
||||
<View key={idx} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
||||
<View style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: stage.active ? theme.colors.primary : theme.colors.cardBorder }} />
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: stage.active ? theme.colors.primary : theme.colors.textSecondary, fontWeight: stage.active ? '700' : '500' }}>{stage.stage}</Text>
|
||||
<Text style={{ fontSize: 12, color: theme.colors.textMuted }}>{stage.time}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Input, Button } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'Form15G15H'>;
|
||||
|
||||
export default function Form15G15HScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [formType, setFormType] = useState('15G');
|
||||
const [panNumber, setPanNumber] = useState('');
|
||||
const [estimatedIncome, setEstimatedIncome] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!panNumber || !estimatedIncome) {
|
||||
toast('Validation Failed', 'error', 'PAN and estimated income are required.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast('Form Submitted successfully', 'success', `Your Form ${formType} has been recorded for the current FY.`);
|
||||
navigation.goBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title="Form 15G / 15H" subtitle="Submit your tax declarations">
|
||||
<Card variant="default" style={styles.card}>
|
||||
<View style={{ flexDirection: 'row', gap: 12, marginBottom: 8 }}>
|
||||
<Button
|
||||
title="Form 15G"
|
||||
variant={formType === '15G' ? 'primary' : 'secondary'}
|
||||
onPress={() => setFormType('15G')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
title="Form 15H (Senior Citizens)"
|
||||
variant={formType === '15H' ? 'primary' : 'secondary'}
|
||||
onPress={() => setFormType('15H')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>PAN Number</Text>
|
||||
<Input value={panNumber} onChangeText={setPanNumber} placeholder="ABCDE1234F" autoCapitalize="characters" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Estimated Income (₹)</Text>
|
||||
<Input value={estimatedIncome} onChangeText={setEstimatedIncome} placeholder="e.g. 350000" keyboardType="numeric" />
|
||||
</View>
|
||||
|
||||
<View style={{ padding: 12, backgroundColor: `${theme.colors.info}15`, borderRadius: 8 }}>
|
||||
<Text style={{ color: theme.colors.info, fontSize: 13, lineHeight: 18 }}>
|
||||
By submitting, you declare that the tax on your estimated total income for the previous year will be nil.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={`Submit Form ${formType}`}
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.submitBtn}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<View style={{ marginTop: 24, gap: 16 }}>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, color: theme.colors.textPrimary, marginLeft: 4 }}>History</Text>
|
||||
<Card style={{ padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<View>
|
||||
<Text style={{ fontWeight: '700', color: theme.colors.textPrimary }}>Form 15G - FY 24-25</Text>
|
||||
<Text style={{ fontSize: 12, color: theme.colors.textSecondary }}>Submitted April 2025</Text>
|
||||
</View>
|
||||
<Button title="Download" variant="ghost" onPress={() => toast('Downloading...', 'info')} />
|
||||
</Card>
|
||||
</View>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { padding: 16, gap: 16 },
|
||||
formGroup: { gap: 8 },
|
||||
label: { fontSize: 13, fontWeight: '600' },
|
||||
submitBtn: { marginTop: 8 },
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Input, Button } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'PositivePay'>;
|
||||
|
||||
export default function PositivePayScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [account, setAccount] = useState('0000XXXX1234');
|
||||
const [chequeNumber, setChequeNumber] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [payee, setPayee] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!chequeNumber || !amount || !payee) {
|
||||
toast('Incomplete Form', 'error', 'Please fill all required details.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast('Verification Submitted', 'success', `Details for Cheque #${chequeNumber} saved securely.`);
|
||||
navigation.goBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title="Positive Pay System" subtitle="Secure your high-value cheques">
|
||||
<Card variant="default" style={styles.card}>
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
|
||||
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Number</Text>
|
||||
<Input value={chequeNumber} onChangeText={setChequeNumber} placeholder="6-digit unique number" keyboardType="number-pad" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Amount</Text>
|
||||
<Input value={amount} onChangeText={setAmount} placeholder="e.g. 50000" keyboardType="numeric" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Payee Name</Text>
|
||||
<Input value={payee} onChangeText={setPayee} placeholder="Exact name on cheque" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Date</Text>
|
||||
<Input value={date} onChangeText={setDate} placeholder="DD/MM/YYYY" />
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Submit Details"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.submitBtn}
|
||||
/>
|
||||
</Card>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { padding: 16, gap: 16 },
|
||||
formGroup: { gap: 8 },
|
||||
label: { fontSize: 13, fontWeight: '600' },
|
||||
submitBtn: { marginTop: 16 },
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell } from '../../../components';
|
||||
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { ServiceRequestAnimation } from '../components/ServiceRequestAnimation';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'ServiceRequestsHub'>;
|
||||
|
||||
export default function ServiceRequestsHubScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'positive_pay',
|
||||
title: 'Positive Pay',
|
||||
icon: 'lock-closed-outline',
|
||||
iconColor: '#F43F5E',
|
||||
onPress: () => navigation.navigate('PositivePay'),
|
||||
},
|
||||
{
|
||||
id: 'form15g',
|
||||
title: 'Form 15G/15H',
|
||||
icon: 'document-attach-outline',
|
||||
iconColor: '#10B981',
|
||||
onPress: () => navigation.navigate('Form15G15H'),
|
||||
},
|
||||
{
|
||||
id: 'special',
|
||||
title: 'Special Instr.',
|
||||
icon: 'create-outline',
|
||||
iconColor: '#6366F1',
|
||||
onPress: () => navigation.navigate('SpecialInstructions'),
|
||||
},
|
||||
],
|
||||
[navigation]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenShell title="Service Requests" subtitle="Submit and track requests">
|
||||
{/* Animated illustration */}
|
||||
<ServiceRequestAnimation />
|
||||
|
||||
{/* Info strip */}
|
||||
<View style={[styles.infoStrip, { backgroundColor: `${theme.colors.info}08`, borderColor: `${theme.colors.info}12` }]}>
|
||||
<View style={[styles.infoDot, { backgroundColor: theme.colors.info }]} />
|
||||
<Text style={[styles.infoText, { color: theme.colors.textSecondary }]}>
|
||||
Submit forms, set up Positive Pay, or add special instructions
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Service grid */}
|
||||
<View style={styles.grid}>
|
||||
{options.map((opt) => (
|
||||
<ServiceGridItem key={opt.id} {...opt} />
|
||||
))}
|
||||
</View>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: 8,
|
||||
},
|
||||
infoStrip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
infoDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
lineHeight: 17,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import type { CompositeScreenProps } from '@react-navigation/native';
|
||||
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { useAppSelector } from '../../../app/hooks';
|
||||
import { Card, ScreenShell } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
|
||||
import { ServiceStatusPill } from '../components/ServiceStatusPill';
|
||||
import { selectServiceRequests } from '../store/serviceSelectors';
|
||||
import type { MainTabParamList, ServicesStackParamList } from '../../../navigation/types';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = CompositeScreenProps<
|
||||
NativeStackScreenProps<ServicesStackParamList, 'ServicesHub'>,
|
||||
BottomTabScreenProps<MainTabParamList, 'Services'>
|
||||
>;
|
||||
|
||||
export default function ServicesHubScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const serviceRequests = useAppSelector(selectServiceRequests);
|
||||
|
||||
const latestRequest = serviceRequests[0] ?? null;
|
||||
const openRequestsCount = serviceRequests.filter(
|
||||
(request) => request.status !== 'resolved',
|
||||
).length;
|
||||
|
||||
const serviceOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'branch_locator',
|
||||
title: 'Find Branch & ATMs',
|
||||
icon: 'map-outline',
|
||||
iconColor: '#10B981',
|
||||
onPress: () => navigation.navigate('BranchLocator'),
|
||||
},
|
||||
{
|
||||
id: 'all_deposits',
|
||||
title: 'Deposits',
|
||||
icon: 'wallet-outline',
|
||||
iconColor: '#124577',
|
||||
onPress: () => navigation.navigate('DepositsOverview'),
|
||||
},
|
||||
{
|
||||
id: 'cheque_services',
|
||||
title: 'Cheque Services',
|
||||
icon: 'book-outline',
|
||||
iconColor: '#3B82F6',
|
||||
onPress: () => navigation.navigate('ChequeServicesHub'),
|
||||
},
|
||||
{
|
||||
id: 'certificates',
|
||||
title: 'Certificates',
|
||||
icon: 'document-text-outline',
|
||||
iconColor: '#F59E0B',
|
||||
onPress: () => navigation.navigate('CertificatesHub'),
|
||||
},
|
||||
{
|
||||
id: 'requests',
|
||||
title: 'Service Requests',
|
||||
icon: 'git-pull-request-outline',
|
||||
iconColor: '#EF4444',
|
||||
onPress: () => navigation.navigate('ServiceRequestsHub'),
|
||||
},
|
||||
{
|
||||
id: 'calculator',
|
||||
title: 'FD/RD Calc',
|
||||
icon: 'calculator-outline',
|
||||
iconColor: '#6366F1',
|
||||
onPress: () => navigation.navigate('DepositCalculator'),
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
title: 'Support Chat',
|
||||
icon: 'chatbubble-ellipses-outline',
|
||||
iconColor: '#0EA5E9',
|
||||
onPress: () =>
|
||||
toast(
|
||||
'Support chat screen pending',
|
||||
'info',
|
||||
'This feature is coming in the next update.',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
title: 'Help Center',
|
||||
icon: 'help-circle-outline',
|
||||
iconColor: '#8B5CF6',
|
||||
onPress: () =>
|
||||
toast('Help center pending', 'info', 'Coming soon.'),
|
||||
},
|
||||
{
|
||||
id: 'cyber_security',
|
||||
title: 'Cyber Security',
|
||||
icon: 'shield-half-outline',
|
||||
iconColor: '#F59E0B',
|
||||
onPress: () => navigation.navigate('AwarenessList'),
|
||||
},
|
||||
{
|
||||
id: 'protection_schemes',
|
||||
title: 'Protection & Schemes',
|
||||
icon: 'umbrella-outline',
|
||||
iconColor: '#EC4899',
|
||||
onPress: () => navigation.navigate('FinancialProtectionMain'),
|
||||
},
|
||||
],
|
||||
[navigation, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenShell title="Service desk" subtitle="All service options in one hub">
|
||||
{/* <View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
|
||||
Service options
|
||||
</Text>
|
||||
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
Choose a service card to continue
|
||||
</Text>
|
||||
</View> */}
|
||||
|
||||
<View style={styles.grid}>
|
||||
{serviceOptions.map((option) => (
|
||||
<ServiceGridItem
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
title={option.title}
|
||||
icon={option.icon}
|
||||
iconColor={option.iconColor}
|
||||
onPress={option.onPress}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Card variant="glass" style={styles.heroCard}>
|
||||
<View style={styles.heroHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.heroIcon,
|
||||
{ backgroundColor: `${theme.colors.primary}12` },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="grid-outline" size={18} color={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.heroCopy}>
|
||||
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
|
||||
Services hub
|
||||
</Text>
|
||||
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
Browse deposit journeys, service support actions, and request tools from one place.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.heroStats}>
|
||||
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
|
||||
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>
|
||||
Open requests
|
||||
</Text>
|
||||
<Text style={[styles.heroStatValue, { color: theme.colors.textPrimary }]}>
|
||||
{openRequestsCount}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
|
||||
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>
|
||||
Latest status
|
||||
</Text>
|
||||
{latestRequest ? (
|
||||
<ServiceStatusPill status={latestRequest.status} />
|
||||
) : (
|
||||
<Text style={[styles.heroStatEmpty, { color: theme.colors.textMuted }]}>
|
||||
No requests
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heroCard: {
|
||||
gap: 16,
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 14,
|
||||
elevation: 2,
|
||||
},
|
||||
heroHeader: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
heroIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heroCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
heroTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
heroSubtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
heroStats: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
heroStat: {
|
||||
flex: 1,
|
||||
borderRadius: 18,
|
||||
padding: 14,
|
||||
gap: 6,
|
||||
},
|
||||
heroStatLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
heroStatValue: {
|
||||
fontSize: 22,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
heroStatEmpty: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionHeader: {
|
||||
gap: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: 60,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Input, Button } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'SpecialInstructions'>;
|
||||
|
||||
export default function SpecialInstructionsScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!title || !description) {
|
||||
toast('Empty fields', 'error', 'Title and description are required.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast('Request Logged', 'success', 'Your special instruction has been submitted to the branch.');
|
||||
navigation.goBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title="Special Instructions" subtitle="Submit custom service requests">
|
||||
<Card variant="default" style={styles.card}>
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Request Title</Text>
|
||||
<Input value={title} onChangeText={setTitle} placeholder="Short objective summary" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Description</Text>
|
||||
<Input
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Detailed instructions..."
|
||||
multiline
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Submit Instruction"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.submitBtn}
|
||||
/>
|
||||
</Card>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { padding: 16, gap: 16 },
|
||||
formGroup: { gap: 8 },
|
||||
label: { fontSize: 13, fontWeight: '600' },
|
||||
submitBtn: { marginTop: 16 },
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { ServicesStackParamList } from '../../../navigation/types';
|
||||
import { ScreenShell, Card, Input, Button } from '../../../components';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
type Props = NativeStackScreenProps<ServicesStackParamList, 'StopPayment'>;
|
||||
|
||||
export default function StopPaymentScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const [account, setAccount] = useState('0000XXXX1234');
|
||||
const [chequeNumber, setChequeNumber] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!chequeNumber) {
|
||||
toast('Cheque number required', 'error', 'Please enter a valid cheque number.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast('Payment Stopped', 'success', `Cheque #${chequeNumber} has been blocked.`);
|
||||
navigation.goBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenShell title="Stop Payment" subtitle="Block a specific cheque">
|
||||
<Card variant="default" style={styles.card}>
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
|
||||
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Number</Text>
|
||||
<Input value={chequeNumber} onChangeText={setChequeNumber} placeholder="6-digit unique number" keyboardType="number-pad" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Reason (Optional)</Text>
|
||||
<Input value={reason} onChangeText={setReason} placeholder="e.g. Lost in transit" />
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Confirm Stop Payment"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.submitBtn}
|
||||
variant="danger"
|
||||
/>
|
||||
</Card>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { padding: 16, gap: 16 },
|
||||
formGroup: { gap: 8 },
|
||||
label: { fontSize: 13, fontWeight: '600' },
|
||||
submitBtn: { marginTop: 16 },
|
||||
});
|
||||
Reference in New Issue
Block a user