@suflon/rnmd-reporting 0.0.3 → 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.
@@ -0,0 +1,162 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Modal,
7
+ TouchableWithoutFeedback,
8
+ Dimensions,
9
+ useColorScheme,
10
+ } from 'react-native';
11
+ import { Icon } from '@suflon/native-ui';
12
+ import { themeColors } from '../../../theme/colors';
13
+
14
+ export type TimeframeOption = 'Daily' | 'Weekly' | 'Monthly';
15
+ export const TIMEFRAMES: TimeframeOption[] = ['Daily', 'Weekly', 'Monthly'];
16
+
17
+ export function getTimeframeDateFilter(timeframe: TimeframeOption): { from_date: string; to_date: string } {
18
+ const now = new Date();
19
+ const to_date = now.toISOString().slice(0, 10);
20
+ const from = new Date(now);
21
+
22
+ if (timeframe === 'Daily') {
23
+ from.setDate(now.getDate());
24
+ } else if (timeframe === 'Weekly') {
25
+ from.setDate(now.getDate() - 7);
26
+ } else if (timeframe === 'Monthly') {
27
+ from.setDate(now.getDate() - 30);
28
+ }
29
+
30
+ const from_date = from.toISOString().slice(0, 10);
31
+ return { from_date, to_date };
32
+ }
33
+
34
+ interface TimeframeDropdownProps {
35
+ value: TimeframeOption;
36
+ onChange: (value: TimeframeOption) => void;
37
+ size?: 'sm' | 'md';
38
+ }
39
+
40
+ export const TimeframeDropdown: React.FC<TimeframeDropdownProps> = ({
41
+ value,
42
+ onChange,
43
+ size = 'md',
44
+ }) => {
45
+ const colorScheme = useColorScheme();
46
+ const isDark = colorScheme === 'dark';
47
+ const [open, setOpen] = useState(false);
48
+ const [coords, setCoords] = useState<{ x: number; y: number; width: number; height: number }>({
49
+ x: 0,
50
+ y: 0,
51
+ width: 0,
52
+ height: 0,
53
+ });
54
+ const buttonRef = useRef<any>(null);
55
+
56
+ const handleOpen = () => {
57
+ buttonRef.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
58
+ setCoords({ x, y, width, height });
59
+ setOpen(true);
60
+ });
61
+ };
62
+
63
+ const screenWidth = Dimensions.get('window').width;
64
+ const rightOffset = Math.max(12, screenWidth - (coords.x + coords.width));
65
+
66
+ return (
67
+ <>
68
+ <TouchableOpacity
69
+ ref={buttonRef}
70
+ onPress={handleOpen}
71
+ activeOpacity={0.8}
72
+ className={`${
73
+ size === 'sm' ? 'px-1.5 py-0.5' : 'px-2 py-1'
74
+ } rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200/80 dark:border-slate-700 flex-row items-center gap-1 shrink-0`}
75
+ >
76
+ <Icon name="sliders" type="Feather" size={size === 'sm' ? 8 : 10} color={themeColors.brand[600]} />
77
+ <Text
78
+ style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
79
+ className={`${
80
+ size === 'sm' ? 'text-[8px]' : 'text-[10px]'
81
+ } font-bold`}
82
+ >
83
+ {value}
84
+ </Text>
85
+ </TouchableOpacity>
86
+
87
+ <Modal
88
+ visible={open}
89
+ transparent={true}
90
+ animationType="none"
91
+ onRequestClose={() => setOpen(false)}
92
+ >
93
+ <TouchableWithoutFeedback onPress={() => setOpen(false)}>
94
+ <View className="flex-1 bg-transparent">
95
+ <View
96
+ style={{
97
+ position: 'absolute',
98
+ top: coords.y + coords.height + 4,
99
+ right: rightOffset,
100
+ backgroundColor: isDark ? themeColors.slate[900] : themeColors.white,
101
+ borderColor: isDark ? themeColors.slate[700] : themeColors.slate[200],
102
+ borderWidth: 1,
103
+ borderRadius: 16,
104
+ paddingVertical: 5,
105
+ paddingHorizontal: 4,
106
+ minWidth: 115,
107
+ shadowColor: '#000',
108
+ shadowOffset: { width: 0, height: 6 },
109
+ shadowOpacity: isDark ? 0.45 : 0.18,
110
+ shadowRadius: 10,
111
+ elevation: 25,
112
+ zIndex: 99999,
113
+ }}
114
+ >
115
+ {TIMEFRAMES.map((tf) => {
116
+ const isSelected = tf === value;
117
+ return (
118
+ <TouchableOpacity
119
+ key={tf}
120
+ activeOpacity={0.8}
121
+ onPress={() => {
122
+ onChange(tf);
123
+ setOpen(false);
124
+ }}
125
+ style={{
126
+ backgroundColor: isSelected
127
+ ? (isDark ? themeColors.slate[800] : themeColors.brand[50])
128
+ : themeColors.transparent,
129
+ borderRadius: 10,
130
+ paddingVertical: 6,
131
+ paddingHorizontal: 8,
132
+ flexDirection: 'row',
133
+ alignItems: 'center',
134
+ gap: 6,
135
+ }}
136
+ >
137
+ <View style={{ width: 14, alignItems: 'center', justifyContent: 'center' }}>
138
+ {isSelected && (
139
+ <Icon name="check" type="Feather" size={11} color={themeColors.brand[600]} />
140
+ )}
141
+ </View>
142
+ <Text
143
+ style={{
144
+ fontSize: 11,
145
+ fontWeight: isSelected ? '700' : '500',
146
+ color: isSelected
147
+ ? (isDark ? themeColors.brand[400] : themeColors.brand[600])
148
+ : (isDark ? themeColors.slate[200] : themeColors.slate[700]),
149
+ }}
150
+ >
151
+ {tf}
152
+ </Text>
153
+ </TouchableOpacity>
154
+ );
155
+ })}
156
+ </View>
157
+ </View>
158
+ </TouchableWithoutFeedback>
159
+ </Modal>
160
+ </>
161
+ );
162
+ };
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { View, TouchableOpacity, Text } from 'react-native';
3
+ import { Icon } from '@suflon/native-ui';
4
+
5
+ interface ViewModeSwitcherProps {
6
+ activeTab: 'chart' | 'listing';
7
+ onSwitchTab: (tab: 'chart' | 'listing') => void;
8
+ }
9
+
10
+ export const ViewModeSwitcher: React.FC<ViewModeSwitcherProps> = ({
11
+ activeTab,
12
+ onSwitchTab,
13
+ }) => {
14
+ const isChart = activeTab === 'chart';
15
+
16
+ return (
17
+ <View className="bg-slate-200/70 dark:bg-slate-800/80 p-1 rounded-2xl flex-row items-center border border-slate-200 dark:border-slate-700/60 shadow-xs mb-3.5">
18
+ <TouchableOpacity
19
+ onPress={() => onSwitchTab('chart')}
20
+ activeOpacity={0.8}
21
+ className={`flex-1 py-2 rounded-xl flex-row items-center justify-center gap-2 ${
22
+ isChart
23
+ ? 'bg-white dark:bg-slate-700'
24
+ : 'bg-transparent'
25
+ }`}
26
+ >
27
+ <Icon
28
+ name="trending-up"
29
+ type="Feather"
30
+ size={12}
31
+ color={isChart ? '#7c3aed' : '#94a3b8'}
32
+ />
33
+ <Text
34
+ className={`text-xs ${
35
+ isChart
36
+ ? 'font-extrabold text-brand-600 dark:text-brand-300'
37
+ : 'font-semibold text-slate-500 dark:text-slate-400'
38
+ }`}
39
+ >
40
+ Analytics & Chart
41
+ </Text>
42
+ </TouchableOpacity>
43
+
44
+ <TouchableOpacity
45
+ onPress={() => onSwitchTab('listing')}
46
+ activeOpacity={0.8}
47
+ className={`flex-1 py-2 rounded-xl flex-row items-center justify-center gap-2 ${
48
+ !isChart
49
+ ? 'bg-white dark:bg-slate-700'
50
+ : 'bg-transparent'
51
+ }`}
52
+ >
53
+ <Icon
54
+ name="list"
55
+ type="Feather"
56
+ size={12}
57
+ color={!isChart ? '#7c3aed' : '#94a3b8'}
58
+ />
59
+ <Text
60
+ className={`text-xs ${
61
+ !isChart
62
+ ? 'font-extrabold text-brand-600 dark:text-brand-300'
63
+ : 'font-semibold text-slate-500 dark:text-slate-400'
64
+ }`}
65
+ >
66
+ Details & Reports
67
+ </Text>
68
+ </TouchableOpacity>
69
+ </View>
70
+ );
71
+ };
@@ -0,0 +1,202 @@
1
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
+ import {
3
+ SafeAreaView,
4
+ ScrollView,
5
+ RefreshControl,
6
+ Alert,
7
+ View,
8
+ Text,
9
+ ActivityIndicator,
10
+ } from 'react-native';
11
+ import {
12
+ useManagementReportStore,
13
+ ServerErrorWrapper,
14
+ } from '@suflon/native-ui';
15
+ import { ManagementReportHeader } from './components/ManagementReportHeader';
16
+ import { KpiMetricsGrid } from './components/KpiMetricsGrid';
17
+ import { AnalyticsChartView } from './components/AnalyticsChartView';
18
+ import { DetailsListingView } from './components/DetailsListingView';
19
+
20
+ interface ManagementReportScreenProps {
21
+ onBackPress?: () => void;
22
+ navigation?: any;
23
+ route?: any;
24
+ }
25
+
26
+ const ManagementReportScreen: React.FC<ManagementReportScreenProps> = ({
27
+ onBackPress,
28
+ navigation,
29
+ route,
30
+ }) => {
31
+ const handleBack = onBackPress || route?.params?.onBackPress || navigation?.goBack;
32
+ const {
33
+ detailLoading,
34
+ executingLoading,
35
+ errorStatus,
36
+ managementReports,
37
+ selectedReport,
38
+ selectedReportDetails,
39
+ activeCategory,
40
+ selectedReportId,
41
+ searchQuery,
42
+ reportExecutionData,
43
+ getManagementReports,
44
+ getManagementReportById,
45
+ setActiveCategory,
46
+ setSelectedReportId,
47
+ setSearchQuery,
48
+ } = useManagementReportStore();
49
+
50
+ const [refreshing, setRefreshing] = useState(false);
51
+ const [selectedDoctor, setSelectedDoctor] = useState<string>('ALL');
52
+
53
+ // Fetch reports on mount
54
+ useEffect(() => {
55
+ getManagementReports(true);
56
+ }, []);
57
+
58
+ // Pull to refresh handler
59
+ const handleRefresh = useCallback(async () => {
60
+ setRefreshing(true);
61
+ await getManagementReports(true);
62
+ if (selectedReportId) {
63
+ await getManagementReportById(selectedReportId, true);
64
+ }
65
+ setRefreshing(false);
66
+ }, [getManagementReports, getManagementReportById, selectedReportId]);
67
+
68
+ // Reports in active category
69
+ const categoryReports = useMemo(() => {
70
+ if (!managementReports || managementReports.length === 0) return [];
71
+ return managementReports.filter(
72
+ (r) => r.mgmt_report_type?.toUpperCase() === activeCategory?.toUpperCase()
73
+ );
74
+ }, [managementReports, activeCategory]);
75
+
76
+ // Report counts per category
77
+ const categoryCounts = useMemo(() => {
78
+ const counts: Record<string, number> = {};
79
+ (managementReports || []).forEach((r) => {
80
+ const type = r.mgmt_report_type?.toUpperCase() || 'OTHER';
81
+ counts[type] = (counts[type] || 0) + 1;
82
+ });
83
+ return counts;
84
+ }, [managementReports]);
85
+
86
+ // Extract unique doctors dynamically from loaded dataset
87
+ const availableDoctors = useMemo(() => {
88
+ const docSet = new Set<string>();
89
+ Object.values(reportExecutionData || {}).forEach((rows) => {
90
+ if (Array.isArray(rows)) {
91
+ rows.forEach((r: any) => {
92
+ const doc = r?.staff || r?.doctor || r?.consultant;
93
+ if (doc && typeof doc === 'string' && doc.trim() !== '' && doc !== '-') {
94
+ docSet.add(doc.trim());
95
+ }
96
+ });
97
+ }
98
+ });
99
+ return Array.from(docSet);
100
+ }, [reportExecutionData]);
101
+
102
+ // Export report action
103
+ const handleExport = useCallback(() => {
104
+ Alert.alert(
105
+ 'Export Report Dataset',
106
+ `Exporting data for ${selectedReport?.name || activeCategory} module.`,
107
+ [{ text: 'OK' }]
108
+ );
109
+ }, [selectedReport, activeCategory]);
110
+
111
+ const screenTitle = selectedReport?.name
112
+ ? `${selectedReport.name} Reports`
113
+ : 'Reports Catalog';
114
+ const screenSubtitle = `${activeCategory} Management Module`;
115
+
116
+ return (
117
+ <SafeAreaView className="flex-1 bg-slate-50 dark:bg-background-lightBlack">
118
+ <ServerErrorWrapper
119
+ errorStatus={errorStatus}
120
+ onRetry={handleRefresh}
121
+ >
122
+ {/* Sticky Management Header & Tabs */}
123
+ <ManagementReportHeader
124
+ title={screenTitle}
125
+ subtitle={screenSubtitle}
126
+ activeCategory={activeCategory}
127
+ onSelectCategory={setActiveCategory}
128
+ categoryReports={categoryReports}
129
+ selectedReportId={selectedReportId}
130
+ onSelectReport={setSelectedReportId}
131
+ searchQuery={searchQuery}
132
+ onSearchChange={setSearchQuery}
133
+ onBackPress={handleBack}
134
+ onRefresh={handleRefresh}
135
+ onExport={handleExport}
136
+ selectedDoctor={selectedDoctor}
137
+ onSelectDoctor={setSelectedDoctor}
138
+ availableDoctors={availableDoctors}
139
+ categoryCounts={categoryCounts}
140
+ />
141
+
142
+ {/* Main Content Area */}
143
+ <ScrollView
144
+ className="flex-1 px-3.5 pt-3.5 space-y-3.5"
145
+ showsVerticalScrollIndicator={false}
146
+ contentContainerStyle={{ paddingBottom: 40 }}
147
+ refreshControl={
148
+ <RefreshControl
149
+ refreshing={refreshing}
150
+ onRefresh={handleRefresh}
151
+ colors={['#7c3aed']}
152
+ tintColor="#7c3aed"
153
+ />
154
+ }
155
+ >
156
+ {detailLoading ? (
157
+ /* Centered Lower Content Loader during Tab Switching */
158
+ <View className="py-20 items-center justify-center">
159
+ <View className="w-12 h-12 rounded-2xl bg-violet-50 dark:bg-slate-800 items-center justify-center mb-3">
160
+ <ActivityIndicator size="small" color="#7c3aed" />
161
+ </View>
162
+ <Text className="text-xs font-bold text-slate-800 dark:text-slate-200">
163
+ Fetching Report Data
164
+ </Text>
165
+ <Text className="text-[10px] text-slate-400 mt-0.5">
166
+ Loading {selectedReport?.name || activeCategory} metrics & charts...
167
+ </Text>
168
+ </View>
169
+ ) : (
170
+ /* Sequential Content Flow: KPIs -> Charts -> Listing Tables */
171
+ <>
172
+ {/* SECTION 1: Key Performance Overview Cards */}
173
+ <KpiMetricsGrid
174
+ details={selectedReportDetails}
175
+ executionData={reportExecutionData}
176
+ loading={executingLoading}
177
+ />
178
+
179
+ {/* SECTION 2: Analytics Charts */}
180
+ <AnalyticsChartView
181
+ details={selectedReportDetails}
182
+ executionData={reportExecutionData}
183
+ loading={executingLoading}
184
+ />
185
+
186
+ {/* SECTION 3: Records Listing & Tables */}
187
+ <DetailsListingView
188
+ details={selectedReportDetails}
189
+ executionData={reportExecutionData}
190
+ searchQuery={searchQuery}
191
+ doctorFilter={selectedDoctor}
192
+ loading={executingLoading}
193
+ />
194
+ </>
195
+ )}
196
+ </ScrollView>
197
+ </ServerErrorWrapper>
198
+ </SafeAreaView>
199
+ );
200
+ };
201
+
202
+ export default ManagementReportScreen;
@@ -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} />;