@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhiraj0720/report1chart",
3
- "version": "3.0.9",
3
+ "version": "3.1.1",
4
4
  "main": "src/index.jsx",
5
5
  "scripts": {
6
6
  "test": "echo 'No tests'"
@@ -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 computedGraphWidth = Math.max(minGraphWidth, labels.length * groupSpacing);
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: 20,
80
- barGap: 4,
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 usableWidth = graphWidth - paddingLeft - paddingRight;
90
- const step = labels.length > 1 ? usableWidth / (labels.length - 1) : usableWidth / 2;
91
- if (labels.length <= 1) return graphWidth / 2;
92
- return paddingLeft + index * step;
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
- <TouchableOpacity onPress={onBack} style={styles.backButton}>
175
- <Text style={styles.backIcon}>‹</Text>
176
- </TouchableOpacity>
177
- <Text style={styles.heroTitle}>Performance Studio</Text>
178
- <Text style={styles.heroSubtitle}>Three-tab live performance monitor</Text>
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
- marginTop: 10,
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
- <TouchableOpacity onPress={onBack} style={styles.backButton}>
203
- <Text style={styles.backIcon}>‹</Text>
204
- </TouchableOpacity>
205
- <Text style={styles.heroTitle}>Gross Profit Dashboard</Text>
206
- <Text style={styles.heroSubtitle}>Compact analytical view with filtered month data</Text>
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
- marginTop: 10,
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
- <TouchableOpacity style={styles.backButton} onPress={onBack}>
238
- <Text style={styles.backIcon}>‹</Text>
239
- </TouchableOpacity>
240
- <Text style={styles.heroTitle}>Transportation Command Center</Text>
241
- <Text style={styles.heroSubtitle}>Filtered API visuals with compact axis charts</Text>
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
- marginTop: 10,
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 Report',
52
- desc: 'Modern performance studio',
51
+ title: 'Performance Studio',
52
+ desc: '',
53
53
  },
54
54
  {
55
55
  id: '2N1',
56
- title: 'Gross Profit by Company & Division',
57
- desc: 'Modern interactive layout',
56
+ title: 'Gross Profit',
57
+ desc: '',
58
58
  },
59
59
  {
60
60
  id: '3N1',
61
- title: 'Transportation Business Analysis',
62
- desc: 'Modern command center layout',
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
- <Text style={styles.cardDesc}>{report.desc}</Text>
133
+ {report.desc ? (
134
+ <Text style={styles.cardDesc}>{report.desc}</Text>
135
+ ) : null}
134
136
  </TouchableOpacity>
135
137
  ))}
136
138
  </ScrollView>