@suflon/rnmd-reporting 0.0.5 → 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.5",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -109,7 +109,7 @@ const SingleChartCard: React.FC<{
109
109
  loading: boolean;
110
110
  }> = ({ detail, rows, loading }) => {
111
111
  const isDark = useColorScheme() === 'dark';
112
- const [timeframe, setTimeframe] = useState<TimeframeOption>('Monthly');
112
+ const [timeframe, setTimeframe] = useState<TimeframeOption>('Daily');
113
113
  const [chartType, setChartType] = useState<'bar' | 'line' | 'trend'>('bar');
114
114
  const [fetching, setFetching] = useState(false);
115
115
 
@@ -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);
@@ -12,146 +12,128 @@ import {
12
12
  import { Icon } from '@suflon/native-ui';
13
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}
@@ -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,6 +1,6 @@
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
5
  import { themeColors } from '../../../theme/colors';
6
6
  import ReportChart from './ReportChart';
@@ -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