@suflon/rnmd-reporting 0.0.1

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.
Files changed (39) hide show
  1. package/App.tsx +69 -0
  2. package/README.md +97 -0
  3. package/app.json +4 -0
  4. package/babel.config.js +18 -0
  5. package/global.css +143 -0
  6. package/index.js +9 -0
  7. package/metro.config.js +209 -0
  8. package/nativewind-env.d.ts +1 -0
  9. package/package.json +106 -0
  10. package/patches/@react-navigation+stack+7.6.16.patch +11 -0
  11. package/patches/@suflon+native-ui+0.0.18.patch +26020 -0
  12. package/patches/react-native+0.83.1.patch +52 -0
  13. package/react-native.config.js +27 -0
  14. package/scripts/fix-suflon-native-ui.js +25 -0
  15. package/scripts/link-react-native-pnpm.js +42 -0
  16. package/src/config/index.ts +10 -0
  17. package/src/context/ConnectionI18nContext.tsx +61 -0
  18. package/src/modules/Reporting/component/README.md +3 -0
  19. package/src/modules/Reporting/component/ReportChart.tsx +882 -0
  20. package/src/modules/Reporting/component/ReportFilterModal.tsx +403 -0
  21. package/src/modules/Reporting/component/ReportingDetail.tsx +481 -0
  22. package/src/modules/Reporting/index.tsx +239 -0
  23. package/src/modules/Reporting/utils.tsx +14 -0
  24. package/src/navigation/index.tsx +31 -0
  25. package/src/screens/ConnectionListScreen.tsx +471 -0
  26. package/src/screens/DevToolsCorner.tsx +810 -0
  27. package/src/screens/NewConnectionModal.tsx +282 -0
  28. package/src/services/ApiService.ts +22 -0
  29. package/src/services/ConnectionService.ts +81 -0
  30. package/src/services/api.ts +83 -0
  31. package/src/stores/connection.store.ts +3 -0
  32. package/src/stores/language.store.ts +54 -0
  33. package/src/theme/colors.ts +56 -0
  34. package/src/types/connection.ts +69 -0
  35. package/src/utils/AsyncStorageUtils.ts +56 -0
  36. package/src/utils/connectionStrings.ts +158 -0
  37. package/src/utils/errorMessage.ts +11 -0
  38. package/tailwind.config.js +196 -0
  39. package/tsconfig.json +25 -0
