@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suflon/rnmd-reporting",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 lbl = String(r[labelKey] || `Item ${i + 1}`);
97
+ const rawLbl = String(r[labelKey] || `Item ${i + 1}`);
70
98
 
71
- return { label: lbl, value: val };
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>('Monthly');
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-2">
199
- {/* Scaled Value Grid with Scrollable Interactive Bars */}
200
- <ScrollView
201
- horizontal
202
- showsHorizontalScrollIndicator={false}
203
- contentContainerStyle={{ paddingHorizontal: 4, alignItems: 'flex-end', height: 160 }}
204
- >
205
- {chartData.map((item, idx) => {
206
- const heightPercent = Math.max(12, Math.round((item.value / maxValue) * 110));
207
- const isTopValue = item.value === maxValue;
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
- {/* Metric Value Label on Top of Bar */}
216
- <Text
217
- style={{ color: isTopValue ? '#a78bfa' : (isDark ? '#E2E8F0' : '#64748b') }}
218
- className="text-[9px] font-extrabold mb-1 font-mono"
219
- numberOfLines={1}
220
- >
221
- {item.value > 1000 ? `${(item.value / 1000).toFixed(1)}k` : item.value}
222
- </Text>
223
-
224
- {/* Rendered Visual Column */}
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
- style={{ height: heightPercent }}
227
- className={`w-full rounded-t-xl ${
228
- isTopValue
229
- ? 'bg-violet-600 dark:bg-violet-500'
230
- : 'bg-violet-400/80 dark:bg-violet-600/60'
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
- {/* Horizontal Base Axis Line */}
235
- <View className="w-full h-0.5 bg-slate-200 dark:bg-slate-700 mt-0.5" />
236
-
237
- {/* X-Axis Dimension Label */}
238
- <Text
239
- style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }}
240
- className="text-[8px] font-semibold text-center mt-1 w-full"
241
- numberOfLines={1}
242
- >
243
- {item.label}
244
- </Text>
245
- </View>
246
- );
247
- })}
248
- </ScrollView>
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 '@/theme/colors';
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>('Monthly');
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 '@/theme/colors';
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 const DoctorFilterDropdown: React.FC<DoctorFilterDropdownProps> = ({
22
- selectedDoctor,
23
- onSelectDoctor,
24
- availableDoctors,
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: 0,
34
+ y: 90,
32
35
  width: 0,
33
- height: 0,
36
+ height: 48,
34
37
  });
35
- const buttonRef = useRef<any>(null);
36
38
 
37
- const handleOpen = () => {
38
- buttonRef.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
39
- setCoords({ x, y, width, height });
40
- setOpen(true);
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
- const hasFilter = selectedDoctor !== 'ALL' && selectedDoctor !== '';
45
- const screenWidth = Dimensions.get('window').width;
46
- const rightOffset = Math.max(16, screenWidth - (coords.x + coords.width));
46
+ React.useImperativeHandle(ref, () => ({
47
+ open: handleOpen,
48
+ }));
47
49
 
48
50
  const allOptions = ['ALL', ...availableDoctors];
49
51
 
50
52
  return (
51
- <>
52
- <TouchableOpacity
53
- ref={buttonRef}
54
- onPress={handleOpen}
55
- activeOpacity={0.8}
56
- className={`w-9 h-9 rounded-xl items-center justify-center border ${
57
- hasFilter
58
- ? 'bg-violet-600 border-violet-600'
59
- : 'bg-slate-100 dark:bg-slate-800 border-slate-200/80 dark:border-slate-700'
60
- }`}
61
- >
62
- <Icon
63
- name="filter"
64
- type="Feather"
65
- size={14}
66
- color={hasFilter ? '#FFFFFF' : '#64748b'}
67
- />
68
- </TouchableOpacity>
69
-
70
- <Modal
71
- visible={open}
72
- transparent={true}
73
- animationType="none"
74
- onRequestClose={() => setOpen(false)}
75
- >
76
- <TouchableWithoutFeedback onPress={() => setOpen(false)}>
77
- <View className="flex-1 bg-transparent">
78
- <View
79
- style={{
80
- position: 'absolute',
81
- top: coords.y + coords.height + 6,
82
- right: rightOffset,
83
- backgroundColor: isDark ? themeColors.slate[900] : themeColors.white,
84
- borderColor: isDark ? themeColors.slate[700] : themeColors.slate[200],
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
- <ScrollView showsVerticalScrollIndicator={false}>
106
- {allOptions.map((doc) => {
107
- const isSelected = doc === selectedDoctor || (doc === 'ALL' && !selectedDoctor);
108
- const label = doc === 'ALL' ? 'All Doctors' : doc;
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
- return (
111
- <TouchableOpacity
112
- key={doc}
113
- activeOpacity={0.8}
114
- onPress={() => {
115
- onSelectDoctor(doc);
116
- setOpen(false);
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
- backgroundColor: isSelected
120
- ? (isDark ? themeColors.slate[800] : themeColors.brand[50])
121
- : themeColors.transparent,
122
- borderRadius: 10,
123
- paddingVertical: 7,
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
- <View style={{ width: 14, alignItems: 'center', justifyContent: 'center' }}>
131
- {isSelected && (
132
- <Icon name="check" type="Feather" size={11} color={themeColors.brand[600]} />
133
- )}
134
- </View>
135
- <Text
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
- </TouchableWithoutFeedback>
154
- </Modal>
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>('Monthly');
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, Text, TouchableOpacity, TextInput, useColorScheme } from 'react-native';
2
+ import { View, useColorScheme } from 'react-native';
3
3
  import {
4
4
  IManagementReport,
5
- Icon,
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: Back Button & Title */}
50
- <View className="flex-row items-center justify-between mb-2">
51
- <View className="flex-row items-center gap-2 flex-1">
52
- {onBackPress && (
53
- <TouchableOpacity
54
- onPress={onBackPress}
55
- activeOpacity={0.8}
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. Compact Search Bar with Doctor Filter Dropdown Popover */}
73
- <View className="w-full flex-row items-center gap-2 mb-2">
74
- <View className="flex-1 flex-row items-center h-10 px-3 rounded-2xl bg-slate-100 dark:bg-slate-800/80 border border-slate-200/70 dark:border-slate-700/80">
75
- <Icon name="search" type="Feather" size={15} color="#94a3b8" />
76
- <TextInput
77
- placeholder="Search reports or creator..."
78
- placeholderTextColor="#94a3b8"
79
- value={searchQuery}
80
- onChangeText={onSearchChange}
81
- style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
82
- className="flex-1 ml-2 text-xs font-medium p-0"
83
- />
84
- {searchQuery ? (
85
- <TouchableOpacity onPress={() => onSearchChange('')} activeOpacity={0.8}>
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 '@/theme/colors';
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?.toUpperCase() === activeCategory?.toUpperCase()
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?.toUpperCase() || 'OTHER';
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 '@/theme/colors';
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 '@/theme/colors';
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 '@/theme/colors';
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 '@/theme/colors';
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 } = {}) => {
@@ -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 '@/modules/Reporting';
5
- import ReportingDetail from '@/modules/Reporting/component/ReportingDetail';
6
- import ManagementReport from '@/modules/ManagementReport';
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;