@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhiraj0720/report1chart",
3
- "version": "3.0.9",
3
+ "version": "3.1.0",
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,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',