@suflon/rnmd-reporting 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export { default as Reporting } from './modules/Reporting';
3
3
  export { default as ReportingDetail } from './modules/Reporting/component/ReportingDetail';
4
4
  export { default as ReportChart } from './modules/Reporting/component/ReportChart';
5
5
  export { default as ReportFilterModal } from './modules/Reporting/component/ReportFilterModal';
6
+ export { default as ManagementReport } from './modules/ManagementReport';
@@ -0,0 +1,294 @@
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 { TimeframeDropdown, TimeframeOption, getTimeframeDateFilter } from './TimeframeDropdown';
16
+
17
+ interface AnalyticsChartViewProps {
18
+ details: IManagementReportDetail[];
19
+ executionData: Record<number, any[]>;
20
+ loading?: boolean;
21
+ }
22
+
23
+ interface ChartItemData {
24
+ label: string;
25
+ value: number;
26
+ }
27
+
28
+ function parseChartData(rows: any[]): ChartItemData[] {
29
+ if (!rows || rows.length === 0) return [];
30
+
31
+ return rows.map((r, i) => {
32
+ if (typeof r === 'number') {
33
+ return { label: `Item ${i + 1}`, value: r };
34
+ }
35
+
36
+ if (typeof r === 'object' && r !== null) {
37
+ const keys = Object.keys(r);
38
+ const labelKey =
39
+ keys.find((k) =>
40
+ [
41
+ 'date',
42
+ 'month',
43
+ 'day',
44
+ 'label',
45
+ 'name',
46
+ 'status',
47
+ 'type',
48
+ 'department',
49
+ 'category',
50
+ 'group_key',
51
+ ].includes(k.toLowerCase())
52
+ ) || keys[0];
53
+
54
+ const valueKey =
55
+ keys.find((k) =>
56
+ [
57
+ 'count',
58
+ 'total',
59
+ 'value',
60
+ 'amount',
61
+ 'total_amount',
62
+ 'net_amount',
63
+ 'sum',
64
+ 'metric_value',
65
+ ].includes(k.toLowerCase())
66
+ ) || keys[1] || keys[0];
67
+
68
+ const val = Number(r[valueKey]) || 0;
69
+ const lbl = String(r[labelKey] || `Item ${i + 1}`);
70
+
71
+ return { label: lbl, value: val };
72
+ }
73
+
74
+ return { label: `Item ${i + 1}`, value: 0 };
75
+ });
76
+ }
77
+
78
+ const SingleChartCard: React.FC<{
79
+ detail: IManagementReportDetail;
80
+ rows: any[];
81
+ loading: boolean;
82
+ }> = ({ detail, rows, loading }) => {
83
+ const isDark = useColorScheme() === 'dark';
84
+ const [timeframe, setTimeframe] = useState<TimeframeOption>('Monthly');
85
+ const [chartType, setChartType] = useState<'bar' | 'line' | 'trend'>('bar');
86
+ const [fetching, setFetching] = useState(false);
87
+
88
+ const executeReportItem = useManagementReportStore((s) => s.executeReportItem);
89
+
90
+ const handleSelectTimeframe = useCallback(
91
+ async (selectedTf: TimeframeOption) => {
92
+ setTimeframe(selectedTf);
93
+ if (detail.report_id) {
94
+ setFetching(true);
95
+ const dateFilter = getTimeframeDateFilter(selectedTf);
96
+ const state = useManagementReportStore.getState();
97
+ const origin = (state.selectedReport?.mgmt_report_type || state.activeCategory || 'opd').toLowerCase();
98
+ await executeReportItem(
99
+ detail.report_id,
100
+ detail.report?.is_dynamic || false,
101
+ { filters: dateFilter },
102
+ origin,
103
+ true
104
+ );
105
+ setFetching(false);
106
+ }
107
+ },
108
+ [detail, executeReportItem]
109
+ );
110
+
111
+ const chartData = useMemo(() => parseChartData(rows), [rows]);
112
+ const maxValue = useMemo(
113
+ () => Math.max(...chartData.map((d) => d.value), 1),
114
+ [chartData]
115
+ );
116
+
117
+ const isBusy = loading || fetching;
118
+ const title = detail.report?.name || detail.report_num || 'Performance Metric';
119
+
120
+ return (
121
+ <View className="p-3.5 rounded-3xl bg-white dark:bg-background-darkSecondaryBg border border-slate-100 dark:border-slate-800 mb-3.5">
122
+ {/* Header: Title & Timeframe Selector */}
123
+ <View className="flex-row items-center justify-between mb-3 pb-2 border-b border-slate-100 dark:border-slate-800">
124
+ <Text
125
+ style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
126
+ className="text-sm font-extrabold tracking-tight flex-1 mr-2"
127
+ numberOfLines={1}
128
+ >
129
+ {title}
130
+ </Text>
131
+
132
+ <View className="flex-row items-center gap-1.5 shrink-0">
133
+ {/* Chart Type Selector */}
134
+ <View className="p-0.5 rounded-xl bg-slate-100 dark:bg-slate-800 flex-row items-center">
135
+ <TouchableOpacity
136
+ onPress={() => setChartType('bar')}
137
+ activeOpacity={0.8}
138
+ className={`p-1 rounded-lg ${chartType === 'bar' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
139
+ >
140
+ <Icon
141
+ name="bar-chart-2"
142
+ type="Feather"
143
+ size={11}
144
+ color={chartType === 'bar' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
145
+ />
146
+ </TouchableOpacity>
147
+ <TouchableOpacity
148
+ onPress={() => setChartType('line')}
149
+ activeOpacity={0.8}
150
+ className={`p-1 rounded-lg ${chartType === 'line' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
151
+ >
152
+ <Icon
153
+ name="activity"
154
+ type="Feather"
155
+ size={11}
156
+ color={chartType === 'line' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
157
+ />
158
+ </TouchableOpacity>
159
+ <TouchableOpacity
160
+ onPress={() => setChartType('trend')}
161
+ activeOpacity={0.8}
162
+ className={`p-1 rounded-lg ${chartType === 'trend' ? 'bg-white dark:bg-slate-700 shadow-sm' : 'bg-transparent'}`}
163
+ >
164
+ <Icon
165
+ name="trending-up"
166
+ type="Feather"
167
+ size={11}
168
+ color={chartType === 'trend' ? '#7c3aed' : (isDark ? '#9CA3AF' : '#94a3b8')}
169
+ />
170
+ </TouchableOpacity>
171
+ </View>
172
+
173
+ {/* Timeframe Dropdown */}
174
+ <TimeframeDropdown
175
+ value={timeframe}
176
+ onChange={handleSelectTimeframe}
177
+ />
178
+ </View>
179
+ </View>
180
+
181
+ {/* Chart Visual Surface */}
182
+ {isBusy ? (
183
+ <View className="h-44 items-center justify-center">
184
+ <ActivityIndicator size="small" color="#7c3aed" />
185
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold mt-2">
186
+ Fetching chart metrics...
187
+ </Text>
188
+ </View>
189
+ ) : chartData.length === 0 ? (
190
+ <View className="h-36 items-center justify-center rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-background-lightBlack">
191
+ <Icon name="bar-chart-2" type="Feather" size={20} color="#94a3b8" />
192
+ <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="text-xs font-bold mt-1.5">No metrics recorded</Text>
193
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] text-center mt-0.5">
194
+ Try adjusting your timeframe filter.
195
+ </Text>
196
+ </View>
197
+ ) : (
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 }}
214
+ >
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 */}
225
+ <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
+ }`}
232
+ />
233
+
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>
249
+
250
+ {/* 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">
252
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-medium">
253
+ Peak: <Text style={{ color: isDark ? '#FFFFFF' : '#0F172A' }} className="font-bold">{maxValue.toLocaleString()}</Text>
254
+ </Text>
255
+ <View className="flex-row items-center gap-1">
256
+ <View className="w-2 h-2 rounded-full bg-violet-600" />
257
+ <Text style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }} className="text-[10px] font-semibold">
258
+ {timeframe} Trend
259
+ </Text>
260
+ </View>
261
+ </View>
262
+ </View>
263
+ )}
264
+ </View>
265
+ );
266
+ };
267
+
268
+ export const AnalyticsChartView: React.FC<AnalyticsChartViewProps> = ({
269
+ details,
270
+ executionData,
271
+ loading = false,
272
+ }) => {
273
+ const chartDetails = details.filter(
274
+ (d) =>
275
+ d.display_size === 'MD' ||
276
+ d.report?.report_type === 'CHART' ||
277
+ d.report?.report_type === 'SUMMARY'
278
+ );
279
+
280
+ if (chartDetails.length === 0) return null;
281
+
282
+ return (
283
+ <View className="space-y-3.5">
284
+ {chartDetails.map((detail) => (
285
+ <SingleChartCard
286
+ key={detail.id || detail.report_id}
287
+ detail={detail}
288
+ rows={executionData[detail.report_id] || []}
289
+ loading={loading}
290
+ />
291
+ ))}
292
+ </View>
293
+ );
294
+ };
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { ScrollView, TouchableOpacity, View, Text } from 'react-native';
3
+ import { Icon } from '@suflon/native-ui';
4
+
5
+ export interface CategoryItem {
6
+ id: string;
7
+ name: string;
8
+ icon: string;
9
+ type: string;
10
+ }
11
+
12
+ export const CATEGORIES_DATA: CategoryItem[] = [
13
+ { id: 'OPD', name: 'OPD', icon: 'calendar', type: 'Feather' },
14
+ { id: 'PRESCRIPTION', name: 'PRESCRIPTION', icon: 'file-text', type: 'Feather' },
15
+ { id: 'SALES', name: 'SALES', icon: 'pie-chart', type: 'Feather' },
16
+ { id: 'PURCHASE', name: 'PURCHASE', icon: 'package', type: 'Feather' },
17
+ { id: 'TRANSACTION', name: 'TRANSACTION', icon: 'credit-card', type: 'Feather' },
18
+ ];
19
+
20
+ interface CategoryTabsProps {
21
+ activeCategory: string;
22
+ onSelectCategory: (category: string) => void;
23
+ reportCounts?: Record<string, number>;
24
+ }
25
+
26
+ export const CategoryTabs: React.FC<CategoryTabsProps> = ({
27
+ activeCategory,
28
+ onSelectCategory,
29
+ reportCounts = {},
30
+ }) => {
31
+ return (
32
+ <View className="py-0.5">
33
+ <ScrollView
34
+ horizontal
35
+ showsHorizontalScrollIndicator={false}
36
+ contentContainerStyle={{ gap: 8 }}
37
+ >
38
+ {CATEGORIES_DATA.map((cat) => {
39
+ const isActive = activeCategory === cat.id;
40
+ const count = reportCounts[cat.id];
41
+
42
+ return (
43
+ <TouchableOpacity
44
+ key={cat.id}
45
+ onPress={() => onSelectCategory(cat.id)}
46
+ activeOpacity={0.8}
47
+ className={`px-3.5 py-1.5 rounded-xl flex-row items-center gap-1.5 border ${
48
+ isActive
49
+ ? 'bg-violet-600 border-violet-600'
50
+ : 'bg-white dark:bg-slate-800/90 border-slate-200/60 dark:border-slate-700/60'
51
+ }`}
52
+ >
53
+ <Icon
54
+ name={cat.icon}
55
+ type={cat.type as any}
56
+ size={12}
57
+ color={isActive ? '#FFFFFF' : '#94a3b8'}
58
+ />
59
+ <Text
60
+ style={{ color: isActive ? '#FFFFFF' : '#94a3b8' }}
61
+ className={`text-xs ${isActive ? 'font-bold' : 'font-normal'}`}
62
+ >
63
+ {cat.name}
64
+ </Text>
65
+ {count !== undefined && count > 0 && (
66
+ <View
67
+ className={`px-1.5 py-0.5 rounded-full ${
68
+ isActive ? 'bg-white/20' : 'bg-slate-100 dark:bg-slate-700'
69
+ }`}
70
+ >
71
+ <Text
72
+ style={{ color: isActive ? '#FFFFFF' : '#94a3b8' }}
73
+ className={`text-[9px] ${isActive ? 'font-bold' : 'font-normal'}`}
74
+ >
75
+ {count}
76
+ </Text>
77
+ </View>
78
+ )}
79
+ </TouchableOpacity>
80
+ );
81
+ })}
82
+ </ScrollView>
83
+ </View>
84
+ );
85
+ };