@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,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
+ };