@dhiraj0720/report1chart 3.0.8 → 3.1.0
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/components/CompactAxisBarLineChart.jsx +17 -7
- package/src/components/CompareOptionsModal.jsx +135 -0
- package/src/components/ModernDataTable.jsx +131 -29
- package/src/screens/Report1ModernScreen.jsx +261 -5
- package/src/screens/Report2ModernScreen.jsx +222 -2
- package/src/screens/Report3ModernScreen.jsx +221 -2
- package/src/utils/filterChartByMonths.js +10 -1
package/package.json
CHANGED
|
@@ -58,6 +58,7 @@ const CompactAxisBarLineChart = ({
|
|
|
58
58
|
maxValue,
|
|
59
59
|
barWidth,
|
|
60
60
|
barGap,
|
|
61
|
+
groupSidePadding,
|
|
61
62
|
} = useMemo(() => {
|
|
62
63
|
const allValues = [...barSeriesA, ...barSeriesB, ...lineSeries].map(toNumber);
|
|
63
64
|
const peak = Math.max(1, ...allValues);
|
|
@@ -66,7 +67,13 @@ const CompactAxisBarLineChart = ({
|
|
|
66
67
|
const computedPaddingTop = 14;
|
|
67
68
|
const computedPaddingBottom = 42;
|
|
68
69
|
const computedChartHeight = height - computedPaddingTop - computedPaddingBottom;
|
|
69
|
-
const
|
|
70
|
+
const computedBarWidth = 20;
|
|
71
|
+
const computedBarGap = 4;
|
|
72
|
+
const computedSidePadding = computedBarWidth + computedBarGap / 2 + 4;
|
|
73
|
+
const computedGraphWidth = Math.max(
|
|
74
|
+
minGraphWidth,
|
|
75
|
+
labels.length * groupSpacing + computedSidePadding * 2,
|
|
76
|
+
);
|
|
70
77
|
|
|
71
78
|
return {
|
|
72
79
|
graphWidth: computedGraphWidth,
|
|
@@ -76,8 +83,9 @@ const CompactAxisBarLineChart = ({
|
|
|
76
83
|
paddingTop: computedPaddingTop,
|
|
77
84
|
chartHeight: computedChartHeight,
|
|
78
85
|
maxValue: peak,
|
|
79
|
-
barWidth:
|
|
80
|
-
barGap:
|
|
86
|
+
barWidth: computedBarWidth,
|
|
87
|
+
barGap: computedBarGap,
|
|
88
|
+
groupSidePadding: computedSidePadding,
|
|
81
89
|
};
|
|
82
90
|
}, [barSeriesA, barSeriesB, groupSpacing, height, labels.length, lineSeries, minGraphWidth]);
|
|
83
91
|
|
|
@@ -86,10 +94,12 @@ const CompactAxisBarLineChart = ({
|
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
const groupCenterX = (index) => {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
const startX = paddingLeft + groupSidePadding;
|
|
98
|
+
const endX = graphWidth - paddingRight - groupSidePadding;
|
|
99
|
+
const usableWidth = Math.max(0, endX - startX);
|
|
100
|
+
const step = labels.length > 1 ? usableWidth / (labels.length - 1) : 0;
|
|
101
|
+
if (labels.length <= 1) return (startX + endX) / 2;
|
|
102
|
+
return startX + index * step;
|
|
93
103
|
};
|
|
94
104
|
|
|
95
105
|
const legendItems = legend || [
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
ScrollView,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
const CompareOptionsModal = ({
|
|
13
|
+
visible,
|
|
14
|
+
title = 'Select Comparison',
|
|
15
|
+
options = [],
|
|
16
|
+
selected,
|
|
17
|
+
onApply,
|
|
18
|
+
onClose,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<Modal visible={visible} transparent animationType="fade">
|
|
22
|
+
<TouchableWithoutFeedback onPress={onClose}>
|
|
23
|
+
<View style={styles.overlay}>
|
|
24
|
+
<TouchableWithoutFeedback>
|
|
25
|
+
<View style={styles.modalContent}>
|
|
26
|
+
<View style={styles.header}>
|
|
27
|
+
<Text style={styles.title}>{title}</Text>
|
|
28
|
+
<TouchableOpacity onPress={onClose}>
|
|
29
|
+
<Text style={styles.closeIcon}>X</Text>
|
|
30
|
+
</TouchableOpacity>
|
|
31
|
+
</View>
|
|
32
|
+
|
|
33
|
+
<ScrollView style={styles.list}>
|
|
34
|
+
{options.map((option) => {
|
|
35
|
+
const active = selected === option.key;
|
|
36
|
+
return (
|
|
37
|
+
<TouchableOpacity
|
|
38
|
+
key={option.key}
|
|
39
|
+
style={[styles.item, active && styles.itemActive]}
|
|
40
|
+
onPress={() => {
|
|
41
|
+
onApply(option.key);
|
|
42
|
+
onClose();
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<Text style={[styles.itemTitle, active && styles.itemTitleActive]}>
|
|
46
|
+
{active ? '✓ ' : ' '}
|
|
47
|
+
{option.label}
|
|
48
|
+
</Text>
|
|
49
|
+
{option.description ? (
|
|
50
|
+
<Text style={[styles.itemDesc, active && styles.itemDescActive]}>
|
|
51
|
+
{option.description}
|
|
52
|
+
</Text>
|
|
53
|
+
) : null}
|
|
54
|
+
</TouchableOpacity>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</ScrollView>
|
|
58
|
+
</View>
|
|
59
|
+
</TouchableWithoutFeedback>
|
|
60
|
+
</View>
|
|
61
|
+
</TouchableWithoutFeedback>
|
|
62
|
+
</Modal>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const styles = StyleSheet.create({
|
|
67
|
+
overlay: {
|
|
68
|
+
flex: 1,
|
|
69
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
},
|
|
73
|
+
modalContent: {
|
|
74
|
+
width: '86%',
|
|
75
|
+
maxHeight: '80%',
|
|
76
|
+
backgroundColor: '#fff',
|
|
77
|
+
borderRadius: 12,
|
|
78
|
+
padding: 16,
|
|
79
|
+
shadowColor: '#000',
|
|
80
|
+
shadowOffset: { width: 0, height: 4 },
|
|
81
|
+
shadowOpacity: 0.3,
|
|
82
|
+
shadowRadius: 8,
|
|
83
|
+
elevation: 10,
|
|
84
|
+
},
|
|
85
|
+
header: {
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
justifyContent: 'space-between',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
marginBottom: 14,
|
|
90
|
+
},
|
|
91
|
+
title: {
|
|
92
|
+
fontSize: 18,
|
|
93
|
+
fontWeight: '700',
|
|
94
|
+
color: '#111',
|
|
95
|
+
},
|
|
96
|
+
closeIcon: {
|
|
97
|
+
fontSize: 24,
|
|
98
|
+
color: '#222',
|
|
99
|
+
fontWeight: '300',
|
|
100
|
+
},
|
|
101
|
+
list: {
|
|
102
|
+
maxHeight: 360,
|
|
103
|
+
},
|
|
104
|
+
item: {
|
|
105
|
+
borderWidth: 1,
|
|
106
|
+
borderColor: '#d9e1ee',
|
|
107
|
+
borderRadius: 10,
|
|
108
|
+
paddingVertical: 10,
|
|
109
|
+
paddingHorizontal: 10,
|
|
110
|
+
marginBottom: 10,
|
|
111
|
+
backgroundColor: '#fff',
|
|
112
|
+
},
|
|
113
|
+
itemActive: {
|
|
114
|
+
borderColor: '#2f6fb8',
|
|
115
|
+
backgroundColor: '#edf5ff',
|
|
116
|
+
},
|
|
117
|
+
itemTitle: {
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
fontWeight: '700',
|
|
120
|
+
color: '#1a2f49',
|
|
121
|
+
},
|
|
122
|
+
itemTitleActive: {
|
|
123
|
+
color: '#0f4f92',
|
|
124
|
+
},
|
|
125
|
+
itemDesc: {
|
|
126
|
+
marginTop: 3,
|
|
127
|
+
fontSize: 12,
|
|
128
|
+
color: '#5a6880',
|
|
129
|
+
},
|
|
130
|
+
itemDescActive: {
|
|
131
|
+
color: '#2c598f',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export default CompareOptionsModal;
|
|
@@ -17,7 +17,46 @@ const normalizeCell = (value) => {
|
|
|
17
17
|
return { text: String(value), color: '#2b3850', weight: '600' };
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const renderCell = (col, row, rowIndex, compact) => {
|
|
21
|
+
const raw = col.render ? col.render(row) : row[col.key];
|
|
22
|
+
const cell = normalizeCell(raw);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
key={`cell-${rowIndex}-${col.key}`}
|
|
27
|
+
style={[
|
|
28
|
+
styles.cell,
|
|
29
|
+
{ width: col.width || (compact ? 110 : 126) },
|
|
30
|
+
col.align === 'right' && styles.alignRight,
|
|
31
|
+
]}
|
|
32
|
+
>
|
|
33
|
+
<Text
|
|
34
|
+
numberOfLines={1}
|
|
35
|
+
ellipsizeMode="tail"
|
|
36
|
+
style={[
|
|
37
|
+
styles.cellText,
|
|
38
|
+
{ color: cell.color, fontWeight: cell.weight },
|
|
39
|
+
col.align === 'right' && styles.textRight,
|
|
40
|
+
col.align === 'center' && styles.textCenter,
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
{cell.text}
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ModernDataTable = ({
|
|
50
|
+
title,
|
|
51
|
+
columns = [],
|
|
52
|
+
rows = [],
|
|
53
|
+
compact = false,
|
|
54
|
+
freezeFirstColumn = false,
|
|
55
|
+
}) => {
|
|
56
|
+
const firstColumn = columns[0];
|
|
57
|
+
const scrollColumns = columns.slice(1);
|
|
58
|
+
const shouldFreeze = freezeFirstColumn && columns.length > 1;
|
|
59
|
+
|
|
21
60
|
return (
|
|
22
61
|
<View style={styles.card}>
|
|
23
62
|
<Text style={styles.title}>{title}</Text>
|
|
@@ -26,6 +65,84 @@ const ModernDataTable = ({ title, columns = [], rows = [], compact = false }) =>
|
|
|
26
65
|
<View style={styles.emptyWrap}>
|
|
27
66
|
<Text style={styles.emptyText}>No table data for selected filters.</Text>
|
|
28
67
|
</View>
|
|
68
|
+
) : shouldFreeze ? (
|
|
69
|
+
<View style={styles.freezeWrap}>
|
|
70
|
+
<View style={styles.frozenSide}>
|
|
71
|
+
<View style={styles.headerRow}>
|
|
72
|
+
<View
|
|
73
|
+
style={[
|
|
74
|
+
styles.cell,
|
|
75
|
+
styles.headerCell,
|
|
76
|
+
styles.frozenCell,
|
|
77
|
+
{ width: firstColumn.width || (compact ? 110 : 126) },
|
|
78
|
+
firstColumn.align === 'right' && styles.alignRight,
|
|
79
|
+
]}
|
|
80
|
+
>
|
|
81
|
+
<Text
|
|
82
|
+
style={[
|
|
83
|
+
styles.headerText,
|
|
84
|
+
firstColumn.align === 'right' && styles.textRight,
|
|
85
|
+
firstColumn.align === 'center' && styles.textCenter,
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
{firstColumn.label}
|
|
89
|
+
</Text>
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
|
|
93
|
+
{rows.map((row, rowIndex) => (
|
|
94
|
+
<View
|
|
95
|
+
key={`frozen-row-${rowIndex}`}
|
|
96
|
+
style={[
|
|
97
|
+
styles.dataRow,
|
|
98
|
+
rowIndex % 2 === 0 ? styles.rowEven : styles.rowOdd,
|
|
99
|
+
]}
|
|
100
|
+
>
|
|
101
|
+
{renderCell(firstColumn, row, rowIndex, compact)}
|
|
102
|
+
</View>
|
|
103
|
+
))}
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
107
|
+
<View>
|
|
108
|
+
<View style={styles.headerRow}>
|
|
109
|
+
{scrollColumns.map((col) => (
|
|
110
|
+
<View
|
|
111
|
+
key={`header-${col.key}`}
|
|
112
|
+
style={[
|
|
113
|
+
styles.cell,
|
|
114
|
+
styles.headerCell,
|
|
115
|
+
{ width: col.width || (compact ? 110 : 126) },
|
|
116
|
+
col.align === 'right' && styles.alignRight,
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
<Text
|
|
120
|
+
style={[
|
|
121
|
+
styles.headerText,
|
|
122
|
+
col.align === 'right' && styles.textRight,
|
|
123
|
+
col.align === 'center' && styles.textCenter,
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{col.label}
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
))}
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
{rows.map((row, rowIndex) => (
|
|
133
|
+
<View
|
|
134
|
+
key={`row-${rowIndex}`}
|
|
135
|
+
style={[
|
|
136
|
+
styles.dataRow,
|
|
137
|
+
rowIndex % 2 === 0 ? styles.rowEven : styles.rowOdd,
|
|
138
|
+
]}
|
|
139
|
+
>
|
|
140
|
+
{scrollColumns.map((col) => renderCell(col, row, rowIndex, compact))}
|
|
141
|
+
</View>
|
|
142
|
+
))}
|
|
143
|
+
</View>
|
|
144
|
+
</ScrollView>
|
|
145
|
+
</View>
|
|
29
146
|
) : (
|
|
30
147
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
31
148
|
<View>
|
|
@@ -61,34 +178,7 @@ const ModernDataTable = ({ title, columns = [], rows = [], compact = false }) =>
|
|
|
61
178
|
rowIndex % 2 === 0 ? styles.rowEven : styles.rowOdd,
|
|
62
179
|
]}
|
|
63
180
|
>
|
|
64
|
-
{columns.map((col) =>
|
|
65
|
-
const raw = col.render ? col.render(row) : row[col.key];
|
|
66
|
-
const cell = normalizeCell(raw);
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<View
|
|
70
|
-
key={`cell-${rowIndex}-${col.key}`}
|
|
71
|
-
style={[
|
|
72
|
-
styles.cell,
|
|
73
|
-
{ width: col.width || (compact ? 110 : 126) },
|
|
74
|
-
col.align === 'right' && styles.alignRight,
|
|
75
|
-
]}
|
|
76
|
-
>
|
|
77
|
-
<Text
|
|
78
|
-
numberOfLines={1}
|
|
79
|
-
ellipsizeMode="tail"
|
|
80
|
-
style={[
|
|
81
|
-
styles.cellText,
|
|
82
|
-
{ color: cell.color, fontWeight: cell.weight },
|
|
83
|
-
col.align === 'right' && styles.textRight,
|
|
84
|
-
col.align === 'center' && styles.textCenter,
|
|
85
|
-
]}
|
|
86
|
-
>
|
|
87
|
-
{cell.text}
|
|
88
|
-
</Text>
|
|
89
|
-
</View>
|
|
90
|
-
);
|
|
91
|
-
})}
|
|
181
|
+
{columns.map((col) => renderCell(col, row, rowIndex, compact))}
|
|
92
182
|
</View>
|
|
93
183
|
))}
|
|
94
184
|
</View>
|
|
@@ -113,6 +203,15 @@ const styles = StyleSheet.create({
|
|
|
113
203
|
color: '#1c2f47',
|
|
114
204
|
marginBottom: 8,
|
|
115
205
|
},
|
|
206
|
+
freezeWrap: {
|
|
207
|
+
flexDirection: 'row',
|
|
208
|
+
},
|
|
209
|
+
frozenSide: {
|
|
210
|
+
borderRightWidth: 1,
|
|
211
|
+
borderRightColor: '#c7d7ef',
|
|
212
|
+
backgroundColor: '#fff',
|
|
213
|
+
zIndex: 2,
|
|
214
|
+
},
|
|
116
215
|
headerRow: {
|
|
117
216
|
flexDirection: 'row',
|
|
118
217
|
backgroundColor: '#1f385a',
|
|
@@ -145,6 +244,9 @@ const styles = StyleSheet.create({
|
|
|
145
244
|
borderRightColor: '#e2ebf7',
|
|
146
245
|
justifyContent: 'center',
|
|
147
246
|
},
|
|
247
|
+
frozenCell: {
|
|
248
|
+
borderRightColor: '#c7d7ef',
|
|
249
|
+
},
|
|
148
250
|
cellText: {
|
|
149
251
|
fontSize: 11.5,
|
|
150
252
|
color: '#2b3850',
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
View,
|
|
9
9
|
} from 'react-native';
|
|
10
10
|
import fetchReport4 from '../api/report4Fetcher';
|
|
11
|
+
import CompactAxisBarLineChart from '../components/CompactAxisBarLineChart';
|
|
12
|
+
import CompactAxisLineChart from '../components/CompactAxisLineChart';
|
|
13
|
+
import CompareOptionsModal from '../components/CompareOptionsModal';
|
|
11
14
|
import ModernDataTable from '../components/ModernDataTable';
|
|
12
15
|
import { formatNumber } from '../utils/formatNumber';
|
|
13
16
|
|
|
@@ -17,6 +20,24 @@ const TABS = [
|
|
|
17
20
|
{ key: 'fg', label: 'FG / BRÜT KAR ORANI' },
|
|
18
21
|
];
|
|
19
22
|
|
|
23
|
+
const COMPARE_OPTIONS = [
|
|
24
|
+
{
|
|
25
|
+
key: 'value_yoy',
|
|
26
|
+
label: '2025 Value vs 2024 Value',
|
|
27
|
+
description: 'Total value comparison in selected tab',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'budget_gap',
|
|
31
|
+
label: '2025 Value vs 2025 Budget',
|
|
32
|
+
description: 'Actual against budget on selected tab',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'top_vs_avg',
|
|
36
|
+
label: 'Top Activity vs Average',
|
|
37
|
+
description: 'Best activity compared with average 2025 value',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
20
41
|
const toNumber = (value) => {
|
|
21
42
|
const numeric = Number(value);
|
|
22
43
|
return Number.isFinite(numeric) ? numeric : 0;
|
|
@@ -27,6 +48,13 @@ const toPercent = (current, previous) => {
|
|
|
27
48
|
return ((current - previous) / Math.abs(previous)) * 100;
|
|
28
49
|
};
|
|
29
50
|
|
|
51
|
+
const toShortLabel = (value) => {
|
|
52
|
+
const text = String(value || '').trim();
|
|
53
|
+
if (!text) return '-';
|
|
54
|
+
if (text.length <= 12) return text;
|
|
55
|
+
return `${text.slice(0, 11)}...`;
|
|
56
|
+
};
|
|
57
|
+
|
|
30
58
|
const asPercentCell = (value) => {
|
|
31
59
|
const numeric = toNumber(value);
|
|
32
60
|
const positive = numeric >= 0;
|
|
@@ -81,6 +109,8 @@ const mapRowByTab = (row, tabKey) => {
|
|
|
81
109
|
const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
82
110
|
const [activeTab, setActiveTab] = useState('kumule');
|
|
83
111
|
const [sortMode, setSortMode] = useState('growth');
|
|
112
|
+
const [compareModal, setCompareModal] = useState(false);
|
|
113
|
+
const [compareMode, setCompareMode] = useState('value_yoy');
|
|
84
114
|
const [rowsByTab, setRowsByTab] = useState({});
|
|
85
115
|
const [loadedByTab, setLoadedByTab] = useState({});
|
|
86
116
|
const [errorByTab, setErrorByTab] = useState({});
|
|
@@ -103,10 +133,6 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
103
133
|
return;
|
|
104
134
|
}
|
|
105
135
|
|
|
106
|
-
if (!force && loadedByTab[tabKey]) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
136
|
setLoading(true);
|
|
111
137
|
setErrorByTab((prev) => ({ ...prev, [tabKey]: null }));
|
|
112
138
|
|
|
@@ -126,7 +152,7 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
126
152
|
} finally {
|
|
127
153
|
setLoading(false);
|
|
128
154
|
}
|
|
129
|
-
}, [endpointByTab,
|
|
155
|
+
}, [endpointByTab, token]);
|
|
130
156
|
|
|
131
157
|
useEffect(() => {
|
|
132
158
|
loadTabData(activeTab);
|
|
@@ -169,6 +195,68 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
169
195
|
}, [sortedRows]);
|
|
170
196
|
|
|
171
197
|
const spotlight = sortedRows[0];
|
|
198
|
+
const compareLabel = COMPARE_OPTIONS.find((item) => item.key === compareMode)?.label || 'Select compare mode';
|
|
199
|
+
|
|
200
|
+
const chartRows = useMemo(() => sortedRows.slice(0, 10), [sortedRows]);
|
|
201
|
+
const chartLabels = useMemo(() => chartRows.map((row) => toShortLabel(row.name)), [chartRows]);
|
|
202
|
+
|
|
203
|
+
const lineChartData = useMemo(() => {
|
|
204
|
+
if (!chartRows.length) return null;
|
|
205
|
+
return {
|
|
206
|
+
labels: chartLabels,
|
|
207
|
+
series: [
|
|
208
|
+
{ name: '2024 Value', data: chartRows.map((row) => row.value2024) },
|
|
209
|
+
{ name: '2025 Value', data: chartRows.map((row) => row.value2025) },
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}, [chartLabels, chartRows]);
|
|
213
|
+
|
|
214
|
+
const barChartData = useMemo(() => {
|
|
215
|
+
if (!chartRows.length) return null;
|
|
216
|
+
return {
|
|
217
|
+
labels: chartLabels,
|
|
218
|
+
series: [
|
|
219
|
+
{ name: '2024 Value', data: chartRows.map((row) => row.value2024) },
|
|
220
|
+
{ name: '2025 Value', data: chartRows.map((row) => row.value2025) },
|
|
221
|
+
{ name: '2025 Budget', data: chartRows.map((row) => row.budget2025) },
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}, [chartLabels, chartRows]);
|
|
225
|
+
|
|
226
|
+
const compareSummary = useMemo(() => {
|
|
227
|
+
if (compareMode === 'budget_gap') {
|
|
228
|
+
return {
|
|
229
|
+
title: '2025 Value vs 2025 Budget',
|
|
230
|
+
primaryLabel: '2025 Value',
|
|
231
|
+
primaryValue: totals.total2025,
|
|
232
|
+
secondaryLabel: '2025 Budget',
|
|
233
|
+
secondaryValue: totals.budget2025,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (compareMode === 'top_vs_avg') {
|
|
238
|
+
const topValue = spotlight?.value2025 || 0;
|
|
239
|
+
const avgValue = sortedRows.length ? totals.total2025 / sortedRows.length : 0;
|
|
240
|
+
return {
|
|
241
|
+
title: 'Top Activity vs Average (2025)',
|
|
242
|
+
primaryLabel: spotlight?.name || 'Top Activity',
|
|
243
|
+
primaryValue: topValue,
|
|
244
|
+
secondaryLabel: 'Average Activity',
|
|
245
|
+
secondaryValue: avgValue,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
title: '2025 Value vs 2024 Value',
|
|
251
|
+
primaryLabel: '2025 Value',
|
|
252
|
+
primaryValue: totals.total2025,
|
|
253
|
+
secondaryLabel: '2024 Value',
|
|
254
|
+
secondaryValue: totals.total2024,
|
|
255
|
+
};
|
|
256
|
+
}, [compareMode, sortedRows.length, spotlight, totals.budget2025, totals.total2024, totals.total2025]);
|
|
257
|
+
|
|
258
|
+
const compareMax = Math.max(1, compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
259
|
+
const compareDelta = toPercent(compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
172
260
|
|
|
173
261
|
return (
|
|
174
262
|
<View style={styles.screen}>
|
|
@@ -222,6 +310,11 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
222
310
|
|
|
223
311
|
{sortedRows.length ? (
|
|
224
312
|
<>
|
|
313
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
314
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
315
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
316
|
+
</TouchableOpacity>
|
|
317
|
+
|
|
225
318
|
<View style={styles.kpiRow}>
|
|
226
319
|
<View style={[styles.kpiCard, styles.kpiWarm]}>
|
|
227
320
|
<Text style={styles.kpiLabel}>2024 Value</Text>
|
|
@@ -257,6 +350,65 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
257
350
|
</View>
|
|
258
351
|
</View>
|
|
259
352
|
|
|
353
|
+
<View style={styles.compareCard}>
|
|
354
|
+
<View style={styles.compareTitleRow}>
|
|
355
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
356
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
357
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
358
|
+
{compareDelta.toFixed(1)}%
|
|
359
|
+
</Text>
|
|
360
|
+
</View>
|
|
361
|
+
|
|
362
|
+
<View style={styles.compareMetric}>
|
|
363
|
+
<View style={styles.compareMetricHead}>
|
|
364
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
365
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.primaryValue)}</Text>
|
|
366
|
+
</View>
|
|
367
|
+
<View style={styles.compareTrack}>
|
|
368
|
+
<View
|
|
369
|
+
style={[
|
|
370
|
+
styles.compareFillPrimary,
|
|
371
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
372
|
+
]}
|
|
373
|
+
/>
|
|
374
|
+
</View>
|
|
375
|
+
</View>
|
|
376
|
+
|
|
377
|
+
<View style={styles.compareMetric}>
|
|
378
|
+
<View style={styles.compareMetricHead}>
|
|
379
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
380
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.secondaryValue)}</Text>
|
|
381
|
+
</View>
|
|
382
|
+
<View style={styles.compareTrack}>
|
|
383
|
+
<View
|
|
384
|
+
style={[
|
|
385
|
+
styles.compareFillSecondary,
|
|
386
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
387
|
+
]}
|
|
388
|
+
/>
|
|
389
|
+
</View>
|
|
390
|
+
</View>
|
|
391
|
+
</View>
|
|
392
|
+
|
|
393
|
+
<CompactAxisLineChart
|
|
394
|
+
data={lineChartData}
|
|
395
|
+
title="Activity Trend (2024 vs 2025)"
|
|
396
|
+
legend={[
|
|
397
|
+
{ label: lineChartData?.series?.[0]?.name || '2024 Value', color: '#F29F45' },
|
|
398
|
+
{ label: lineChartData?.series?.[1]?.name || '2025 Value', color: '#2E7DD1' },
|
|
399
|
+
]}
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
<CompactAxisBarLineChart
|
|
403
|
+
data={barChartData}
|
|
404
|
+
title="2025 Value vs Budget by Activity"
|
|
405
|
+
legend={[
|
|
406
|
+
{ label: barChartData?.series?.[0]?.name || '2024 Value', color: '#F29F45' },
|
|
407
|
+
{ label: barChartData?.series?.[1]?.name || '2025 Value', color: '#2E7DD1' },
|
|
408
|
+
{ label: barChartData?.series?.[2]?.name || '2025 Budget', color: '#8AB6E8' },
|
|
409
|
+
]}
|
|
410
|
+
/>
|
|
411
|
+
|
|
260
412
|
{spotlight ? (
|
|
261
413
|
<View style={styles.spotlightCard}>
|
|
262
414
|
<Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
|
|
@@ -331,6 +483,7 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
331
483
|
|
|
332
484
|
<ModernDataTable
|
|
333
485
|
title="Performance Table"
|
|
486
|
+
freezeFirstColumn
|
|
334
487
|
columns={[
|
|
335
488
|
{ key: 'name', label: 'Activity', width: 150 },
|
|
336
489
|
{ key: 'value2024', label: '2024', width: 120, align: 'right', render: (row) => formatNumber(row.value2024) },
|
|
@@ -360,6 +513,15 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
360
513
|
|
|
361
514
|
<View style={styles.footerSpacing} />
|
|
362
515
|
</ScrollView>
|
|
516
|
+
|
|
517
|
+
<CompareOptionsModal
|
|
518
|
+
visible={compareModal}
|
|
519
|
+
title="Compare Performance"
|
|
520
|
+
options={COMPARE_OPTIONS}
|
|
521
|
+
selected={compareMode}
|
|
522
|
+
onApply={setCompareMode}
|
|
523
|
+
onClose={() => setCompareModal(false)}
|
|
524
|
+
/>
|
|
363
525
|
</View>
|
|
364
526
|
);
|
|
365
527
|
};
|
|
@@ -453,6 +615,26 @@ const styles = StyleSheet.create({
|
|
|
453
615
|
paddingHorizontal: 14,
|
|
454
616
|
paddingTop: 14,
|
|
455
617
|
},
|
|
618
|
+
compareButton: {
|
|
619
|
+
backgroundColor: '#1a2640',
|
|
620
|
+
borderColor: '#2f3d5f',
|
|
621
|
+
borderWidth: 1,
|
|
622
|
+
borderRadius: 12,
|
|
623
|
+
paddingVertical: 10,
|
|
624
|
+
paddingHorizontal: 12,
|
|
625
|
+
marginBottom: 10,
|
|
626
|
+
},
|
|
627
|
+
compareButtonLabel: {
|
|
628
|
+
fontSize: 11,
|
|
629
|
+
color: '#9eb1d3',
|
|
630
|
+
marginBottom: 4,
|
|
631
|
+
fontWeight: '700',
|
|
632
|
+
},
|
|
633
|
+
compareButtonValue: {
|
|
634
|
+
fontSize: 13,
|
|
635
|
+
color: '#ffffff',
|
|
636
|
+
fontWeight: '700',
|
|
637
|
+
},
|
|
456
638
|
center: {
|
|
457
639
|
paddingVertical: 24,
|
|
458
640
|
alignItems: 'center',
|
|
@@ -531,6 +713,80 @@ const styles = StyleSheet.create({
|
|
|
531
713
|
fontWeight: '800',
|
|
532
714
|
fontSize: 13,
|
|
533
715
|
},
|
|
716
|
+
compareCard: {
|
|
717
|
+
backgroundColor: '#1a2640',
|
|
718
|
+
borderColor: '#303d58',
|
|
719
|
+
borderWidth: 1,
|
|
720
|
+
borderRadius: 14,
|
|
721
|
+
paddingHorizontal: 12,
|
|
722
|
+
paddingVertical: 12,
|
|
723
|
+
marginBottom: 10,
|
|
724
|
+
},
|
|
725
|
+
compareTitleRow: {
|
|
726
|
+
flexDirection: 'row',
|
|
727
|
+
justifyContent: 'space-between',
|
|
728
|
+
alignItems: 'center',
|
|
729
|
+
marginBottom: 8,
|
|
730
|
+
},
|
|
731
|
+
compareTitle: {
|
|
732
|
+
color: '#d9e3fa',
|
|
733
|
+
fontSize: 12,
|
|
734
|
+
fontWeight: '700',
|
|
735
|
+
flex: 1,
|
|
736
|
+
marginRight: 8,
|
|
737
|
+
},
|
|
738
|
+
compareDelta: {
|
|
739
|
+
fontSize: 12,
|
|
740
|
+
fontWeight: '800',
|
|
741
|
+
paddingHorizontal: 8,
|
|
742
|
+
paddingVertical: 4,
|
|
743
|
+
borderRadius: 999,
|
|
744
|
+
},
|
|
745
|
+
compareDeltaUp: {
|
|
746
|
+
color: '#7ff0c8',
|
|
747
|
+
backgroundColor: '#1d463a',
|
|
748
|
+
},
|
|
749
|
+
compareDeltaDown: {
|
|
750
|
+
color: '#ff9ca6',
|
|
751
|
+
backgroundColor: '#522d34',
|
|
752
|
+
},
|
|
753
|
+
compareMetric: {
|
|
754
|
+
marginTop: 6,
|
|
755
|
+
},
|
|
756
|
+
compareMetricHead: {
|
|
757
|
+
flexDirection: 'row',
|
|
758
|
+
justifyContent: 'space-between',
|
|
759
|
+
alignItems: 'center',
|
|
760
|
+
marginBottom: 4,
|
|
761
|
+
},
|
|
762
|
+
compareMetricLabel: {
|
|
763
|
+
color: '#b9cae8',
|
|
764
|
+
fontSize: 11,
|
|
765
|
+
flex: 1,
|
|
766
|
+
marginRight: 8,
|
|
767
|
+
},
|
|
768
|
+
compareMetricValue: {
|
|
769
|
+
color: '#fff',
|
|
770
|
+
fontSize: 12,
|
|
771
|
+
fontWeight: '700',
|
|
772
|
+
},
|
|
773
|
+
compareTrack: {
|
|
774
|
+
width: '100%',
|
|
775
|
+
height: 9,
|
|
776
|
+
borderRadius: 999,
|
|
777
|
+
backgroundColor: '#2a3652',
|
|
778
|
+
overflow: 'hidden',
|
|
779
|
+
},
|
|
780
|
+
compareFillPrimary: {
|
|
781
|
+
height: '100%',
|
|
782
|
+
borderRadius: 999,
|
|
783
|
+
backgroundColor: '#2E7DD1',
|
|
784
|
+
},
|
|
785
|
+
compareFillSecondary: {
|
|
786
|
+
height: '100%',
|
|
787
|
+
borderRadius: 999,
|
|
788
|
+
backgroundColor: '#F29F45',
|
|
789
|
+
},
|
|
534
790
|
spotlightCard: {
|
|
535
791
|
backgroundColor: '#1a2640',
|
|
536
792
|
borderColor: '#303d58',
|
|
@@ -13,6 +13,7 @@ import DivisionFilterModal from '../components/DivisionFilterModal';
|
|
|
13
13
|
import MonthFilterModal from '../components/MonthFilterModal';
|
|
14
14
|
import CompactAxisLineChart from '../components/CompactAxisLineChart';
|
|
15
15
|
import CompactAxisBarLineChart from '../components/CompactAxisBarLineChart';
|
|
16
|
+
import CompareOptionsModal from '../components/CompareOptionsModal';
|
|
16
17
|
import ModernDataTable from '../components/ModernDataTable';
|
|
17
18
|
import { filterChartByMonths } from '../utils/filterChartByMonths';
|
|
18
19
|
import { formatNumber } from '../utils/formatNumber';
|
|
@@ -27,6 +28,32 @@ const toPercent = (current, previous) => {
|
|
|
27
28
|
return ((current - previous) / Math.abs(previous)) * 100;
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
const toMonthKey = (value) => {
|
|
32
|
+
return String(value ?? '')
|
|
33
|
+
.trim()
|
|
34
|
+
.toLocaleUpperCase('tr-TR')
|
|
35
|
+
.normalize('NFD')
|
|
36
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const COMPARE_OPTIONS = [
|
|
40
|
+
{
|
|
41
|
+
key: 'profit_yoy',
|
|
42
|
+
label: '2025 Profit vs 2024 Profit',
|
|
43
|
+
description: 'Compare total profit values by selected filters',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'profit_budget',
|
|
47
|
+
label: '2025 Profit vs 2025 Budget',
|
|
48
|
+
description: 'Compare actual profit against budget',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'teu_yoy',
|
|
52
|
+
label: '2025 TEU vs 2024 TEU',
|
|
53
|
+
description: 'Compare total TEU movement year-over-year',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
30
57
|
const TrendBadge = ({ value }) => {
|
|
31
58
|
const positive = value >= 0;
|
|
32
59
|
return (
|
|
@@ -69,6 +96,8 @@ const MetricCell = ({ label, value, accent }) => (
|
|
|
69
96
|
const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
70
97
|
const [divisionModal, setDivisionModal] = useState(false);
|
|
71
98
|
const [monthsModal, setMonthsModal] = useState(false);
|
|
99
|
+
const [compareModal, setCompareModal] = useState(false);
|
|
100
|
+
const [compareMode, setCompareMode] = useState('profit_yoy');
|
|
72
101
|
const [divisions, setDivisions] = useState([]);
|
|
73
102
|
const [division, setDivision] = useState(null);
|
|
74
103
|
const [table, setTable] = useState(null);
|
|
@@ -101,7 +130,8 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
101
130
|
if (!preserveSelection) {
|
|
102
131
|
return monthLabels;
|
|
103
132
|
}
|
|
104
|
-
const
|
|
133
|
+
const monthKeys = new Set(monthLabels.map(toMonthKey));
|
|
134
|
+
const kept = prev.filter((month) => monthKeys.has(toMonthKey(month)));
|
|
105
135
|
return kept.length ? kept : monthLabels;
|
|
106
136
|
});
|
|
107
137
|
},
|
|
@@ -147,7 +177,8 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
147
177
|
|
|
148
178
|
const rows = useMemo(() => {
|
|
149
179
|
if (!selectedMonths.length) return [];
|
|
150
|
-
|
|
180
|
+
const selectedKeys = new Set(selectedMonths.map(toMonthKey));
|
|
181
|
+
return baseRows.filter((row) => selectedKeys.has(toMonthKey(row.monthLabel)));
|
|
151
182
|
}, [baseRows, selectedMonths]);
|
|
152
183
|
|
|
153
184
|
const filteredLine = useMemo(() => {
|
|
@@ -176,6 +207,46 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
176
207
|
}, null);
|
|
177
208
|
}, [rows]);
|
|
178
209
|
|
|
210
|
+
const compareLabel = COMPARE_OPTIONS.find((item) => item.key === compareMode)?.label || 'Select comparison';
|
|
211
|
+
const compareSummary = useMemo(() => {
|
|
212
|
+
if (compareMode === 'profit_budget') {
|
|
213
|
+
return {
|
|
214
|
+
title: '2025 Profit vs 2025 Budget',
|
|
215
|
+
primaryLabel: '2025 Profit',
|
|
216
|
+
primaryValue: total2025Profit,
|
|
217
|
+
secondaryLabel: '2025 Budget',
|
|
218
|
+
secondaryValue: total2025Budget,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (compareMode === 'teu_yoy') {
|
|
222
|
+
return {
|
|
223
|
+
title: '2025 TEU vs 2024 TEU',
|
|
224
|
+
primaryLabel: '2025 TEU',
|
|
225
|
+
primaryValue: total2025Teu,
|
|
226
|
+
secondaryLabel: '2024 TEU',
|
|
227
|
+
secondaryValue: total2024Teu,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
title: '2025 Profit vs 2024 Profit',
|
|
233
|
+
primaryLabel: '2025 Profit',
|
|
234
|
+
primaryValue: total2025Profit,
|
|
235
|
+
secondaryLabel: '2024 Profit',
|
|
236
|
+
secondaryValue: total2024Profit,
|
|
237
|
+
};
|
|
238
|
+
}, [
|
|
239
|
+
compareMode,
|
|
240
|
+
total2024Profit,
|
|
241
|
+
total2024Teu,
|
|
242
|
+
total2025Budget,
|
|
243
|
+
total2025Profit,
|
|
244
|
+
total2025Teu,
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
const compareMax = Math.max(1, compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
248
|
+
const compareDelta = toPercent(compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
249
|
+
|
|
179
250
|
if (loading && (!table || !lineData || !barData)) {
|
|
180
251
|
return (
|
|
181
252
|
<View style={styles.loaderWrap}>
|
|
@@ -222,6 +293,11 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
222
293
|
</TouchableOpacity>
|
|
223
294
|
</View>
|
|
224
295
|
|
|
296
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
297
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
298
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
299
|
+
</TouchableOpacity>
|
|
300
|
+
|
|
225
301
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.kpiScroll}>
|
|
226
302
|
<KpiCard
|
|
227
303
|
label="2024 Profit"
|
|
@@ -258,6 +334,46 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
258
334
|
</View>
|
|
259
335
|
</View>
|
|
260
336
|
|
|
337
|
+
<View style={styles.compareCard}>
|
|
338
|
+
<View style={styles.compareTitleRow}>
|
|
339
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
340
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
341
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
342
|
+
{compareDelta.toFixed(1)}%
|
|
343
|
+
</Text>
|
|
344
|
+
</View>
|
|
345
|
+
|
|
346
|
+
<View style={styles.compareMetric}>
|
|
347
|
+
<View style={styles.compareMetricHead}>
|
|
348
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
349
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.primaryValue)}</Text>
|
|
350
|
+
</View>
|
|
351
|
+
<View style={styles.compareTrack}>
|
|
352
|
+
<View
|
|
353
|
+
style={[
|
|
354
|
+
styles.compareFillPrimary,
|
|
355
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
356
|
+
]}
|
|
357
|
+
/>
|
|
358
|
+
</View>
|
|
359
|
+
</View>
|
|
360
|
+
|
|
361
|
+
<View style={styles.compareMetric}>
|
|
362
|
+
<View style={styles.compareMetricHead}>
|
|
363
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
364
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.secondaryValue)}</Text>
|
|
365
|
+
</View>
|
|
366
|
+
<View style={styles.compareTrack}>
|
|
367
|
+
<View
|
|
368
|
+
style={[
|
|
369
|
+
styles.compareFillSecondary,
|
|
370
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
371
|
+
]}
|
|
372
|
+
/>
|
|
373
|
+
</View>
|
|
374
|
+
</View>
|
|
375
|
+
</View>
|
|
376
|
+
|
|
261
377
|
<CompactAxisLineChart
|
|
262
378
|
data={filteredLine}
|
|
263
379
|
title="Profit Amount Trend (Line)"
|
|
@@ -298,6 +414,7 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
298
414
|
|
|
299
415
|
<ModernDataTable
|
|
300
416
|
title="Gross Profit Table"
|
|
417
|
+
freezeFirstColumn
|
|
301
418
|
columns={[
|
|
302
419
|
{ key: 'monthLabel', label: 'Month', width: 110 },
|
|
303
420
|
{ key: 'teu2024', label: '2024 TEU', width: 108, align: 'right', render: (row) => formatNumber(row.teu2024) },
|
|
@@ -336,6 +453,15 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
336
453
|
onApply={setSelectedMonths}
|
|
337
454
|
onClose={() => setMonthsModal(false)}
|
|
338
455
|
/>
|
|
456
|
+
|
|
457
|
+
<CompareOptionsModal
|
|
458
|
+
visible={compareModal}
|
|
459
|
+
title="Compare Gross Profit"
|
|
460
|
+
options={COMPARE_OPTIONS}
|
|
461
|
+
selected={compareMode}
|
|
462
|
+
onApply={setCompareMode}
|
|
463
|
+
onClose={() => setCompareModal(false)}
|
|
464
|
+
/>
|
|
339
465
|
</View>
|
|
340
466
|
);
|
|
341
467
|
};
|
|
@@ -433,6 +559,26 @@ const styles = StyleSheet.create({
|
|
|
433
559
|
color: '#14253d',
|
|
434
560
|
fontWeight: '700',
|
|
435
561
|
},
|
|
562
|
+
compareButton: {
|
|
563
|
+
backgroundColor: '#fff',
|
|
564
|
+
borderRadius: 14,
|
|
565
|
+
borderWidth: 1,
|
|
566
|
+
borderColor: '#d2deee',
|
|
567
|
+
paddingVertical: 10,
|
|
568
|
+
paddingHorizontal: 12,
|
|
569
|
+
marginBottom: 12,
|
|
570
|
+
},
|
|
571
|
+
compareButtonLabel: {
|
|
572
|
+
fontSize: 11,
|
|
573
|
+
color: '#5e6878',
|
|
574
|
+
marginBottom: 4,
|
|
575
|
+
fontWeight: '700',
|
|
576
|
+
},
|
|
577
|
+
compareButtonValue: {
|
|
578
|
+
fontSize: 13,
|
|
579
|
+
color: '#14253d',
|
|
580
|
+
fontWeight: '700',
|
|
581
|
+
},
|
|
436
582
|
kpiScroll: {
|
|
437
583
|
marginBottom: 12,
|
|
438
584
|
},
|
|
@@ -492,6 +638,80 @@ const styles = StyleSheet.create({
|
|
|
492
638
|
fontWeight: '700',
|
|
493
639
|
color: '#152842',
|
|
494
640
|
},
|
|
641
|
+
compareCard: {
|
|
642
|
+
backgroundColor: '#fff',
|
|
643
|
+
borderRadius: 14,
|
|
644
|
+
borderWidth: 1,
|
|
645
|
+
borderColor: '#d2deee',
|
|
646
|
+
paddingVertical: 12,
|
|
647
|
+
paddingHorizontal: 12,
|
|
648
|
+
marginBottom: 12,
|
|
649
|
+
},
|
|
650
|
+
compareTitleRow: {
|
|
651
|
+
flexDirection: 'row',
|
|
652
|
+
alignItems: 'center',
|
|
653
|
+
justifyContent: 'space-between',
|
|
654
|
+
marginBottom: 8,
|
|
655
|
+
},
|
|
656
|
+
compareTitle: {
|
|
657
|
+
fontSize: 13,
|
|
658
|
+
fontWeight: '700',
|
|
659
|
+
color: '#152842',
|
|
660
|
+
flex: 1,
|
|
661
|
+
marginRight: 8,
|
|
662
|
+
},
|
|
663
|
+
compareDelta: {
|
|
664
|
+
fontSize: 12,
|
|
665
|
+
fontWeight: '800',
|
|
666
|
+
paddingHorizontal: 8,
|
|
667
|
+
paddingVertical: 4,
|
|
668
|
+
borderRadius: 999,
|
|
669
|
+
},
|
|
670
|
+
compareDeltaUp: {
|
|
671
|
+
color: '#15724a',
|
|
672
|
+
backgroundColor: '#e4f8ef',
|
|
673
|
+
},
|
|
674
|
+
compareDeltaDown: {
|
|
675
|
+
color: '#b43c44',
|
|
676
|
+
backgroundColor: '#ffe9ea',
|
|
677
|
+
},
|
|
678
|
+
compareMetric: {
|
|
679
|
+
marginTop: 6,
|
|
680
|
+
},
|
|
681
|
+
compareMetricHead: {
|
|
682
|
+
flexDirection: 'row',
|
|
683
|
+
justifyContent: 'space-between',
|
|
684
|
+
alignItems: 'center',
|
|
685
|
+
marginBottom: 4,
|
|
686
|
+
},
|
|
687
|
+
compareMetricLabel: {
|
|
688
|
+
fontSize: 11,
|
|
689
|
+
color: '#5e6878',
|
|
690
|
+
flex: 1,
|
|
691
|
+
marginRight: 8,
|
|
692
|
+
},
|
|
693
|
+
compareMetricValue: {
|
|
694
|
+
fontSize: 12,
|
|
695
|
+
fontWeight: '700',
|
|
696
|
+
color: '#152842',
|
|
697
|
+
},
|
|
698
|
+
compareTrack: {
|
|
699
|
+
width: '100%',
|
|
700
|
+
height: 9,
|
|
701
|
+
borderRadius: 999,
|
|
702
|
+
backgroundColor: '#ecf2fb',
|
|
703
|
+
overflow: 'hidden',
|
|
704
|
+
},
|
|
705
|
+
compareFillPrimary: {
|
|
706
|
+
height: '100%',
|
|
707
|
+
borderRadius: 999,
|
|
708
|
+
backgroundColor: '#2E7DD1',
|
|
709
|
+
},
|
|
710
|
+
compareFillSecondary: {
|
|
711
|
+
height: '100%',
|
|
712
|
+
borderRadius: 999,
|
|
713
|
+
backgroundColor: '#F29F45',
|
|
714
|
+
},
|
|
495
715
|
trendBadge: {
|
|
496
716
|
borderRadius: 999,
|
|
497
717
|
paddingVertical: 4,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
fetchReport3Line,
|
|
15
15
|
fetchReport3Table,
|
|
16
16
|
} from '../api/report3Fetcher';
|
|
17
|
+
import CompareOptionsModal from '../components/CompareOptionsModal';
|
|
17
18
|
import MonthFilterModal from '../components/MonthFilterModal';
|
|
18
19
|
import CompactAxisBarLineChart from '../components/CompactAxisBarLineChart';
|
|
19
20
|
import CompactAxisLineChart from '../components/CompactAxisLineChart';
|
|
@@ -35,6 +36,32 @@ const percentOf = (part, total) => {
|
|
|
35
36
|
return (part / total) * 100;
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
const toMonthKey = (value) => {
|
|
40
|
+
return String(value ?? '')
|
|
41
|
+
.trim()
|
|
42
|
+
.toLocaleUpperCase('tr-TR')
|
|
43
|
+
.normalize('NFD')
|
|
44
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const COMPARE_OPTIONS = [
|
|
48
|
+
{
|
|
49
|
+
key: 'load_yoy',
|
|
50
|
+
label: '2025 Load vs 2024 Load',
|
|
51
|
+
description: 'Compare total load counts with selected filters',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'revenue_yoy',
|
|
55
|
+
label: '2025 Revenue vs 2024 Revenue',
|
|
56
|
+
description: 'Compare transportation revenue year-over-year',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'revenue_budget',
|
|
60
|
+
label: '2025 Revenue vs 2025 Budget',
|
|
61
|
+
description: 'Compare actual revenue against planned budget',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
38
65
|
const compactNumber = (value) => {
|
|
39
66
|
const num = toNumber(value);
|
|
40
67
|
if (Math.abs(num) >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`;
|
|
@@ -99,6 +126,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
99
126
|
const [loading, setLoading] = useState(true);
|
|
100
127
|
const [refreshing, setRefreshing] = useState(false);
|
|
101
128
|
const [monthsModal, setMonthsModal] = useState(false);
|
|
129
|
+
const [compareModal, setCompareModal] = useState(false);
|
|
130
|
+
const [compareMode, setCompareMode] = useState('load_yoy');
|
|
102
131
|
const [table, setTable] = useState(null);
|
|
103
132
|
const [lineData, setLineData] = useState(null);
|
|
104
133
|
const [barData, setBarData] = useState(null);
|
|
@@ -120,7 +149,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
120
149
|
const months = lineRes?.labels || [];
|
|
121
150
|
setSelectedMonths((prev) => {
|
|
122
151
|
if (!preserveSelection) return months;
|
|
123
|
-
const
|
|
152
|
+
const monthKeys = new Set(months.map(toMonthKey));
|
|
153
|
+
const kept = prev.filter((month) => monthKeys.has(toMonthKey(month)));
|
|
124
154
|
return kept.length ? kept : months;
|
|
125
155
|
});
|
|
126
156
|
},
|
|
@@ -153,7 +183,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
153
183
|
|
|
154
184
|
const rows = useMemo(() => {
|
|
155
185
|
if (!selectedMonths.length) return [];
|
|
156
|
-
|
|
186
|
+
const selectedKeys = new Set(selectedMonths.map(toMonthKey));
|
|
187
|
+
return baseRows.filter((row) => selectedKeys.has(toMonthKey(row.monthLabel)));
|
|
157
188
|
}, [baseRows, selectedMonths]);
|
|
158
189
|
|
|
159
190
|
const filteredLine = useMemo(() => {
|
|
@@ -190,6 +221,45 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
190
221
|
const loadYoY = percentChange(load2025Total, load2024Total);
|
|
191
222
|
const revenueYoY = percentChange(revenue2025Total, revenue2024Total);
|
|
192
223
|
const budgetCoverage = percentOf(revenue2025Total, budget2025Total);
|
|
224
|
+
const compareLabel = COMPARE_OPTIONS.find((item) => item.key === compareMode)?.label || 'Select comparison';
|
|
225
|
+
const compareSummary = useMemo(() => {
|
|
226
|
+
if (compareMode === 'revenue_yoy') {
|
|
227
|
+
return {
|
|
228
|
+
title: '2025 Revenue vs 2024 Revenue',
|
|
229
|
+
primaryLabel: '2025 Revenue',
|
|
230
|
+
primaryValue: revenue2025Total,
|
|
231
|
+
secondaryLabel: '2024 Revenue',
|
|
232
|
+
secondaryValue: revenue2024Total,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (compareMode === 'revenue_budget') {
|
|
237
|
+
return {
|
|
238
|
+
title: '2025 Revenue vs 2025 Budget',
|
|
239
|
+
primaryLabel: '2025 Revenue',
|
|
240
|
+
primaryValue: revenue2025Total,
|
|
241
|
+
secondaryLabel: '2025 Budget',
|
|
242
|
+
secondaryValue: budget2025Total,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
title: '2025 Load vs 2024 Load',
|
|
248
|
+
primaryLabel: '2025 Load',
|
|
249
|
+
primaryValue: load2025Total,
|
|
250
|
+
secondaryLabel: '2024 Load',
|
|
251
|
+
secondaryValue: load2024Total,
|
|
252
|
+
};
|
|
253
|
+
}, [
|
|
254
|
+
budget2025Total,
|
|
255
|
+
compareMode,
|
|
256
|
+
load2024Total,
|
|
257
|
+
load2025Total,
|
|
258
|
+
revenue2024Total,
|
|
259
|
+
revenue2025Total,
|
|
260
|
+
]);
|
|
261
|
+
const compareMax = Math.max(1, compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
262
|
+
const compareDelta = percentChange(compareSummary.primaryValue, compareSummary.secondaryValue);
|
|
193
263
|
|
|
194
264
|
const topMonth = useMemo(() => {
|
|
195
265
|
if (!rows.length) return null;
|
|
@@ -268,6 +338,11 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
268
338
|
</View>
|
|
269
339
|
</View>
|
|
270
340
|
|
|
341
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
342
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
343
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
344
|
+
</TouchableOpacity>
|
|
345
|
+
|
|
271
346
|
<View style={styles.statRow}>
|
|
272
347
|
<StatTile
|
|
273
348
|
label="Load YoY"
|
|
@@ -297,6 +372,46 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
297
372
|
/>
|
|
298
373
|
</View>
|
|
299
374
|
|
|
375
|
+
<View style={styles.compareCard}>
|
|
376
|
+
<View style={styles.compareTitleRow}>
|
|
377
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
378
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
379
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
380
|
+
{compareDelta.toFixed(1)}%
|
|
381
|
+
</Text>
|
|
382
|
+
</View>
|
|
383
|
+
|
|
384
|
+
<View style={styles.compareMetric}>
|
|
385
|
+
<View style={styles.compareMetricHead}>
|
|
386
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
387
|
+
<Text style={styles.compareMetricValue}>{compactNumber(compareSummary.primaryValue)}</Text>
|
|
388
|
+
</View>
|
|
389
|
+
<View style={styles.compareTrack}>
|
|
390
|
+
<View
|
|
391
|
+
style={[
|
|
392
|
+
styles.compareFillPrimary,
|
|
393
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
394
|
+
]}
|
|
395
|
+
/>
|
|
396
|
+
</View>
|
|
397
|
+
</View>
|
|
398
|
+
|
|
399
|
+
<View style={styles.compareMetric}>
|
|
400
|
+
<View style={styles.compareMetricHead}>
|
|
401
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
402
|
+
<Text style={styles.compareMetricValue}>{compactNumber(compareSummary.secondaryValue)}</Text>
|
|
403
|
+
</View>
|
|
404
|
+
<View style={styles.compareTrack}>
|
|
405
|
+
<View
|
|
406
|
+
style={[
|
|
407
|
+
styles.compareFillSecondary,
|
|
408
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
409
|
+
]}
|
|
410
|
+
/>
|
|
411
|
+
</View>
|
|
412
|
+
</View>
|
|
413
|
+
</View>
|
|
414
|
+
|
|
300
415
|
<CompactAxisLineChart
|
|
301
416
|
data={filteredLine}
|
|
302
417
|
title="Load Amount Trend (Line)"
|
|
@@ -368,6 +483,7 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
368
483
|
|
|
369
484
|
<ModernDataTable
|
|
370
485
|
title="Transportation Table"
|
|
486
|
+
freezeFirstColumn
|
|
371
487
|
columns={[
|
|
372
488
|
{ key: 'monthLabel', label: 'Month', width: 110 },
|
|
373
489
|
{ key: 'loadCount2024', label: '2024 Load', width: 110, align: 'right', render: (row) => compactNumber(row.loadCount2024) },
|
|
@@ -398,6 +514,15 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
398
514
|
onApply={setSelectedMonths}
|
|
399
515
|
onClose={() => setMonthsModal(false)}
|
|
400
516
|
/>
|
|
517
|
+
|
|
518
|
+
<CompareOptionsModal
|
|
519
|
+
visible={compareModal}
|
|
520
|
+
title="Compare Transportation"
|
|
521
|
+
options={COMPARE_OPTIONS}
|
|
522
|
+
selected={compareMode}
|
|
523
|
+
onApply={setCompareMode}
|
|
524
|
+
onClose={() => setCompareModal(false)}
|
|
525
|
+
/>
|
|
401
526
|
</View>
|
|
402
527
|
);
|
|
403
528
|
};
|
|
@@ -520,6 +645,26 @@ const styles = StyleSheet.create({
|
|
|
520
645
|
modeButtonTextActive: {
|
|
521
646
|
color: '#fff',
|
|
522
647
|
},
|
|
648
|
+
compareButton: {
|
|
649
|
+
borderRadius: 14,
|
|
650
|
+
paddingVertical: 10,
|
|
651
|
+
paddingHorizontal: 12,
|
|
652
|
+
backgroundColor: '#fffdf8',
|
|
653
|
+
borderWidth: 1,
|
|
654
|
+
borderColor: '#dfd6c3',
|
|
655
|
+
marginBottom: 10,
|
|
656
|
+
},
|
|
657
|
+
compareButtonLabel: {
|
|
658
|
+
fontSize: 11,
|
|
659
|
+
color: '#736b57',
|
|
660
|
+
marginBottom: 4,
|
|
661
|
+
fontWeight: '700',
|
|
662
|
+
},
|
|
663
|
+
compareButtonValue: {
|
|
664
|
+
fontSize: 13,
|
|
665
|
+
color: '#2e2a21',
|
|
666
|
+
fontWeight: '700',
|
|
667
|
+
},
|
|
523
668
|
statRow: {
|
|
524
669
|
flexDirection: 'row',
|
|
525
670
|
marginBottom: 10,
|
|
@@ -559,6 +704,80 @@ const styles = StyleSheet.create({
|
|
|
559
704
|
fontSize: 11,
|
|
560
705
|
color: '#6b6860',
|
|
561
706
|
},
|
|
707
|
+
compareCard: {
|
|
708
|
+
backgroundColor: '#fffdf8',
|
|
709
|
+
borderRadius: 14,
|
|
710
|
+
borderWidth: 1,
|
|
711
|
+
borderColor: '#dfd6c3',
|
|
712
|
+
paddingVertical: 12,
|
|
713
|
+
paddingHorizontal: 12,
|
|
714
|
+
marginBottom: 10,
|
|
715
|
+
},
|
|
716
|
+
compareTitleRow: {
|
|
717
|
+
flexDirection: 'row',
|
|
718
|
+
alignItems: 'center',
|
|
719
|
+
justifyContent: 'space-between',
|
|
720
|
+
marginBottom: 8,
|
|
721
|
+
},
|
|
722
|
+
compareTitle: {
|
|
723
|
+
fontSize: 13,
|
|
724
|
+
fontWeight: '700',
|
|
725
|
+
color: '#2b2a22',
|
|
726
|
+
flex: 1,
|
|
727
|
+
marginRight: 8,
|
|
728
|
+
},
|
|
729
|
+
compareDelta: {
|
|
730
|
+
fontSize: 12,
|
|
731
|
+
fontWeight: '800',
|
|
732
|
+
paddingHorizontal: 8,
|
|
733
|
+
paddingVertical: 4,
|
|
734
|
+
borderRadius: 999,
|
|
735
|
+
},
|
|
736
|
+
compareDeltaUp: {
|
|
737
|
+
color: '#15724a',
|
|
738
|
+
backgroundColor: '#e4f8ef',
|
|
739
|
+
},
|
|
740
|
+
compareDeltaDown: {
|
|
741
|
+
color: '#b43c44',
|
|
742
|
+
backgroundColor: '#ffe9ea',
|
|
743
|
+
},
|
|
744
|
+
compareMetric: {
|
|
745
|
+
marginTop: 6,
|
|
746
|
+
},
|
|
747
|
+
compareMetricHead: {
|
|
748
|
+
flexDirection: 'row',
|
|
749
|
+
justifyContent: 'space-between',
|
|
750
|
+
alignItems: 'center',
|
|
751
|
+
marginBottom: 4,
|
|
752
|
+
},
|
|
753
|
+
compareMetricLabel: {
|
|
754
|
+
fontSize: 11,
|
|
755
|
+
color: '#6b6860',
|
|
756
|
+
flex: 1,
|
|
757
|
+
marginRight: 8,
|
|
758
|
+
},
|
|
759
|
+
compareMetricValue: {
|
|
760
|
+
fontSize: 12,
|
|
761
|
+
color: '#2e2a21',
|
|
762
|
+
fontWeight: '700',
|
|
763
|
+
},
|
|
764
|
+
compareTrack: {
|
|
765
|
+
width: '100%',
|
|
766
|
+
height: 9,
|
|
767
|
+
borderRadius: 999,
|
|
768
|
+
backgroundColor: '#efe8da',
|
|
769
|
+
overflow: 'hidden',
|
|
770
|
+
},
|
|
771
|
+
compareFillPrimary: {
|
|
772
|
+
height: '100%',
|
|
773
|
+
borderRadius: 999,
|
|
774
|
+
backgroundColor: '#179a79',
|
|
775
|
+
},
|
|
776
|
+
compareFillSecondary: {
|
|
777
|
+
height: '100%',
|
|
778
|
+
borderRadius: 999,
|
|
779
|
+
backgroundColor: '#f4a04a',
|
|
780
|
+
},
|
|
562
781
|
sectionTitle: {
|
|
563
782
|
fontSize: 15,
|
|
564
783
|
fontWeight: '800',
|
|
@@ -12,8 +12,17 @@ export const filterChartByMonths = (chart, selectedMonths) => {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const toMonthKey = (value) => {
|
|
16
|
+
return String(value ?? '')
|
|
17
|
+
.trim()
|
|
18
|
+
.toLocaleUpperCase('tr-TR')
|
|
19
|
+
.normalize('NFD')
|
|
20
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
21
|
+
};
|
|
22
|
+
const selectedKeys = new Set(selectedMonths.map(toMonthKey));
|
|
23
|
+
|
|
15
24
|
const indices = chart.labels
|
|
16
|
-
.map((label, i) => (
|
|
25
|
+
.map((label, i) => (selectedKeys.has(toMonthKey(label)) ? i : -1))
|
|
17
26
|
.filter(i => i !== -1);
|
|
18
27
|
|
|
19
28
|
return {
|