@dhiraj0720/report1chart 3.0.9 → 3.1.1
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/screens/Report1ModernScreen.jsx +272 -6
- package/src/screens/Report2ModernScreen.jsx +222 -6
- package/src/screens/Report3ModernScreen.jsx +221 -6
- package/src/screens/ReportListScreen.jsx +9 -7
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;
|
|
@@ -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({});
|
|
@@ -165,17 +195,81 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
165
195
|
}, [sortedRows]);
|
|
166
196
|
|
|
167
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);
|
|
168
260
|
|
|
169
261
|
return (
|
|
170
262
|
<View style={styles.screen}>
|
|
171
263
|
<View style={styles.hero}>
|
|
172
264
|
<View style={styles.heroGlowLeft} />
|
|
173
265
|
<View style={styles.heroGlowRight} />
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
266
|
+
<View style={styles.heroTopRow}>
|
|
267
|
+
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
268
|
+
<Text style={styles.backIcon}>‹</Text>
|
|
269
|
+
</TouchableOpacity>
|
|
270
|
+
<Text numberOfLines={1} style={styles.heroTitle}>Performance Studio</Text>
|
|
271
|
+
</View>
|
|
272
|
+
<Text style={styles.heroSubtitle}>Three-tab performance monitor</Text>
|
|
179
273
|
</View>
|
|
180
274
|
|
|
181
275
|
<View style={styles.tabsContainer}>
|
|
@@ -218,6 +312,11 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
218
312
|
|
|
219
313
|
{sortedRows.length ? (
|
|
220
314
|
<>
|
|
315
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
316
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
317
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
318
|
+
</TouchableOpacity>
|
|
319
|
+
|
|
221
320
|
<View style={styles.kpiRow}>
|
|
222
321
|
<View style={[styles.kpiCard, styles.kpiWarm]}>
|
|
223
322
|
<Text style={styles.kpiLabel}>2024 Value</Text>
|
|
@@ -253,6 +352,65 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
253
352
|
</View>
|
|
254
353
|
</View>
|
|
255
354
|
|
|
355
|
+
<View style={styles.compareCard}>
|
|
356
|
+
<View style={styles.compareTitleRow}>
|
|
357
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
358
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
359
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
360
|
+
{compareDelta.toFixed(1)}%
|
|
361
|
+
</Text>
|
|
362
|
+
</View>
|
|
363
|
+
|
|
364
|
+
<View style={styles.compareMetric}>
|
|
365
|
+
<View style={styles.compareMetricHead}>
|
|
366
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
367
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.primaryValue)}</Text>
|
|
368
|
+
</View>
|
|
369
|
+
<View style={styles.compareTrack}>
|
|
370
|
+
<View
|
|
371
|
+
style={[
|
|
372
|
+
styles.compareFillPrimary,
|
|
373
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
374
|
+
]}
|
|
375
|
+
/>
|
|
376
|
+
</View>
|
|
377
|
+
</View>
|
|
378
|
+
|
|
379
|
+
<View style={styles.compareMetric}>
|
|
380
|
+
<View style={styles.compareMetricHead}>
|
|
381
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
382
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.secondaryValue)}</Text>
|
|
383
|
+
</View>
|
|
384
|
+
<View style={styles.compareTrack}>
|
|
385
|
+
<View
|
|
386
|
+
style={[
|
|
387
|
+
styles.compareFillSecondary,
|
|
388
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
389
|
+
]}
|
|
390
|
+
/>
|
|
391
|
+
</View>
|
|
392
|
+
</View>
|
|
393
|
+
</View>
|
|
394
|
+
|
|
395
|
+
<CompactAxisLineChart
|
|
396
|
+
data={lineChartData}
|
|
397
|
+
title="Activity Trend (2024 vs 2025)"
|
|
398
|
+
legend={[
|
|
399
|
+
{ label: lineChartData?.series?.[0]?.name || '2024 Value', color: '#F29F45' },
|
|
400
|
+
{ label: lineChartData?.series?.[1]?.name || '2025 Value', color: '#2E7DD1' },
|
|
401
|
+
]}
|
|
402
|
+
/>
|
|
403
|
+
|
|
404
|
+
<CompactAxisBarLineChart
|
|
405
|
+
data={barChartData}
|
|
406
|
+
title="2025 Value vs Budget by Activity"
|
|
407
|
+
legend={[
|
|
408
|
+
{ label: barChartData?.series?.[0]?.name || '2024 Value', color: '#F29F45' },
|
|
409
|
+
{ label: barChartData?.series?.[1]?.name || '2025 Value', color: '#2E7DD1' },
|
|
410
|
+
{ label: barChartData?.series?.[2]?.name || '2025 Budget', color: '#8AB6E8' },
|
|
411
|
+
]}
|
|
412
|
+
/>
|
|
413
|
+
|
|
256
414
|
{spotlight ? (
|
|
257
415
|
<View style={styles.spotlightCard}>
|
|
258
416
|
<Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
|
|
@@ -357,6 +515,15 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
357
515
|
|
|
358
516
|
<View style={styles.footerSpacing} />
|
|
359
517
|
</ScrollView>
|
|
518
|
+
|
|
519
|
+
<CompareOptionsModal
|
|
520
|
+
visible={compareModal}
|
|
521
|
+
title="Compare Performance"
|
|
522
|
+
options={COMPARE_OPTIONS}
|
|
523
|
+
selected={compareMode}
|
|
524
|
+
onApply={setCompareMode}
|
|
525
|
+
onClose={() => setCompareModal(false)}
|
|
526
|
+
/>
|
|
360
527
|
</View>
|
|
361
528
|
);
|
|
362
529
|
};
|
|
@@ -373,6 +540,10 @@ const styles = StyleSheet.create({
|
|
|
373
540
|
backgroundColor: '#131d30',
|
|
374
541
|
overflow: 'hidden',
|
|
375
542
|
},
|
|
543
|
+
heroTopRow: {
|
|
544
|
+
flexDirection: 'row',
|
|
545
|
+
alignItems: 'center',
|
|
546
|
+
},
|
|
376
547
|
heroGlowLeft: {
|
|
377
548
|
position: 'absolute',
|
|
378
549
|
width: 180,
|
|
@@ -406,7 +577,8 @@ const styles = StyleSheet.create({
|
|
|
406
577
|
marginTop: -2,
|
|
407
578
|
},
|
|
408
579
|
heroTitle: {
|
|
409
|
-
|
|
580
|
+
marginLeft: 10,
|
|
581
|
+
flex: 1,
|
|
410
582
|
color: '#fff',
|
|
411
583
|
fontSize: 22,
|
|
412
584
|
fontWeight: '800',
|
|
@@ -450,6 +622,26 @@ const styles = StyleSheet.create({
|
|
|
450
622
|
paddingHorizontal: 14,
|
|
451
623
|
paddingTop: 14,
|
|
452
624
|
},
|
|
625
|
+
compareButton: {
|
|
626
|
+
backgroundColor: '#1a2640',
|
|
627
|
+
borderColor: '#2f3d5f',
|
|
628
|
+
borderWidth: 1,
|
|
629
|
+
borderRadius: 12,
|
|
630
|
+
paddingVertical: 10,
|
|
631
|
+
paddingHorizontal: 12,
|
|
632
|
+
marginBottom: 10,
|
|
633
|
+
},
|
|
634
|
+
compareButtonLabel: {
|
|
635
|
+
fontSize: 11,
|
|
636
|
+
color: '#9eb1d3',
|
|
637
|
+
marginBottom: 4,
|
|
638
|
+
fontWeight: '700',
|
|
639
|
+
},
|
|
640
|
+
compareButtonValue: {
|
|
641
|
+
fontSize: 13,
|
|
642
|
+
color: '#ffffff',
|
|
643
|
+
fontWeight: '700',
|
|
644
|
+
},
|
|
453
645
|
center: {
|
|
454
646
|
paddingVertical: 24,
|
|
455
647
|
alignItems: 'center',
|
|
@@ -528,6 +720,80 @@ const styles = StyleSheet.create({
|
|
|
528
720
|
fontWeight: '800',
|
|
529
721
|
fontSize: 13,
|
|
530
722
|
},
|
|
723
|
+
compareCard: {
|
|
724
|
+
backgroundColor: '#1a2640',
|
|
725
|
+
borderColor: '#303d58',
|
|
726
|
+
borderWidth: 1,
|
|
727
|
+
borderRadius: 14,
|
|
728
|
+
paddingHorizontal: 12,
|
|
729
|
+
paddingVertical: 12,
|
|
730
|
+
marginBottom: 10,
|
|
731
|
+
},
|
|
732
|
+
compareTitleRow: {
|
|
733
|
+
flexDirection: 'row',
|
|
734
|
+
justifyContent: 'space-between',
|
|
735
|
+
alignItems: 'center',
|
|
736
|
+
marginBottom: 8,
|
|
737
|
+
},
|
|
738
|
+
compareTitle: {
|
|
739
|
+
color: '#d9e3fa',
|
|
740
|
+
fontSize: 12,
|
|
741
|
+
fontWeight: '700',
|
|
742
|
+
flex: 1,
|
|
743
|
+
marginRight: 8,
|
|
744
|
+
},
|
|
745
|
+
compareDelta: {
|
|
746
|
+
fontSize: 12,
|
|
747
|
+
fontWeight: '800',
|
|
748
|
+
paddingHorizontal: 8,
|
|
749
|
+
paddingVertical: 4,
|
|
750
|
+
borderRadius: 999,
|
|
751
|
+
},
|
|
752
|
+
compareDeltaUp: {
|
|
753
|
+
color: '#7ff0c8',
|
|
754
|
+
backgroundColor: '#1d463a',
|
|
755
|
+
},
|
|
756
|
+
compareDeltaDown: {
|
|
757
|
+
color: '#ff9ca6',
|
|
758
|
+
backgroundColor: '#522d34',
|
|
759
|
+
},
|
|
760
|
+
compareMetric: {
|
|
761
|
+
marginTop: 6,
|
|
762
|
+
},
|
|
763
|
+
compareMetricHead: {
|
|
764
|
+
flexDirection: 'row',
|
|
765
|
+
justifyContent: 'space-between',
|
|
766
|
+
alignItems: 'center',
|
|
767
|
+
marginBottom: 4,
|
|
768
|
+
},
|
|
769
|
+
compareMetricLabel: {
|
|
770
|
+
color: '#b9cae8',
|
|
771
|
+
fontSize: 11,
|
|
772
|
+
flex: 1,
|
|
773
|
+
marginRight: 8,
|
|
774
|
+
},
|
|
775
|
+
compareMetricValue: {
|
|
776
|
+
color: '#fff',
|
|
777
|
+
fontSize: 12,
|
|
778
|
+
fontWeight: '700',
|
|
779
|
+
},
|
|
780
|
+
compareTrack: {
|
|
781
|
+
width: '100%',
|
|
782
|
+
height: 9,
|
|
783
|
+
borderRadius: 999,
|
|
784
|
+
backgroundColor: '#2a3652',
|
|
785
|
+
overflow: 'hidden',
|
|
786
|
+
},
|
|
787
|
+
compareFillPrimary: {
|
|
788
|
+
height: '100%',
|
|
789
|
+
borderRadius: 999,
|
|
790
|
+
backgroundColor: '#2E7DD1',
|
|
791
|
+
},
|
|
792
|
+
compareFillSecondary: {
|
|
793
|
+
height: '100%',
|
|
794
|
+
borderRadius: 999,
|
|
795
|
+
backgroundColor: '#F29F45',
|
|
796
|
+
},
|
|
531
797
|
spotlightCard: {
|
|
532
798
|
backgroundColor: '#1a2640',
|
|
533
799
|
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';
|
|
@@ -35,6 +36,24 @@ const toMonthKey = (value) => {
|
|
|
35
36
|
.replace(/[\u0300-\u036f]/g, '');
|
|
36
37
|
};
|
|
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
|
+
|
|
38
57
|
const TrendBadge = ({ value }) => {
|
|
39
58
|
const positive = value >= 0;
|
|
40
59
|
return (
|
|
@@ -77,6 +96,8 @@ const MetricCell = ({ label, value, accent }) => (
|
|
|
77
96
|
const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
78
97
|
const [divisionModal, setDivisionModal] = useState(false);
|
|
79
98
|
const [monthsModal, setMonthsModal] = useState(false);
|
|
99
|
+
const [compareModal, setCompareModal] = useState(false);
|
|
100
|
+
const [compareMode, setCompareMode] = useState('profit_yoy');
|
|
80
101
|
const [divisions, setDivisions] = useState([]);
|
|
81
102
|
const [division, setDivision] = useState(null);
|
|
82
103
|
const [table, setTable] = useState(null);
|
|
@@ -186,6 +207,46 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
186
207
|
}, null);
|
|
187
208
|
}, [rows]);
|
|
188
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
|
+
|
|
189
250
|
if (loading && (!table || !lineData || !barData)) {
|
|
190
251
|
return (
|
|
191
252
|
<View style={styles.loaderWrap}>
|
|
@@ -199,11 +260,13 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
199
260
|
<View style={styles.hero}>
|
|
200
261
|
<View style={styles.heroDotLarge} />
|
|
201
262
|
<View style={styles.heroDotSmall} />
|
|
202
|
-
<
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
263
|
+
<View style={styles.heroTopRow}>
|
|
264
|
+
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
|
265
|
+
<Text style={styles.backIcon}>‹</Text>
|
|
266
|
+
</TouchableOpacity>
|
|
267
|
+
<Text numberOfLines={1} style={styles.heroTitle}>Gross Profit</Text>
|
|
268
|
+
</View>
|
|
269
|
+
<Text style={styles.heroSubtitle}>Filtered month data overview</Text>
|
|
207
270
|
</View>
|
|
208
271
|
|
|
209
272
|
<ScrollView
|
|
@@ -232,6 +295,11 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
232
295
|
</TouchableOpacity>
|
|
233
296
|
</View>
|
|
234
297
|
|
|
298
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
299
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
300
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
301
|
+
</TouchableOpacity>
|
|
302
|
+
|
|
235
303
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.kpiScroll}>
|
|
236
304
|
<KpiCard
|
|
237
305
|
label="2024 Profit"
|
|
@@ -268,6 +336,46 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
268
336
|
</View>
|
|
269
337
|
</View>
|
|
270
338
|
|
|
339
|
+
<View style={styles.compareCard}>
|
|
340
|
+
<View style={styles.compareTitleRow}>
|
|
341
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
342
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
343
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
344
|
+
{compareDelta.toFixed(1)}%
|
|
345
|
+
</Text>
|
|
346
|
+
</View>
|
|
347
|
+
|
|
348
|
+
<View style={styles.compareMetric}>
|
|
349
|
+
<View style={styles.compareMetricHead}>
|
|
350
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
351
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.primaryValue)}</Text>
|
|
352
|
+
</View>
|
|
353
|
+
<View style={styles.compareTrack}>
|
|
354
|
+
<View
|
|
355
|
+
style={[
|
|
356
|
+
styles.compareFillPrimary,
|
|
357
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
358
|
+
]}
|
|
359
|
+
/>
|
|
360
|
+
</View>
|
|
361
|
+
</View>
|
|
362
|
+
|
|
363
|
+
<View style={styles.compareMetric}>
|
|
364
|
+
<View style={styles.compareMetricHead}>
|
|
365
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
366
|
+
<Text style={styles.compareMetricValue}>{formatNumber(compareSummary.secondaryValue)}</Text>
|
|
367
|
+
</View>
|
|
368
|
+
<View style={styles.compareTrack}>
|
|
369
|
+
<View
|
|
370
|
+
style={[
|
|
371
|
+
styles.compareFillSecondary,
|
|
372
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
373
|
+
]}
|
|
374
|
+
/>
|
|
375
|
+
</View>
|
|
376
|
+
</View>
|
|
377
|
+
</View>
|
|
378
|
+
|
|
271
379
|
<CompactAxisLineChart
|
|
272
380
|
data={filteredLine}
|
|
273
381
|
title="Profit Amount Trend (Line)"
|
|
@@ -347,6 +455,15 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
347
455
|
onApply={setSelectedMonths}
|
|
348
456
|
onClose={() => setMonthsModal(false)}
|
|
349
457
|
/>
|
|
458
|
+
|
|
459
|
+
<CompareOptionsModal
|
|
460
|
+
visible={compareModal}
|
|
461
|
+
title="Compare Gross Profit"
|
|
462
|
+
options={COMPARE_OPTIONS}
|
|
463
|
+
selected={compareMode}
|
|
464
|
+
onApply={setCompareMode}
|
|
465
|
+
onClose={() => setCompareModal(false)}
|
|
466
|
+
/>
|
|
350
467
|
</View>
|
|
351
468
|
);
|
|
352
469
|
};
|
|
@@ -369,6 +486,10 @@ const styles = StyleSheet.create({
|
|
|
369
486
|
paddingBottom: 16,
|
|
370
487
|
overflow: 'hidden',
|
|
371
488
|
},
|
|
489
|
+
heroTopRow: {
|
|
490
|
+
flexDirection: 'row',
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
},
|
|
372
493
|
heroDotLarge: {
|
|
373
494
|
position: 'absolute',
|
|
374
495
|
width: 180,
|
|
@@ -402,7 +523,8 @@ const styles = StyleSheet.create({
|
|
|
402
523
|
marginTop: -2,
|
|
403
524
|
},
|
|
404
525
|
heroTitle: {
|
|
405
|
-
|
|
526
|
+
marginLeft: 10,
|
|
527
|
+
flex: 1,
|
|
406
528
|
fontSize: 22,
|
|
407
529
|
fontWeight: '800',
|
|
408
530
|
color: '#fff',
|
|
@@ -444,6 +566,26 @@ const styles = StyleSheet.create({
|
|
|
444
566
|
color: '#14253d',
|
|
445
567
|
fontWeight: '700',
|
|
446
568
|
},
|
|
569
|
+
compareButton: {
|
|
570
|
+
backgroundColor: '#fff',
|
|
571
|
+
borderRadius: 14,
|
|
572
|
+
borderWidth: 1,
|
|
573
|
+
borderColor: '#d2deee',
|
|
574
|
+
paddingVertical: 10,
|
|
575
|
+
paddingHorizontal: 12,
|
|
576
|
+
marginBottom: 12,
|
|
577
|
+
},
|
|
578
|
+
compareButtonLabel: {
|
|
579
|
+
fontSize: 11,
|
|
580
|
+
color: '#5e6878',
|
|
581
|
+
marginBottom: 4,
|
|
582
|
+
fontWeight: '700',
|
|
583
|
+
},
|
|
584
|
+
compareButtonValue: {
|
|
585
|
+
fontSize: 13,
|
|
586
|
+
color: '#14253d',
|
|
587
|
+
fontWeight: '700',
|
|
588
|
+
},
|
|
447
589
|
kpiScroll: {
|
|
448
590
|
marginBottom: 12,
|
|
449
591
|
},
|
|
@@ -503,6 +645,80 @@ const styles = StyleSheet.create({
|
|
|
503
645
|
fontWeight: '700',
|
|
504
646
|
color: '#152842',
|
|
505
647
|
},
|
|
648
|
+
compareCard: {
|
|
649
|
+
backgroundColor: '#fff',
|
|
650
|
+
borderRadius: 14,
|
|
651
|
+
borderWidth: 1,
|
|
652
|
+
borderColor: '#d2deee',
|
|
653
|
+
paddingVertical: 12,
|
|
654
|
+
paddingHorizontal: 12,
|
|
655
|
+
marginBottom: 12,
|
|
656
|
+
},
|
|
657
|
+
compareTitleRow: {
|
|
658
|
+
flexDirection: 'row',
|
|
659
|
+
alignItems: 'center',
|
|
660
|
+
justifyContent: 'space-between',
|
|
661
|
+
marginBottom: 8,
|
|
662
|
+
},
|
|
663
|
+
compareTitle: {
|
|
664
|
+
fontSize: 13,
|
|
665
|
+
fontWeight: '700',
|
|
666
|
+
color: '#152842',
|
|
667
|
+
flex: 1,
|
|
668
|
+
marginRight: 8,
|
|
669
|
+
},
|
|
670
|
+
compareDelta: {
|
|
671
|
+
fontSize: 12,
|
|
672
|
+
fontWeight: '800',
|
|
673
|
+
paddingHorizontal: 8,
|
|
674
|
+
paddingVertical: 4,
|
|
675
|
+
borderRadius: 999,
|
|
676
|
+
},
|
|
677
|
+
compareDeltaUp: {
|
|
678
|
+
color: '#15724a',
|
|
679
|
+
backgroundColor: '#e4f8ef',
|
|
680
|
+
},
|
|
681
|
+
compareDeltaDown: {
|
|
682
|
+
color: '#b43c44',
|
|
683
|
+
backgroundColor: '#ffe9ea',
|
|
684
|
+
},
|
|
685
|
+
compareMetric: {
|
|
686
|
+
marginTop: 6,
|
|
687
|
+
},
|
|
688
|
+
compareMetricHead: {
|
|
689
|
+
flexDirection: 'row',
|
|
690
|
+
justifyContent: 'space-between',
|
|
691
|
+
alignItems: 'center',
|
|
692
|
+
marginBottom: 4,
|
|
693
|
+
},
|
|
694
|
+
compareMetricLabel: {
|
|
695
|
+
fontSize: 11,
|
|
696
|
+
color: '#5e6878',
|
|
697
|
+
flex: 1,
|
|
698
|
+
marginRight: 8,
|
|
699
|
+
},
|
|
700
|
+
compareMetricValue: {
|
|
701
|
+
fontSize: 12,
|
|
702
|
+
fontWeight: '700',
|
|
703
|
+
color: '#152842',
|
|
704
|
+
},
|
|
705
|
+
compareTrack: {
|
|
706
|
+
width: '100%',
|
|
707
|
+
height: 9,
|
|
708
|
+
borderRadius: 999,
|
|
709
|
+
backgroundColor: '#ecf2fb',
|
|
710
|
+
overflow: 'hidden',
|
|
711
|
+
},
|
|
712
|
+
compareFillPrimary: {
|
|
713
|
+
height: '100%',
|
|
714
|
+
borderRadius: 999,
|
|
715
|
+
backgroundColor: '#2E7DD1',
|
|
716
|
+
},
|
|
717
|
+
compareFillSecondary: {
|
|
718
|
+
height: '100%',
|
|
719
|
+
borderRadius: 999,
|
|
720
|
+
backgroundColor: '#F29F45',
|
|
721
|
+
},
|
|
506
722
|
trendBadge: {
|
|
507
723
|
borderRadius: 999,
|
|
508
724
|
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';
|
|
@@ -43,6 +44,24 @@ const toMonthKey = (value) => {
|
|
|
43
44
|
.replace(/[\u0300-\u036f]/g, '');
|
|
44
45
|
};
|
|
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
|
+
|
|
46
65
|
const compactNumber = (value) => {
|
|
47
66
|
const num = toNumber(value);
|
|
48
67
|
if (Math.abs(num) >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`;
|
|
@@ -107,6 +126,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
107
126
|
const [loading, setLoading] = useState(true);
|
|
108
127
|
const [refreshing, setRefreshing] = useState(false);
|
|
109
128
|
const [monthsModal, setMonthsModal] = useState(false);
|
|
129
|
+
const [compareModal, setCompareModal] = useState(false);
|
|
130
|
+
const [compareMode, setCompareMode] = useState('load_yoy');
|
|
110
131
|
const [table, setTable] = useState(null);
|
|
111
132
|
const [lineData, setLineData] = useState(null);
|
|
112
133
|
const [barData, setBarData] = useState(null);
|
|
@@ -200,6 +221,45 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
200
221
|
const loadYoY = percentChange(load2025Total, load2024Total);
|
|
201
222
|
const revenueYoY = percentChange(revenue2025Total, revenue2024Total);
|
|
202
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);
|
|
203
263
|
|
|
204
264
|
const topMonth = useMemo(() => {
|
|
205
265
|
if (!rows.length) return null;
|
|
@@ -234,11 +294,13 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
234
294
|
<View style={styles.hero}>
|
|
235
295
|
<View style={styles.heroBlobLeft} />
|
|
236
296
|
<View style={styles.heroBlobRight} />
|
|
237
|
-
<
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
297
|
+
<View style={styles.heroTopRow}>
|
|
298
|
+
<TouchableOpacity style={styles.backButton} onPress={onBack}>
|
|
299
|
+
<Text style={styles.backIcon}>‹</Text>
|
|
300
|
+
</TouchableOpacity>
|
|
301
|
+
<Text numberOfLines={1} style={styles.heroTitle}>Transport</Text>
|
|
302
|
+
</View>
|
|
303
|
+
<Text style={styles.heroSubtitle}>Filtered API visuals with compact charts</Text>
|
|
242
304
|
</View>
|
|
243
305
|
|
|
244
306
|
<ScrollView
|
|
@@ -278,6 +340,11 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
278
340
|
</View>
|
|
279
341
|
</View>
|
|
280
342
|
|
|
343
|
+
<TouchableOpacity style={styles.compareButton} onPress={() => setCompareModal(true)}>
|
|
344
|
+
<Text style={styles.compareButtonLabel}>Compare</Text>
|
|
345
|
+
<Text style={styles.compareButtonValue}>{compareLabel}</Text>
|
|
346
|
+
</TouchableOpacity>
|
|
347
|
+
|
|
281
348
|
<View style={styles.statRow}>
|
|
282
349
|
<StatTile
|
|
283
350
|
label="Load YoY"
|
|
@@ -307,6 +374,46 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
307
374
|
/>
|
|
308
375
|
</View>
|
|
309
376
|
|
|
377
|
+
<View style={styles.compareCard}>
|
|
378
|
+
<View style={styles.compareTitleRow}>
|
|
379
|
+
<Text style={styles.compareTitle}>{compareSummary.title}</Text>
|
|
380
|
+
<Text style={[styles.compareDelta, compareDelta >= 0 ? styles.compareDeltaUp : styles.compareDeltaDown]}>
|
|
381
|
+
{compareDelta >= 0 ? '+' : ''}
|
|
382
|
+
{compareDelta.toFixed(1)}%
|
|
383
|
+
</Text>
|
|
384
|
+
</View>
|
|
385
|
+
|
|
386
|
+
<View style={styles.compareMetric}>
|
|
387
|
+
<View style={styles.compareMetricHead}>
|
|
388
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.primaryLabel}</Text>
|
|
389
|
+
<Text style={styles.compareMetricValue}>{compactNumber(compareSummary.primaryValue)}</Text>
|
|
390
|
+
</View>
|
|
391
|
+
<View style={styles.compareTrack}>
|
|
392
|
+
<View
|
|
393
|
+
style={[
|
|
394
|
+
styles.compareFillPrimary,
|
|
395
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.primaryValue) / compareMax) * 100)}%` },
|
|
396
|
+
]}
|
|
397
|
+
/>
|
|
398
|
+
</View>
|
|
399
|
+
</View>
|
|
400
|
+
|
|
401
|
+
<View style={styles.compareMetric}>
|
|
402
|
+
<View style={styles.compareMetricHead}>
|
|
403
|
+
<Text style={styles.compareMetricLabel}>{compareSummary.secondaryLabel}</Text>
|
|
404
|
+
<Text style={styles.compareMetricValue}>{compactNumber(compareSummary.secondaryValue)}</Text>
|
|
405
|
+
</View>
|
|
406
|
+
<View style={styles.compareTrack}>
|
|
407
|
+
<View
|
|
408
|
+
style={[
|
|
409
|
+
styles.compareFillSecondary,
|
|
410
|
+
{ width: `${Math.min(100, (Math.abs(compareSummary.secondaryValue) / compareMax) * 100)}%` },
|
|
411
|
+
]}
|
|
412
|
+
/>
|
|
413
|
+
</View>
|
|
414
|
+
</View>
|
|
415
|
+
</View>
|
|
416
|
+
|
|
310
417
|
<CompactAxisLineChart
|
|
311
418
|
data={filteredLine}
|
|
312
419
|
title="Load Amount Trend (Line)"
|
|
@@ -409,6 +516,15 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
409
516
|
onApply={setSelectedMonths}
|
|
410
517
|
onClose={() => setMonthsModal(false)}
|
|
411
518
|
/>
|
|
519
|
+
|
|
520
|
+
<CompareOptionsModal
|
|
521
|
+
visible={compareModal}
|
|
522
|
+
title="Compare Transportation"
|
|
523
|
+
options={COMPARE_OPTIONS}
|
|
524
|
+
selected={compareMode}
|
|
525
|
+
onApply={setCompareMode}
|
|
526
|
+
onClose={() => setCompareModal(false)}
|
|
527
|
+
/>
|
|
412
528
|
</View>
|
|
413
529
|
);
|
|
414
530
|
};
|
|
@@ -431,6 +547,10 @@ const styles = StyleSheet.create({
|
|
|
431
547
|
paddingBottom: 18,
|
|
432
548
|
overflow: 'hidden',
|
|
433
549
|
},
|
|
550
|
+
heroTopRow: {
|
|
551
|
+
flexDirection: 'row',
|
|
552
|
+
alignItems: 'center',
|
|
553
|
+
},
|
|
434
554
|
heroBlobLeft: {
|
|
435
555
|
position: 'absolute',
|
|
436
556
|
width: 160,
|
|
@@ -464,7 +584,8 @@ const styles = StyleSheet.create({
|
|
|
464
584
|
marginTop: -2,
|
|
465
585
|
},
|
|
466
586
|
heroTitle: {
|
|
467
|
-
|
|
587
|
+
marginLeft: 10,
|
|
588
|
+
flex: 1,
|
|
468
589
|
color: '#fff',
|
|
469
590
|
fontSize: 22,
|
|
470
591
|
fontWeight: '800',
|
|
@@ -531,6 +652,26 @@ const styles = StyleSheet.create({
|
|
|
531
652
|
modeButtonTextActive: {
|
|
532
653
|
color: '#fff',
|
|
533
654
|
},
|
|
655
|
+
compareButton: {
|
|
656
|
+
borderRadius: 14,
|
|
657
|
+
paddingVertical: 10,
|
|
658
|
+
paddingHorizontal: 12,
|
|
659
|
+
backgroundColor: '#fffdf8',
|
|
660
|
+
borderWidth: 1,
|
|
661
|
+
borderColor: '#dfd6c3',
|
|
662
|
+
marginBottom: 10,
|
|
663
|
+
},
|
|
664
|
+
compareButtonLabel: {
|
|
665
|
+
fontSize: 11,
|
|
666
|
+
color: '#736b57',
|
|
667
|
+
marginBottom: 4,
|
|
668
|
+
fontWeight: '700',
|
|
669
|
+
},
|
|
670
|
+
compareButtonValue: {
|
|
671
|
+
fontSize: 13,
|
|
672
|
+
color: '#2e2a21',
|
|
673
|
+
fontWeight: '700',
|
|
674
|
+
},
|
|
534
675
|
statRow: {
|
|
535
676
|
flexDirection: 'row',
|
|
536
677
|
marginBottom: 10,
|
|
@@ -570,6 +711,80 @@ const styles = StyleSheet.create({
|
|
|
570
711
|
fontSize: 11,
|
|
571
712
|
color: '#6b6860',
|
|
572
713
|
},
|
|
714
|
+
compareCard: {
|
|
715
|
+
backgroundColor: '#fffdf8',
|
|
716
|
+
borderRadius: 14,
|
|
717
|
+
borderWidth: 1,
|
|
718
|
+
borderColor: '#dfd6c3',
|
|
719
|
+
paddingVertical: 12,
|
|
720
|
+
paddingHorizontal: 12,
|
|
721
|
+
marginBottom: 10,
|
|
722
|
+
},
|
|
723
|
+
compareTitleRow: {
|
|
724
|
+
flexDirection: 'row',
|
|
725
|
+
alignItems: 'center',
|
|
726
|
+
justifyContent: 'space-between',
|
|
727
|
+
marginBottom: 8,
|
|
728
|
+
},
|
|
729
|
+
compareTitle: {
|
|
730
|
+
fontSize: 13,
|
|
731
|
+
fontWeight: '700',
|
|
732
|
+
color: '#2b2a22',
|
|
733
|
+
flex: 1,
|
|
734
|
+
marginRight: 8,
|
|
735
|
+
},
|
|
736
|
+
compareDelta: {
|
|
737
|
+
fontSize: 12,
|
|
738
|
+
fontWeight: '800',
|
|
739
|
+
paddingHorizontal: 8,
|
|
740
|
+
paddingVertical: 4,
|
|
741
|
+
borderRadius: 999,
|
|
742
|
+
},
|
|
743
|
+
compareDeltaUp: {
|
|
744
|
+
color: '#15724a',
|
|
745
|
+
backgroundColor: '#e4f8ef',
|
|
746
|
+
},
|
|
747
|
+
compareDeltaDown: {
|
|
748
|
+
color: '#b43c44',
|
|
749
|
+
backgroundColor: '#ffe9ea',
|
|
750
|
+
},
|
|
751
|
+
compareMetric: {
|
|
752
|
+
marginTop: 6,
|
|
753
|
+
},
|
|
754
|
+
compareMetricHead: {
|
|
755
|
+
flexDirection: 'row',
|
|
756
|
+
justifyContent: 'space-between',
|
|
757
|
+
alignItems: 'center',
|
|
758
|
+
marginBottom: 4,
|
|
759
|
+
},
|
|
760
|
+
compareMetricLabel: {
|
|
761
|
+
fontSize: 11,
|
|
762
|
+
color: '#6b6860',
|
|
763
|
+
flex: 1,
|
|
764
|
+
marginRight: 8,
|
|
765
|
+
},
|
|
766
|
+
compareMetricValue: {
|
|
767
|
+
fontSize: 12,
|
|
768
|
+
color: '#2e2a21',
|
|
769
|
+
fontWeight: '700',
|
|
770
|
+
},
|
|
771
|
+
compareTrack: {
|
|
772
|
+
width: '100%',
|
|
773
|
+
height: 9,
|
|
774
|
+
borderRadius: 999,
|
|
775
|
+
backgroundColor: '#efe8da',
|
|
776
|
+
overflow: 'hidden',
|
|
777
|
+
},
|
|
778
|
+
compareFillPrimary: {
|
|
779
|
+
height: '100%',
|
|
780
|
+
borderRadius: 999,
|
|
781
|
+
backgroundColor: '#179a79',
|
|
782
|
+
},
|
|
783
|
+
compareFillSecondary: {
|
|
784
|
+
height: '100%',
|
|
785
|
+
borderRadius: 999,
|
|
786
|
+
backgroundColor: '#f4a04a',
|
|
787
|
+
},
|
|
573
788
|
sectionTitle: {
|
|
574
789
|
fontSize: 15,
|
|
575
790
|
fontWeight: '800',
|
|
@@ -48,18 +48,18 @@ const OLD_REPORTS = [
|
|
|
48
48
|
const NEW_REPORTS = [
|
|
49
49
|
{
|
|
50
50
|
id: '1N1',
|
|
51
|
-
title: 'Performance
|
|
52
|
-
desc: '
|
|
51
|
+
title: 'Performance Studio',
|
|
52
|
+
desc: '',
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
id: '2N1',
|
|
56
|
-
title: 'Gross Profit
|
|
57
|
-
desc: '
|
|
56
|
+
title: 'Gross Profit',
|
|
57
|
+
desc: '',
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
id: '3N1',
|
|
61
|
-
title: '
|
|
62
|
-
desc: '
|
|
61
|
+
title: 'Transport',
|
|
62
|
+
desc: '',
|
|
63
63
|
},
|
|
64
64
|
];
|
|
65
65
|
|
|
@@ -130,7 +130,9 @@ const ReportListScreen = ({ onSelect, onExit }) => {
|
|
|
130
130
|
activeOpacity={0.88}
|
|
131
131
|
>
|
|
132
132
|
<Text style={styles.cardTitle}>{report.title}</Text>
|
|
133
|
-
|
|
133
|
+
{report.desc ? (
|
|
134
|
+
<Text style={styles.cardDesc}>{report.desc}</Text>
|
|
135
|
+
) : null}
|
|
134
136
|
</TouchableOpacity>
|
|
135
137
|
))}
|
|
136
138
|
</ScrollView>
|