@suflon/rnmd-reporting 0.0.4 → 0.0.5

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.5",
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 };
@@ -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 {
@@ -10,7 +10,7 @@ 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
15
  interface DoctorFilterDropdownProps {
16
16
  selectedDoctor: string;
@@ -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'];
@@ -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} />;
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo, useContext } from 'react';
2
2
  import { ScrollView, View, Text, TouchableOpacity, SafeAreaView, useColorScheme } from 'react-native';
3
3
  import { useNavigation, useRoute } 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
 
@@ -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;