@@ -0,0 +1,481 @@
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { ScrollView, View, Text, TouchableOpacity, SafeAreaView, useColorScheme } from 'react-native';
3
+ import { useRoute, useNavigation } from '@react-navigation/native';
4
+ import { useReportStore, IReport, Header, SearchFilter, Loader, Icon } from '@suflon/native-ui';
5
+ import { themeColors } from '@/theme/colors';
6
+ import ReportChart from './ReportChart';
7
+ import ReportFilterModal from './ReportFilterModal';
8
+
9
+ const formatKeyLabel = (key: string) => {
10
+ return key
11
+ .replace(/_/g, ' ')
12
+ .replace(/\b\w/g, (char) => char.toUpperCase());
13
+ };
14
+
15
+ const ReportingDetail = () => {
16
+ const navigation = useNavigation<any>();
17
+ const route = useRoute<any>();
18
+ const report = route.params?.report as IReport;
19
+ const colorScheme = useColorScheme();
20
+ const isDarkMode = colorScheme === 'dark';
21
+
22
+ const { reportMetadata, loading, getReportMetadata, executeReport, reportResults } = useReportStore();
23
+
24
+ const [isLocalLoading, setIsLocalLoading] = useState(true);
25
+ const [activeReportId, setActiveReportId] = useState<string | number | null>(null);
26
+
27
+ const isDynamic = report?.report_type === 'SUMMARY' || report?.is_dynamic;
28
+
29
+ // Detect report_type from backend response metadata or item prop
30
+ const rawReportType = (reportMetadata?.metadata?.meta?.report_type || report?.report_type || '').toLowerCase();
31
+ const isChartReport = rawReportType === 'chart' || rawReportType === 'summary';
32
+ const isRowTableReport = rawReportType === 'row_table' || rawReportType === 'listing';
33
+
34
+ const [viewTab, setViewTab] = useState<'card' | 'table' | 'chart'>(isChartReport ? 'chart' : 'card');
35
+ const [filterModalVisible, setFilterModalVisible] = useState(false);
36
+ const [searchQuery, setSearchQuery] = useState('');
37
+ const [expandedCardId, setExpandedCardId] = useState<string | number | null>(null);
38
+
39
+ // Dynamic tabs based on report_type from backend response
40
+ const VIEW_TABS = useMemo(() => {
41
+ if (isChartReport) {
42
+ return [
43
+ { key: 'chart' as const, label: 'Chart View', icon: 'bar-chart-2' },
44
+ ];
45
+ }
46
+ return [
47
+ { key: 'card' as const, label: 'Card View', icon: 'grid' },
48
+ { key: 'table' as const, label: 'Table Grid', icon: 'list' },
49
+ ];
50
+ }, [isChartReport]);
51
+
52
+ // Keep viewTab valid if report_type changes
53
+ useEffect(() => {
54
+ if (isChartReport && viewTab !== 'chart') {
55
+ setViewTab('chart');
56
+ } else if (!isChartReport && viewTab === 'chart') {
57
+ setViewTab('card');
58
+ }
59
+ }, [isChartReport, viewTab]);
60
+
61
+ useEffect(() => {
62
+ if (report?.id) {
63
+ setIsLocalLoading(true);
64
+ getReportMetadata(report.id).then((meta) => {
65
+ const isMetaDynamic = Boolean(
66
+ meta?.metadata?.meta?.is_dynamic === true ||
67
+ report?.is_dynamic === true
68
+ );
69
+
70
+ const filterBy = meta?.metadata?.filter_by || [];
71
+ const dateFilter = filterBy.find((f: any) => f.key === 'date_range');
72
+
73
+ const todayStr = new Date().toISOString().split('T')[0];
74
+ const threeMonthsAgoStr = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
75
+
76
+ const fromDate = dateFilter?.defaultValue?.from_date || dateFilter?.defaultValue?.from || threeMonthsAgoStr;
77
+ const toDate = dateFilter?.defaultValue?.to_date || dateFilter?.defaultValue?.to || todayStr;
78
+
79
+ const dateRange = {
80
+ from_date: fromDate,
81
+ to_date: toDate
82
+ };
83
+
84
+ const displayByList = meta?.metadata?.display_by || [];
85
+ const defaultDisplay = displayByList.find((d: any) => d.isDefault) || displayByList[0];
86
+ const defaultDisplayKey = meta?.metadata?.meta?.default_display_key || defaultDisplay?.key || '';
87
+
88
+ const opByList = meta?.metadata?.operation_by || [];
89
+ const defaultOp = opByList.find((o: any) => o.isDefault) || opByList[0];
90
+
91
+ const initialPayload: any = {};
92
+
93
+ if (isMetaDynamic) {
94
+ // Dynamic reports -> filters is a LIST / ARRAY
95
+ initialPayload.filters = [
96
+ {
97
+ key: 'date_range',
98
+ value: dateRange
99
+ }
100
+ ];
101
+
102
+ if (defaultDisplayKey) {
103
+ initialPayload.display_by = defaultDisplayKey;
104
+ }
105
+ if (defaultOp) {
106
+ initialPayload.operation_by = {
107
+ key: defaultOp.key,
108
+ operation: defaultOp.operation,
109
+ label: defaultOp.label || `${defaultOp.operation} of ${defaultOp.key}`
110
+ };
111
+ }
112
+ } else {
113
+ // Static reports -> filters is a DICTIONARY / OBJECT
114
+ initialPayload.filters = {
115
+ date_range: dateRange
116
+ };
117
+ }
118
+
119
+ executeReport(report.id, isMetaDynamic, initialPayload)
120
+ .then(() => {
121
+ setActiveReportId(report.id);
122
+ })
123
+ .finally(() => {
124
+ setIsLocalLoading(false);
125
+ });
126
+ }).catch(() => {
127
+ setIsLocalLoading(false);
128
+ });
129
+ }
130
+ }, [report?.id, getReportMetadata, executeReport, isDynamic]);
131
+
132
+ const handleApplyFilters = (payload: any) => {
133
+ setFilterModalVisible(false);
134
+ if (report?.id) {
135
+ setIsLocalLoading(true);
136
+ const isMetaDynamic = Boolean(
137
+ reportMetadata?.metadata?.meta?.is_dynamic === true ||
138
+ report?.is_dynamic === true
139
+ );
140
+ executeReport(report.id, isMetaDynamic, payload).finally(() => {
141
+ setIsLocalLoading(false);
142
+ });
143
+ }
144
+ };
145
+
146
+ const records = useMemo(() => {
147
+ if (!reportResults) return [];
148
+ if (Array.isArray(reportResults)) return reportResults;
149
+ if (Array.isArray((reportResults as any)?.data)) return (reportResults as any).data;
150
+ return [];
151
+ }, [reportResults]);
152
+
153
+ const hiddenColumns = useMemo(() => {
154
+ const cols = reportMetadata?.metadata?.meta?.hidden_columns;
155
+ if (Array.isArray(cols)) return cols;
156
+ return ['id', 'region_id', 'company_id', 'dept_id', 'product_id', 'inventory_id', 'staff_id', 'patient_id'];
157
+ }, [reportMetadata]);
158
+
159
+ const visibleKeys = useMemo(() => {
160
+ if (records.length === 0) return [];
161
+ const firstRow = records[0];
162
+ return Object.keys(firstRow).filter((k) => !hiddenColumns.includes(k));
163
+ }, [records, hiddenColumns]);
164
+
165
+ const filteredRecords = useMemo(() => {
166
+ if (!searchQuery) return records;
167
+ return records.filter((rec: any) => {
168
+ const str = JSON.stringify(rec).toLowerCase();
169
+ return str.includes(searchQuery.toLowerCase());
170
+ });
171
+ }, [records, searchQuery]);
172
+
173
+ const chartData = useMemo(() => {
174
+ if (records.length === 0) return [];
175
+
176
+ const firstRow = records[0];
177
+ const statusKeys = ['completed', 'cancelled', 'rescheduled', 'noshow', 'upcoming', 'scheduled', 'pending'];
178
+ const foundStatusKeys = Object.keys(firstRow).filter(k => statusKeys.includes(k.toLowerCase()));
179
+
180
+ // If dataset has status breakdown columns, aggregate & render status-based graph bars
181
+ if (foundStatusKeys.length > 0) {
182
+ const statusTotals: Record<string, number> = {};
183
+ foundStatusKeys.forEach(key => {
184
+ statusTotals[key] = records.reduce((sum: number, r: any) => sum + (Number(r[key]) || 0), 0);
185
+ });
186
+
187
+ return Object.entries(statusTotals).map(([k, val]) => ({
188
+ label: formatKeyLabel(k),
189
+ value: val,
190
+ }));
191
+ }
192
+
193
+ // Default chart data mapping
194
+ return records.map((item: any, idx: number) => {
195
+ const keys = Object.keys(item);
196
+ const labelKey = keys.find(k => ['group_key', 'label', 'name', 'title', 'diagnosis_title', 'status', 'date'].includes(k)) || keys[0] || `Item ${idx + 1}`;
197
+ const valueKey = keys.find(k => ['metric_value', 'value', 'count', 'total', 'fees', 'amount', 'net_amount', 'balance_amount'].includes(k)) || keys[1] || keys[0];
198
+
199
+ return {
200
+ label: String(item[labelKey] ?? `Item ${idx + 1}`),
201
+ value: Number(item[valueKey]) || 0,
202
+ };
203
+ });
204
+ }, [records]);
205
+
206
+ const activeFilters = useMemo(() => {
207
+ const filters: string[] = [];
208
+ const filterList = reportMetadata?.metadata?.filter_by || [];
209
+ filterList.forEach((f: any) => {
210
+ if (f.defaultValue) {
211
+ if (typeof f.defaultValue === 'object' && (f.defaultValue.from_date || f.defaultValue.from)) {
212
+ const from = f.defaultValue.from_date || f.defaultValue.from;
213
+ const to = f.defaultValue.to_date || f.defaultValue.to;
214
+ filters.push(`${from} - ${to}`);
215
+ } else if (typeof f.defaultValue === 'string') {
216
+ filters.push(f.defaultValue);
217
+ }
218
+ }
219
+ });
220
+ return filters.length > 0 ? filters : ['All Filters'];
221
+ }, [reportMetadata]);
222
+
223
+ return (
224
+ <SafeAreaView className="flex-1 bg-slate-50 dark:bg-background-lightBlack">
225
+ {/* Suflon Header Component */}
226
+ <Header
227
+ title={report?.name || reportMetadata?.report_name || 'Report Detail'}
228
+ showBackButton={true}
229
+ onBackPress={() => navigation.goBack()}
230
+ actionIconName="filter"
231
+ actionIconType="Feather"
232
+ actionIconSize={18}
233
+ actionIconColor="#FFFFFF"
234
+ onActionPress={() => setFilterModalVisible(true)}
235
+ showActionIcon={true}
236
+ />
237
+
238
+ {/* Top View Mode Switcher Pills - Hidden in Chart View mode */}
239
+ {!isChartReport && (
240
+ <View className="flex-row bg-white dark:bg-background-darkSecondaryBg px-4 py-2.5 gap-2 border-b border-slate-100 dark:border-gray-800">
241
+ {VIEW_TABS.map((tab) => {
242
+ const isActive = viewTab === tab.key;
243
+ return (
244
+ <TouchableOpacity
245
+ key={tab.key}
246
+ onPress={() => setViewTab(tab.key)}
247
+ style={{
248
+ backgroundColor: isActive ? themeColors.brand[600] : (isDarkMode ? '#2C2C2E' : '#F1F5F9'),
249
+ }}
250
+ className="flex-1 flex-row items-center justify-center py-2.5 rounded-xl gap-1.5"
251
+ >
252
+ <Icon
253
+ name={tab.icon}
254
+ type="Feather"
255
+ size={14}
256
+ color={isActive ? '#FFFFFF' : (isDarkMode ? '#FFFFFF' : themeColors.slate[600])}
257
+ />
258
+ <Text
259
+ style={{ color: isActive ? '#FFFFFF' : (isDarkMode ? '#FFFFFF' : '#334155') }}
260
+ className="text-xs font-extrabold"
261
+ >
262
+ {tab.label}
263
+ </Text>
264
+ </TouchableOpacity>
265
+ );
266
+ })}
267
+ </View>
268
+ )}
269
+
270
+ {/* Show Full Screen / Container Loader while fetching data */}
271
+ {isLocalLoading || loading || activeReportId !== report?.id ? (
272
+ <View className="flex-1 justify-center items-center bg-slate-50 dark:bg-background-lightBlack">
273
+ <Loader message="Fetching report data..." color={themeColors.brand[600]} />
274
+ </View>
275
+ ) : (
276
+ <ScrollView
277
+ className="flex-1"
278
+ showsVerticalScrollIndicator={false}
279
+ contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 14, paddingBottom: 130 }}
280
+ >
281
+ {/* Active Filter Banner */}
282
+ <View className="bg-white dark:bg-background-darkSecondaryBg px-3 py-2.5 rounded-2xl border border-slate-200 dark:border-gray-800 flex-row items-center justify-between mb-3">
283
+ <View className="flex-row items-center gap-1.5 flex-wrap flex-1">
284
+ <Text className="text-[9px] font-extrabold text-slate-400 dark:text-gray-400 tracking-wider">
285
+ ACTIVE FILTER:
286
+ </Text>
287
+ {activeFilters.map((f, i) => (
288
+ <View key={i} className="bg-slate-100 dark:bg-gray-800 px-2 py-0.5 rounded-md">
289
+ <Text className="text-[10px] font-bold text-slate-700 dark:text-gray-100">
290
+ {f}
291
+ </Text>
292
+ </View>
293
+ ))}
294
+ </View>
295
+
296
+ <TouchableOpacity onPress={() => setFilterModalVisible(true)} className="flex-row items-center gap-1">
297
+ <Icon name="edit-3" type="Feather" size={12} color={themeColors.brand[600]} />
298
+ <Text className="text-[11px] font-extrabold text-violet-600 dark:text-violet-400">
299
+ Edit
300
+ </Text>
301
+ </TouchableOpacity>
302
+ </View>
303
+
304
+ {/* Chart View Content */}
305
+ {viewTab === 'chart' ? (
306
+ <ReportChart
307
+ data={chartData}
308
+ records={records}
309
+ visibleKeys={visibleKeys}
310
+ title={report?.name || reportMetadata?.report_name || 'Diagnosis Frequency Curve'}
311
+ subtitle="GRAPH VISUALIZATION"
312
+ isDarkMode={isDarkMode}
313
+ />
314
+ ) : (
315
+ <View className="gap-3">
316
+ {/* Search Component from Suflon Native UI */}
317
+ <SearchFilter
318
+ placeholder="Search records..."
319
+ value={searchQuery}
320
+ onChangeText={setSearchQuery}
321
+ />
322
+
323
+ {/* Mobile Cards Mode */}
324
+ {filteredRecords.length === 0 ? (
325
+ <View className="bg-white dark:bg-background-darkSecondaryBg p-6 rounded-2xl items-center mt-2.5 border border-slate-200 dark:border-gray-800">
326
+ <Text className="text-slate-500 dark:text-gray-300 text-xs font-semibold">No records found</Text>
327
+ </View>
328
+ ) : viewTab === 'card' ? (
329
+ filteredRecords.map((item: any, idx: number) => {
330
+ const cardId = item.txn_number || item.id || item.app_id || item.code || item.sysid || item.appointment_num || `#${idx + 1}`;
331
+ const isExpanded = expandedCardId === cardId;
332
+
333
+ const statusKey = visibleKeys.find(k => k.toLowerCase().includes('status')) || '';
334
+ const statusVal = statusKey ? String(item[statusKey] || '') : '';
335
+ const isSuccess = statusVal.toUpperCase().includes('COMPLETED') || statusVal.toUpperCase().includes('PAID') || statusVal.toUpperCase().includes('APPROVED');
336
+
337
+ const gridKeys = visibleKeys.filter(k => k !== statusKey).slice(0, 4);
338
+ const detailKeys = visibleKeys.filter(k => k !== statusKey && !gridKeys.includes(k));
339
+
340
+ return (
341
+ <View
342
+ key={idx}
343
+ className="bg-white dark:bg-background-darkSecondaryBg rounded-2xl p-3.5 border border-slate-200 dark:border-gray-800 gap-2.5"
344
+ >
345
+ {/* Card Top Header Row */}
346
+ <View className="flex-row justify-between items-center border-b border-slate-100 dark:border-gray-800 pb-2">
347
+ <View className="flex-row items-center gap-1.5">
348
+ <View className={`w-2 h-2 rounded-full ${isSuccess ? 'bg-emerald-500' : 'bg-amber-500'}`} />
349
+ <Text style={{ color: isDarkMode ? '#FFFFFF' : '#0F172A' }} className="text-xs font-extrabold">
350
+ {cardId}
351
+ </Text>
352
+ </View>
353
+
354
+ {statusVal ? (
355
+ <View className={`px-2 py-0.5 rounded-md ${isSuccess ? 'bg-emerald-100 dark:bg-emerald-950/50' : 'bg-amber-100 dark:bg-amber-950/50'}`}>
356
+ <Text className={`text-[9px] font-extrabold ${isSuccess ? 'text-emerald-800 dark:text-emerald-300' : 'text-amber-800 dark:text-amber-300'}`}>
357
+ {statusVal}
358
+ </Text>
359
+ </View>
360
+ ) : null}
361
+ </View>
362
+
363
+ {/* 2x2 Grid Dynamic Info */}
364
+ <View className="flex-row flex-wrap gap-2.5">
365
+ {gridKeys.map((key) => {
366
+ const rawVal = item[key];
367
+ const displayVal = (rawVal === null || rawVal === undefined || rawVal === '' || String(rawVal).trim() === '') ? '-' : String(rawVal);
368
+ return (
369
+ <View key={key} className="w-[47%]">
370
+ <Text style={{ color: isDarkMode ? '#9CA3AF' : '#64748B' }} className="text-[8px] font-extrabold tracking-wider">
371
+ {formatKeyLabel(key)}
372
+ </Text>
373
+ <Text style={{ color: isDarkMode ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold mt-0.5" numberOfLines={1}>
374
+ {displayVal}
375
+ </Text>
376
+ </View>
377
+ );
378
+ })}
379
+ </View>
380
+
381
+ {/* Collapsible Full Details */}
382
+ {detailKeys.length > 0 && (
383
+ <>
384
+ <TouchableOpacity
385
+ onPress={() => setExpandedCardId(isExpanded ? null : cardId)}
386
+ className="flex-row justify-between items-center pt-2 border-t border-slate-100 dark:border-gray-800"
387
+ >
388
+ <Text className="text-[10px] font-bold text-violet-600 dark:text-violet-400">
389
+ View Full Response Details
390
+ </Text>
391
+ <Icon
392
+ name={isExpanded ? "chevron-up" : "chevron-down"}
393
+ type="Feather"
394
+ size={14}
395
+ color={themeColors.brand[600]}
396
+ />
397
+ </TouchableOpacity>
398
+
399
+ {isExpanded && (
400
+ <View className="bg-slate-50 dark:bg-background-lightBlack p-2.5 rounded-xl gap-1.5 border border-slate-100 dark:border-gray-800">
401
+ {detailKeys.map((key) => {
402
+ const rawDetailVal = item[key];
403
+ const displayDetailVal = (rawDetailVal === null || rawDetailVal === undefined || rawDetailVal === '' || String(rawDetailVal).trim() === '') ? '-' : String(rawDetailVal);
404
+ return (
405
+ <View key={key} className="flex-row justify-between">
406
+ <Text style={{ color: isDarkMode ? '#9CA3AF' : '#64748B' }} className="text-[10px] font-semibold">{formatKeyLabel(key)}:</Text>
407
+ <Text style={{ color: isDarkMode ? '#FFFFFF' : '#0F172A' }} className="text-[10px] font-bold">
408
+ {displayDetailVal}
409
+ </Text>
410
+ </View>
411
+ );
412
+ })}
413
+ </View>
414
+ )}
415
+ </>
416
+ )}
417
+ </View>
418
+ );
419
+ })
420
+ ) : (
421
+ /* Dynamic Compact Grid Table Mode */
422
+ <View className="bg-white dark:bg-background-darkSecondaryBg rounded-2xl border border-slate-200 dark:border-gray-800 overflow-hidden">
423
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
424
+ <View>
425
+ <View className="flex-row bg-slate-50 dark:bg-background-lightBlack p-2.5 border-b border-slate-200 dark:border-gray-800">
426
+ {visibleKeys.map((key) => (
427
+ <Text key={key} style={{ color: isDarkMode ? '#E2E8F0' : '#475569' }} className="w-[110px] text-[10px] font-extrabold">
428
+ {formatKeyLabel(key).toUpperCase()}
429
+ </Text>
430
+ ))}
431
+ </View>
432
+ {filteredRecords.map((item: any, idx: number) => (
433
+ <View key={idx} className="flex-row p-2.5 border-b border-slate-100 dark:border-gray-800 items-center">
434
+ {visibleKeys.map((key) => {
435
+ const rawCellVal = item[key];
436
+ const displayCellVal = (rawCellVal === null || rawCellVal === undefined || rawCellVal === '' || String(rawCellVal).trim() === '') ? '-' : String(rawCellVal);
437
+ return (
438
+ <Text key={key} style={{ color: isDarkMode ? '#FFFFFF' : '#0F172A' }} className="w-[110px] text-[10px] font-semibold" numberOfLines={1}>
439
+ {displayCellVal}
440
+ </Text>
441
+ );
442
+ })}
443
+ </View>
444
+ ))}
445
+ </View>
446
+ </ScrollView>
447
+ </View>
448
+ )}
449
+
450
+ {/* Pagination Footer */}
451
+ <View className="flex-row justify-between items-center pt-2">
452
+ <Text className="text-[11px] text-slate-500 dark:text-gray-300">
453
+ Showing 1-{filteredRecords.length} of {filteredRecords.length} records
454
+ </Text>
455
+ <View className="flex-row items-center gap-1.5">
456
+ <TouchableOpacity className="w-6 h-6 rounded-md bg-white dark:bg-background-darkSecondaryBg border border-slate-200 dark:border-gray-800 items-center justify-center">
457
+ <Icon name="chevron-left" type="Feather" size={14} color={isDarkMode ? '#FFFFFF' : themeColors.slate[400]} />
458
+ </TouchableOpacity>
459
+ <Text className="text-[11px] font-extrabold text-slate-900 dark:text-white">1</Text>
460
+ <TouchableOpacity className="w-6 h-6 rounded-md bg-white dark:bg-background-darkSecondaryBg border border-slate-200 dark:border-gray-800 items-center justify-center">
461
+ <Icon name="chevron-right" type="Feather" size={14} color={isDarkMode ? '#FFFFFF' : themeColors.slate[400]} />
462
+ </TouchableOpacity>
463
+ </View>
464
+ </View>
465
+ </View>
466
+ )}
467
+ </ScrollView>
468
+ )}
469
+
470
+ {/* Slide-Up Filter Drawer Sheet */}
471
+ <ReportFilterModal
472
+ isVisible={filterModalVisible}
473
+ onClose={() => setFilterModalVisible(false)}
474
+ metadata={reportMetadata}
475
+ onApply={handleApplyFilters}
476
+ />
477
+ </SafeAreaView>
478
+ );
479
+ };
480
+
481
+ export default ReportingDetail;