@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.
@@ -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
+ };