@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.
- package/App.tsx +69 -0
- package/README.md +97 -0
- package/app.json +4 -0
- package/babel.config.js +18 -0
- package/global.css +143 -0
- package/index.js +9 -0
- package/metro.config.js +209 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +106 -0
- package/patches/@react-navigation+stack+7.6.16.patch +11 -0
- package/patches/@suflon+native-ui+0.0.18.patch +26020 -0
- package/patches/react-native+0.83.1.patch +52 -0
- package/react-native.config.js +27 -0
- package/scripts/fix-suflon-native-ui.js +25 -0
- package/scripts/link-react-native-pnpm.js +42 -0
- package/src/config/index.ts +10 -0
- package/src/context/ConnectionI18nContext.tsx +61 -0
- package/src/modules/Reporting/component/README.md +3 -0
- package/src/modules/Reporting/component/ReportChart.tsx +882 -0
- package/src/modules/Reporting/component/ReportFilterModal.tsx +403 -0
- package/src/modules/Reporting/component/ReportingDetail.tsx +481 -0
- package/src/modules/Reporting/index.tsx +239 -0
- package/src/modules/Reporting/utils.tsx +14 -0
- package/src/navigation/index.tsx +31 -0
- package/src/screens/ConnectionListScreen.tsx +471 -0
- package/src/screens/DevToolsCorner.tsx +810 -0
- package/src/screens/NewConnectionModal.tsx +282 -0
- package/src/services/ApiService.ts +22 -0
- package/src/services/ConnectionService.ts +81 -0
- package/src/services/api.ts +83 -0
- package/src/stores/connection.store.ts +3 -0
- package/src/stores/language.store.ts +54 -0
- package/src/theme/colors.ts +56 -0
- package/src/types/connection.ts +69 -0
- package/src/utils/AsyncStorageUtils.ts +56 -0
- package/src/utils/connectionStrings.ts +158 -0
- package/src/utils/errorMessage.ts +11 -0
- package/tailwind.config.js +196 -0
- 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;
|