@dhiraj0720/report1chart 3.0.8 → 3.1.0

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