ubkerp.transport.module 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +136 -0
- package/lib/commonjs/assets/index.js +6 -0
- package/lib/commonjs/assets/index.js.map +1 -0
- package/lib/commonjs/components/index.js +6 -0
- package/lib/commonjs/components/index.js.map +1 -0
- package/lib/commonjs/constants/index.js +6 -0
- package/lib/commonjs/constants/index.js.map +1 -0
- package/lib/commonjs/hooks/index.js +6 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/navigation/index.js +6 -0
- package/lib/commonjs/navigation/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/providers/TransportContext.js +9 -0
- package/lib/commonjs/providers/TransportContext.js.map +1 -0
- package/lib/commonjs/providers/TransportProvider.js +46 -0
- package/lib/commonjs/providers/TransportProvider.js.map +1 -0
- package/lib/commonjs/providers/types.js +6 -0
- package/lib/commonjs/providers/types.js.map +1 -0
- package/lib/commonjs/providers/useTransport.js +16 -0
- package/lib/commonjs/providers/useTransport.js.map +1 -0
- package/lib/commonjs/screens/StudentRouteDetails/StudentRouteDetailsScreen.js +308 -0
- package/lib/commonjs/screens/StudentRouteDetails/StudentRouteDetailsScreen.js.map +1 -0
- package/lib/commonjs/screens/TransportDashboard/TransportDashboard.js +87 -0
- package/lib/commonjs/screens/TransportDashboard/TransportDashboard.js.map +1 -0
- package/lib/commonjs/services/api/createApiClient.js +116 -0
- package/lib/commonjs/services/api/createApiClient.js.map +1 -0
- package/lib/commonjs/services/api/types.js +6 -0
- package/lib/commonjs/services/api/types.js.map +1 -0
- package/lib/commonjs/services/index.js +6 -0
- package/lib/commonjs/services/index.js.map +1 -0
- package/lib/commonjs/services/transport/studentRouteDetails.js +10 -0
- package/lib/commonjs/services/transport/studentRouteDetails.js.map +1 -0
- package/lib/commonjs/services/transport/types.js +2 -0
- package/lib/commonjs/services/transport/types.js.map +1 -0
- package/lib/commonjs/store/index.js +6 -0
- package/lib/commonjs/store/index.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/types/transport.js +2 -0
- package/lib/commonjs/types/transport.js.map +1 -0
- package/lib/commonjs/utils/index.js +6 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/module/assets/index.js +4 -0
- package/lib/module/assets/index.js.map +1 -0
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/constants/index.js +4 -0
- package/lib/module/constants/index.js.map +1 -0
- package/lib/module/hooks/index.js +4 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/navigation/index.js +4 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/providers/TransportContext.js +5 -0
- package/lib/module/providers/TransportContext.js.map +1 -0
- package/lib/module/providers/TransportProvider.js +41 -0
- package/lib/module/providers/TransportProvider.js.map +1 -0
- package/lib/module/providers/types.js +4 -0
- package/lib/module/providers/types.js.map +1 -0
- package/lib/module/providers/useTransport.js +12 -0
- package/lib/module/providers/useTransport.js.map +1 -0
- package/lib/module/screens/StudentRouteDetails/StudentRouteDetailsScreen.js +303 -0
- package/lib/module/screens/StudentRouteDetails/StudentRouteDetailsScreen.js.map +1 -0
- package/lib/module/screens/TransportDashboard/TransportDashboard.js +82 -0
- package/lib/module/screens/TransportDashboard/TransportDashboard.js.map +1 -0
- package/lib/module/services/api/createApiClient.js +111 -0
- package/lib/module/services/api/createApiClient.js.map +1 -0
- package/lib/module/services/api/types.js +4 -0
- package/lib/module/services/api/types.js.map +1 -0
- package/lib/module/services/index.js +4 -0
- package/lib/module/services/index.js.map +1 -0
- package/lib/module/services/transport/studentRouteDetails.js +6 -0
- package/lib/module/services/transport/studentRouteDetails.js.map +1 -0
- package/lib/module/services/transport/types.js +2 -0
- package/lib/module/services/transport/types.js.map +1 -0
- package/lib/module/store/index.js +4 -0
- package/lib/module/store/index.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/transport.js +2 -0
- package/lib/module/types/transport.js.map +1 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/typescript/assets/index.d.ts +2 -0
- package/lib/typescript/assets/index.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +2 -0
- package/lib/typescript/components/index.d.ts.map +1 -0
- package/lib/typescript/constants/index.d.ts +2 -0
- package/lib/typescript/constants/index.d.ts.map +1 -0
- package/lib/typescript/hooks/index.d.ts +2 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/navigation/index.d.ts +2 -0
- package/lib/typescript/navigation/index.d.ts.map +1 -0
- package/lib/typescript/providers/TransportContext.d.ts +8 -0
- package/lib/typescript/providers/TransportContext.d.ts.map +1 -0
- package/lib/typescript/providers/TransportProvider.d.ts +3 -0
- package/lib/typescript/providers/TransportProvider.d.ts.map +1 -0
- package/lib/typescript/providers/types.d.ts +28 -0
- package/lib/typescript/providers/types.d.ts.map +1 -0
- package/lib/typescript/providers/useTransport.d.ts +2 -0
- package/lib/typescript/providers/useTransport.d.ts.map +1 -0
- package/lib/typescript/screens/StudentRouteDetails/StudentRouteDetailsScreen.d.ts +6 -0
- package/lib/typescript/screens/StudentRouteDetails/StudentRouteDetailsScreen.d.ts.map +1 -0
- package/lib/typescript/screens/TransportDashboard/TransportDashboard.d.ts +12 -0
- package/lib/typescript/screens/TransportDashboard/TransportDashboard.d.ts.map +1 -0
- package/lib/typescript/services/api/createApiClient.d.ts +18 -0
- package/lib/typescript/services/api/createApiClient.d.ts.map +1 -0
- package/lib/typescript/services/api/types.d.ts +18 -0
- package/lib/typescript/services/api/types.d.ts.map +1 -0
- package/lib/typescript/services/index.d.ts +2 -0
- package/lib/typescript/services/index.d.ts.map +1 -0
- package/lib/typescript/services/transport/studentRouteDetails.d.ts +5 -0
- package/lib/typescript/services/transport/studentRouteDetails.d.ts.map +1 -0
- package/lib/typescript/services/transport/types.d.ts +7 -0
- package/lib/typescript/services/transport/types.d.ts.map +1 -0
- package/lib/typescript/store/index.d.ts +2 -0
- package/lib/typescript/store/index.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +2 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/types/transport.d.ts +12 -0
- package/lib/typescript/types/transport.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +2 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/assets/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/src/constants/index.ts +2 -0
- package/src/hooks/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/navigation/index.ts +2 -0
- package/src/providers/TransportContext.ts +11 -0
- package/src/providers/TransportProvider.tsx +47 -0
- package/src/providers/types.ts +28 -0
- package/src/providers/useTransport.ts +11 -0
- package/src/screens/StudentRouteDetails/StudentRouteDetailsScreen.tsx +284 -0
- package/src/screens/TransportDashboard/TransportDashboard.tsx +93 -0
- package/src/services/api/createApiClient.ts +170 -0
- package/src/services/api/types.ts +30 -0
- package/src/services/index.ts +2 -0
- package/src/services/transport/studentRouteDetails.ts +10 -0
- package/src/services/transport/types.ts +7 -0
- package/src/store/index.ts +2 -0
- package/src/types/index.ts +2 -0
- package/src/types/transport.ts +12 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Alert,
|
|
5
|
+
Linking,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
View,
|
|
11
|
+
Image,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import {useTransport} from '../../providers/useTransport';
|
|
14
|
+
import {getStudentRouteDetails} from '../../services/transport/studentRouteDetails';
|
|
15
|
+
import type {StudentRouteDetails} from '../../types/transport';
|
|
16
|
+
|
|
17
|
+
export type StudentRouteDetailsScreenProps = {
|
|
18
|
+
renderTop?: () => React.ReactNode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function buildDriverImageUrl(args: {fileBaseUrl?: string; driverImage?: string}) {
|
|
22
|
+
if (!args.fileBaseUrl || !args.driverImage) return undefined;
|
|
23
|
+
const base = args.fileBaseUrl.endsWith('/')
|
|
24
|
+
? args.fileBaseUrl.slice(0, -1)
|
|
25
|
+
: args.fileBaseUrl;
|
|
26
|
+
const img = args.driverImage.startsWith('/')
|
|
27
|
+
? args.driverImage.slice(1)
|
|
28
|
+
: args.driverImage;
|
|
29
|
+
return `${base}/staff/document/${img}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function StudentRouteDetailsScreen({renderTop}: StudentRouteDetailsScreenProps) {
|
|
33
|
+
const {api, fileBaseUrl} = useTransport();
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [data, setData] = useState<StudentRouteDetails | null>(null);
|
|
36
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
const driverImageUrl = useMemo(() => {
|
|
39
|
+
return buildDriverImageUrl({
|
|
40
|
+
fileBaseUrl,
|
|
41
|
+
driverImage: data?.driver_image,
|
|
42
|
+
});
|
|
43
|
+
}, [data?.driver_image, fileBaseUrl]);
|
|
44
|
+
|
|
45
|
+
const load = useCallback(async () => {
|
|
46
|
+
try {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
setLoadError(null);
|
|
49
|
+
const res = await getStudentRouteDetails(api);
|
|
50
|
+
const first = Array.isArray(res.data) ? res.data[0] : undefined;
|
|
51
|
+
setData(first ?? null);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
setData(null);
|
|
54
|
+
setLoadError('Failed to load transport route details');
|
|
55
|
+
} finally {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}, [api]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
load();
|
|
62
|
+
}, [load]);
|
|
63
|
+
|
|
64
|
+
const handleCall = useCallback(async () => {
|
|
65
|
+
const phone = data?.contact_number;
|
|
66
|
+
if (!phone) {
|
|
67
|
+
Alert.alert('Error', 'Contact number not available');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const phoneNumber = phone.toString().replace(/[^0-9+]/g, '');
|
|
72
|
+
const url = `tel:${phoneNumber}`;
|
|
73
|
+
const supported = await Linking.canOpenURL(url);
|
|
74
|
+
if (!supported) {
|
|
75
|
+
Alert.alert('Error', 'Phone calls are not supported on this device');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await Linking.openURL(url);
|
|
79
|
+
}, [data?.contact_number]);
|
|
80
|
+
|
|
81
|
+
if (loading) {
|
|
82
|
+
return (
|
|
83
|
+
<View style={styles.center}>
|
|
84
|
+
<ActivityIndicator size="large" />
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!data?.route_title) {
|
|
90
|
+
return (
|
|
91
|
+
<View style={styles.center}>
|
|
92
|
+
{loadError ? (
|
|
93
|
+
<Text style={styles.errorText}>{loadError}</Text>
|
|
94
|
+
) : (
|
|
95
|
+
<Text style={styles.noDataText}>No Data Found</Text>
|
|
96
|
+
)}
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={styles.container}>
|
|
103
|
+
{renderTop ? renderTop() : null}
|
|
104
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
105
|
+
<View style={styles.routeTitleCard}>
|
|
106
|
+
<Text style={styles.routeTitle}>{data.route_title}</Text>
|
|
107
|
+
<Text style={styles.routeSubtitle}>Route Details</Text>
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
<View style={styles.card}>
|
|
111
|
+
<Text style={styles.cardTitle}>Vehicle Information</Text>
|
|
112
|
+
|
|
113
|
+
<View style={styles.detailRow}>
|
|
114
|
+
<Text style={styles.detailLabel}>Vehicle No:</Text>
|
|
115
|
+
<Text style={styles.detailValue}>{data.vehicle_no ?? '-'}</Text>
|
|
116
|
+
</View>
|
|
117
|
+
<View style={styles.detailRow}>
|
|
118
|
+
<Text style={styles.detailLabel}>Model:</Text>
|
|
119
|
+
<Text style={styles.detailValue}>{data.vehicle_model ?? '-'}</Text>
|
|
120
|
+
</View>
|
|
121
|
+
<View style={styles.detailRow}>
|
|
122
|
+
<Text style={styles.detailLabel}>Stop Name:</Text>
|
|
123
|
+
<Text style={styles.detailValue}>{data.stop_name ?? '-'}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
<View style={styles.detailRow}>
|
|
126
|
+
<Text style={styles.detailLabel}>Pick Up:</Text>
|
|
127
|
+
<Text style={[styles.detailValue, styles.pickup]}>
|
|
128
|
+
{data.pickup_time ?? '-'}
|
|
129
|
+
</Text>
|
|
130
|
+
</View>
|
|
131
|
+
<View style={styles.detailRow}>
|
|
132
|
+
<Text style={styles.detailLabel}>Drop Time:</Text>
|
|
133
|
+
<Text style={[styles.detailValue, styles.drop]}>
|
|
134
|
+
{data.drop_time ?? '-'}
|
|
135
|
+
</Text>
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
|
|
139
|
+
<View style={styles.card}>
|
|
140
|
+
<Text style={styles.cardTitle}>Driver Information</Text>
|
|
141
|
+
|
|
142
|
+
<View style={styles.driverContainer}>
|
|
143
|
+
<View style={styles.driverImageContainer}>
|
|
144
|
+
{driverImageUrl ? (
|
|
145
|
+
<Image style={styles.driverImage} source={{uri: driverImageUrl}} />
|
|
146
|
+
) : (
|
|
147
|
+
<View style={styles.driverImageFallback} />
|
|
148
|
+
)}
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
<View style={styles.driverInfo}>
|
|
152
|
+
<Text style={styles.driverLabel}>Driver Name</Text>
|
|
153
|
+
<Text style={styles.driverName}>{data.driver ?? '-'}</Text>
|
|
154
|
+
</View>
|
|
155
|
+
|
|
156
|
+
<TouchableOpacity onPress={handleCall} style={styles.callButton}>
|
|
157
|
+
<Text style={styles.callText}>Call</Text>
|
|
158
|
+
</TouchableOpacity>
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
</ScrollView>
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const styles = StyleSheet.create({
|
|
167
|
+
container: {
|
|
168
|
+
flex: 1,
|
|
169
|
+
backgroundColor: '#FFFFFF',
|
|
170
|
+
},
|
|
171
|
+
center: {
|
|
172
|
+
flex: 1,
|
|
173
|
+
justifyContent: 'center',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
backgroundColor: '#FFFFFF',
|
|
176
|
+
},
|
|
177
|
+
noDataText: {
|
|
178
|
+
fontSize: 16,
|
|
179
|
+
color: '#6B7280',
|
|
180
|
+
},
|
|
181
|
+
errorText: {
|
|
182
|
+
fontSize: 16,
|
|
183
|
+
color: '#DC2626',
|
|
184
|
+
textAlign: 'center',
|
|
185
|
+
paddingHorizontal: 24,
|
|
186
|
+
},
|
|
187
|
+
routeTitleCard: {
|
|
188
|
+
margin: 16,
|
|
189
|
+
borderRadius: 16,
|
|
190
|
+
padding: 16,
|
|
191
|
+
backgroundColor: '#F3F4F6',
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
},
|
|
194
|
+
routeTitle: {
|
|
195
|
+
fontSize: 18,
|
|
196
|
+
fontWeight: '700',
|
|
197
|
+
color: '#111827',
|
|
198
|
+
},
|
|
199
|
+
routeSubtitle: {
|
|
200
|
+
marginTop: 6,
|
|
201
|
+
fontSize: 12,
|
|
202
|
+
color: '#6B7280',
|
|
203
|
+
},
|
|
204
|
+
card: {
|
|
205
|
+
marginHorizontal: 16,
|
|
206
|
+
marginBottom: 16,
|
|
207
|
+
borderRadius: 16,
|
|
208
|
+
padding: 16,
|
|
209
|
+
backgroundColor: '#FFFFFF',
|
|
210
|
+
borderWidth: 1,
|
|
211
|
+
borderColor: '#E5E7EB',
|
|
212
|
+
},
|
|
213
|
+
cardTitle: {
|
|
214
|
+
fontSize: 16,
|
|
215
|
+
fontWeight: '700',
|
|
216
|
+
color: '#111827',
|
|
217
|
+
marginBottom: 12,
|
|
218
|
+
},
|
|
219
|
+
detailRow: {
|
|
220
|
+
flexDirection: 'row',
|
|
221
|
+
justifyContent: 'space-between',
|
|
222
|
+
paddingVertical: 6,
|
|
223
|
+
},
|
|
224
|
+
detailLabel: {
|
|
225
|
+
fontSize: 14,
|
|
226
|
+
color: '#374151',
|
|
227
|
+
},
|
|
228
|
+
detailValue: {
|
|
229
|
+
fontSize: 14,
|
|
230
|
+
color: '#111827',
|
|
231
|
+
fontWeight: '600',
|
|
232
|
+
},
|
|
233
|
+
pickup: {
|
|
234
|
+
color: '#16A34A',
|
|
235
|
+
},
|
|
236
|
+
drop: {
|
|
237
|
+
color: '#DC2626',
|
|
238
|
+
},
|
|
239
|
+
driverContainer: {
|
|
240
|
+
flexDirection: 'row',
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
},
|
|
243
|
+
driverImageContainer: {
|
|
244
|
+
width: 56,
|
|
245
|
+
height: 56,
|
|
246
|
+
borderRadius: 28,
|
|
247
|
+
overflow: 'hidden',
|
|
248
|
+
backgroundColor: '#F3F4F6',
|
|
249
|
+
},
|
|
250
|
+
driverImage: {
|
|
251
|
+
width: 56,
|
|
252
|
+
height: 56,
|
|
253
|
+
},
|
|
254
|
+
driverImageFallback: {
|
|
255
|
+
width: 56,
|
|
256
|
+
height: 56,
|
|
257
|
+
backgroundColor: '#E5E7EB',
|
|
258
|
+
},
|
|
259
|
+
driverInfo: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
marginLeft: 12,
|
|
262
|
+
},
|
|
263
|
+
driverLabel: {
|
|
264
|
+
fontSize: 12,
|
|
265
|
+
color: '#6B7280',
|
|
266
|
+
},
|
|
267
|
+
driverName: {
|
|
268
|
+
fontSize: 14,
|
|
269
|
+
fontWeight: '700',
|
|
270
|
+
color: '#111827',
|
|
271
|
+
marginTop: 2,
|
|
272
|
+
},
|
|
273
|
+
callButton: {
|
|
274
|
+
paddingHorizontal: 12,
|
|
275
|
+
paddingVertical: 8,
|
|
276
|
+
borderRadius: 10,
|
|
277
|
+
backgroundColor: '#DCFCE7',
|
|
278
|
+
},
|
|
279
|
+
callText: {
|
|
280
|
+
color: '#16A34A',
|
|
281
|
+
fontWeight: '700',
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, {useMemo} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type ListRenderItem,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
export type TransportDashboardItem = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TransportDashboardProps = {
|
|
19
|
+
items?: TransportDashboardItem[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function TransportDashboard({items}: TransportDashboardProps) {
|
|
23
|
+
const data = useMemo(() => items ?? [], [items]);
|
|
24
|
+
|
|
25
|
+
const renderItem: ListRenderItem<TransportDashboardItem> = ({item}) => {
|
|
26
|
+
return (
|
|
27
|
+
<Pressable onPress={item.onPress} style={styles.card}>
|
|
28
|
+
<View style={styles.iconContainer}>{item.icon ?? null}</View>
|
|
29
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
30
|
+
</Pressable>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={styles.container}>
|
|
36
|
+
<FlatList
|
|
37
|
+
data={data}
|
|
38
|
+
renderItem={renderItem}
|
|
39
|
+
numColumns={2}
|
|
40
|
+
contentContainerStyle={styles.listContainer}
|
|
41
|
+
keyExtractor={item => item.id}
|
|
42
|
+
/>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
container: {
|
|
49
|
+
flex: 1,
|
|
50
|
+
backgroundColor: '#FFFFFF',
|
|
51
|
+
},
|
|
52
|
+
listContainer: {
|
|
53
|
+
padding: 16,
|
|
54
|
+
paddingTop: 30,
|
|
55
|
+
},
|
|
56
|
+
card: {
|
|
57
|
+
flex: 1,
|
|
58
|
+
backgroundColor: '#FFFFFF',
|
|
59
|
+
margin: 10,
|
|
60
|
+
borderRadius: 20,
|
|
61
|
+
paddingVertical: 24,
|
|
62
|
+
paddingHorizontal: 16,
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
shadowColor: '#000',
|
|
66
|
+
shadowOffset: {
|
|
67
|
+
width: 0,
|
|
68
|
+
height: 4,
|
|
69
|
+
},
|
|
70
|
+
shadowOpacity: 0.1,
|
|
71
|
+
shadowRadius: 8,
|
|
72
|
+
elevation: 5,
|
|
73
|
+
borderWidth: 1,
|
|
74
|
+
borderColor: '#F0F0F0',
|
|
75
|
+
},
|
|
76
|
+
iconContainer: {
|
|
77
|
+
width: 80,
|
|
78
|
+
height: 80,
|
|
79
|
+
backgroundColor: '#E3F2FD',
|
|
80
|
+
borderRadius: 40,
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
marginBottom: 16,
|
|
84
|
+
},
|
|
85
|
+
title: {
|
|
86
|
+
fontSize: 16,
|
|
87
|
+
fontWeight: '600',
|
|
88
|
+
color: '#111827',
|
|
89
|
+
textAlign: 'center',
|
|
90
|
+
letterSpacing: 0.5,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import type {AxiosError, AxiosRequestConfig} from 'axios';
|
|
3
|
+
import type {ApiClient, ApiError} from './types';
|
|
4
|
+
import type {
|
|
5
|
+
GetAuthToken,
|
|
6
|
+
RefreshAuthToken,
|
|
7
|
+
TransportHeaders,
|
|
8
|
+
} from '../../providers/types';
|
|
9
|
+
|
|
10
|
+
type CreateApiClientArgs = {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
authToken?: string;
|
|
13
|
+
getAuthToken?: GetAuthToken;
|
|
14
|
+
headers?: TransportHeaders;
|
|
15
|
+
refreshAuthToken?: RefreshAuthToken;
|
|
16
|
+
onApiError?: (error: ApiError) => void;
|
|
17
|
+
retry?: {
|
|
18
|
+
retries?: number;
|
|
19
|
+
retryDelayMs?: number;
|
|
20
|
+
retryOnStatuses?: number[];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type InternalAxiosRequestConfig = AxiosRequestConfig & {
|
|
25
|
+
__retryCount?: number;
|
|
26
|
+
__isRetryAfterRefresh?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function normalizeAxiosError(err: AxiosError): ApiError {
|
|
30
|
+
return {
|
|
31
|
+
message: err.message,
|
|
32
|
+
status: err.response?.status,
|
|
33
|
+
code: err.code,
|
|
34
|
+
url: err.config?.url,
|
|
35
|
+
method: err.config?.method,
|
|
36
|
+
data: err.response?.data,
|
|
37
|
+
raw: err,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function resolveToken(args: {
|
|
42
|
+
authToken?: string;
|
|
43
|
+
getAuthToken?: GetAuthToken;
|
|
44
|
+
}) {
|
|
45
|
+
if (args.authToken) return args.authToken;
|
|
46
|
+
if (!args.getAuthToken) return undefined;
|
|
47
|
+
const token = await args.getAuthToken();
|
|
48
|
+
return token ?? undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function delay(ms: number) {
|
|
52
|
+
return new Promise<void>(resolve => setTimeout(() => resolve(), ms));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createApiClient(args: CreateApiClientArgs): ApiClient {
|
|
56
|
+
const baseUrl = args.baseUrl.endsWith('/')
|
|
57
|
+
? args.baseUrl.slice(0, -1)
|
|
58
|
+
: args.baseUrl;
|
|
59
|
+
const instance = axios.create({
|
|
60
|
+
baseURL: baseUrl,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const retryConfig = {
|
|
64
|
+
retries: args.retry?.retries ?? 1,
|
|
65
|
+
retryDelayMs: args.retry?.retryDelayMs ?? 500,
|
|
66
|
+
retryOnStatuses: args.retry?.retryOnStatuses ?? [408, 429, 500, 502, 503, 504],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let refreshPromise: Promise<string | null | undefined> | null = null;
|
|
70
|
+
|
|
71
|
+
instance.interceptors.request.use(async (config: any) => {
|
|
72
|
+
const headers: Record<string, string> = {
|
|
73
|
+
...(args.headers ?? {}),
|
|
74
|
+
...(config.headers as Record<string, string> | undefined),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const hasAuthorizationHeader =
|
|
78
|
+
typeof headers.Authorization === 'string' && headers.Authorization.length > 0;
|
|
79
|
+
|
|
80
|
+
if (!hasAuthorizationHeader) {
|
|
81
|
+
const token = await resolveToken({
|
|
82
|
+
authToken: args.authToken,
|
|
83
|
+
getAuthToken: args.getAuthToken,
|
|
84
|
+
});
|
|
85
|
+
if (token) {
|
|
86
|
+
headers.Authorization = token;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
config.headers = headers as any;
|
|
91
|
+
return config;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
instance.interceptors.response.use(
|
|
95
|
+
res => res,
|
|
96
|
+
async (error: AxiosError) => {
|
|
97
|
+
const normalized = normalizeAxiosError(error);
|
|
98
|
+
const config = (error.config ?? {}) as InternalAxiosRequestConfig;
|
|
99
|
+
const status = error.response?.status;
|
|
100
|
+
|
|
101
|
+
const shouldRefresh =
|
|
102
|
+
status === 401 && !!args.refreshAuthToken && !config.__isRetryAfterRefresh;
|
|
103
|
+
|
|
104
|
+
if (shouldRefresh && args.refreshAuthToken) {
|
|
105
|
+
const currentToken = await resolveToken({
|
|
106
|
+
authToken: args.authToken,
|
|
107
|
+
getAuthToken: args.getAuthToken,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!refreshPromise) {
|
|
111
|
+
refreshPromise = args.refreshAuthToken({
|
|
112
|
+
failedRequest: {
|
|
113
|
+
url: config.url,
|
|
114
|
+
method: config.method,
|
|
115
|
+
status,
|
|
116
|
+
},
|
|
117
|
+
currentToken: currentToken ?? null,
|
|
118
|
+
}).finally(() => {
|
|
119
|
+
refreshPromise = null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newToken = await refreshPromise;
|
|
124
|
+
if (newToken) {
|
|
125
|
+
config.__isRetryAfterRefresh = true;
|
|
126
|
+
config.headers = {
|
|
127
|
+
...(config.headers as Record<string, string> | undefined),
|
|
128
|
+
Authorization: newToken,
|
|
129
|
+
};
|
|
130
|
+
return instance.request(config);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const shouldRetry =
|
|
135
|
+
(status && retryConfig.retryOnStatuses.includes(status)) ||
|
|
136
|
+
(!status && Boolean(error.code));
|
|
137
|
+
|
|
138
|
+
if (shouldRetry) {
|
|
139
|
+
config.__retryCount = (config.__retryCount ?? 0) + 1;
|
|
140
|
+
if (config.__retryCount <= retryConfig.retries) {
|
|
141
|
+
await delay(retryConfig.retryDelayMs);
|
|
142
|
+
return instance.request(config);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
args.onApiError?.(normalized);
|
|
147
|
+
return Promise.reject(error);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
instance,
|
|
153
|
+
request<T = unknown>(config: AxiosRequestConfig) {
|
|
154
|
+
return instance.request<T>(config);
|
|
155
|
+
},
|
|
156
|
+
async get<T = unknown>(url: string, config?: AxiosRequestConfig) {
|
|
157
|
+
const res = await instance.get<T>(url, config);
|
|
158
|
+
return res.data;
|
|
159
|
+
},
|
|
160
|
+
async post<T = unknown>(
|
|
161
|
+
url: string,
|
|
162
|
+
data?: unknown,
|
|
163
|
+
config?: AxiosRequestConfig,
|
|
164
|
+
) {
|
|
165
|
+
const res = await instance.post<T>(url, data, config);
|
|
166
|
+
return res.data;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AxiosError,
|
|
3
|
+
AxiosInstance,
|
|
4
|
+
AxiosRequestConfig,
|
|
5
|
+
AxiosResponse,
|
|
6
|
+
} from 'axios';
|
|
7
|
+
|
|
8
|
+
export type ApiRequestConfig = AxiosRequestConfig;
|
|
9
|
+
|
|
10
|
+
export type ApiError = {
|
|
11
|
+
message: string;
|
|
12
|
+
status?: number;
|
|
13
|
+
code?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
method?: string;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
raw?: AxiosError;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ApiClient = {
|
|
21
|
+
instance: AxiosInstance;
|
|
22
|
+
request<T = unknown>(config: ApiRequestConfig): Promise<AxiosResponse<T>>;
|
|
23
|
+
get<T = unknown>(url: string, config?: ApiRequestConfig): Promise<T>;
|
|
24
|
+
post<T = unknown>(
|
|
25
|
+
url: string,
|
|
26
|
+
data?: unknown,
|
|
27
|
+
config?: ApiRequestConfig,
|
|
28
|
+
): Promise<T>;
|
|
29
|
+
};
|
|
30
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {ApiClient} from '../api/types';
|
|
2
|
+
import type {StudentRouteDetails} from '../../types/transport';
|
|
3
|
+
import type {ApiEnvelope} from './types';
|
|
4
|
+
|
|
5
|
+
export async function getStudentRouteDetails(api: ApiClient) {
|
|
6
|
+
return api.get<ApiEnvelope<StudentRouteDetails[]>>(
|
|
7
|
+
'/student/studentAPP/TransportDetails/studentRoutedetails',
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type StudentRouteDetails = {
|
|
2
|
+
route_title?: string;
|
|
3
|
+
vehicle_no?: string;
|
|
4
|
+
vehicle_model?: string;
|
|
5
|
+
stop_name?: string;
|
|
6
|
+
pickup_time?: string;
|
|
7
|
+
drop_time?: string;
|
|
8
|
+
driver?: string;
|
|
9
|
+
driver_image?: string;
|
|
10
|
+
contact_number?: string | number;
|
|
11
|
+
};
|
|
12
|
+
|