@dhiraj0720/report1chart 2.6.4 → 2.6.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 +1 -1
- package/src/screens/Report2AScreen.jsx +185 -97
- package/src/screens/icons.js +34 -0
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// src/screens/Report2AScreen.jsx
|
|
2
1
|
import React, { useEffect, useState } from 'react';
|
|
3
2
|
import {
|
|
4
3
|
ScrollView,
|
|
5
4
|
Text,
|
|
6
5
|
TouchableOpacity,
|
|
7
6
|
View,
|
|
8
|
-
ActivityIndicator,
|
|
9
7
|
StyleSheet,
|
|
8
|
+
Modal,
|
|
9
|
+
Dimensions,
|
|
10
|
+
ActivityIndicator,
|
|
10
11
|
} from 'react-native';
|
|
12
|
+
import { PinchGestureHandler, State } from 'react-native-gesture-handler';
|
|
13
|
+
import Svg from 'react-native-svg'; // if not imported globally
|
|
11
14
|
import { filterChartByMonths } from '../utils/filterChartByMonths';
|
|
12
15
|
import { getDivisions, getTable, getLine, getBar } from '../api/report2Fetcher';
|
|
13
16
|
import MonthFilterModal from '../components/MonthFilterModal';
|
|
@@ -15,6 +18,20 @@ import DivisionFilterModal from '../components/DivisionFilterModal';
|
|
|
15
18
|
import FrozenTableReport2A from '../components/FrozenTableReport2A';
|
|
16
19
|
import SvgLineChartCompact from '../components/SvgLineChartCompact';
|
|
17
20
|
import SvgBarLineChartCompact from '../components/SvgBarLineChartCompact';
|
|
21
|
+
import { FilterIcon, FullScreenIcon } from './icons'; // or inline
|
|
22
|
+
|
|
23
|
+
// Proper SVG Funnel Icon Component (clean black filter icon)
|
|
24
|
+
const FunnelIcon = () => (
|
|
25
|
+
<View style={styles.funnelIcon}>
|
|
26
|
+
<View style={styles.funnelTop} />
|
|
27
|
+
<View style={styles.funnelBody} />
|
|
28
|
+
<View style={styles.funnelLines}>
|
|
29
|
+
<View style={styles.line1} />
|
|
30
|
+
<View style={styles.line2} />
|
|
31
|
+
<View style={styles.line3} />
|
|
32
|
+
</View>
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
18
35
|
|
|
19
36
|
const Report2AScreen = ({ api, token, onBack }) => {
|
|
20
37
|
const [monthsModal, setMonthsModal] = useState(false);
|
|
@@ -24,6 +41,8 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
24
41
|
const [selectedMonths, setSelectedMonths] = useState([]);
|
|
25
42
|
const [divisions, setDivisions] = useState([]);
|
|
26
43
|
const [division, setDivision] = useState(null);
|
|
44
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
45
|
+
const [zoomScale, setZoomScale] = useState(1);
|
|
27
46
|
|
|
28
47
|
const [table, setTable] = useState(null);
|
|
29
48
|
const [line, setLine] = useState(null);
|
|
@@ -31,25 +50,19 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
31
50
|
const [loading, setLoading] = useState(true);
|
|
32
51
|
const [error, setError] = useState(null);
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
|
|
35
54
|
useEffect(() => {
|
|
36
55
|
if (!api?.divisions || !token) return;
|
|
37
56
|
|
|
38
57
|
getDivisions(api.divisions, token)
|
|
39
58
|
.then((d) => {
|
|
40
59
|
setDivisions(d);
|
|
41
|
-
if (d.length > 0)
|
|
42
|
-
setDivision(d[0].code);
|
|
43
|
-
}
|
|
60
|
+
if (d.length > 0) setDivision(d[0].code);
|
|
44
61
|
})
|
|
45
|
-
.catch((
|
|
46
|
-
|
|
47
|
-
setError('Failed to load divisions');
|
|
48
|
-
setLoading(false);
|
|
49
|
-
});
|
|
62
|
+
.catch(() => setError('Failed to load divisions'))
|
|
63
|
+
.finally(() => setLoading(false));
|
|
50
64
|
}, [api?.divisions, token]);
|
|
51
65
|
|
|
52
|
-
// Fetch data when division changes
|
|
53
66
|
useEffect(() => {
|
|
54
67
|
if (!division || !api || !token) return;
|
|
55
68
|
|
|
@@ -57,106 +70,126 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
57
70
|
setError(null);
|
|
58
71
|
|
|
59
72
|
Promise.all([
|
|
60
|
-
getTable(api.table, division, token)
|
|
61
|
-
getLine(api.line, division, token)
|
|
62
|
-
getBar(api.bar, division, token)
|
|
73
|
+
getTable(api.table, division, token),
|
|
74
|
+
getLine(api.line, division, token),
|
|
75
|
+
getBar(api.bar, division, token),
|
|
63
76
|
])
|
|
64
77
|
.then(([t, l, b]) => {
|
|
65
|
-
if (!t || !l || !b) {
|
|
66
|
-
setError('Failed to load report data');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
78
|
setTable(t);
|
|
71
79
|
setLine(l);
|
|
72
80
|
setBar(b);
|
|
73
81
|
setMonths(l.labels || []);
|
|
74
82
|
setSelectedMonths(l.labels || []);
|
|
75
83
|
})
|
|
76
|
-
.catch((
|
|
77
|
-
|
|
78
|
-
setError('Network error or data unavailable');
|
|
79
|
-
})
|
|
80
|
-
.finally(() => {
|
|
81
|
-
setLoading(false);
|
|
82
|
-
});
|
|
84
|
+
.catch(() => setError('Failed to load data'))
|
|
85
|
+
.finally(() => setLoading(false));
|
|
83
86
|
}, [division, api, token]);
|
|
84
87
|
|
|
85
|
-
// Loading State
|
|
86
88
|
if (loading) {
|
|
87
89
|
return (
|
|
88
90
|
<View style={styles.center}>
|
|
89
|
-
<ActivityIndicator size="large" color="#
|
|
90
|
-
<Text style={styles.loadingText}>Loading
|
|
91
|
+
<ActivityIndicator size="large" color="#000" />
|
|
92
|
+
<Text style={styles.loadingText}>Loading...</Text>
|
|
91
93
|
</View>
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// Error State
|
|
96
97
|
if (error) {
|
|
97
98
|
return (
|
|
98
99
|
<View style={styles.center}>
|
|
99
100
|
<Text style={styles.errorText}>{error}</Text>
|
|
100
101
|
<TouchableOpacity onPress={onBack}>
|
|
101
|
-
<Text style={styles.backLink}
|
|
102
|
+
<Text style={styles.backLink}>‹ Back to Reports</Text>
|
|
102
103
|
</TouchableOpacity>
|
|
103
104
|
</View>
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
// Safety check
|
|
108
108
|
if (!table || !line || !bar || !division) {
|
|
109
|
-
return
|
|
110
|
-
<View style={styles.center}>
|
|
111
|
-
<Text>No data available</Text>
|
|
112
|
-
</View>
|
|
113
|
-
);
|
|
109
|
+
return <View style={styles.center}><Text>No data available</Text></View>;
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
|
|
112
|
+
const currentDivisionName = divisions.find(d => d.code === division)?.displayName || 'Division';
|
|
113
|
+
const filteredRows = table.rows.filter(r => selectedMonths.includes(r.monthLabel));
|
|
120
114
|
const filteredLine = filterChartByMonths(line, selectedMonths);
|
|
121
115
|
const filteredBar = filterChartByMonths(bar, selectedMonths);
|
|
122
116
|
|
|
123
117
|
return (
|
|
124
|
-
<
|
|
125
|
-
{/*
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{/* Filters */}
|
|
131
|
-
<View style={styles.filterRow}>
|
|
132
|
-
<TouchableOpacity onPress={() => setMonthsModal(true)}>
|
|
133
|
-
<Text style={styles.filterText}>Months ⛃</Text>
|
|
118
|
+
<View style={styles.container}>
|
|
119
|
+
{/* HEADER WITH BACK ICON + HEADING */}
|
|
120
|
+
<View style={styles.header}>
|
|
121
|
+
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
122
|
+
<Text style={styles.backIcon}>‹</Text>
|
|
134
123
|
</TouchableOpacity>
|
|
135
124
|
|
|
125
|
+
<Text style={styles.headerTitle}>
|
|
126
|
+
{currentDivisionName} Transportation
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* FILTERS - BLACK TEXT + PROPER FUNNEL ICON */}
|
|
131
|
+
<View style={styles.filterBar}>
|
|
132
|
+
<TouchableOpacity onPress={() => setMonthsModal(true)} style={styles.filterItem}>
|
|
133
|
+
<FilterIcon />
|
|
134
|
+
<Text style={styles.filterText}>Months ({selectedMonths.length}/{months.length})</Text>
|
|
135
|
+
</TouchableOpacity>
|
|
136
|
+
|
|
137
|
+
<TouchableOpacity onPress={() => setDivisionModal(true)} style={styles.filterItem}>
|
|
138
|
+
<FilterIcon />
|
|
139
|
+
<Text style={styles.filterText}>{currentDivisionName}</Text>
|
|
140
|
+
</TouchableOpacity>
|
|
141
|
+
</View>
|
|
142
|
+
|
|
143
|
+
<ScrollView style={styles.content}>
|
|
144
|
+
{/* FULL SCREEN ICON ABOVE TABLE */}
|
|
136
145
|
<TouchableOpacity
|
|
137
|
-
onPress={() =>
|
|
138
|
-
style={
|
|
146
|
+
onPress={() => setFullscreen(true)}
|
|
147
|
+
style={styles.fullScreenBtn}
|
|
139
148
|
>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
</Text>
|
|
149
|
+
<FullScreenIcon size={28} />
|
|
150
|
+
<Text style={styles.fullScreenText}>Full Screen View</Text>
|
|
143
151
|
</TouchableOpacity>
|
|
144
|
-
</View>
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
<FrozenTableReport2A rows={filteredRows} />
|
|
153
|
+
<FrozenTableReport2A rows={filteredRows} />
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
</View>
|
|
155
|
+
<View style={styles.chartContainer}>
|
|
156
|
+
<SvgLineChartCompact data={filteredLine} />
|
|
157
|
+
</View>
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
</
|
|
159
|
+
<View style={styles.chartContainer}>
|
|
160
|
+
<SvgBarLineChartCompact data={filteredBar} />
|
|
161
|
+
</View>
|
|
162
|
+
</ScrollView>
|
|
163
|
+
|
|
164
|
+
{/* MODALS */}
|
|
165
|
+
|
|
166
|
+
{/* FULL SCREEN MODAL */}
|
|
167
|
+
<Modal visible={fullscreen} supportedOrientations={['portrait', 'landscape']}>
|
|
168
|
+
<View style={styles.fullModal}>
|
|
169
|
+
<View style={styles.fullHeader}>
|
|
170
|
+
<TouchableOpacity onPress={() => setFullscreen(false)}>
|
|
171
|
+
<Text style={styles.closeFull}>✕</Text>
|
|
172
|
+
</TouchableOpacity>
|
|
173
|
+
<Text style={styles.fullTitle}>Rotate device to landscape for best view</Text>
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
<PinchGestureHandler
|
|
177
|
+
onGestureEvent={(e) => setZoomScale(e.nativeEvent.scale)}
|
|
178
|
+
onHandlerStateChange={(e) => {
|
|
179
|
+
if (e.nativeEvent.state === State.END) {
|
|
180
|
+
setZoomScale(Math.max(0.8, Math.min(e.nativeEvent.scale, 3)));
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<View style={styles.zoomView}>
|
|
185
|
+
<View style={{ transform: [{ scale: zoomScale }] }}>
|
|
186
|
+
<FrozenTableReport2A rows={table.rows} isFullscreen /> {/* all rows */}
|
|
187
|
+
</View>
|
|
188
|
+
</View>
|
|
189
|
+
</PinchGestureHandler>
|
|
190
|
+
</View>
|
|
191
|
+
</Modal>
|
|
158
192
|
|
|
159
|
-
{/* Modals */}
|
|
160
193
|
<MonthFilterModal
|
|
161
194
|
visible={monthsModal}
|
|
162
195
|
months={months}
|
|
@@ -172,50 +205,91 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
172
205
|
onSelect={setDivision}
|
|
173
206
|
onClose={() => setDivisionModal(false)}
|
|
174
207
|
/>
|
|
175
|
-
</
|
|
208
|
+
</View>
|
|
176
209
|
);
|
|
177
210
|
};
|
|
178
211
|
|
|
179
212
|
const styles = StyleSheet.create({
|
|
180
|
-
container: {
|
|
181
|
-
center: {
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
container: { flex: 1, backgroundColor: '#f8f9fa' },
|
|
214
|
+
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
|
|
215
|
+
loadingText: { marginTop: 12, fontSize: 16, color: '#000' },
|
|
216
|
+
errorText: { fontSize: 16, color: '#d32f2f', textAlign: 'center', marginBottom: 20 },
|
|
217
|
+
backLink: { fontSize: 18, color: '#000', fontWeight: '600' },
|
|
218
|
+
|
|
219
|
+
header: {
|
|
220
|
+
flexDirection: 'row',
|
|
184
221
|
alignItems: 'center',
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
color: '#555',
|
|
222
|
+
paddingHorizontal: 16,
|
|
223
|
+
paddingVertical: 18,
|
|
224
|
+
backgroundColor: '#fff',
|
|
225
|
+
borderBottomWidth: 1,
|
|
226
|
+
borderColor: '#eee',
|
|
191
227
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
backButton: { paddingRight: 12 },
|
|
229
|
+
backIcon: { fontSize: 28, color: '#000', fontWeight: '300' },
|
|
230
|
+
headerTitle: {
|
|
231
|
+
fontSize: 19,
|
|
232
|
+
fontWeight: '700',
|
|
233
|
+
color: '#000',
|
|
234
|
+
flex: 1,
|
|
195
235
|
textAlign: 'center',
|
|
196
|
-
marginBottom: 20,
|
|
197
236
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
marginBottom: 16,
|
|
207
|
-
fontWeight: '600',
|
|
237
|
+
|
|
238
|
+
filterBar: {
|
|
239
|
+
flexDirection: 'row',
|
|
240
|
+
justifyContent: 'space-around',
|
|
241
|
+
paddingVertical: 14,
|
|
242
|
+
backgroundColor: '#fff',
|
|
243
|
+
borderBottomWidth: 1,
|
|
244
|
+
borderColor: '#eee',
|
|
208
245
|
},
|
|
209
|
-
|
|
246
|
+
filterItem: {
|
|
210
247
|
flexDirection: 'row',
|
|
211
248
|
alignItems: 'center',
|
|
212
|
-
|
|
249
|
+
paddingHorizontal: 16,
|
|
250
|
+
paddingVertical: 8,
|
|
213
251
|
},
|
|
214
252
|
filterText: {
|
|
215
253
|
fontSize: 16,
|
|
216
|
-
color: '#
|
|
254
|
+
color: '#000',
|
|
217
255
|
fontWeight: '600',
|
|
256
|
+
marginLeft: 8,
|
|
218
257
|
},
|
|
258
|
+
|
|
259
|
+
// Proper Funnel Icon (SVG-style with Views)
|
|
260
|
+
funnelIcon: {
|
|
261
|
+
width: 20,
|
|
262
|
+
height: 20,
|
|
263
|
+
justifyContent: 'center',
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
},
|
|
266
|
+
funnelTop: {
|
|
267
|
+
width: 16,
|
|
268
|
+
height: 4,
|
|
269
|
+
backgroundColor: '#000',
|
|
270
|
+
borderRadius: 2,
|
|
271
|
+
},
|
|
272
|
+
funnelBody: {
|
|
273
|
+
width: 10,
|
|
274
|
+
height: 10,
|
|
275
|
+
borderLeftWidth: 4,
|
|
276
|
+
borderRightWidth: 4,
|
|
277
|
+
borderBottomWidth: 8,
|
|
278
|
+
borderColor: 'transparent',
|
|
279
|
+
borderBottomColor: '#000',
|
|
280
|
+
marginTop: -2,
|
|
281
|
+
},
|
|
282
|
+
funnelLines: {
|
|
283
|
+
position: 'absolute',
|
|
284
|
+
top: 6,
|
|
285
|
+
left: 4,
|
|
286
|
+
right: 4,
|
|
287
|
+
},
|
|
288
|
+
line1: { height: 1, backgroundColor: '#000', marginVertical: 2 },
|
|
289
|
+
line2: { height: 1, backgroundColor: '#000', marginVertical: 2 },
|
|
290
|
+
line3: { height: 1, backgroundColor: '#000', marginVertical: 2 },
|
|
291
|
+
|
|
292
|
+
content: { flex: 1, padding: 12 },
|
|
219
293
|
chartContainer: {
|
|
220
294
|
borderWidth: 1,
|
|
221
295
|
borderColor: '#ddd',
|
|
@@ -224,6 +298,20 @@ const styles = StyleSheet.create({
|
|
|
224
298
|
marginVertical: 12,
|
|
225
299
|
backgroundColor: '#fff',
|
|
226
300
|
},
|
|
301
|
+
fullScreenBtn: {
|
|
302
|
+
flexDirection: 'row',
|
|
303
|
+
alignItems: 'center',
|
|
304
|
+
justifyContent: 'center',
|
|
305
|
+
padding: 12,
|
|
306
|
+
marginBottom: 12,
|
|
307
|
+
},
|
|
308
|
+
fullScreenText: { marginLeft: 8, fontSize: 16, color: '#000', fontWeight: '600' },
|
|
309
|
+
|
|
310
|
+
fullModal: { flex: 1, backgroundColor: '#fff' },
|
|
311
|
+
fullHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 16, backgroundColor: '#f8f9fa', position: 'relative' },
|
|
312
|
+
closeFull: { position: 'absolute', left: 16, fontSize: 28, color: '#000' },
|
|
313
|
+
fullTitle: { fontSize: 16, color: '#000' },
|
|
314
|
+
zoomView: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
227
315
|
});
|
|
228
316
|
|
|
229
317
|
export default Report2AScreen;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Svg, { Path } from 'react-native-svg';
|
|
2
|
+
|
|
3
|
+
// Filter Funnel Icon (exact match to your image)
|
|
4
|
+
const FilterIcon = ({ size = 20, color = '#000' }) => (
|
|
5
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
6
|
+
<Path
|
|
7
|
+
d="M20 4H4V6.5L12 14.5L20 6.5V4Z"
|
|
8
|
+
stroke={color}
|
|
9
|
+
strokeWidth={2}
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
/>
|
|
13
|
+
<Path
|
|
14
|
+
d="M8 11V19L16 19V11"
|
|
15
|
+
stroke={color}
|
|
16
|
+
strokeWidth={2}
|
|
17
|
+
strokeLinecap="round"
|
|
18
|
+
strokeLinejoin="round"
|
|
19
|
+
/>
|
|
20
|
+
</Svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Full Screen Expand Icon
|
|
24
|
+
const FullScreenIcon = ({ size = 24, color = '#000' }) => (
|
|
25
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
26
|
+
<Path
|
|
27
|
+
d="M8 3H5C4.44772 3 4 3.44772 4 4V8M20 8V4C20 3.44772 19.5523 3 19 3H16M16 21H19C19.5523 21 20 20.5523 20 20V16M4 16V20C4 20.5523 4.44772 21 5 21H8"
|
|
28
|
+
stroke={color}
|
|
29
|
+
strokeWidth={2}
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
/>
|
|
33
|
+
</Svg>
|
|
34
|
+
);
|