@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.
- package/App.tsx +2 -2
- package/index.js +3 -0
- package/metro.config.js +31 -0
- package/package.json +4 -3
- package/patches/@suflon+native-ui+0.0.18.patch +1715 -4257
- package/src/index.ts +1 -0
- package/src/modules/ManagementReport/components/AnalyticsChartView.tsx +530 -0
- package/src/modules/ManagementReport/components/CategoryTabs.tsx +85 -0
- package/src/modules/ManagementReport/components/DetailsListingView.tsx +781 -0
- package/src/modules/ManagementReport/components/DoctorFilterDropdown.tsx +157 -0
- package/src/modules/ManagementReport/components/KpiMetricsGrid.tsx +183 -0
- package/src/modules/ManagementReport/components/ManagementFilterModal.tsx +142 -0
- package/src/modules/ManagementReport/components/ManagementReportHeader.tsx +115 -0
- package/src/modules/ManagementReport/components/SubModuleTabs.tsx +58 -0
- package/src/modules/ManagementReport/components/TimeframeDropdown.tsx +162 -0
- package/src/modules/ManagementReport/components/ViewModeSwitcher.tsx +71 -0
- package/src/modules/ManagementReport/index.tsx +202 -0
- package/src/modules/Reporting/component/ReportChart.tsx +1 -1
- package/src/modules/Reporting/component/ReportFilterModal.tsx +1 -1
- package/src/modules/Reporting/component/ReportingDetail.tsx +86 -98
- package/src/modules/Reporting/index.tsx +72 -79
- package/src/navigation/index.tsx +17 -6
- package/src/screens/DevToolsCorner.tsx +43 -8
- package/tailwind.config.js +11 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
Modal,
|
|
7
|
+
TouchableWithoutFeedback,
|
|
8
|
+
Dimensions,
|
|
9
|
+
ScrollView,
|
|
10
|
+
useColorScheme,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { Icon } from '@suflon/native-ui';
|
|
13
|
+
import { themeColors } from '../../../theme/colors';
|
|
14
|
+
|
|
15
|
+
interface DoctorFilterDropdownProps {
|
|
16
|
+
selectedDoctor: string;
|
|
17
|
+
onSelectDoctor: (doctor: string) => void;
|
|
18
|
+
availableDoctors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DoctorFilterDropdown: React.FC<DoctorFilterDropdownProps> = ({
|
|
22
|
+
selectedDoctor,
|
|
23
|
+
onSelectDoctor,
|
|
24
|
+
availableDoctors,
|
|
25
|
+
}) => {
|
|
26
|
+
const colorScheme = useColorScheme();
|
|
27
|
+
const isDark = colorScheme === 'dark';
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [coords, setCoords] = useState<{ x: number; y: number; width: number; height: number }>({
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
width: 0,
|
|
33
|
+
height: 0,
|
|
34
|
+
});
|
|
35
|
+
const buttonRef = useRef<any>(null);
|
|
36
|
+
|
|
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
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const hasFilter = selectedDoctor !== 'ALL' && selectedDoctor !== '';
|
|
45
|
+
const screenWidth = Dimensions.get('window').width;
|
|
46
|
+
const rightOffset = Math.max(16, screenWidth - (coords.x + coords.width));
|
|
47
|
+
|
|
48
|
+
const allOptions = ['ALL', ...availableDoctors];
|
|
49
|
+
|
|
50
|
+
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>
|
|
104
|
+
|
|
105
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
106
|
+
{allOptions.map((doc) => {
|
|
107
|
+
const isSelected = doc === selectedDoctor || (doc === 'ALL' && !selectedDoctor);
|
|
108
|
+
const label = doc === 'ALL' ? 'All Doctors' : doc;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<TouchableOpacity
|
|
112
|
+
key={doc}
|
|
113
|
+
activeOpacity={0.8}
|
|
114
|
+
onPress={() => {
|
|
115
|
+
onSelectDoctor(doc);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}}
|
|
118
|
+
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,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
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>
|
|
152
|
+
</View>
|
|
153
|
+
</TouchableWithoutFeedback>
|
|
154
|
+
</Modal>
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { View, Text, ActivityIndicator, useColorScheme } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
IManagementReportDetail,
|
|
5
|
+
useManagementReportStore,
|
|
6
|
+
} from '@suflon/native-ui';
|
|
7
|
+
import { TimeframeDropdown, TimeframeOption, getTimeframeDateFilter } from './TimeframeDropdown';
|
|
8
|
+
|
|
9
|
+
interface KpiMetricsGridProps {
|
|
10
|
+
details: IManagementReportDetail[];
|
|
11
|
+
executionData: Record<number, any[]>;
|
|
12
|
+
loading?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractMetricValue(
|
|
16
|
+
data: any[],
|
|
17
|
+
item: IManagementReportDetail,
|
|
18
|
+
index: number
|
|
19
|
+
): { value: string; subtext?: string } {
|
|
20
|
+
if (data && data.length > 0) {
|
|
21
|
+
const first = data[0];
|
|
22
|
+
if (typeof first === 'number' || typeof first === 'string') {
|
|
23
|
+
return { value: String(first) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof first === 'object' && first !== null) {
|
|
27
|
+
const val =
|
|
28
|
+
first.value ??
|
|
29
|
+
first.total ??
|
|
30
|
+
first.count ??
|
|
31
|
+
first.net_amount ??
|
|
32
|
+
first.paid_amount ??
|
|
33
|
+
first.stock_value ??
|
|
34
|
+
first.total_value ??
|
|
35
|
+
first.total_count ??
|
|
36
|
+
first.stock_count ??
|
|
37
|
+
first.amount ??
|
|
38
|
+
first.total_amount ??
|
|
39
|
+
first.volume ??
|
|
40
|
+
first[Object.keys(first)[0]];
|
|
41
|
+
|
|
42
|
+
const label = first.label || first.name || item.report?.description;
|
|
43
|
+
|
|
44
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
45
|
+
const formatted =
|
|
46
|
+
typeof val === 'number'
|
|
47
|
+
? val > 1000
|
|
48
|
+
? `₹${val.toLocaleString()}`
|
|
49
|
+
: val.toLocaleString()
|
|
50
|
+
: String(val);
|
|
51
|
+
return { value: formatted, subtext: label };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { value: String(data.length) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { value: '0', subtext: item.report?.description || 'No data recorded' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const KpiCardItem: React.FC<{
|
|
62
|
+
item: IManagementReportDetail;
|
|
63
|
+
index: number;
|
|
64
|
+
data: any[];
|
|
65
|
+
loading: boolean;
|
|
66
|
+
}> = ({ item, index, data, loading }) => {
|
|
67
|
+
const isDark = useColorScheme() === 'dark';
|
|
68
|
+
const [timeframe, setTimeframe] = useState<TimeframeOption>('Monthly');
|
|
69
|
+
const [fetching, setFetching] = useState(false);
|
|
70
|
+
|
|
71
|
+
const executeReportItem = useManagementReportStore((s) => s.executeReportItem);
|
|
72
|
+
|
|
73
|
+
const handleSelectTimeframe = useCallback(
|
|
74
|
+
async (selectedTf: TimeframeOption) => {
|
|
75
|
+
setTimeframe(selectedTf);
|
|
76
|
+
if (item.report_id) {
|
|
77
|
+
setFetching(true);
|
|
78
|
+
const dateFilter = getTimeframeDateFilter(selectedTf);
|
|
79
|
+
const state = useManagementReportStore.getState();
|
|
80
|
+
const origin = (state.selectedReport?.mgmt_report_type || state.activeCategory || 'opd').toLowerCase();
|
|
81
|
+
await executeReportItem(
|
|
82
|
+
item.report_id,
|
|
83
|
+
item.report?.is_dynamic || false,
|
|
84
|
+
{ filters: dateFilter },
|
|
85
|
+
origin,
|
|
86
|
+
true
|
|
87
|
+
);
|
|
88
|
+
setFetching(false);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[item, executeReportItem]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const metric = extractMetricValue(data, item, index);
|
|
95
|
+
const isBusy = loading || fetching;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View className="w-1/2 p-1">
|
|
99
|
+
<View className="p-3 rounded-2xl bg-white dark:bg-background-darkSecondaryBg border border-slate-100 dark:border-slate-800 justify-between min-h-[95px]">
|
|
100
|
+
{/* Top Header: Title & Timeframe Selector */}
|
|
101
|
+
<View className="flex-row items-center justify-between mb-1.5">
|
|
102
|
+
<Text
|
|
103
|
+
style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
|
|
104
|
+
className="text-[11px] font-extrabold flex-1 mr-1"
|
|
105
|
+
numberOfLines={1}
|
|
106
|
+
>
|
|
107
|
+
{item.report?.name || item.report_num}
|
|
108
|
+
</Text>
|
|
109
|
+
|
|
110
|
+
<TimeframeDropdown
|
|
111
|
+
value={timeframe}
|
|
112
|
+
onChange={handleSelectTimeframe}
|
|
113
|
+
size="sm"
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{/* Value Display */}
|
|
118
|
+
<View className="pt-0.5">
|
|
119
|
+
{isBusy ? (
|
|
120
|
+
<View className="py-1">
|
|
121
|
+
<ActivityIndicator size="small" color="#7c3aed" />
|
|
122
|
+
</View>
|
|
123
|
+
) : (
|
|
124
|
+
<Text
|
|
125
|
+
style={{ color: isDark ? '#FFFFFF' : '#0F172A' }}
|
|
126
|
+
className="text-2xl font-black tracking-tight"
|
|
127
|
+
>
|
|
128
|
+
{metric.value}
|
|
129
|
+
</Text>
|
|
130
|
+
)}
|
|
131
|
+
<Text
|
|
132
|
+
style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }}
|
|
133
|
+
className="text-[9px] font-semibold mt-0.5"
|
|
134
|
+
numberOfLines={1}
|
|
135
|
+
>
|
|
136
|
+
{metric.subtext || item.report?.description || 'Synchronized Metric'}
|
|
137
|
+
</Text>
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
</View>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const KpiMetricsGrid: React.FC<KpiMetricsGridProps> = ({
|
|
145
|
+
details,
|
|
146
|
+
executionData,
|
|
147
|
+
loading = false,
|
|
148
|
+
}) => {
|
|
149
|
+
const isDark = useColorScheme() === 'dark';
|
|
150
|
+
const cardDetails = details.filter(
|
|
151
|
+
(d) => d.display_size === 'XS' || d.report?.report_type === 'CARD'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (cardDetails.length === 0) return null;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<View className="mb-3.5">
|
|
158
|
+
<View className="flex-row items-center justify-between mb-2">
|
|
159
|
+
<Text
|
|
160
|
+
style={{ color: isDark ? '#9CA3AF' : '#94A3B8' }}
|
|
161
|
+
className="text-[10px] font-extrabold uppercase tracking-wider"
|
|
162
|
+
>
|
|
163
|
+
Key Performance Overview
|
|
164
|
+
</Text>
|
|
165
|
+
<Text className="text-[10px] font-bold text-violet-600 dark:text-violet-400">
|
|
166
|
+
{cardDetails.length} Metrics Live
|
|
167
|
+
</Text>
|
|
168
|
+
</View>
|
|
169
|
+
|
|
170
|
+
<View className="flex-row flex-wrap -mx-1">
|
|
171
|
+
{cardDetails.map((item, index) => (
|
|
172
|
+
<KpiCardItem
|
|
173
|
+
key={item.id}
|
|
174
|
+
item={item}
|
|
175
|
+
index={index}
|
|
176
|
+
data={executionData[item.report_id] || []}
|
|
177
|
+
loading={loading}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</View>
|
|
181
|
+
</View>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ScrollView,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { BottomModal, ModalTitle } from '@suflon/native-ui';
|
|
9
|
+
|
|
10
|
+
export interface FilterValues {
|
|
11
|
+
doctor: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ManagementFilterModalProps {
|
|
15
|
+
visible: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
onApply: (filters: FilterValues) => void;
|
|
18
|
+
onReset: () => void;
|
|
19
|
+
currentFilters?: FilterValues;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DOCTORS = [
|
|
23
|
+
'ALL',
|
|
24
|
+
'william s smith',
|
|
25
|
+
'William A Smith',
|
|
26
|
+
'Usman Shaikh',
|
|
27
|
+
'Usman Bin Shaikh',
|
|
28
|
+
'Amit Kumar Sharma',
|
|
29
|
+
'Aarif Ab Chaudhary',
|
|
30
|
+
'Sohail Shaikh',
|
|
31
|
+
'Prod test doctor',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const ManagementFilterModal: React.FC<ManagementFilterModalProps> = ({
|
|
35
|
+
visible,
|
|
36
|
+
onClose,
|
|
37
|
+
onApply,
|
|
38
|
+
onReset,
|
|
39
|
+
currentFilters,
|
|
40
|
+
}) => {
|
|
41
|
+
const [selectedDoctor, setSelectedDoctor] = useState<string>(
|
|
42
|
+
currentFilters?.doctor || 'ALL'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (currentFilters?.doctor) {
|
|
47
|
+
setSelectedDoctor(currentFilters.doctor);
|
|
48
|
+
}
|
|
49
|
+
}, [currentFilters]);
|
|
50
|
+
|
|
51
|
+
const handleApply = () => {
|
|
52
|
+
onApply({
|
|
53
|
+
doctor: selectedDoctor,
|
|
54
|
+
});
|
|
55
|
+
onClose();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleReset = () => {
|
|
59
|
+
setSelectedDoctor('ALL');
|
|
60
|
+
onReset();
|
|
61
|
+
onClose();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<BottomModal
|
|
66
|
+
isVisible={visible}
|
|
67
|
+
onClose={onClose}
|
|
68
|
+
heightMode="auto"
|
|
69
|
+
>
|
|
70
|
+
<View className="px-5 pb-6 pt-1 space-y-4">
|
|
71
|
+
{/* Header with Title and Close Button */}
|
|
72
|
+
<ModalTitle
|
|
73
|
+
heading="Filter Reports"
|
|
74
|
+
showCloseIcon={true}
|
|
75
|
+
onClose={onClose}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
{/* Doctor Selection Section */}
|
|
79
|
+
<View className="space-y-2">
|
|
80
|
+
<Text className="text-[11px] font-bold uppercase tracking-wider text-slate-400">
|
|
81
|
+
Select Doctor / Staff
|
|
82
|
+
</Text>
|
|
83
|
+
<ScrollView
|
|
84
|
+
horizontal
|
|
85
|
+
showsHorizontalScrollIndicator={false}
|
|
86
|
+
contentContainerStyle={{ gap: 8 }}
|
|
87
|
+
className="py-1"
|
|
88
|
+
>
|
|
89
|
+
{DOCTORS.map((doc) => {
|
|
90
|
+
const isSelected = selectedDoctor.toLowerCase() === doc.toLowerCase();
|
|
91
|
+
return (
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
key={doc}
|
|
94
|
+
onPress={() => setSelectedDoctor(doc)}
|
|
95
|
+
activeOpacity={0.8}
|
|
96
|
+
className={`px-3.5 py-2 rounded-2xl border ${
|
|
97
|
+
isSelected
|
|
98
|
+
? 'bg-violet-600 border-violet-600'
|
|
99
|
+
: 'bg-slate-100 dark:bg-slate-800 border-slate-200 dark:border-slate-700'
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
102
|
+
<Text
|
|
103
|
+
className={`text-xs ${
|
|
104
|
+
isSelected
|
|
105
|
+
? 'font-bold text-white'
|
|
106
|
+
: 'font-medium text-slate-700 dark:text-slate-300'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
{doc === 'ALL' ? 'All Doctors' : doc}
|
|
110
|
+
</Text>
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</ScrollView>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{/* Action Buttons: Apply and Clear */}
|
|
118
|
+
<View className="flex-row items-center gap-3 pt-2">
|
|
119
|
+
<TouchableOpacity
|
|
120
|
+
onPress={handleReset}
|
|
121
|
+
activeOpacity={0.8}
|
|
122
|
+
className="flex-1 py-3 rounded-2xl bg-slate-100 dark:bg-slate-800 items-center justify-center border border-slate-200 dark:border-slate-700"
|
|
123
|
+
>
|
|
124
|
+
<Text className="text-xs font-bold text-slate-700 dark:text-slate-300">
|
|
125
|
+
Clear Filter
|
|
126
|
+
</Text>
|
|
127
|
+
</TouchableOpacity>
|
|
128
|
+
|
|
129
|
+
<TouchableOpacity
|
|
130
|
+
onPress={handleApply}
|
|
131
|
+
activeOpacity={0.8}
|
|
132
|
+
className="flex-1 py-3 rounded-2xl bg-violet-600 items-center justify-center shadow-lg shadow-violet-500/25"
|
|
133
|
+
>
|
|
134
|
+
<Text className="text-xs font-bold text-white">
|
|
135
|
+
Apply Filter
|
|
136
|
+
</Text>
|
|
137
|
+
</TouchableOpacity>
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
</BottomModal>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, TextInput, useColorScheme } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
IManagementReport,
|
|
5
|
+
Icon,
|
|
6
|
+
} from '@suflon/native-ui';
|
|
7
|
+
import { CategoryTabs } from './CategoryTabs';
|
|
8
|
+
import { SubModuleTabs } from './SubModuleTabs';
|
|
9
|
+
import { DoctorFilterDropdown } from './DoctorFilterDropdown';
|
|
10
|
+
|
|
11
|
+
interface ManagementReportHeaderProps {
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
activeCategory: string;
|
|
15
|
+
onSelectCategory: (category: string) => void;
|
|
16
|
+
categoryReports: IManagementReport[];
|
|
17
|
+
selectedReportId: number | null;
|
|
18
|
+
onSelectReport: (id: number) => void;
|
|
19
|
+
searchQuery: string;
|
|
20
|
+
onSearchChange: (query: string) => void;
|
|
21
|
+
onBackPress?: () => void;
|
|
22
|
+
onRefresh?: () => void;
|
|
23
|
+
onExport?: () => void;
|
|
24
|
+
selectedDoctor: string;
|
|
25
|
+
onSelectDoctor: (doctor: string) => void;
|
|
26
|
+
availableDoctors: string[];
|
|
27
|
+
categoryCounts?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ManagementReportHeader: React.FC<ManagementReportHeaderProps> = ({
|
|
31
|
+
title,
|
|
32
|
+
activeCategory,
|
|
33
|
+
onSelectCategory,
|
|
34
|
+
categoryReports,
|
|
35
|
+
selectedReportId,
|
|
36
|
+
onSelectReport,
|
|
37
|
+
searchQuery,
|
|
38
|
+
onSearchChange,
|
|
39
|
+
onBackPress,
|
|
40
|
+
selectedDoctor,
|
|
41
|
+
onSelectDoctor,
|
|
42
|
+
availableDoctors,
|
|
43
|
+
categoryCounts,
|
|
44
|
+
}) => {
|
|
45
|
+
const isDark = useColorScheme() === 'dark';
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<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>
|
|
71
|
+
|
|
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>
|
|
90
|
+
|
|
91
|
+
<DoctorFilterDropdown
|
|
92
|
+
selectedDoctor={selectedDoctor}
|
|
93
|
+
onSelectDoctor={onSelectDoctor}
|
|
94
|
+
availableDoctors={availableDoctors}
|
|
95
|
+
/>
|
|
96
|
+
</View>
|
|
97
|
+
|
|
98
|
+
{/* 3. Category Carousel Tabs (OPD, PRESCRIPTION, SALES, etc.) */}
|
|
99
|
+
<View className="mb-1">
|
|
100
|
+
<CategoryTabs
|
|
101
|
+
activeCategory={activeCategory}
|
|
102
|
+
onSelectCategory={onSelectCategory}
|
|
103
|
+
reportCounts={categoryCounts}
|
|
104
|
+
/>
|
|
105
|
+
</View>
|
|
106
|
+
|
|
107
|
+
{/* 4. Sub-Module Report Tabs (Symptoms, Medicines, Diagnoses, etc.) */}
|
|
108
|
+
<SubModuleTabs
|
|
109
|
+
reports={categoryReports}
|
|
110
|
+
selectedReportId={selectedReportId}
|
|
111
|
+
onSelectReport={onSelectReport}
|
|
112
|
+
/>
|
|
113
|
+
</View>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, TouchableOpacity, Text, View } from 'react-native';
|
|
3
|
+
import { IManagementReport } from '@suflon/native-ui';
|
|
4
|
+
|
|
5
|
+
interface SubModuleTabsProps {
|
|
6
|
+
reports: IManagementReport[];
|
|
7
|
+
selectedReportId: number | null;
|
|
8
|
+
onSelectReport: (id: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SubModuleTabs: React.FC<SubModuleTabsProps> = ({
|
|
12
|
+
reports,
|
|
13
|
+
selectedReportId,
|
|
14
|
+
onSelectReport,
|
|
15
|
+
}) => {
|
|
16
|
+
if (!reports || reports.length <= 1) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View className="py-0.5">
|
|
20
|
+
<ScrollView
|
|
21
|
+
horizontal
|
|
22
|
+
showsHorizontalScrollIndicator={false}
|
|
23
|
+
contentContainerStyle={{ gap: 6 }}
|
|
24
|
+
>
|
|
25
|
+
{reports.map((report) => {
|
|
26
|
+
const isSelected = selectedReportId === report.id;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<TouchableOpacity
|
|
30
|
+
key={report.id}
|
|
31
|
+
onPress={() => onSelectReport(report.id)}
|
|
32
|
+
activeOpacity={0.8}
|
|
33
|
+
style={{
|
|
34
|
+
borderBottomColor: isSelected ? '#7c3aed' : 'transparent',
|
|
35
|
+
borderBottomWidth: isSelected ? 2 : 0,
|
|
36
|
+
}}
|
|
37
|
+
className={`px-2.5 py-1.5 rounded-lg ${
|
|
38
|
+
isSelected
|
|
39
|
+
? 'bg-violet-50 dark:bg-slate-800'
|
|
40
|
+
: 'bg-transparent'
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
<Text
|
|
44
|
+
style={{
|
|
45
|
+
color: isSelected ? '#7c3aed' : '#94a3b8',
|
|
46
|
+
fontWeight: isSelected ? '700' : '500',
|
|
47
|
+
}}
|
|
48
|
+
className="text-xs"
|
|
49
|
+
>
|
|
50
|
+
{report.name}
|
|
51
|
+
</Text>
|
|
52
|
+
</TouchableOpacity>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</ScrollView>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|