@suflon/rnmd-reporting 0.0.4 → 0.0.6
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/package.json +1 -1
- package/src/modules/ManagementReport/components/AnalyticsChartView.tsx +288 -52
- package/src/modules/ManagementReport/components/DetailsListingView.tsx +2 -2
- package/src/modules/ManagementReport/components/DoctorFilterDropdown.tsx +100 -118
- package/src/modules/ManagementReport/components/KpiMetricsGrid.tsx +1 -1
- package/src/modules/ManagementReport/components/ManagementReportHeader.tsx +45 -42
- package/src/modules/ManagementReport/components/TimeframeDropdown.tsx +1 -1
- package/src/modules/ManagementReport/index.tsx +33 -2
- package/src/modules/Reporting/component/ReportChart.tsx +1 -1
- package/src/modules/Reporting/component/ReportFilterModal.tsx +1 -1
- package/src/modules/Reporting/component/ReportingDetail.tsx +18 -4
- package/src/modules/Reporting/index.tsx +1 -1
- package/src/navigation/index.tsx +3 -3
package/package.json
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
ActivityIndicator,
|
|
8
8
|
useColorScheme,
|
|
9
|
+
Dimensions,
|
|
9
10
|
} from 'react-native';
|
|
11
|
+
import Svg, { Path, Circle, Defs, LinearGradient, Stop, Line as SvgLine } from 'react-native-svg';
|
|
10
12
|
import {
|
|
11
13
|
IManagementReportDetail,
|
|
12
14
|
Icon,
|
|
@@ -25,6 +27,29 @@ interface ChartItemData {
|
|
|
25
27
|
value: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
function formatLabel(label: string): string {
|
|
31
|
+
if (!label) return '-';
|
|
32
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(label)) {
|
|
33
|
+
const parts = label.split('-');
|
|
34
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
35
|
+
const m = parseInt(parts[1], 10) - 1;
|
|
36
|
+
return `${parts[2]} ${months[m] || parts[1]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return label
|
|
40
|
+
.replace(/_/g, ' ')
|
|
41
|
+
.replace(/([A-Z])/g, ' $1')
|
|
42
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatYAxisValue(val: number): string {
|
|
47
|
+
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
|
|
48
|
+
if (val >= 1000) return `${(val / 1000).toFixed(1)}k`;
|
|
49
|
+
if (Number.isInteger(val)) return String(val);
|
|
50
|
+
return val.toFixed(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
function parseChartData(rows: any[]): ChartItemData[] {
|
|
29
54
|
if (!rows || rows.length === 0) return [];
|
|
30
55
|
|
|
@@ -48,6 +73,8 @@ function parseChartData(rows: any[]): ChartItemData[] {
|
|
|
48
73
|
'department',
|
|
49
74
|
'category',
|
|
50
75
|
'group_key',
|
|
76
|
+
'appointment_type',
|
|
77
|
+
'severity',
|
|
51
78
|
].includes(k.toLowerCase())
|
|
52
79
|
) || keys[0];
|
|
53
80
|
|
|
@@ -62,13 +89,14 @@ function parseChartData(rows: any[]): ChartItemData[] {
|
|
|
62
89
|
'net_amount',
|
|
63
90
|
'sum',
|
|
64
91
|
'metric_value',
|
|
92
|
+
'patients',
|
|
65
93
|
].includes(k.toLowerCase())
|
|
66
94
|
) || keys[1] || keys[0];
|
|
67
95
|
|
|
68
96
|
const val = Number(r[valueKey]) || 0;
|
|
69
|
-
const
|
|
97
|
+
const rawLbl = String(r[labelKey] || `Item ${i + 1}`);
|
|
70
98
|
|
|
71
|
-
return { label:
|
|
99
|
+
return { label: formatLabel(rawLbl), value: val };
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
return { label: `Item ${i + 1}`, value: 0 };
|
|
@@ -81,7 +109,7 @@ const SingleChartCard: React.FC<{
|
|
|
81
109
|
loading: boolean;
|
|
82
110
|
}> = ({ detail, rows, loading }) => {
|
|
83
111
|
const isDark = useColorScheme() === 'dark';
|
|
84
|
-
const [timeframe, setTimeframe] = useState<TimeframeOption>('
|
|
112
|
+
const [timeframe, setTimeframe] = useState<TimeframeOption>('Daily');
|
|
85
113
|
const [chartType, setChartType] = useState<'bar' | 'line' | 'trend'>('bar');
|
|
86
114
|
const [fetching, setFetching] = useState(false);
|
|
87
115
|
|
|
@@ -114,8 +142,49 @@ const SingleChartCard: React.FC<{
|
|
|
114
142
|
[chartData]
|
|
115
143
|
);
|
|
116
144
|
|
|
145
|
+
// Generate 4 evenly spaced Y-axis ticks
|
|
146
|
+
const yTicks = useMemo(() => {
|
|
147
|
+
return [
|
|
148
|
+
maxValue,
|
|
149
|
+
Math.round(maxValue * 0.66),
|
|
150
|
+
Math.round(maxValue * 0.33),
|
|
151
|
+
0,
|
|
152
|
+
];
|
|
153
|
+
}, [maxValue]);
|
|
154
|
+
|
|
117
155
|
const isBusy = loading || fetching;
|
|
118
156
|
const title = detail.report?.name || detail.report_num || 'Performance Metric';
|
|
157
|
+
const isCompact = chartData.length <= 4;
|
|
158
|
+
const chartHeight = 130;
|
|
159
|
+
const cardWidth = Dimensions.get('window').width - 90;
|
|
160
|
+
const svgWidth = Math.max(cardWidth, chartData.length * 64);
|
|
161
|
+
|
|
162
|
+
// SVG Line / Trend calculations
|
|
163
|
+
const svgPoints = useMemo(() => {
|
|
164
|
+
if (chartData.length === 0) return [];
|
|
165
|
+
const step = chartData.length > 1 ? svgWidth / (chartData.length - 1) : svgWidth / 2;
|
|
166
|
+
const paddingBottom = 15;
|
|
167
|
+
const paddingTop = 15;
|
|
168
|
+
const availableHeight = chartHeight - paddingTop - paddingBottom;
|
|
169
|
+
|
|
170
|
+
return chartData.map((d, i) => {
|
|
171
|
+
const x = chartData.length === 1 ? svgWidth / 2 : i * step;
|
|
172
|
+
const y = chartHeight - paddingBottom - (d.value / maxValue) * availableHeight;
|
|
173
|
+
return { x, y, value: d.value, label: d.label };
|
|
174
|
+
});
|
|
175
|
+
}, [chartData, maxValue, svgWidth, chartHeight]);
|
|
176
|
+
|
|
177
|
+
const linePath = useMemo(() => {
|
|
178
|
+
if (svgPoints.length === 0) return '';
|
|
179
|
+
return svgPoints.reduce((acc, p, i) => `${acc} ${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`, '');
|
|
180
|
+
}, [svgPoints]);
|
|
181
|
+
|
|
182
|
+
const areaPath = useMemo(() => {
|
|
183
|
+
if (svgPoints.length === 0) return '';
|
|
184
|
+
const lastX = svgPoints[svgPoints.length - 1].x;
|
|
185
|
+
const firstX = svgPoints[0].x;
|
|
186
|
+
return `${linePath} L ${lastX},${chartHeight - 10} L ${firstX},${chartHeight - 10} Z`;
|
|
187
|
+
}, [linePath, svgPoints, chartHeight]);
|
|
119
188
|
|
|
120
189
|
return (
|
|
121
190
|
<View className="p-3.5 rounded-3xl bg-white dark:bg-background-darkSecondaryBg border border-slate-100 dark:border-slate-800 mb-3.5">
|
|
@@ -195,67 +264,234 @@ const SingleChartCard: React.FC<{
|
|
|
195
264
|
</Text>
|
|
196
265
|
</View>
|
|
197
266
|
) : (
|
|
198
|
-
<View className="pt-
|
|
199
|
-
{/*
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<View
|
|
211
|
-
key={idx}
|
|
212
|
-
className="items-center justify-end mr-3.5"
|
|
213
|
-
style={{ width: 44 }}
|
|
267
|
+
<View className="pt-1">
|
|
268
|
+
{/* Main Chart Body: Left Y-Axis Scale Column + Chart Canvas */}
|
|
269
|
+
<View className="flex-row">
|
|
270
|
+
{/* 1. Left Y-Axis Labels Column */}
|
|
271
|
+
<View className="w-9 justify-between items-end pr-1.5 pb-6" style={{ height: chartHeight + 24 }}>
|
|
272
|
+
{yTicks.map((tick, i) => (
|
|
273
|
+
<Text
|
|
274
|
+
key={i}
|
|
275
|
+
style={{ color: isDark ? '#64748B' : '#94A3B8' }}
|
|
276
|
+
className="text-[9px] font-mono font-medium text-right"
|
|
277
|
+
numberOfLines={1}
|
|
214
278
|
>
|
|
215
|
-
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
279
|
+
{formatYAxisValue(tick)}
|
|
280
|
+
</Text>
|
|
281
|
+
))}
|
|
282
|
+
</View>
|
|
283
|
+
|
|
284
|
+
{/* 2. Chart Grid & Plot Area */}
|
|
285
|
+
<View className="flex-1 relative">
|
|
286
|
+
{/* Background Horizontal Guide Lines */}
|
|
287
|
+
<View className="absolute inset-0 justify-between pb-6 pointer-events-none" style={{ height: chartHeight + 24 }}>
|
|
288
|
+
{yTicks.map((_, i) => (
|
|
225
289
|
<View
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
290
|
+
key={i}
|
|
291
|
+
style={{
|
|
292
|
+
height: 1,
|
|
293
|
+
backgroundColor: i === yTicks.length - 1
|
|
294
|
+
? (isDark ? '#475569' : '#cbd5e1')
|
|
295
|
+
: (isDark ? '#1e293b' : '#f1f5f9'),
|
|
296
|
+
}}
|
|
297
|
+
className="w-full"
|
|
232
298
|
/>
|
|
299
|
+
))}
|
|
300
|
+
</View>
|
|
301
|
+
|
|
302
|
+
{/* Chart Mode Rendering */}
|
|
303
|
+
{chartType === 'bar' ? (
|
|
304
|
+
isCompact ? (
|
|
305
|
+
/* Balanced Full-Width Grid for 2-4 items */
|
|
306
|
+
<View>
|
|
307
|
+
<View className="flex-row justify-around items-end px-1" style={{ height: chartHeight }}>
|
|
308
|
+
{chartData.map((item, idx) => {
|
|
309
|
+
const barHeight = Math.max(14, Math.round((item.value / maxValue) * (chartHeight - 30)));
|
|
310
|
+
const isTopValue = item.value === maxValue;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<View
|
|
314
|
+
key={idx}
|
|
315
|
+
className="items-center justify-end flex-1 max-w-[68px] px-1"
|
|
316
|
+
>
|
|
317
|
+
<Text
|
|
318
|
+
style={{
|
|
319
|
+
color: isTopValue ? (isDark ? '#A78BFA' : '#7C3AED') : (isDark ? '#E2E8F0' : '#475569'),
|
|
320
|
+
}}
|
|
321
|
+
className="text-[9px] font-extrabold mb-1 font-mono"
|
|
322
|
+
numberOfLines={1}
|
|
323
|
+
>
|
|
324
|
+
{formatYAxisValue(item.value)}
|
|
325
|
+
</Text>
|
|
233
326
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
327
|
+
<View
|
|
328
|
+
style={{ height: barHeight }}
|
|
329
|
+
className={`w-full rounded-t-xl ${
|
|
330
|
+
isTopValue
|
|
331
|
+
? 'bg-violet-600 dark:bg-violet-500'
|
|
332
|
+
: 'bg-violet-400/80 dark:bg-violet-600/60'
|
|
333
|
+
}`}
|
|
334
|
+
/>
|
|
335
|
+
</View>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</View>
|
|
339
|
+
|
|
340
|
+
{/* Continuous X-Axis Baseline */}
|
|
341
|
+
<View className="w-full h-[1.5px] bg-slate-300 dark:bg-slate-700" />
|
|
342
|
+
|
|
343
|
+
{/* Category Labels below Baseline */}
|
|
344
|
+
<View className="flex-row justify-around px-1 pt-1.5">
|
|
345
|
+
{chartData.map((item, idx) => (
|
|
346
|
+
<View key={idx} className="flex-1 max-w-[68px] items-center px-0.5">
|
|
347
|
+
<Text
|
|
348
|
+
style={{ color: isDark ? '#E2E8F0' : '#475569' }}
|
|
349
|
+
className="text-[9px] font-semibold text-center leading-tight"
|
|
350
|
+
numberOfLines={2}
|
|
351
|
+
>
|
|
352
|
+
{item.label}
|
|
353
|
+
</Text>
|
|
354
|
+
</View>
|
|
355
|
+
))}
|
|
356
|
+
</View>
|
|
357
|
+
</View>
|
|
358
|
+
) : (
|
|
359
|
+
/* Horizontal Scroll for > 4 items */
|
|
360
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
361
|
+
<View>
|
|
362
|
+
<View className="flex-row items-end px-1" style={{ height: chartHeight }}>
|
|
363
|
+
{chartData.map((item, idx) => {
|
|
364
|
+
const barHeight = Math.max(14, Math.round((item.value / maxValue) * (chartHeight - 30)));
|
|
365
|
+
const isTopValue = item.value === maxValue;
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<View
|
|
369
|
+
key={idx}
|
|
370
|
+
className="items-center justify-end mr-3"
|
|
371
|
+
style={{ width: 52 }}
|
|
372
|
+
>
|
|
373
|
+
<Text
|
|
374
|
+
style={{
|
|
375
|
+
color: isTopValue ? (isDark ? '#A78BFA' : '#7C3AED') : (isDark ? '#E2E8F0' : '#475569'),
|
|
376
|
+
}}
|
|
377
|
+
className="text-[9px] font-extrabold mb-1 font-mono"
|
|
378
|
+
numberOfLines={1}
|
|
379
|
+
>
|
|
380
|
+
{formatYAxisValue(item.value)}
|
|
381
|
+
</Text>
|
|
382
|
+
|
|
383
|
+
<View
|
|
384
|
+
style={{ height: barHeight }}
|
|
385
|
+
className={`w-full rounded-t-xl ${
|
|
386
|
+
isTopValue
|
|
387
|
+
? 'bg-violet-600 dark:bg-violet-500'
|
|
388
|
+
: 'bg-violet-400/80 dark:bg-violet-600/60'
|
|
389
|
+
}`}
|
|
390
|
+
/>
|
|
391
|
+
</View>
|
|
392
|
+
);
|
|
393
|
+
})}
|
|
394
|
+
</View>
|
|
395
|
+
|
|
396
|
+
{/* Continuous X-Axis Baseline */}
|
|
397
|
+
<View className="w-full h-[1.5px] bg-slate-300 dark:bg-slate-700" />
|
|
398
|
+
|
|
399
|
+
{/* Category Labels below Baseline */}
|
|
400
|
+
<View className="flex-row px-1 pt-1.5">
|
|
401
|
+
{chartData.map((item, idx) => (
|
|
402
|
+
<View key={idx} style={{ width: 52 }} className="mr-3 items-center">
|
|
403
|
+
<Text
|
|
404
|
+
style={{ color: isDark ? '#E2E8F0' : '#475569' }}
|
|
405
|
+
className="text-[9px] font-semibold text-center leading-tight"
|
|
406
|
+
numberOfLines={2}
|
|
407
|
+
>
|
|
408
|
+
{item.label}
|
|
409
|
+
</Text>
|
|
410
|
+
</View>
|
|
411
|
+
))}
|
|
412
|
+
</View>
|
|
413
|
+
</View>
|
|
414
|
+
</ScrollView>
|
|
415
|
+
)
|
|
416
|
+
) : (
|
|
417
|
+
/* LINE / TREND SVG GRAPH MODE */
|
|
418
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
419
|
+
<View style={{ width: svgWidth }}>
|
|
420
|
+
<Svg width={svgWidth} height={chartHeight}>
|
|
421
|
+
<Defs>
|
|
422
|
+
<LinearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
|
|
423
|
+
<Stop offset="0%" stopColor="#7c3aed" stopOpacity="0.35" />
|
|
424
|
+
<Stop offset="100%" stopColor="#7c3aed" stopOpacity="0.0" />
|
|
425
|
+
</LinearGradient>
|
|
426
|
+
</Defs>
|
|
427
|
+
|
|
428
|
+
{/* Continuous X-Axis Baseline */}
|
|
429
|
+
<SvgLine
|
|
430
|
+
x1="0"
|
|
431
|
+
y1={chartHeight - 10}
|
|
432
|
+
x2={svgWidth}
|
|
433
|
+
y2={chartHeight - 10}
|
|
434
|
+
stroke={isDark ? '#475569' : '#cbd5e1'}
|
|
435
|
+
strokeWidth="1.5"
|
|
436
|
+
/>
|
|
437
|
+
|
|
438
|
+
{/* Trend Area Gradient Underfill */}
|
|
439
|
+
{chartType === 'trend' && (
|
|
440
|
+
<Path d={areaPath} fill="url(#gradientArea)" />
|
|
441
|
+
)}
|
|
442
|
+
|
|
443
|
+
{/* Line Path */}
|
|
444
|
+
<Path
|
|
445
|
+
d={linePath}
|
|
446
|
+
fill="none"
|
|
447
|
+
stroke="#7c3aed"
|
|
448
|
+
strokeWidth="2.5"
|
|
449
|
+
strokeLinecap="round"
|
|
450
|
+
strokeLinejoin="round"
|
|
451
|
+
/>
|
|
452
|
+
|
|
453
|
+
{/* Data Points */}
|
|
454
|
+
{svgPoints.map((p, i) => (
|
|
455
|
+
<Circle
|
|
456
|
+
key={i}
|
|
457
|
+
cx={p.x}
|
|
458
|
+
cy={p.y}
|
|
459
|
+
r="4"
|
|
460
|
+
fill={isDark ? '#1C1C1E' : '#FFFFFF'}
|
|
461
|
+
stroke="#7c3aed"
|
|
462
|
+
strokeWidth="2"
|
|
463
|
+
/>
|
|
464
|
+
))}
|
|
465
|
+
</Svg>
|
|
466
|
+
|
|
467
|
+
{/* Data Labels under Points */}
|
|
468
|
+
<View className="flex-row justify-between w-full px-1 pt-1.5">
|
|
469
|
+
{svgPoints.map((p, i) => (
|
|
470
|
+
<View key={i} style={{ width: 56, alignItems: 'center' }}>
|
|
471
|
+
<Text style={{ color: isDark ? '#E2E8F0' : '#475569' }} className="text-[8px] font-semibold text-center" numberOfLines={1}>
|
|
472
|
+
{p.label}
|
|
473
|
+
</Text>
|
|
474
|
+
<Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-[9px] font-mono font-bold mt-0.5">
|
|
475
|
+
{formatYAxisValue(p.value)}
|
|
476
|
+
</Text>
|
|
477
|
+
</View>
|
|
478
|
+
))}
|
|
479
|
+
</View>
|
|
480
|
+
</View>
|
|
481
|
+
</ScrollView>
|
|
482
|
+
)}
|
|
483
|
+
</View>
|
|
484
|
+
</View>
|
|
249
485
|
|
|
250
486
|
{/* Subtitle / Legend Footer */}
|
|
251
|
-
<View className="flex-row items-center justify-between pt-2 mt-2 border-t border-slate-100 dark:border-slate-800">
|
|
487
|
+
<View className="flex-row items-center justify-between pt-2.5 mt-2 border-t border-slate-100 dark:border-slate-800">
|
|
252
488
|
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-medium">
|
|
253
489
|
Peak: <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="font-bold">{maxValue.toLocaleString()}</Text>
|
|
254
490
|
</Text>
|
|
255
491
|
<View className="flex-row items-center gap-1">
|
|
256
492
|
<View className="w-2 h-2 rounded-full bg-violet-600" />
|
|
257
493
|
<Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold">
|
|
258
|
-
{timeframe} Trend
|
|
494
|
+
{chartType === 'bar' ? `${timeframe} Comparison` : chartType === 'line' ? 'Line Trajectory' : 'Trend Area'}
|
|
259
495
|
</Text>
|
|
260
496
|
</View>
|
|
261
497
|
</View>
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
Icon,
|
|
13
13
|
useManagementReportStore,
|
|
14
14
|
} from '@suflon/native-ui';
|
|
15
|
-
import { themeColors } from '
|
|
15
|
+
import { themeColors } from '../../../theme/colors';
|
|
16
16
|
import { TimeframeDropdown, TimeframeOption, getTimeframeDateFilter } from './TimeframeDropdown';
|
|
17
17
|
|
|
18
18
|
interface DetailsListingViewProps {
|
|
@@ -214,7 +214,7 @@ const SingleReportTableCard: React.FC<{
|
|
|
214
214
|
loading,
|
|
215
215
|
}) => {
|
|
216
216
|
const isDark = useColorScheme() === 'dark';
|
|
217
|
-
const [timeframe, setTimeframe] = useState<TimeframeOption>('
|
|
217
|
+
const [timeframe, setTimeframe] = useState<TimeframeOption>('Daily');
|
|
218
218
|
const [layoutMode, setLayoutMode] = useState<'table' | 'card'>('table');
|
|
219
219
|
const [statusFilter, setStatusFilter] = useState<string>('ALL');
|
|
220
220
|
const [fetching, setFetching] = useState(false);
|
|
@@ -10,148 +10,130 @@ import {
|
|
|
10
10
|
useColorScheme,
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { Icon } from '@suflon/native-ui';
|
|
13
|
-
import { themeColors } from '
|
|
13
|
+
import { themeColors } from '../../../theme/colors';
|
|
14
14
|
|
|
15
|
-
interface DoctorFilterDropdownProps {
|
|
15
|
+
export interface DoctorFilterDropdownProps {
|
|
16
16
|
selectedDoctor: string;
|
|
17
17
|
onSelectDoctor: (doctor: string) => void;
|
|
18
18
|
availableDoctors: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
export interface DoctorFilterDropdownRef {
|
|
22
|
+
open: (coords?: { x: number; y: number; width: number; height: number }) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DoctorFilterDropdown = React.forwardRef<
|
|
26
|
+
DoctorFilterDropdownRef,
|
|
27
|
+
DoctorFilterDropdownProps
|
|
28
|
+
>(({ selectedDoctor, onSelectDoctor, availableDoctors }, ref) => {
|
|
26
29
|
const colorScheme = useColorScheme();
|
|
27
30
|
const isDark = colorScheme === 'dark';
|
|
28
31
|
const [open, setOpen] = useState(false);
|
|
29
32
|
const [coords, setCoords] = useState<{ x: number; y: number; width: number; height: number }>({
|
|
30
33
|
x: 0,
|
|
31
|
-
y:
|
|
34
|
+
y: 90,
|
|
32
35
|
width: 0,
|
|
33
|
-
height:
|
|
36
|
+
height: 48,
|
|
34
37
|
});
|
|
35
|
-
const buttonRef = useRef<any>(null);
|
|
36
38
|
|
|
37
|
-
const handleOpen = () => {
|
|
38
|
-
|
|
39
|
-
setCoords(
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const handleOpen = (anchorCoords?: { x: number; y: number; width: number; height: number }) => {
|
|
40
|
+
if (anchorCoords) {
|
|
41
|
+
setCoords(anchorCoords);
|
|
42
|
+
}
|
|
43
|
+
setOpen(true);
|
|
42
44
|
};
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
React.useImperativeHandle(ref, () => ({
|
|
47
|
+
open: handleOpen,
|
|
48
|
+
}));
|
|
47
49
|
|
|
48
50
|
const allOptions = ['ALL', ...availableDoctors];
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
borderWidth: 1,
|
|
86
|
-
borderRadius: 16,
|
|
87
|
-
paddingVertical: 6,
|
|
88
|
-
paddingHorizontal: 4,
|
|
89
|
-
minWidth: 170,
|
|
90
|
-
maxHeight: 250,
|
|
91
|
-
shadowColor: '#000',
|
|
92
|
-
shadowOffset: { width: 0, height: 6 },
|
|
93
|
-
shadowOpacity: isDark ? 0.45 : 0.18,
|
|
94
|
-
shadowRadius: 10,
|
|
95
|
-
elevation: 25,
|
|
96
|
-
zIndex: 99999,
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
<View className="px-2.5 py-1 border-b border-slate-100 dark:border-slate-800 mb-1">
|
|
100
|
-
<Text className="text-[10px] font-extrabold uppercase tracking-wider text-slate-400">
|
|
101
|
-
Filter by Doctor
|
|
102
|
-
</Text>
|
|
103
|
-
</View>
|
|
53
|
+
<Modal
|
|
54
|
+
visible={open}
|
|
55
|
+
transparent={true}
|
|
56
|
+
animationType="none"
|
|
57
|
+
onRequestClose={() => setOpen(false)}
|
|
58
|
+
>
|
|
59
|
+
<TouchableWithoutFeedback onPress={() => setOpen(false)}>
|
|
60
|
+
<View className="flex-1 bg-transparent">
|
|
61
|
+
<View
|
|
62
|
+
style={{
|
|
63
|
+
position: 'absolute',
|
|
64
|
+
top: (coords.y || 90) + (coords.height || 48) + 6,
|
|
65
|
+
right: 16,
|
|
66
|
+
backgroundColor: isDark ? themeColors.slate[900] : themeColors.white,
|
|
67
|
+
borderColor: isDark ? themeColors.slate[700] : themeColors.slate[200],
|
|
68
|
+
borderWidth: 1,
|
|
69
|
+
borderRadius: 16,
|
|
70
|
+
paddingVertical: 6,
|
|
71
|
+
paddingHorizontal: 4,
|
|
72
|
+
minWidth: 180,
|
|
73
|
+
maxHeight: 250,
|
|
74
|
+
shadowColor: '#000',
|
|
75
|
+
shadowOffset: { width: 0, height: 6 },
|
|
76
|
+
shadowOpacity: isDark ? 0.45 : 0.18,
|
|
77
|
+
shadowRadius: 10,
|
|
78
|
+
elevation: 25,
|
|
79
|
+
zIndex: 99999,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<View className="px-2.5 py-1 border-b border-slate-100 dark:border-slate-800 mb-1">
|
|
83
|
+
<Text className="text-[10px] font-extrabold uppercase tracking-wider text-slate-400">
|
|
84
|
+
Filter by Doctor
|
|
85
|
+
</Text>
|
|
86
|
+
</View>
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
89
|
+
{allOptions.map((doc) => {
|
|
90
|
+
const isSelected = doc === selectedDoctor || (doc === 'ALL' && !selectedDoctor);
|
|
91
|
+
const label = doc === 'ALL' ? 'All Doctors' : doc;
|
|
109
92
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
93
|
+
return (
|
|
94
|
+
<TouchableOpacity
|
|
95
|
+
key={doc}
|
|
96
|
+
activeOpacity={0.8}
|
|
97
|
+
onPress={() => {
|
|
98
|
+
onSelectDoctor(doc);
|
|
99
|
+
setOpen(false);
|
|
100
|
+
}}
|
|
101
|
+
style={{
|
|
102
|
+
backgroundColor: isSelected
|
|
103
|
+
? (isDark ? themeColors.slate[800] : themeColors.brand[50])
|
|
104
|
+
: themeColors.transparent,
|
|
105
|
+
borderRadius: 10,
|
|
106
|
+
paddingVertical: 7,
|
|
107
|
+
paddingHorizontal: 8,
|
|
108
|
+
flexDirection: 'row',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
gap: 6,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<View style={{ width: 14, alignItems: 'center', justifyContent: 'center' }}>
|
|
114
|
+
{isSelected && (
|
|
115
|
+
<Icon name="check" type="Feather" size={11} color={themeColors.brand[600]} />
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
<Text
|
|
118
119
|
style={{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
paddingHorizontal: 8,
|
|
125
|
-
flexDirection: 'row',
|
|
126
|
-
alignItems: 'center',
|
|
127
|
-
gap: 6,
|
|
120
|
+
fontSize: 11,
|
|
121
|
+
fontWeight: isSelected ? '700' : '500',
|
|
122
|
+
color: isSelected
|
|
123
|
+
? (isDark ? themeColors.brand[400] : themeColors.brand[600])
|
|
124
|
+
: (isDark ? themeColors.slate[200] : themeColors.slate[700]),
|
|
128
125
|
}}
|
|
126
|
+
numberOfLines={1}
|
|
129
127
|
>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
style={{
|
|
137
|
-
fontSize: 11,
|
|
138
|
-
fontWeight: isSelected ? '700' : '500',
|
|
139
|
-
color: isSelected
|
|
140
|
-
? (isDark ? themeColors.brand[400] : themeColors.brand[600])
|
|
141
|
-
: (isDark ? themeColors.slate[200] : themeColors.slate[700]),
|
|
142
|
-
}}
|
|
143
|
-
numberOfLines={1}
|
|
144
|
-
>
|
|
145
|
-
{label}
|
|
146
|
-
</Text>
|
|
147
|
-
</TouchableOpacity>
|
|
148
|
-
);
|
|
149
|
-
})}
|
|
150
|
-
</ScrollView>
|
|
151
|
-
</View>
|
|
128
|
+
{label}
|
|
129
|
+
</Text>
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</ScrollView>
|
|
152
134
|
</View>
|
|
153
|
-
</
|
|
154
|
-
</
|
|
155
|
-
|
|
135
|
+
</View>
|
|
136
|
+
</TouchableWithoutFeedback>
|
|
137
|
+
</Modal>
|
|
156
138
|
);
|
|
157
|
-
};
|
|
139
|
+
});
|
|
@@ -65,7 +65,7 @@ const KpiCardItem: React.FC<{
|
|
|
65
65
|
loading: boolean;
|
|
66
66
|
}> = ({ item, index, data, loading }) => {
|
|
67
67
|
const isDark = useColorScheme() === 'dark';
|
|
68
|
-
const [timeframe, setTimeframe] = useState<TimeframeOption>('
|
|
68
|
+
const [timeframe, setTimeframe] = useState<TimeframeOption>('Daily');
|
|
69
69
|
const [fetching, setFetching] = useState(false);
|
|
70
70
|
|
|
71
71
|
const executeReportItem = useManagementReportStore((s) => s.executeReportItem);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, useColorScheme } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
IManagementReport,
|
|
5
|
-
|
|
5
|
+
Header,
|
|
6
|
+
SearchFilter,
|
|
6
7
|
} from '@suflon/native-ui';
|
|
7
8
|
import { CategoryTabs } from './CategoryTabs';
|
|
8
9
|
import { SubModuleTabs } from './SubModuleTabs';
|
|
@@ -25,6 +26,9 @@ interface ManagementReportHeaderProps {
|
|
|
25
26
|
onSelectDoctor: (doctor: string) => void;
|
|
26
27
|
availableDoctors: string[];
|
|
27
28
|
categoryCounts?: Record<string, number>;
|
|
29
|
+
showFilter?: boolean;
|
|
30
|
+
onFilterPress?: () => void;
|
|
31
|
+
currentFilter?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export const ManagementReportHeader: React.FC<ManagementReportHeaderProps> = ({
|
|
@@ -41,54 +45,53 @@ export const ManagementReportHeader: React.FC<ManagementReportHeaderProps> = ({
|
|
|
41
45
|
onSelectDoctor,
|
|
42
46
|
availableDoctors,
|
|
43
47
|
categoryCounts,
|
|
48
|
+
showFilter = true,
|
|
49
|
+
onFilterPress,
|
|
50
|
+
currentFilter,
|
|
44
51
|
}) => {
|
|
45
52
|
const isDark = useColorScheme() === 'dark';
|
|
53
|
+
const doctorDropdownRef = React.useRef<any>(null);
|
|
54
|
+
const searchBarRef = React.useRef<any>(null);
|
|
55
|
+
|
|
56
|
+
const handleFilterClick = () => {
|
|
57
|
+
if (onFilterPress) {
|
|
58
|
+
onFilterPress();
|
|
59
|
+
} else {
|
|
60
|
+
searchBarRef.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
61
|
+
doctorDropdownRef.current?.open({ x, y, width, height });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const isFilterActive = currentFilter || (selectedDoctor && selectedDoctor !== 'ALL' ? 'active' : '');
|
|
46
67
|
|
|
47
68
|
return (
|
|
48
69
|
<View className="border-b border-slate-100 dark:border-slate-800/80 bg-white dark:bg-background-lightBlack px-3.5 pt-2.5 pb-2">
|
|
49
|
-
{/* 1. Top Header Bar
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
className="w-7 h-7 rounded-full bg-slate-100 dark:bg-slate-800 items-center justify-center shrink-0"
|
|
57
|
-
>
|
|
58
|
-
<Icon name="arrow-left" type="Feather" size={14} color={isDark ? '#FFFFFF' : '#64748b'} />
|
|
59
|
-
</TouchableOpacity>
|
|
60
|
-
)}
|
|
61
|
-
|
|
62
|
-
<Text
|
|
63
|
-
style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
|
|
64
|
-
className="text-[15px] font-extrabold tracking-tight leading-tight flex-1"
|
|
65
|
-
numberOfLines={1}
|
|
66
|
-
>
|
|
67
|
-
{title}
|
|
68
|
-
</Text>
|
|
69
|
-
</View>
|
|
70
|
-
</View>
|
|
70
|
+
{/* 1. Top Header Bar from Suflon Native UI */}
|
|
71
|
+
<Header
|
|
72
|
+
title={title}
|
|
73
|
+
showBackButton={!!onBackPress}
|
|
74
|
+
onBackPress={onBackPress}
|
|
75
|
+
className="px-0 mb-2"
|
|
76
|
+
/>
|
|
71
77
|
|
|
72
|
-
{/* 2.
|
|
73
|
-
<View
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
<Icon name="x" type="Feather" size={13} color="#94a3b8" />
|
|
87
|
-
</TouchableOpacity>
|
|
88
|
-
) : null}
|
|
89
|
-
</View>
|
|
78
|
+
{/* 2. Search Filter from Suflon Native UI with Integrated Filter Modal */}
|
|
79
|
+
<View
|
|
80
|
+
ref={searchBarRef}
|
|
81
|
+
collapsable={false}
|
|
82
|
+
className="w-full mb-2"
|
|
83
|
+
>
|
|
84
|
+
<SearchFilter
|
|
85
|
+
placeholder="Search reports or creator..."
|
|
86
|
+
value={searchQuery}
|
|
87
|
+
onChangeText={onSearchChange}
|
|
88
|
+
showFilter={showFilter}
|
|
89
|
+
onFilterPress={handleFilterClick}
|
|
90
|
+
currentFilter={isFilterActive}
|
|
91
|
+
/>
|
|
90
92
|
|
|
91
93
|
<DoctorFilterDropdown
|
|
94
|
+
ref={doctorDropdownRef}
|
|
92
95
|
selectedDoctor={selectedDoctor}
|
|
93
96
|
onSelectDoctor={onSelectDoctor}
|
|
94
97
|
availableDoctors={availableDoctors}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useColorScheme,
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
import { Icon } from '@suflon/native-ui';
|
|
12
|
-
import { themeColors } from '
|
|
12
|
+
import { themeColors } from '../../../theme/colors';
|
|
13
13
|
|
|
14
14
|
export type TimeframeOption = 'Daily' | 'Weekly' | 'Monthly';
|
|
15
15
|
export const TIMEFRAMES: TimeframeOption[] = ['Daily', 'Weekly', 'Monthly'];
|
|
@@ -11,11 +11,13 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
useManagementReportStore,
|
|
13
13
|
ServerErrorWrapper,
|
|
14
|
+
Icon,
|
|
14
15
|
} from '@suflon/native-ui';
|
|
15
16
|
import { ManagementReportHeader } from './components/ManagementReportHeader';
|
|
16
17
|
import { KpiMetricsGrid } from './components/KpiMetricsGrid';
|
|
17
18
|
import { AnalyticsChartView } from './components/AnalyticsChartView';
|
|
18
19
|
import { DetailsListingView } from './components/DetailsListingView';
|
|
20
|
+
import { TouchableOpacity } from 'react-native';
|
|
19
21
|
|
|
20
22
|
interface ManagementReportScreenProps {
|
|
21
23
|
onBackPress?: () => void;
|
|
@@ -69,7 +71,7 @@ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
|
|
|
69
71
|
const categoryReports = useMemo(() => {
|
|
70
72
|
if (!managementReports || managementReports.length === 0) return [];
|
|
71
73
|
return managementReports.filter(
|
|
72
|
-
(r) => r.mgmt_report_type
|
|
74
|
+
(r) => (r.mgmt_report_type || '').trim().toUpperCase() === (activeCategory || '').trim().toUpperCase()
|
|
73
75
|
);
|
|
74
76
|
}, [managementReports, activeCategory]);
|
|
75
77
|
|
|
@@ -77,7 +79,7 @@ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
|
|
|
77
79
|
const categoryCounts = useMemo(() => {
|
|
78
80
|
const counts: Record<string, number> = {};
|
|
79
81
|
(managementReports || []).forEach((r) => {
|
|
80
|
-
const type = r.mgmt_report_type
|
|
82
|
+
const type = (r.mgmt_report_type || 'OTHER').trim().toUpperCase();
|
|
81
83
|
counts[type] = (counts[type] || 0) + 1;
|
|
82
84
|
});
|
|
83
85
|
return counts;
|
|
@@ -113,6 +115,8 @@ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
|
|
|
113
115
|
: 'Reports Catalog';
|
|
114
116
|
const screenSubtitle = `${activeCategory} Management Module`;
|
|
115
117
|
|
|
118
|
+
const hasContent = selectedReportDetails && selectedReportDetails.length > 0;
|
|
119
|
+
|
|
116
120
|
return (
|
|
117
121
|
<SafeAreaView className="flex-1 bg-slate-50 dark:bg-background-lightBlack">
|
|
118
122
|
<ServerErrorWrapper
|
|
@@ -137,6 +141,8 @@ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
|
|
|
137
141
|
onSelectDoctor={setSelectedDoctor}
|
|
138
142
|
availableDoctors={availableDoctors}
|
|
139
143
|
categoryCounts={categoryCounts}
|
|
144
|
+
showFilter={true}
|
|
145
|
+
currentFilter={selectedDoctor !== 'ALL' ? 'active' : ''}
|
|
140
146
|
/>
|
|
141
147
|
|
|
142
148
|
{/* Main Content Area */}
|
|
@@ -166,6 +172,31 @@ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
|
|
|
166
172
|
Loading {selectedReport?.name || activeCategory} metrics & charts...
|
|
167
173
|
</Text>
|
|
168
174
|
</View>
|
|
175
|
+
) : categoryReports.length === 0 || !selectedReport || !hasContent ? (
|
|
176
|
+
/* Informative Empty State for categories without published reports or empty data */
|
|
177
|
+
<View className="py-16 px-6 items-center justify-center bg-white dark:bg-background-darkSecondaryBg rounded-2xl border border-slate-100 dark:border-slate-800 mt-2">
|
|
178
|
+
<View className="w-14 h-14 rounded-2xl bg-violet-50 dark:bg-slate-800 items-center justify-center mb-3.5">
|
|
179
|
+
<Icon name="bar-chart-2" type="Feather" size={24} color="#7c3aed" />
|
|
180
|
+
</View>
|
|
181
|
+
<Text className="text-sm font-bold text-slate-800 dark:text-slate-100 text-center">
|
|
182
|
+
{categoryReports.length === 0
|
|
183
|
+
? `No ${activeCategory} Reports Available`
|
|
184
|
+
: `No Data for ${selectedReport?.name || activeCategory}`}
|
|
185
|
+
</Text>
|
|
186
|
+
<Text className="text-xs text-slate-400 text-center mt-1 px-4 leading-4">
|
|
187
|
+
{categoryReports.length === 0
|
|
188
|
+
? `There are currently no published reports configured for the ${activeCategory.toLowerCase()} module.`
|
|
189
|
+
: 'Report structure is available, but no KPI cards or data tables were found.'}
|
|
190
|
+
</Text>
|
|
191
|
+
<TouchableOpacity
|
|
192
|
+
onPress={handleRefresh}
|
|
193
|
+
activeOpacity={0.8}
|
|
194
|
+
className="mt-4 px-4 py-2 bg-violet-600 rounded-xl flex-row items-center gap-1.5"
|
|
195
|
+
>
|
|
196
|
+
<Icon name="refresh-cw" type="Feather" size={13} color="#FFFFFF" />
|
|
197
|
+
<Text className="text-xs font-semibold text-white">Refresh Reports</Text>
|
|
198
|
+
</TouchableOpacity>
|
|
199
|
+
</View>
|
|
169
200
|
) : (
|
|
170
201
|
/* Sequential Content Flow: KPIs -> Charts -> Listing Tables */
|
|
171
202
|
<>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { View, Text, Dimensions, TouchableOpacity, ScrollView, PanResponder } from 'react-native';
|
|
3
3
|
import Svg, { Path, Circle, G, Line as SvgLine, Text as SvgText } from 'react-native-svg';
|
|
4
|
-
import { themeColors } from '
|
|
4
|
+
import { themeColors } from '../../../theme/colors';
|
|
5
5
|
import { Icon } from '@suflon/native-ui';
|
|
6
6
|
|
|
7
7
|
interface ChartData {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
useStaffStore,
|
|
12
12
|
IReportMetadata
|
|
13
13
|
} from '@suflon/native-ui';
|
|
14
|
-
import { themeColors } from '
|
|
14
|
+
import { themeColors } from '../../../theme/colors';
|
|
15
15
|
|
|
16
16
|
const CustomView = View;
|
|
17
17
|
const CustomText = ({ variant, className, style, ...props }: any) => <Text style={style} {...props} />;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo, useContext } from 'react';
|
|
2
|
-
import { ScrollView, View, Text, TouchableOpacity, SafeAreaView, useColorScheme } from 'react-native';
|
|
3
|
-
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
1
|
+
import React, { useEffect, useState, useMemo, useContext, useCallback } from 'react';
|
|
2
|
+
import { ScrollView, View, Text, TouchableOpacity, SafeAreaView, useColorScheme, BackHandler } from 'react-native';
|
|
3
|
+
import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
|
|
4
4
|
import { useReportStore, IReport, Header, SearchFilter, Loader, Icon } from '@suflon/native-ui';
|
|
5
|
-
import { themeColors } from '
|
|
5
|
+
import { themeColors } from '../../../theme/colors';
|
|
6
6
|
import ReportChart from './ReportChart';
|
|
7
7
|
import ReportFilterModal from './ReportFilterModal';
|
|
8
8
|
|
|
@@ -33,6 +33,20 @@ const ReportingDetail = (props: any) => {
|
|
|
33
33
|
const [isLocalLoading, setIsLocalLoading] = useState(true);
|
|
34
34
|
const [activeReportId, setActiveReportId] = useState<string | number | null>(null);
|
|
35
35
|
|
|
36
|
+
useFocusEffect(
|
|
37
|
+
useCallback(() => {
|
|
38
|
+
const onBack = () => {
|
|
39
|
+
if (navigation?.canGoBack?.()) {
|
|
40
|
+
navigation.goBack();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
const sub = BackHandler.addEventListener('hardwareBackPress', onBack);
|
|
46
|
+
return () => sub.remove();
|
|
47
|
+
}, [navigation])
|
|
48
|
+
);
|
|
49
|
+
|
|
36
50
|
const isDynamic = report?.report_type === 'SUMMARY' || report?.is_dynamic;
|
|
37
51
|
|
|
38
52
|
// Detect report_type from backend response metadata or item prop
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
|
2
2
|
import { ScrollView, TouchableOpacity, View, Text, SafeAreaView, useColorScheme } from 'react-native';
|
|
3
3
|
import { useNavigation } from '@react-navigation/native';
|
|
4
4
|
import { useReportStore, IReport, Header, SearchFilter, Loader, Icon } from '@suflon/native-ui';
|
|
5
|
-
import { themeColors } from '
|
|
5
|
+
import { themeColors } from '../../theme/colors';
|
|
6
6
|
import { SUB_CLASS_ICON_MAP, SUB_CLASS_LABEL_MAP } from './utils';
|
|
7
7
|
|
|
8
8
|
const Reporting = ({ onBackPress }: { onBackPress?: () => void } = {}) => {
|
package/src/navigation/index.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, StyleSheet } from 'react-native';
|
|
3
3
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
4
|
-
import Reporting from '
|
|
5
|
-
import ReportingDetail from '
|
|
6
|
-
import ManagementReport from '
|
|
4
|
+
import Reporting from '../modules/Reporting';
|
|
5
|
+
import ReportingDetail from '../modules/Reporting/component/ReportingDetail';
|
|
6
|
+
import ManagementReport from '../modules/ManagementReport';
|
|
7
7
|
|
|
8
8
|
export type AppNavigationProps = {
|
|
9
9
|
onBackPress?: () => void;
|