@dhiraj0720/report1chart 3.0.9 → 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
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,6 +195,68 @@ 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}>
|
|
@@ -218,6 +310,11 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
218
310
|
|
|
219
311
|
{sortedRows.length ? (
|
|
220
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
|
+
|
|
221
318
|
<View style={styles.kpiRow}>
|
|
222
319
|
<View style={[styles.kpiCard, styles.kpiWarm]}>
|
|
223
320
|
<Text style={styles.kpiLabel}>2024 Value</Text>
|
|
@@ -253,6 +350,65 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
253
350
|
</View>
|
|
254
351
|
</View>
|
|
255
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
|
+
|
|
256
412
|
{spotlight ? (
|
|
257
413
|
<View style={styles.spotlightCard}>
|
|
258
414
|
<Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
|
|
@@ -357,6 +513,15 @@ const Report1ModernScreen = ({ api, token, onBack }) => {
|
|
|
357
513
|
|
|
358
514
|
<View style={styles.footerSpacing} />
|
|
359
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
|
+
/>
|
|
360
525
|
</View>
|
|
361
526
|
);
|
|
362
527
|
};
|
|
@@ -450,6 +615,26 @@ const styles = StyleSheet.create({
|
|
|
450
615
|
paddingHorizontal: 14,
|
|
451
616
|
paddingTop: 14,
|
|
452
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
|
+
},
|
|
453
638
|
center: {
|
|
454
639
|
paddingVertical: 24,
|
|
455
640
|
alignItems: 'center',
|
|
@@ -528,6 +713,80 @@ const styles = StyleSheet.create({
|
|
|
528
713
|
fontWeight: '800',
|
|
529
714
|
fontSize: 13,
|
|
530
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
|
+
},
|
|
531
790
|
spotlightCard: {
|
|
532
791
|
backgroundColor: '#1a2640',
|
|
533
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';
|
|
@@ -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}>
|
|
@@ -232,6 +293,11 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
232
293
|
</TouchableOpacity>
|
|
233
294
|
</View>
|
|
234
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
|
+
|
|
235
301
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.kpiScroll}>
|
|
236
302
|
<KpiCard
|
|
237
303
|
label="2024 Profit"
|
|
@@ -268,6 +334,46 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
268
334
|
</View>
|
|
269
335
|
</View>
|
|
270
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
|
+
|
|
271
377
|
<CompactAxisLineChart
|
|
272
378
|
data={filteredLine}
|
|
273
379
|
title="Profit Amount Trend (Line)"
|
|
@@ -347,6 +453,15 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
|
|
|
347
453
|
onApply={setSelectedMonths}
|
|
348
454
|
onClose={() => setMonthsModal(false)}
|
|
349
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
|
+
/>
|
|
350
465
|
</View>
|
|
351
466
|
);
|
|
352
467
|
};
|
|
@@ -444,6 +559,26 @@ const styles = StyleSheet.create({
|
|
|
444
559
|
color: '#14253d',
|
|
445
560
|
fontWeight: '700',
|
|
446
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
|
+
},
|
|
447
582
|
kpiScroll: {
|
|
448
583
|
marginBottom: 12,
|
|
449
584
|
},
|
|
@@ -503,6 +638,80 @@ const styles = StyleSheet.create({
|
|
|
503
638
|
fontWeight: '700',
|
|
504
639
|
color: '#152842',
|
|
505
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
|
+
},
|
|
506
715
|
trendBadge: {
|
|
507
716
|
borderRadius: 999,
|
|
508
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';
|
|
@@ -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;
|
|
@@ -278,6 +338,11 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
278
338
|
</View>
|
|
279
339
|
</View>
|
|
280
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
|
+
|
|
281
346
|
<View style={styles.statRow}>
|
|
282
347
|
<StatTile
|
|
283
348
|
label="Load YoY"
|
|
@@ -307,6 +372,46 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
307
372
|
/>
|
|
308
373
|
</View>
|
|
309
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
|
+
|
|
310
415
|
<CompactAxisLineChart
|
|
311
416
|
data={filteredLine}
|
|
312
417
|
title="Load Amount Trend (Line)"
|
|
@@ -409,6 +514,15 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
|
|
|
409
514
|
onApply={setSelectedMonths}
|
|
410
515
|
onClose={() => setMonthsModal(false)}
|
|
411
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
|
+
/>
|
|
412
526
|
</View>
|
|
413
527
|
);
|
|
414
528
|
};
|
|
@@ -531,6 +645,26 @@ const styles = StyleSheet.create({
|
|
|
531
645
|
modeButtonTextActive: {
|
|
532
646
|
color: '#fff',
|
|
533
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
|
+
},
|
|
534
668
|
statRow: {
|
|
535
669
|
flexDirection: 'row',
|
|
536
670
|
marginBottom: 10,
|
|
@@ -570,6 +704,80 @@ const styles = StyleSheet.create({
|
|
|
570
704
|
fontSize: 11,
|
|
571
705
|
color: '#6b6860',
|
|
572
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
|
+
},
|
|
573
781
|
sectionTitle: {
|
|
574
782
|
fontSize: 15,
|
|
575
783
|
fontWeight: '800',
|