@suflon/rnmd-reporting 0.0.3 → 0.0.4
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 +2 -2
- package/index.js +3 -0
- package/metro.config.js +31 -0
- package/package.json +4 -3
- package/patches/@suflon+native-ui+0.0.18.patch +1715 -4257
- package/src/index.ts +1 -0
- package/src/modules/ManagementReport/components/AnalyticsChartView.tsx +294 -0
- package/src/modules/ManagementReport/components/CategoryTabs.tsx +85 -0
- package/src/modules/ManagementReport/components/DetailsListingView.tsx +781 -0
- package/src/modules/ManagementReport/components/DoctorFilterDropdown.tsx +157 -0
- package/src/modules/ManagementReport/components/KpiMetricsGrid.tsx +183 -0
- package/src/modules/ManagementReport/components/ManagementFilterModal.tsx +142 -0
- package/src/modules/ManagementReport/components/ManagementReportHeader.tsx +115 -0
- package/src/modules/ManagementReport/components/SubModuleTabs.tsx +58 -0
- package/src/modules/ManagementReport/components/TimeframeDropdown.tsx +162 -0
- package/src/modules/ManagementReport/components/ViewModeSwitcher.tsx +71 -0
- package/src/modules/ManagementReport/index.tsx +202 -0
- package/src/modules/Reporting/component/ReportingDetail.tsx +85 -97
- package/src/modules/Reporting/index.tsx +71 -78
- package/src/navigation/index.tsx +15 -4
- package/src/screens/DevToolsCorner.tsx +43 -8
- package/tailwind.config.js +11 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
useColorScheme,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import {
|
|
11
|
+
IManagementReportDetail,
|
|
12
|
+
Icon,
|
|
13
|
+
useManagementReportStore,
|
|
14
|
+
} from '@suflon/native-ui';
|
|
15
|
+
import { themeColors } from '@/theme/colors';
|
|
16
|
+
import { TimeframeDropdown, TimeframeOption, getTimeframeDateFilter } from './TimeframeDropdown';
|
|
17
|
+
|
|
18
|
+
interface DetailsListingViewProps {
|
|
19
|
+
details: IManagementReportDetail[];
|
|
20
|
+
executionData: Record<number, any[]>;
|
|
21
|
+
searchQuery?: string;
|
|
22
|
+
doctorFilter?: string;
|
|
23
|
+
loading?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IGNORED_KEYS = new Set([
|
|
27
|
+
'raw',
|
|
28
|
+
'created_at',
|
|
29
|
+
'updated_at',
|
|
30
|
+
'modified_by',
|
|
31
|
+
'tenant_id',
|
|
32
|
+
'company_id',
|
|
33
|
+
'region_id',
|
|
34
|
+
'counter',
|
|
35
|
+
'currency',
|
|
36
|
+
'source',
|
|
37
|
+
'is_reschedule',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function formatColumnTitle(key: string): string {
|
|
41
|
+
const customMap: Record<string, string> = {
|
|
42
|
+
plan_num: 'Plan #',
|
|
43
|
+
plan_type: 'Plan Type',
|
|
44
|
+
txn_number: 'Txn No',
|
|
45
|
+
txn_date: 'Date',
|
|
46
|
+
prescription_num: 'Presc #',
|
|
47
|
+
prescription_date: 'Date',
|
|
48
|
+
appointment_date: 'Date',
|
|
49
|
+
schedule_date: 'Schedule Date',
|
|
50
|
+
actual_date: 'Actual Date',
|
|
51
|
+
start_date: 'Start Date',
|
|
52
|
+
end_date: 'End Date',
|
|
53
|
+
appointment_type: 'Mode',
|
|
54
|
+
service_type: 'Service',
|
|
55
|
+
total_amount: 'Total Amt',
|
|
56
|
+
return_amount: 'Return Amt',
|
|
57
|
+
refunded_amount: 'Refund',
|
|
58
|
+
balance_amount: 'Balance',
|
|
59
|
+
discount_amount: 'Discount',
|
|
60
|
+
tax_amount: 'Tax',
|
|
61
|
+
mrp: 'MRP',
|
|
62
|
+
fees: 'Fee',
|
|
63
|
+
payment_status: 'Payment',
|
|
64
|
+
approval_status: 'Approval',
|
|
65
|
+
notes: 'Notes',
|
|
66
|
+
patient: 'Patient',
|
|
67
|
+
staff: 'Doctor / Staff',
|
|
68
|
+
status: 'Status',
|
|
69
|
+
severity: 'Severity',
|
|
70
|
+
title: 'Title / Description',
|
|
71
|
+
number: 'Ref #',
|
|
72
|
+
phone: 'Phone',
|
|
73
|
+
total: 'Total',
|
|
74
|
+
completed: 'Completed',
|
|
75
|
+
cancelled: 'Cancelled',
|
|
76
|
+
upcoming: 'Upcoming',
|
|
77
|
+
overdue: 'Overdue',
|
|
78
|
+
count: 'Count',
|
|
79
|
+
quantity: 'Qty',
|
|
80
|
+
qty: 'Qty',
|
|
81
|
+
price: 'Price',
|
|
82
|
+
rate: 'Rate',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (customMap[key]) return customMap[key];
|
|
86
|
+
|
|
87
|
+
return key
|
|
88
|
+
.replace(/_/g, ' ')
|
|
89
|
+
.replace(/([A-Z])/g, ' $1')
|
|
90
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
91
|
+
.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isAmountField(key: string): boolean {
|
|
95
|
+
const k = key.toLowerCase();
|
|
96
|
+
return (
|
|
97
|
+
k === 'mrp' ||
|
|
98
|
+
k === 'fees' ||
|
|
99
|
+
k === 'fee' ||
|
|
100
|
+
k.includes('amount') ||
|
|
101
|
+
k.includes('tax') ||
|
|
102
|
+
k.includes('discount') ||
|
|
103
|
+
k.includes('refund') ||
|
|
104
|
+
k.includes('balance') ||
|
|
105
|
+
k.includes('price') ||
|
|
106
|
+
k.includes('cost') ||
|
|
107
|
+
(k.includes('total_') && !k.includes('count'))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isCountField(key: string): boolean {
|
|
112
|
+
const k = key.toLowerCase();
|
|
113
|
+
return (
|
|
114
|
+
k === 'total' ||
|
|
115
|
+
k === 'completed' ||
|
|
116
|
+
k === 'cancelled' ||
|
|
117
|
+
k === 'upcoming' ||
|
|
118
|
+
k === 'overdue' ||
|
|
119
|
+
k.includes('count') ||
|
|
120
|
+
k === 'quantity' ||
|
|
121
|
+
k === 'qty'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isStatusField(key: string): boolean {
|
|
126
|
+
const k = key.toLowerCase();
|
|
127
|
+
return (
|
|
128
|
+
k === 'status' ||
|
|
129
|
+
k === 'severity' ||
|
|
130
|
+
k === 'payment_status' ||
|
|
131
|
+
k === 'approval_status' ||
|
|
132
|
+
k === 'state'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isIdField(key: string): boolean {
|
|
137
|
+
const k = key.toLowerCase();
|
|
138
|
+
return (
|
|
139
|
+
k === 'plan_num' ||
|
|
140
|
+
k === 'txn_number' ||
|
|
141
|
+
k === 'prescription_num' ||
|
|
142
|
+
k === 'number' ||
|
|
143
|
+
k === 'invoice_num' ||
|
|
144
|
+
k === 'bill_num' ||
|
|
145
|
+
k === 'order_num' ||
|
|
146
|
+
k === 'id' ||
|
|
147
|
+
k === 'code'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getColumnWidth(key: string): number {
|
|
152
|
+
if (isIdField(key)) return 80;
|
|
153
|
+
if (isStatusField(key)) return 86;
|
|
154
|
+
if (isCountField(key)) return 68;
|
|
155
|
+
if (isAmountField(key)) return 84;
|
|
156
|
+
if (key.includes('date')) return 86;
|
|
157
|
+
if (key.includes('time')) return 70;
|
|
158
|
+
if (key === 'plan_type' || key === 'service_type' || key === 'appointment_type') return 90;
|
|
159
|
+
if (key === 'title' || key === 'diagnosis' || key === 'category') return 130;
|
|
160
|
+
if (key === 'patient' || key === 'customer_name' || key === 'name') return 115;
|
|
161
|
+
if (key === 'staff' || key === 'doctor' || key === 'consultant') return 115;
|
|
162
|
+
if (key === 'notes') return 95;
|
|
163
|
+
if (key === 'phone') return 82;
|
|
164
|
+
return 85;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const StatusBadge: React.FC<{ value: string }> = ({ value }) => {
|
|
168
|
+
const isDark = useColorScheme() === 'dark';
|
|
169
|
+
if (!value || value === '-') {
|
|
170
|
+
return <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-medium">-</Text>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const vLower = value.toLowerCase();
|
|
174
|
+
const isSuccess = ['completed', 'paid', 'approved', 'active', 'safe', 'mild'].includes(vLower);
|
|
175
|
+
const isWarning = ['upcoming', 'scheduled', 'pending', 'in_progress', 'moderate'].includes(vLower);
|
|
176
|
+
const isDanger = ['cancelled', 'rejected', 'expired', 'severe', 'high', 'overdue'].includes(vLower);
|
|
177
|
+
|
|
178
|
+
const badgeClass = isSuccess
|
|
179
|
+
? 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800'
|
|
180
|
+
: isWarning
|
|
181
|
+
? 'bg-violet-50 dark:bg-violet-950/40 text-violet-700 dark:text-violet-300 border-violet-200 dark:border-violet-800'
|
|
182
|
+
: isDanger
|
|
183
|
+
? 'bg-rose-50 dark:bg-rose-950/40 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800'
|
|
184
|
+
: 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-700';
|
|
185
|
+
|
|
186
|
+
const badgeTextColor = isSuccess
|
|
187
|
+
? (isDark ? '#6EE7B7' : '#047857')
|
|
188
|
+
: isWarning
|
|
189
|
+
? (isDark ? '#C4B5FD' : '#6D28D9')
|
|
190
|
+
: isDanger
|
|
191
|
+
? (isDark ? '#FDA4AF' : '#BE123C')
|
|
192
|
+
: (isDark ? '#E5E7EB' : '#334155');
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<View className={`px-1.5 py-0.5 rounded-lg self-start border ${badgeClass}`}>
|
|
196
|
+
<Text style={{ color: badgeTextColor }} className="text-[9px] font-bold uppercase" numberOfLines={1}>
|
|
197
|
+
{value}
|
|
198
|
+
</Text>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const SingleReportTableCard: React.FC<{
|
|
204
|
+
detail: IManagementReportDetail;
|
|
205
|
+
rows: any[];
|
|
206
|
+
searchQuery: string;
|
|
207
|
+
doctorFilter: string;
|
|
208
|
+
loading: boolean;
|
|
209
|
+
}> = ({
|
|
210
|
+
detail,
|
|
211
|
+
rows,
|
|
212
|
+
searchQuery,
|
|
213
|
+
doctorFilter,
|
|
214
|
+
loading,
|
|
215
|
+
}) => {
|
|
216
|
+
const isDark = useColorScheme() === 'dark';
|
|
217
|
+
const [timeframe, setTimeframe] = useState<TimeframeOption>('Monthly');
|
|
218
|
+
const [layoutMode, setLayoutMode] = useState<'table' | 'card'>('table');
|
|
219
|
+
const [statusFilter, setStatusFilter] = useState<string>('ALL');
|
|
220
|
+
const [fetching, setFetching] = useState(false);
|
|
221
|
+
const PAGE_SIZE = 10;
|
|
222
|
+
const [currentPage, setCurrentPage] = useState<number>(1);
|
|
223
|
+
|
|
224
|
+
const executeReportItem = useManagementReportStore((s) => s.executeReportItem);
|
|
225
|
+
|
|
226
|
+
const handleSelectTimeframe = useCallback(
|
|
227
|
+
async (selectedTf: TimeframeOption) => {
|
|
228
|
+
setTimeframe(selectedTf);
|
|
229
|
+
if (detail.report_id) {
|
|
230
|
+
setFetching(true);
|
|
231
|
+
const dateFilter = getTimeframeDateFilter(selectedTf);
|
|
232
|
+
const state = useManagementReportStore.getState();
|
|
233
|
+
const origin = (state.selectedReport?.mgmt_report_type || state.activeCategory || 'opd').toLowerCase();
|
|
234
|
+
await executeReportItem(
|
|
235
|
+
detail.report_id,
|
|
236
|
+
detail.report?.is_dynamic || false,
|
|
237
|
+
{ filters: dateFilter },
|
|
238
|
+
origin,
|
|
239
|
+
true
|
|
240
|
+
);
|
|
241
|
+
setFetching(false);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[detail, executeReportItem]
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const validRows = useMemo(() => {
|
|
248
|
+
if (!Array.isArray(rows)) return [];
|
|
249
|
+
return rows.filter((r) => r && typeof r === 'object');
|
|
250
|
+
}, [rows]);
|
|
251
|
+
|
|
252
|
+
const dynamicColumns = useMemo(() => {
|
|
253
|
+
if (validRows.length === 0) return [];
|
|
254
|
+
const keysSet = new Set<string>();
|
|
255
|
+
|
|
256
|
+
validRows.forEach((r) => {
|
|
257
|
+
Object.keys(r).forEach((k) => {
|
|
258
|
+
if (!IGNORED_KEYS.has(k) && r[k] !== undefined) {
|
|
259
|
+
keysSet.add(k);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const allKeys = Array.from(keysSet);
|
|
265
|
+
|
|
266
|
+
return allKeys.sort((a, b) => {
|
|
267
|
+
const getPriority = (k: string) => {
|
|
268
|
+
if (isIdField(k)) return 1;
|
|
269
|
+
if (k.includes('date')) return 2;
|
|
270
|
+
if (k === 'staff' || k === 'doctor' || k === 'consultant') return 3;
|
|
271
|
+
if (k === 'patient' || k === 'customer_name' || k === 'vendor_name' || k === 'name') return 4;
|
|
272
|
+
if (k === 'title' || k === 'diagnosis' || k === 'plan_type' || k === 'service_type' || k === 'category') return 5;
|
|
273
|
+
if (k.includes('time')) return 6;
|
|
274
|
+
if (isAmountField(k)) return 7;
|
|
275
|
+
if (isCountField(k)) return 8;
|
|
276
|
+
if (isStatusField(k)) return 9;
|
|
277
|
+
return 10;
|
|
278
|
+
};
|
|
279
|
+
return getPriority(a) - getPriority(b);
|
|
280
|
+
});
|
|
281
|
+
}, [validRows]);
|
|
282
|
+
|
|
283
|
+
const availableStatuses = useMemo(() => {
|
|
284
|
+
const set = new Set<string>();
|
|
285
|
+
validRows.forEach((r) => {
|
|
286
|
+
const st = r.status || r.severity || r.payment_status || r.plan_type;
|
|
287
|
+
if (st && st !== '-' && typeof st === 'string') {
|
|
288
|
+
set.add(st);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return Array.from(set);
|
|
292
|
+
}, [validRows]);
|
|
293
|
+
|
|
294
|
+
const filteredRows = useMemo(() => {
|
|
295
|
+
const q = searchQuery.toLowerCase().trim();
|
|
296
|
+
const doc = doctorFilter && doctorFilter !== 'ALL' ? doctorFilter.toLowerCase().trim() : '';
|
|
297
|
+
|
|
298
|
+
return validRows.filter((r) => {
|
|
299
|
+
const matchSearch =
|
|
300
|
+
!q ||
|
|
301
|
+
Object.values(r).some((v) =>
|
|
302
|
+
typeof v === 'string' || typeof v === 'number'
|
|
303
|
+
? String(v).toLowerCase().includes(q)
|
|
304
|
+
: false
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const staffVal = (r.staff || r.doctor || r.consultant || '').toLowerCase();
|
|
308
|
+
const matchDoc = !doc || staffVal.includes(doc);
|
|
309
|
+
|
|
310
|
+
const stVal = (r.status || r.severity || r.payment_status || r.plan_type || '').toLowerCase();
|
|
311
|
+
const matchStatus =
|
|
312
|
+
statusFilter === 'ALL' || stVal === statusFilter.toLowerCase();
|
|
313
|
+
|
|
314
|
+
return matchSearch && matchDoc && matchStatus;
|
|
315
|
+
});
|
|
316
|
+
}, [validRows, searchQuery, doctorFilter, statusFilter]);
|
|
317
|
+
|
|
318
|
+
React.useEffect(() => {
|
|
319
|
+
setCurrentPage(1);
|
|
320
|
+
}, [searchQuery, doctorFilter, statusFilter]);
|
|
321
|
+
|
|
322
|
+
const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE));
|
|
323
|
+
const startIdx = (currentPage - 1) * PAGE_SIZE;
|
|
324
|
+
const endIdx = Math.min(startIdx + PAGE_SIZE, filteredRows.length);
|
|
325
|
+
const displayedRows = useMemo(() => {
|
|
326
|
+
return filteredRows.slice(startIdx, endIdx);
|
|
327
|
+
}, [filteredRows, startIdx, endIdx]);
|
|
328
|
+
|
|
329
|
+
const totalTableWidth = useMemo(() => {
|
|
330
|
+
const colsWidth = dynamicColumns.reduce((sum, col) => sum + getColumnWidth(col), 0);
|
|
331
|
+
return Math.max(colsWidth + 24, 380);
|
|
332
|
+
}, [dynamicColumns]);
|
|
333
|
+
|
|
334
|
+
const isBusy = loading || fetching;
|
|
335
|
+
const title = detail.report?.name || detail.report_num || 'Records Listing';
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<View className="p-3.5 rounded-3xl bg-white dark:bg-background-darkSecondaryBg border border-slate-100 dark:border-slate-800 mb-3.5">
|
|
339
|
+
{/* Header: Table Title & Action Controls */}
|
|
340
|
+
<View className="flex-row items-center justify-between pb-2.5 border-b border-slate-100 dark:border-slate-800">
|
|
341
|
+
<View className="flex-1 mr-2">
|
|
342
|
+
<Text
|
|
343
|
+
style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
|
|
344
|
+
className="text-[14px] font-extrabold tracking-tight leading-tight"
|
|
345
|
+
numberOfLines={1}
|
|
346
|
+
>
|
|
347
|
+
{title}
|
|
348
|
+
</Text>
|
|
349
|
+
<Text
|
|
350
|
+
style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }}
|
|
351
|
+
className="text-[10px] font-medium mt-0.5"
|
|
352
|
+
numberOfLines={1}
|
|
353
|
+
>
|
|
354
|
+
{filteredRows.length} total records ({timeframe})
|
|
355
|
+
</Text>
|
|
356
|
+
</View>
|
|
357
|
+
|
|
358
|
+
{/* Right Action Controls: Timeframe Dropdown + Interactive Table/Cards Switcher */}
|
|
359
|
+
<View className="flex-row items-center gap-1.5 shrink-0">
|
|
360
|
+
<TimeframeDropdown
|
|
361
|
+
value={timeframe}
|
|
362
|
+
onChange={handleSelectTimeframe}
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
{/* Interactive Table / Cards Pill Switcher */}
|
|
366
|
+
{filteredRows.length > 0 && (
|
|
367
|
+
<View className="p-0.5 rounded-xl bg-slate-100 dark:bg-slate-800 flex-row items-center">
|
|
368
|
+
<TouchableOpacity
|
|
369
|
+
onPress={() => setLayoutMode('table')}
|
|
370
|
+
activeOpacity={0.7}
|
|
371
|
+
hitSlop={{ top: 6, bottom: 6, left: 6, right: 4 }}
|
|
372
|
+
style={{
|
|
373
|
+
backgroundColor: layoutMode === 'table' ? (isDark ? themeColors.slate[700] : themeColors.white) : 'transparent',
|
|
374
|
+
}}
|
|
375
|
+
className="px-2 py-1 rounded-lg flex-row items-center gap-1"
|
|
376
|
+
>
|
|
377
|
+
<Icon
|
|
378
|
+
name="align-justify"
|
|
379
|
+
type="Feather"
|
|
380
|
+
size={10}
|
|
381
|
+
color={layoutMode === 'table' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
|
|
382
|
+
/>
|
|
383
|
+
<Text
|
|
384
|
+
style={{
|
|
385
|
+
color: layoutMode === 'table' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8'),
|
|
386
|
+
fontWeight: layoutMode === 'table' ? '700' : '500',
|
|
387
|
+
}}
|
|
388
|
+
className="text-[11px]"
|
|
389
|
+
>
|
|
390
|
+
Table
|
|
391
|
+
</Text>
|
|
392
|
+
</TouchableOpacity>
|
|
393
|
+
|
|
394
|
+
<TouchableOpacity
|
|
395
|
+
onPress={() => setLayoutMode('card')}
|
|
396
|
+
activeOpacity={0.7}
|
|
397
|
+
hitSlop={{ top: 6, bottom: 6, left: 4, right: 6 }}
|
|
398
|
+
style={{
|
|
399
|
+
backgroundColor: layoutMode === 'card' ? (isDark ? themeColors.slate[700] : themeColors.white) : 'transparent',
|
|
400
|
+
}}
|
|
401
|
+
className="px-2 py-1 rounded-lg flex-row items-center gap-1"
|
|
402
|
+
>
|
|
403
|
+
<Icon
|
|
404
|
+
name="grid"
|
|
405
|
+
type="Feather"
|
|
406
|
+
size={10}
|
|
407
|
+
color={layoutMode === 'card' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
|
|
408
|
+
/>
|
|
409
|
+
<Text
|
|
410
|
+
style={{
|
|
411
|
+
color: layoutMode === 'card' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8'),
|
|
412
|
+
fontWeight: layoutMode === 'card' ? '700' : '500',
|
|
413
|
+
}}
|
|
414
|
+
className="text-[11px]"
|
|
415
|
+
>
|
|
416
|
+
Cards
|
|
417
|
+
</Text>
|
|
418
|
+
</TouchableOpacity>
|
|
419
|
+
</View>
|
|
420
|
+
)}
|
|
421
|
+
</View>
|
|
422
|
+
</View>
|
|
423
|
+
|
|
424
|
+
{/* Dynamic Status Quick Filter Chips */}
|
|
425
|
+
{availableStatuses.length > 1 && (
|
|
426
|
+
<ScrollView
|
|
427
|
+
horizontal
|
|
428
|
+
showsHorizontalScrollIndicator={false}
|
|
429
|
+
contentContainerStyle={{ gap: 6 }}
|
|
430
|
+
className="pt-2 pb-1"
|
|
431
|
+
>
|
|
432
|
+
<TouchableOpacity
|
|
433
|
+
onPress={() => setStatusFilter('ALL')}
|
|
434
|
+
activeOpacity={0.8}
|
|
435
|
+
style={{
|
|
436
|
+
backgroundColor: statusFilter === 'ALL' ? '#7c3aed' : (isDark ? themeColors.slate[800] : themeColors.slate[100]),
|
|
437
|
+
borderColor: statusFilter === 'ALL' ? '#7c3aed' : (isDark ? themeColors.slate[700] : themeColors.slate[200]),
|
|
438
|
+
}}
|
|
439
|
+
className="px-2.5 py-1 rounded-xl border"
|
|
440
|
+
>
|
|
441
|
+
<Text
|
|
442
|
+
style={{ color: statusFilter === 'ALL' ? '#FFFFFF' : (isDark ? '#9CA3AF' : '#64748b') }}
|
|
443
|
+
className="text-[10px] font-bold"
|
|
444
|
+
>
|
|
445
|
+
All ({validRows.length})
|
|
446
|
+
</Text>
|
|
447
|
+
</TouchableOpacity>
|
|
448
|
+
|
|
449
|
+
{availableStatuses.map((st) => {
|
|
450
|
+
const isActive = statusFilter === st;
|
|
451
|
+
const count = validRows.filter(
|
|
452
|
+
(r) => (r.status || r.severity || r.payment_status || r.plan_type) === st
|
|
453
|
+
).length;
|
|
454
|
+
return (
|
|
455
|
+
<TouchableOpacity
|
|
456
|
+
key={st}
|
|
457
|
+
onPress={() => setStatusFilter(st)}
|
|
458
|
+
activeOpacity={0.8}
|
|
459
|
+
style={{
|
|
460
|
+
backgroundColor: isActive ? '#7c3aed' : (isDark ? themeColors.slate[800] : themeColors.slate[100]),
|
|
461
|
+
borderColor: isActive ? '#7c3aed' : (isDark ? themeColors.slate[700] : themeColors.slate[200]),
|
|
462
|
+
}}
|
|
463
|
+
className="px-2.5 py-1 rounded-xl border"
|
|
464
|
+
>
|
|
465
|
+
<Text
|
|
466
|
+
style={{ color: isActive ? '#FFFFFF' : (isDark ? '#9CA3AF' : '#64748b') }}
|
|
467
|
+
className="text-[10px] font-bold capitalize"
|
|
468
|
+
>
|
|
469
|
+
{st} ({count})
|
|
470
|
+
</Text>
|
|
471
|
+
</TouchableOpacity>
|
|
472
|
+
);
|
|
473
|
+
})}
|
|
474
|
+
</ScrollView>
|
|
475
|
+
)}
|
|
476
|
+
|
|
477
|
+
{/* Main Content Layout: Table vs Cards */}
|
|
478
|
+
{isBusy ? (
|
|
479
|
+
<View className="py-10 items-center justify-center">
|
|
480
|
+
<ActivityIndicator size="small" color="#7c3aed" />
|
|
481
|
+
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold mt-2">
|
|
482
|
+
Loading {timeframe} records...
|
|
483
|
+
</Text>
|
|
484
|
+
</View>
|
|
485
|
+
) : filteredRows.length === 0 ? (
|
|
486
|
+
<View className="py-7 px-4 items-center justify-center rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-background-lightBlack mt-1">
|
|
487
|
+
<Icon name="inbox" type="Feather" size={18} color="#94a3b8" />
|
|
488
|
+
<Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold mt-1">
|
|
489
|
+
No records for {timeframe}
|
|
490
|
+
</Text>
|
|
491
|
+
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] text-center mt-0.5">
|
|
492
|
+
{searchQuery ? 'No results matched your search query.' : 'Try selecting Monthly or adjusting filters.'}
|
|
493
|
+
</Text>
|
|
494
|
+
</View>
|
|
495
|
+
) : layoutMode === 'table' ? (
|
|
496
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={true} className="mt-1">
|
|
497
|
+
<View
|
|
498
|
+
style={{ minWidth: totalTableWidth }}
|
|
499
|
+
className="overflow-hidden"
|
|
500
|
+
>
|
|
501
|
+
{/* Dynamic Table Header */}
|
|
502
|
+
<View className="flex-row py-2 px-2.5 border-b border-slate-100 dark:border-slate-800/80">
|
|
503
|
+
{dynamicColumns.map((colKey) => (
|
|
504
|
+
<Text
|
|
505
|
+
key={colKey}
|
|
506
|
+
style={{
|
|
507
|
+
width: getColumnWidth(colKey),
|
|
508
|
+
color: isDark ? '#9CA3AF' : '#94A3B8',
|
|
509
|
+
}}
|
|
510
|
+
className="text-[9px] uppercase font-bold pr-1.5"
|
|
511
|
+
numberOfLines={1}
|
|
512
|
+
>
|
|
513
|
+
{formatColumnTitle(colKey)}
|
|
514
|
+
</Text>
|
|
515
|
+
))}
|
|
516
|
+
</View>
|
|
517
|
+
|
|
518
|
+
{/* Dynamic Table Rows */}
|
|
519
|
+
{displayedRows.map((row, rowIdx) => (
|
|
520
|
+
<View
|
|
521
|
+
key={rowIdx}
|
|
522
|
+
className="flex-row items-center py-2.5 px-2.5 border-b border-slate-100 dark:border-slate-800/60"
|
|
523
|
+
>
|
|
524
|
+
{dynamicColumns.map((colKey) => {
|
|
525
|
+
const val = row[colKey];
|
|
526
|
+
const width = getColumnWidth(colKey);
|
|
527
|
+
|
|
528
|
+
if (isStatusField(colKey)) {
|
|
529
|
+
return (
|
|
530
|
+
<View key={colKey} style={{ width }} className="pr-1.5 justify-center">
|
|
531
|
+
<StatusBadge value={String(val || '-')} />
|
|
532
|
+
</View>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (isAmountField(colKey)) {
|
|
537
|
+
const formatted =
|
|
538
|
+
val != null && val !== '' && !isNaN(Number(val))
|
|
539
|
+
? `₹${Number(val).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`
|
|
540
|
+
: String(val || '-');
|
|
541
|
+
return (
|
|
542
|
+
<Text
|
|
543
|
+
key={colKey}
|
|
544
|
+
style={{
|
|
545
|
+
width,
|
|
546
|
+
color: isDark ? '#FFFFFF' : '#0F172A',
|
|
547
|
+
}}
|
|
548
|
+
className="text-[11px] font-bold pr-1.5 font-mono"
|
|
549
|
+
numberOfLines={1}
|
|
550
|
+
>
|
|
551
|
+
{formatted}
|
|
552
|
+
</Text>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (isCountField(colKey)) {
|
|
557
|
+
const isOverdue = colKey === 'overdue' && Number(val) > 0;
|
|
558
|
+
const isCompleted = colKey === 'completed' && Number(val) > 0;
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<Text
|
|
562
|
+
key={colKey}
|
|
563
|
+
style={{
|
|
564
|
+
width,
|
|
565
|
+
color: isOverdue
|
|
566
|
+
? (isDark ? '#FDA4AF' : '#E11D48')
|
|
567
|
+
: isCompleted
|
|
568
|
+
? (isDark ? '#6EE7B7' : '#059669')
|
|
569
|
+
: (isDark ? '#E5E7EB' : '#334155'),
|
|
570
|
+
}}
|
|
571
|
+
className="text-xs font-bold pr-1.5"
|
|
572
|
+
numberOfLines={1}
|
|
573
|
+
>
|
|
574
|
+
{val !== undefined && val !== null ? String(val) : '-'}
|
|
575
|
+
</Text>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (isIdField(colKey)) {
|
|
580
|
+
return (
|
|
581
|
+
<Text
|
|
582
|
+
key={colKey}
|
|
583
|
+
style={{ width }}
|
|
584
|
+
className="text-[10px] font-mono font-bold text-violet-600 dark:text-violet-400 pr-1.5"
|
|
585
|
+
numberOfLines={1}
|
|
586
|
+
>
|
|
587
|
+
{String(val || `#${startIdx + rowIdx + 1}`)}
|
|
588
|
+
</Text>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const isSecondaryText =
|
|
593
|
+
colKey.includes('date') || colKey === 'staff' || colKey === 'doctor' || colKey === 'notes';
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<Text
|
|
597
|
+
key={colKey}
|
|
598
|
+
style={{
|
|
599
|
+
width,
|
|
600
|
+
color: isSecondaryText
|
|
601
|
+
? (isDark ? '#9CA3AF' : '#94A3B8')
|
|
602
|
+
: (isDark ? '#E2E8F0' : '#1E293B'),
|
|
603
|
+
}}
|
|
604
|
+
className="text-xs pr-1.5 font-medium"
|
|
605
|
+
numberOfLines={1}
|
|
606
|
+
>
|
|
607
|
+
{val === '' || val === null || val === undefined ? '-' : String(val)}
|
|
608
|
+
</Text>
|
|
609
|
+
);
|
|
610
|
+
})}
|
|
611
|
+
</View>
|
|
612
|
+
))}
|
|
613
|
+
</View>
|
|
614
|
+
</ScrollView>
|
|
615
|
+
) : (
|
|
616
|
+
/* Theme-Aligned Dynamic Cards Layout */
|
|
617
|
+
<View className="space-y-2 mt-2">
|
|
618
|
+
{displayedRows.map((row, rowIdx) => {
|
|
619
|
+
const idKey = dynamicColumns.find(isIdField);
|
|
620
|
+
const statusKey = dynamicColumns.find(isStatusField);
|
|
621
|
+
const amountKey = dynamicColumns.find(isAmountField);
|
|
622
|
+
const titleKey = dynamicColumns.find(
|
|
623
|
+
(k) => k === 'patient' || k === 'customer_name' || k === 'title' || k === 'name' || k === 'plan_type'
|
|
624
|
+
);
|
|
625
|
+
const otherKeys = dynamicColumns.filter(
|
|
626
|
+
(k) => k !== idKey && k !== statusKey && k !== amountKey && k !== titleKey
|
|
627
|
+
).slice(0, 4);
|
|
628
|
+
|
|
629
|
+
return (
|
|
630
|
+
<View
|
|
631
|
+
key={rowIdx}
|
|
632
|
+
className="p-3 rounded-2xl bg-white dark:bg-slate-800 border border-slate-100 dark:border-slate-700/70 space-y-2"
|
|
633
|
+
>
|
|
634
|
+
<View className="flex-row items-center justify-between">
|
|
635
|
+
<View className="flex-row items-center gap-2">
|
|
636
|
+
{idKey && (
|
|
637
|
+
<Text className="text-[10px] font-mono font-bold text-violet-600 dark:text-violet-400">
|
|
638
|
+
{String(row[idKey] || `#${startIdx + rowIdx + 1}`)}
|
|
639
|
+
</Text>
|
|
640
|
+
)}
|
|
641
|
+
{titleKey && (
|
|
642
|
+
<Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold" numberOfLines={1}>
|
|
643
|
+
{row[titleKey] ? String(row[titleKey]) : '-'}
|
|
644
|
+
</Text>
|
|
645
|
+
)}
|
|
646
|
+
</View>
|
|
647
|
+
{statusKey && <StatusBadge value={String(row[statusKey] || '-')} />}
|
|
648
|
+
</View>
|
|
649
|
+
|
|
650
|
+
<View className="flex-row flex-wrap items-center justify-between pt-1.5 border-t border-slate-100 dark:border-slate-700/60 gap-y-1.5">
|
|
651
|
+
{otherKeys.map((k) => (
|
|
652
|
+
<View key={k} className="min-w-[45%]">
|
|
653
|
+
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[9px]">{formatColumnTitle(k)}</Text>
|
|
654
|
+
<Text style={{ color: isDark ? '#E2E8F0' : '#1E293B' }} className="text-xs font-semibold mt-0.5">
|
|
655
|
+
{row[k] !== undefined && row[k] !== null && row[k] !== '' ? String(row[k]) : '-'}
|
|
656
|
+
</Text>
|
|
657
|
+
</View>
|
|
658
|
+
))}
|
|
659
|
+
{amountKey && (
|
|
660
|
+
<View className="min-w-[45%] items-end">
|
|
661
|
+
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[9px]">{formatColumnTitle(amountKey)}</Text>
|
|
662
|
+
<Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold mt-0.5">
|
|
663
|
+
{row[amountKey] != null && !isNaN(Number(row[amountKey]))
|
|
664
|
+
? `₹${Number(row[amountKey]).toLocaleString()}`
|
|
665
|
+
: String(row[amountKey] || '-')}
|
|
666
|
+
</Text>
|
|
667
|
+
</View>
|
|
668
|
+
)}
|
|
669
|
+
</View>
|
|
670
|
+
</View>
|
|
671
|
+
);
|
|
672
|
+
})}
|
|
673
|
+
</View>
|
|
674
|
+
)}
|
|
675
|
+
|
|
676
|
+
{/* Bottom Clean Pagination Bar */}
|
|
677
|
+
{filteredRows.length > 0 && (
|
|
678
|
+
<View className="flex-row items-center justify-between pt-2.5 mt-2 border-t border-slate-100 dark:border-slate-800">
|
|
679
|
+
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-medium">
|
|
680
|
+
Showing {startIdx + 1}-{endIdx} of {filteredRows.length}
|
|
681
|
+
</Text>
|
|
682
|
+
|
|
683
|
+
<View className="flex-row items-center gap-1">
|
|
684
|
+
<TouchableOpacity
|
|
685
|
+
onPress={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
686
|
+
disabled={currentPage <= 1}
|
|
687
|
+
activeOpacity={0.8}
|
|
688
|
+
className={`w-6 h-6 rounded-lg items-center justify-center border ${
|
|
689
|
+
currentPage > 1
|
|
690
|
+
? 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700'
|
|
691
|
+
: 'bg-slate-50 dark:bg-slate-900 border-slate-100 dark:border-slate-800 opacity-40'
|
|
692
|
+
}`}
|
|
693
|
+
>
|
|
694
|
+
<Icon name="chevron-left" type="Feather" size={11} color={currentPage > 1 ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')} />
|
|
695
|
+
</TouchableOpacity>
|
|
696
|
+
|
|
697
|
+
<Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-[10px] font-bold px-1">
|
|
698
|
+
{currentPage} / {totalPages}
|
|
699
|
+
</Text>
|
|
700
|
+
|
|
701
|
+
<TouchableOpacity
|
|
702
|
+
onPress={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
703
|
+
disabled={currentPage >= totalPages}
|
|
704
|
+
activeOpacity={0.8}
|
|
705
|
+
className={`w-6 h-6 rounded-lg items-center justify-center border ${
|
|
706
|
+
currentPage < totalPages
|
|
707
|
+
? 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700'
|
|
708
|
+
: 'bg-slate-50 dark:bg-slate-900 border-slate-100 dark:border-slate-800 opacity-40'
|
|
709
|
+
}`}
|
|
710
|
+
>
|
|
711
|
+
<Icon name="chevron-right" type="Feather" size={11} color={currentPage < totalPages ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')} />
|
|
712
|
+
</TouchableOpacity>
|
|
713
|
+
</View>
|
|
714
|
+
</View>
|
|
715
|
+
)}
|
|
716
|
+
</View>
|
|
717
|
+
);
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
export const DetailsListingView: React.FC<DetailsListingViewProps> = ({
|
|
721
|
+
details,
|
|
722
|
+
executionData,
|
|
723
|
+
searchQuery = '',
|
|
724
|
+
doctorFilter = 'ALL',
|
|
725
|
+
loading = false,
|
|
726
|
+
}) => {
|
|
727
|
+
const listingDetails = useMemo(() => {
|
|
728
|
+
let target = details.filter(
|
|
729
|
+
(d) =>
|
|
730
|
+
d.display_size === 'LG' ||
|
|
731
|
+
d.report?.report_type === 'LISTING' ||
|
|
732
|
+
d.report?.report_type === 'TABLE'
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (target.length === 0) {
|
|
736
|
+
target = details.filter(
|
|
737
|
+
(d) =>
|
|
738
|
+
d.display_size !== 'XS' &&
|
|
739
|
+
d.display_size !== 'SM' &&
|
|
740
|
+
d.report?.report_type !== 'CARD' &&
|
|
741
|
+
d.report?.report_type !== 'SUMMARY'
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (target.length === 0) {
|
|
746
|
+
target = details.filter((d) => {
|
|
747
|
+
const rows = executionData[d.report_id];
|
|
748
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (target.length === 0 && details.length > 0) {
|
|
753
|
+
target = details.filter((d) => d.report?.report_type !== 'CARD' && d.display_size !== 'XS');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return target;
|
|
757
|
+
}, [details, executionData]);
|
|
758
|
+
|
|
759
|
+
if (listingDetails.length === 0) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return (
|
|
764
|
+
<View className="space-y-3.5">
|
|
765
|
+
{listingDetails.map((d) => {
|
|
766
|
+
const rows = executionData[d.report_id] || [];
|
|
767
|
+
|
|
768
|
+
return (
|
|
769
|
+
<SingleReportTableCard
|
|
770
|
+
key={d.id || d.report_id}
|
|
771
|
+
detail={d}
|
|
772
|
+
rows={rows}
|
|
773
|
+
searchQuery={searchQuery}
|
|
774
|
+
doctorFilter={doctorFilter}
|
|
775
|
+
loading={loading}
|
|
776
|
+
/>
|
|
777
|
+
);
|
|
778
|
+
})}
|
|
779
|
+
</View>
|
|
780
|
+
);
|
|
781
|
+
};
|