@dhiraj0720/report1chart 2.5.4 → 2.5.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": "@dhiraj0720/report1chart",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "main": "src/index.jsx",
5
5
  "scripts": {
6
6
  "test": "echo 'No tests'"
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { Modal, View, Text, TouchableOpacity, ScrollView } from 'react-native';
3
+
4
+ const DivisionFilterModal = ({ visible, divisions, selected, onSelect, onClose }) => (
5
+ <Modal visible={visible} transparent animationType="fade">
6
+ <View style={{ flex: 1, backgroundColor: '#0008', justifyContent: 'center' }}>
7
+ <View style={{ backgroundColor: '#fff', margin: 20, borderRadius: 12, padding: 16 }}>
8
+ <Text style={{ fontWeight: '700', marginBottom: 8 }}>Select Division</Text>
9
+
10
+ <ScrollView style={{ maxHeight: 260 }}>
11
+ {divisions.map(d => (
12
+ <TouchableOpacity
13
+ key={d.code}
14
+ onPress={() => { onSelect(d.code); onClose(); }}
15
+ >
16
+ <Text style={{ padding: 8, fontWeight: selected === d.code ? '700' : '400' }}>
17
+ {selected === d.code ? '✓ ' : ''}{d.displayName}
18
+ </Text>
19
+ </TouchableOpacity>
20
+ ))}
21
+ </ScrollView>
22
+ </View>
23
+ </View>
24
+ </Modal>
25
+ );
26
+
27
+ export default DivisionFilterModal;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { View, Text, ScrollView, StyleSheet } from 'react-native';
3
+ import { formatNumber } from '../utils/formatNumber';
4
+
5
+ const Arrow = ({ v }) => (
6
+ <Text style={{ color: v >= 0 ? 'green' : 'red', fontWeight: '700' }}>
7
+ {v >= 0 ? '↑' : '↓'} {Math.abs(v)}%
8
+ </Text>
9
+ );
10
+
11
+ const Cell = ({ children, bold }) => (
12
+ <View style={styles.cell}>
13
+ <Text numberOfLines={1} style={bold && styles.bold}>{children}</Text>
14
+ </View>
15
+ );
16
+
17
+ const FrozenTableReport2A = ({ rows }) => (
18
+ <View style={styles.container}>
19
+ <View style={styles.frozen}>
20
+ <Cell bold>AY</Cell>
21
+ {rows.map((r, i) => <Cell key={i} bold>{r.monthLabel}</Cell>)}
22
+ </View>
23
+
24
+ <ScrollView horizontal>
25
+ <View>
26
+ <View style={styles.header}>
27
+ <Cell bold>2024</Cell>
28
+ <Cell bold>2025</Cell>
29
+ <Cell bold>%</Cell>
30
+ </View>
31
+
32
+ {rows.map((r, i) => (
33
+ <View key={i} style={styles.row}>
34
+ <Cell>{formatNumber(r.teu2024)}</Cell>
35
+ <Cell>{formatNumber(r.teu2025)}</Cell>
36
+ <Cell><Arrow v={r.teuChangePercent} /></Cell>
37
+ </View>
38
+ ))}
39
+ </View>
40
+ </ScrollView>
41
+ </View>
42
+ );
43
+
44
+ export default FrozenTableReport2A;
45
+
46
+ const styles = StyleSheet.create({
47
+ container: { flexDirection: 'row', marginVertical: 8 },
48
+ frozen: { width: 90, backgroundColor: '#f4f4f4' },
49
+ header: { flexDirection: 'row', backgroundColor: '#f4f4f4' },
50
+ row: { flexDirection: 'row' },
51
+ cell: {
52
+ width: 90,
53
+ padding: 6,
54
+ borderBottomWidth: 1,
55
+ borderColor: '#ddd',
56
+ justifyContent: 'center',
57
+ alignItems: 'center',
58
+ },
59
+ bold: { fontWeight: '700' },
60
+ });
@@ -7,7 +7,7 @@ import {
7
7
  StyleSheet,
8
8
  SafeAreaView,
9
9
  } from 'react-native';
10
- import SafeScreen from './components/SafeScreen';
10
+ import SafeScreen from './SafeScreen';
11
11
  import FrozenTableReport1A from './FrozenTableReport1A';
12
12
 
13
13
  const FullScreenTableModal = ({ visible, rows, onClose }) => {
@@ -0,0 +1,47 @@
1
+ import React, { useState } from 'react';
2
+ import { Modal, View, Text, TouchableOpacity, ScrollView } from 'react-native';
3
+
4
+ const MonthFilterModal = ({ visible, months, selected, onApply, onClose }) => {
5
+ const [local, setLocal] = useState(selected);
6
+
7
+ const toggle = (m) => {
8
+ if (local.includes(m)) {
9
+ setLocal(local.filter(x => x !== m));
10
+ } else {
11
+ setLocal([...local, m]);
12
+ }
13
+ };
14
+
15
+ return (
16
+ <Modal visible={visible} transparent animationType="fade">
17
+ <View style={{ flex: 1, backgroundColor: '#0008', justifyContent: 'center' }}>
18
+ <View style={{ backgroundColor: '#fff', margin: 20, borderRadius: 12, padding: 16 }}>
19
+ <Text style={{ fontWeight: '700', marginBottom: 8 }}>Select Months</Text>
20
+
21
+ <TouchableOpacity onPress={() => setLocal(months)}>
22
+ <Text style={{ color: '#1e88e5', marginBottom: 8 }}>Select All</Text>
23
+ </TouchableOpacity>
24
+
25
+ <ScrollView style={{ maxHeight: 260 }}>
26
+ {months.map(m => (
27
+ <TouchableOpacity key={m} onPress={() => toggle(m)}>
28
+ <Text style={{ padding: 8, fontWeight: local.includes(m) ? '700' : '400' }}>
29
+ {local.includes(m) ? '✓ ' : ''}{m}
30
+ </Text>
31
+ </TouchableOpacity>
32
+ ))}
33
+ </ScrollView>
34
+
35
+ <TouchableOpacity
36
+ style={{ marginTop: 12, alignSelf: 'flex-end' }}
37
+ onPress={() => { onApply(local); onClose(); }}
38
+ >
39
+ <Text style={{ fontWeight: '700' }}>Apply</Text>
40
+ </TouchableOpacity>
41
+ </View>
42
+ </View>
43
+ </Modal>
44
+ );
45
+ };
46
+
47
+ export default MonthFilterModal;
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import { View, Text, ScrollView, StyleSheet } from 'react-native';
3
+ import Svg, { Rect, Line, Circle, Text as SvgText } from 'react-native-svg';
4
+ import { formatNumber } from '../utils/formatNumber';
5
+
6
+ const SvgBarLineChartCompact = ({ data }) => {
7
+ if (!data?.series || !data.labels?.length) return null;
8
+
9
+ const height = 220;
10
+ const graphWidth = Math.max(320, data.labels.length * 80);
11
+
12
+ const paddingLeft = 50;
13
+ const paddingRight = 10;
14
+ const paddingTop = 20;
15
+ const paddingBottom = 35;
16
+
17
+ const chartHeight = height - paddingTop - paddingBottom;
18
+
19
+ const MAX = 1000000; // 1.0M
20
+ const barWidth = 14;
21
+
22
+ const y = (v) =>
23
+ paddingTop + ((MAX - v) / MAX) * chartHeight;
24
+
25
+ return (
26
+ <View style={styles.container}>
27
+ <Text style={styles.title}>{data.title}</Text>
28
+
29
+ <View style={{ flexDirection: 'row' }}>
30
+ {/* FIXED Y AXIS */}
31
+ <Svg width={paddingLeft} height={height}>
32
+ {[1000000, 500000, 0].map((v, i) => (
33
+ <SvgText
34
+ key={i}
35
+ x={paddingLeft - 6}
36
+ y={y(v) + 4}
37
+ fontSize="10"
38
+ textAnchor="end"
39
+ fill="#444"
40
+ >
41
+ {(v / 1_000_000).toFixed(1)}M
42
+ </SvgText>
43
+ ))}
44
+ </Svg>
45
+
46
+ {/* SCROLLABLE GRAPH */}
47
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
48
+ <Svg width={graphWidth} height={height}>
49
+ {/* GRID */}
50
+ {[1000000, 500000, 0].map((v, i) => (
51
+ <Line
52
+ key={i}
53
+ x1={0}
54
+ x2={graphWidth}
55
+ y1={y(v)}
56
+ y2={y(v)}
57
+ stroke="#ccc"
58
+ strokeDasharray="4"
59
+ />
60
+ ))}
61
+
62
+ {data.labels.map((label, i) => {
63
+ const x = paddingLeft + i * 70;
64
+
65
+ return (
66
+ <React.Fragment key={i}>
67
+ {/* 2024 BAR */}
68
+ <Rect
69
+ x={x}
70
+ y={y(data.series[0].data[i])}
71
+ width={barWidth}
72
+ height={chartHeight - (y(data.series[0].data[i]) - paddingTop)}
73
+ fill="#E07A3F"
74
+ />
75
+
76
+ {/* 2025 BAR */}
77
+ <Rect
78
+ x={x + barWidth + 4}
79
+ y={y(data.series[1].data[i])}
80
+ width={barWidth}
81
+ height={chartHeight - (y(data.series[1].data[i]) - paddingTop)}
82
+ fill="#4E79A7"
83
+ />
84
+
85
+ {/* BUDGET DOT + LINE */}
86
+ <Circle
87
+ cx={x + barWidth}
88
+ cy={y(data.series[2].data[i])}
89
+ r={4}
90
+ fill="#8AB6E8"
91
+ />
92
+
93
+ {/* VALUE */}
94
+ <SvgText
95
+ x={x + barWidth}
96
+ y={y(data.series[2].data[i]) - 8}
97
+ fontSize="9"
98
+ textAnchor="middle"
99
+ >
100
+ {formatNumber(data.series[2].data[i])}
101
+ </SvgText>
102
+
103
+ {/* X LABEL */}
104
+ <SvgText
105
+ x={x + barWidth}
106
+ y={height - 10}
107
+ fontSize="9"
108
+ textAnchor="middle"
109
+ transform={`rotate(-30 ${x + barWidth} ${height - 10})`}
110
+ >
111
+ {label}
112
+ </SvgText>
113
+ </React.Fragment>
114
+ );
115
+ })}
116
+ </Svg>
117
+ </ScrollView>
118
+ </View>
119
+
120
+ {/* LEGEND */}
121
+ <View style={styles.legend}>
122
+ <Text style={{ color: '#E07A3F' }}>● {data.series[0].name}</Text>
123
+ <Text style={{ color: '#4E79A7', marginLeft: 12 }}>
124
+ ● {data.series[1].name}
125
+ </Text>
126
+ <Text style={{ color: '#8AB6E8', marginLeft: 12 }}>
127
+ ● {data.series[2].name}
128
+ </Text>
129
+ </View>
130
+ </View>
131
+ );
132
+ };
133
+
134
+ export default SvgBarLineChartCompact;
135
+
136
+ const styles = StyleSheet.create({
137
+ container: { marginVertical: 6 },
138
+ title: {
139
+ fontWeight: '700',
140
+ marginBottom: 4,
141
+ fontSize: 13,
142
+ },
143
+ legend: {
144
+ flexDirection: 'row',
145
+ marginTop: 4,
146
+ flexWrap: 'wrap',
147
+ },
148
+ });
@@ -0,0 +1,156 @@
1
+ import React from 'react';
2
+ import { View, Text, ScrollView, StyleSheet } from 'react-native';
3
+ import Svg, { Line, Circle, Text as SvgText } from 'react-native-svg';
4
+ import { formatNumber } from '../utils/formatNumber';
5
+
6
+ const SvgLineChartCompact = ({ data }) => {
7
+ if (!data?.series || !data.labels?.length) return null;
8
+
9
+ const height = 200;
10
+ const graphWidth = Math.max(320, data.labels.length * 70);
11
+
12
+ const paddingLeft = 50; // Y axis space
13
+ const paddingRight = 10;
14
+ const paddingTop = 20;
15
+ const paddingBottom = 35;
16
+
17
+ // FIXED AXIS (as per image)
18
+ const MIN = 5000;
19
+ const MAX = 10000;
20
+
21
+ const chartHeight = height - paddingTop - paddingBottom;
22
+ const xStep =
23
+ data.labels.length === 1
24
+ ? 0
25
+ : (graphWidth - paddingLeft - paddingRight) /
26
+ (data.labels.length - 1);
27
+
28
+ const y = (v) =>
29
+ paddingTop +
30
+ ((MAX - v) / (MAX - MIN)) * chartHeight;
31
+
32
+ const colors = ['#E07A3F', '#4E79A7']; // orange / blue
33
+
34
+ return (
35
+ <View style={styles.container}>
36
+ <Text style={styles.title}>{data.title}</Text>
37
+
38
+ <View style={{ flexDirection: 'row' }}>
39
+ {/* FIXED Y AXIS */}
40
+ <Svg width={paddingLeft} height={height}>
41
+ {[10000, 5000].map((v, i) => (
42
+ <SvgText
43
+ key={i}
44
+ x={paddingLeft - 6}
45
+ y={y(v) + 4}
46
+ fontSize="10"
47
+ textAnchor="end"
48
+ fill="#444"
49
+ >
50
+ {formatNumber(v)}
51
+ </SvgText>
52
+ ))}
53
+ </Svg>
54
+
55
+ {/* SCROLLABLE GRAPH */}
56
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
57
+ <Svg width={graphWidth} height={height}>
58
+ {/* GRID LINES */}
59
+ {[10000, 5000].map((v, i) => (
60
+ <Line
61
+ key={i}
62
+ x1={0}
63
+ x2={graphWidth}
64
+ y1={y(v)}
65
+ y2={y(v)}
66
+ stroke="#ccc"
67
+ strokeDasharray="4"
68
+ />
69
+ ))}
70
+
71
+ {/* LINES + DOTS */}
72
+ {data.series.map((s, si) =>
73
+ s.data.map((val, i) => {
74
+ const x =
75
+ paddingLeft + (data.labels.length === 1 ? 0 : i * xStep);
76
+ const yVal = y(val);
77
+
78
+ return (
79
+ <React.Fragment key={`${si}-${i}`}>
80
+ {/* CONNECTING LINE */}
81
+ {i < s.data.length - 1 && (
82
+ <Line
83
+ x1={x}
84
+ y1={yVal}
85
+ x2={paddingLeft + (i + 1) * xStep}
86
+ y2={y(s.data[i + 1])}
87
+ stroke={colors[si]}
88
+ strokeWidth={2}
89
+ strokeDasharray={si === 0 ? '4' : '0'}
90
+ />
91
+ )}
92
+
93
+ {/* DOT */}
94
+ <Circle cx={x} cy={yVal} r={4} fill={colors[si]} />
95
+
96
+ {/* VALUE */}
97
+ <SvgText
98
+ x={x}
99
+ y={yVal - 8}
100
+ fontSize="9"
101
+ textAnchor="middle"
102
+ fill="#333"
103
+ >
104
+ {formatNumber(val)}
105
+ </SvgText>
106
+ </React.Fragment>
107
+ );
108
+ })
109
+ )}
110
+
111
+ {/* X AXIS LABELS */}
112
+ {data.labels.map((label, i) => {
113
+ const x =
114
+ paddingLeft + (data.labels.length === 1 ? 0 : i * xStep);
115
+ return (
116
+ <SvgText
117
+ key={i}
118
+ x={x}
119
+ y={height - 10}
120
+ fontSize="9"
121
+ textAnchor="middle"
122
+ transform={`rotate(-30 ${x} ${height - 10})`}
123
+ >
124
+ {label}
125
+ </SvgText>
126
+ );
127
+ })}
128
+ </Svg>
129
+ </ScrollView>
130
+ </View>
131
+
132
+ {/* LEGEND */}
133
+ <View style={styles.legend}>
134
+ <Text style={{ color: colors[0] }}>● {data.series[0].name}</Text>
135
+ <Text style={{ color: colors[1], marginLeft: 16 }}>
136
+ ● {data.series[1].name}
137
+ </Text>
138
+ </View>
139
+ </View>
140
+ );
141
+ };
142
+
143
+ export default SvgLineChartCompact;
144
+
145
+ const styles = StyleSheet.create({
146
+ container: { marginVertical: 6 },
147
+ title: {
148
+ fontWeight: '700',
149
+ marginBottom: 4,
150
+ fontSize: 13,
151
+ },
152
+ legend: {
153
+ flexDirection: 'row',
154
+ marginTop: 4,
155
+ },
156
+ });
package/src/index.jsx CHANGED
@@ -6,6 +6,7 @@ import Report1Screen from './screens/Report1Screen';
6
6
  import Report2Screen from './screens/Report2Screen';
7
7
  import Report3Screen from './screens/Report3Screen';
8
8
  import Report1AScreen from './screens/Report1AScreen';
9
+ import Report2AScreen from './screens/Report2AScreen';
9
10
 
10
11
  const AnalyticsReports = ({ config, onExit }) => {
11
12
  const [active, setActive] = useState(null);
@@ -70,6 +71,17 @@ const AnalyticsReports = ({ config, onExit }) => {
70
71
  );
71
72
  }
72
73
 
74
+ if (active === '2A') {
75
+ return (
76
+ <SafeScreen>
77
+ <Report2AScreen
78
+ endpoint={config.report1.url}
79
+ token={config.token}
80
+ onBack={() => setActive(null)}
81
+ />
82
+ </SafeScreen>
83
+ );
84
+ }
73
85
 
74
86
  // 👉 REPORT 2
75
87
  if (active === 2) {
@@ -0,0 +1,99 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
+
4
+ import { getDivisions, getTable, getLine, getBar } from '../api/report2Fetcher';
5
+ import MonthFilterModal from '../components/MonthFilterModal';
6
+ import DivisionFilterModal from '../components/DivisionFilterModal';
7
+ import FrozenTableReport2A from '../components/FrozenTableReport2A';
8
+ import SvgLineChartCompact from '../components/SvgLineChartCompact';
9
+ import SvgBarLineChartCompact from '../components/SvgBarLineChartCompact';
10
+
11
+ const Report2AScreen = ({ api, token, onBack }) => {
12
+ const [monthsModal, setMonthsModal] = useState(false);
13
+ const [divisionModal, setDivisionModal] = useState(false);
14
+
15
+ const [months, setMonths] = useState([]);
16
+ const [selectedMonths, setSelectedMonths] = useState([]);
17
+ const [divisions, setDivisions] = useState([]);
18
+ const [division, setDivision] = useState(null);
19
+
20
+ const [table, setTable] = useState(null);
21
+ const [line, setLine] = useState(null);
22
+ const [bar, setBar] = useState(null);
23
+
24
+ useEffect(() => {
25
+ getDivisions(api.divisions, token).then(d => {
26
+ setDivisions(d);
27
+ setDivision(d[0]?.code);
28
+ });
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ if (!division) return;
33
+ Promise.all([
34
+ getTable(api.table, division, token),
35
+ getLine(api.line, division, token),
36
+ getBar(api.bar, division, token),
37
+ ]).then(([t, l, b]) => {
38
+ setTable(t);
39
+ setLine(l);
40
+ setBar(b);
41
+ setMonths(l.labels);
42
+ setSelectedMonths(l.labels);
43
+ });
44
+ }, [division]);
45
+
46
+ if (!table || !line || !bar) return null;
47
+
48
+ const rows = table.rows.filter(r =>
49
+ selectedMonths.includes(r.monthLabel)
50
+ );
51
+
52
+ return (
53
+ <ScrollView style={{ padding: 12 }}>
54
+ <Text onPress={onBack}>‹ Back</Text>
55
+
56
+ {/* FILTER BAR */}
57
+ <View style={{ flexDirection: 'row', marginVertical: 6 }}>
58
+ <TouchableOpacity onPress={() => setMonthsModal(true)}>
59
+ <Text>Months ⛃</Text>
60
+ </TouchableOpacity>
61
+
62
+ <TouchableOpacity
63
+ onPress={() => setDivisionModal(true)}
64
+ style={{ marginLeft: 16 }}
65
+ >
66
+ <Text>Divisions ⛃</Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+
70
+ <FrozenTableReport2A rows={rows} />
71
+
72
+ <View style={{ borderTopWidth: 1, borderBottomWidth: 1, marginVertical: 6 }}>
73
+ <SvgLineChartCompact data={line} />
74
+ </View>
75
+
76
+ <View style={{ borderTopWidth: 1, borderBottomWidth: 1, marginVertical: 6 }}>
77
+ <SvgBarLineChartCompact data={bar} />
78
+ </View>
79
+
80
+ <MonthFilterModal
81
+ visible={monthsModal}
82
+ months={months}
83
+ selected={selectedMonths}
84
+ onApply={setSelectedMonths}
85
+ onClose={() => setMonthsModal(false)}
86
+ />
87
+
88
+ <DivisionFilterModal
89
+ visible={divisionModal}
90
+ divisions={divisions}
91
+ selected={division}
92
+ onSelect={setDivision}
93
+ onClose={() => setDivisionModal(false)}
94
+ />
95
+ </ScrollView>
96
+ );
97
+ };
98
+
99
+ export default Report2AScreen;
@@ -1,12 +1,4 @@
1
- export const formatNumber = (value) => {
2
- if (value >= 1_000_000_000) {
3
- return (value / 1_000_000_000).toFixed(1) + 'bn';
4
- }
5
- if (value >= 1_000_000) {
6
- return (value / 1_000_000).toFixed(1) + 'M';
7
- }
8
- if (value >= 1_000) {
9
- return (value / 1_000).toFixed(0) + 'k';
10
- }
11
- return value.toString();
1
+ export const formatNumber = (v) => {
2
+ if (v === null || v === undefined) return '-';
3
+ return Number(v).toLocaleString('en-US');
12
4
  };