@dhiraj0720/report1chart 3.0.7 → 3.0.9

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.7",
3
+ "version": "3.0.9",
4
4
  "main": "src/index.jsx",
5
5
  "scripts": {
6
6
  "test": "echo 'No tests'"
@@ -36,8 +36,8 @@ const CompactAxisBarLineChart = ({
36
36
  title,
37
37
  legend,
38
38
  height = 220,
39
- minGraphWidth = 360,
40
- groupSpacing = 86,
39
+ minGraphWidth = 300,
40
+ groupSpacing = 64,
41
41
  }) => {
42
42
  const labels = data?.labels || [];
43
43
  const series = data?.series || [];
@@ -76,8 +76,8 @@ const CompactAxisBarLineChart = ({
76
76
  paddingTop: computedPaddingTop,
77
77
  chartHeight: computedChartHeight,
78
78
  maxValue: peak,
79
- barWidth: 18,
80
- barGap: 8,
79
+ barWidth: 20,
80
+ barGap: 4,
81
81
  };
82
82
  }, [barSeriesA, barSeriesB, groupSpacing, height, labels.length, lineSeries, minGraphWidth]);
83
83
 
@@ -251,7 +251,7 @@ const CompactAxisBarLineChart = ({
251
251
  fontSize="10"
252
252
  fill="#4f6076"
253
253
  textAnchor="middle"
254
- transform={`rotate(-35 ${cx} ${height - 14})`}
254
+ transform={`rotate(-30 ${cx} ${height - 14})`}
255
255
  >
256
256
  {label}
257
257
  </SvgText>
@@ -26,8 +26,8 @@ const CompactAxisLineChart = ({
26
26
  title,
27
27
  legend,
28
28
  height = 210,
29
- minGraphWidth = 340,
30
- pointSpacing = 72,
29
+ minGraphWidth = 300,
30
+ pointSpacing = 56,
31
31
  }) => {
32
32
  const labels = data?.labels || [];
33
33
  const series = data?.series || [];
@@ -171,7 +171,7 @@ const CompactAxisLineChart = ({
171
171
  fontSize="10"
172
172
  fill="#4f6076"
173
173
  textAnchor="middle"
174
- transform={`rotate(-35 ${toX(index)} ${height - 14})`}
174
+ transform={`rotate(-30 ${toX(index)} ${height - 14})`}
175
175
  >
176
176
  {label}
177
177
  </SvgText>
@@ -0,0 +1,276 @@
1
+ import React from 'react';
2
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
3
+
4
+ const normalizeCell = (value) => {
5
+ if (value === null || value === undefined) {
6
+ return { text: '-', color: '#2b3850', weight: '600' };
7
+ }
8
+
9
+ if (typeof value === 'object' && !Array.isArray(value) && value.text !== undefined) {
10
+ return {
11
+ text: value.text,
12
+ color: value.color || '#2b3850',
13
+ weight: value.weight || '600',
14
+ };
15
+ }
16
+
17
+ return { text: String(value), color: '#2b3850', weight: '600' };
18
+ };
19
+
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
+
60
+ return (
61
+ <View style={styles.card}>
62
+ <Text style={styles.title}>{title}</Text>
63
+
64
+ {!rows.length ? (
65
+ <View style={styles.emptyWrap}>
66
+ <Text style={styles.emptyText}>No table data for selected filters.</Text>
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>
146
+ ) : (
147
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
148
+ <View>
149
+ <View style={styles.headerRow}>
150
+ {columns.map((col) => (
151
+ <View
152
+ key={`header-${col.key}`}
153
+ style={[
154
+ styles.cell,
155
+ styles.headerCell,
156
+ { width: col.width || (compact ? 110 : 126) },
157
+ col.align === 'right' && styles.alignRight,
158
+ ]}
159
+ >
160
+ <Text
161
+ style={[
162
+ styles.headerText,
163
+ col.align === 'right' && styles.textRight,
164
+ col.align === 'center' && styles.textCenter,
165
+ ]}
166
+ >
167
+ {col.label}
168
+ </Text>
169
+ </View>
170
+ ))}
171
+ </View>
172
+
173
+ {rows.map((row, rowIndex) => (
174
+ <View
175
+ key={`row-${rowIndex}`}
176
+ style={[
177
+ styles.dataRow,
178
+ rowIndex % 2 === 0 ? styles.rowEven : styles.rowOdd,
179
+ ]}
180
+ >
181
+ {columns.map((col) => renderCell(col, row, rowIndex, compact))}
182
+ </View>
183
+ ))}
184
+ </View>
185
+ </ScrollView>
186
+ )}
187
+ </View>
188
+ );
189
+ };
190
+
191
+ const styles = StyleSheet.create({
192
+ card: {
193
+ backgroundColor: '#ffffff',
194
+ borderWidth: 1,
195
+ borderColor: '#d4deed',
196
+ borderRadius: 14,
197
+ padding: 10,
198
+ marginBottom: 12,
199
+ },
200
+ title: {
201
+ fontSize: 13,
202
+ fontWeight: '800',
203
+ color: '#1c2f47',
204
+ marginBottom: 8,
205
+ },
206
+ freezeWrap: {
207
+ flexDirection: 'row',
208
+ },
209
+ frozenSide: {
210
+ borderRightWidth: 1,
211
+ borderRightColor: '#c7d7ef',
212
+ backgroundColor: '#fff',
213
+ zIndex: 2,
214
+ },
215
+ headerRow: {
216
+ flexDirection: 'row',
217
+ backgroundColor: '#1f385a',
218
+ borderTopLeftRadius: 10,
219
+ borderTopRightRadius: 10,
220
+ overflow: 'hidden',
221
+ },
222
+ headerCell: {
223
+ borderBottomWidth: 1,
224
+ borderBottomColor: '#2d4d78',
225
+ },
226
+ headerText: {
227
+ fontSize: 11,
228
+ color: '#f3f8ff',
229
+ fontWeight: '700',
230
+ },
231
+ dataRow: {
232
+ flexDirection: 'row',
233
+ },
234
+ rowEven: {
235
+ backgroundColor: '#f8fbff',
236
+ },
237
+ rowOdd: {
238
+ backgroundColor: '#ffffff',
239
+ },
240
+ cell: {
241
+ paddingVertical: 10,
242
+ paddingHorizontal: 8,
243
+ borderRightWidth: 1,
244
+ borderRightColor: '#e2ebf7',
245
+ justifyContent: 'center',
246
+ },
247
+ frozenCell: {
248
+ borderRightColor: '#c7d7ef',
249
+ },
250
+ cellText: {
251
+ fontSize: 11.5,
252
+ color: '#2b3850',
253
+ },
254
+ alignRight: {
255
+ alignItems: 'flex-end',
256
+ },
257
+ textRight: {
258
+ textAlign: 'right',
259
+ },
260
+ textCenter: {
261
+ textAlign: 'center',
262
+ },
263
+ emptyWrap: {
264
+ borderWidth: 1,
265
+ borderColor: '#dce5f3',
266
+ borderRadius: 10,
267
+ paddingVertical: 18,
268
+ alignItems: 'center',
269
+ },
270
+ emptyText: {
271
+ color: '#6b7a91',
272
+ fontSize: 12,
273
+ },
274
+ });
275
+
276
+ export default ModernDataTable;
package/src/index.jsx CHANGED
@@ -80,7 +80,7 @@ if (active === '1N1') {
80
80
  return (
81
81
  <SafeScreen>
82
82
  <Report1ModernScreen
83
- endpoint={config.report1.url}
83
+ api={config.report4}
84
84
  token={config.token}
85
85
  onBack={() => setActive(null)}
86
86
  />
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  ScrollView,
@@ -7,10 +7,16 @@ import {
7
7
  TouchableOpacity,
8
8
  View,
9
9
  } from 'react-native';
10
- import Svg, { Circle } from 'react-native-svg';
11
- import fetchReport1 from '../api/report1Fetcher';
10
+ import fetchReport4 from '../api/report4Fetcher';
11
+ import ModernDataTable from '../components/ModernDataTable';
12
12
  import { formatNumber } from '../utils/formatNumber';
13
13
 
14
+ const TABS = [
15
+ { key: 'kumule', label: 'KÜMÜLE' },
16
+ { key: 'faaliyet', label: 'FAALİYET KAR/ZARAR' },
17
+ { key: 'fg', label: 'FG / BRÜT KAR ORANI' },
18
+ ];
19
+
14
20
  const toNumber = (value) => {
15
21
  const numeric = Number(value);
16
22
  return Number.isFinite(numeric) ? numeric : 0;
@@ -21,86 +27,144 @@ const toPercent = (current, previous) => {
21
27
  return ((current - previous) / Math.abs(previous)) * 100;
22
28
  };
23
29
 
24
- const RingGauge = ({ progress }) => {
25
- const size = 58;
26
- const stroke = 7;
27
- const radius = (size - stroke) / 2;
28
- const circumference = 2 * Math.PI * radius;
29
- const clamped = Math.max(0, Math.min(100, progress));
30
- const offset = circumference - (circumference * clamped) / 100;
30
+ const asPercentCell = (value) => {
31
+ const numeric = toNumber(value);
32
+ const positive = numeric >= 0;
33
+ return {
34
+ text: `${positive ? '+' : ''}${numeric.toFixed(1)}%`,
35
+ color: positive ? '#15724a' : '#b43c44',
36
+ weight: '700',
37
+ };
38
+ };
31
39
 
32
- return (
33
- <Svg width={size} height={size}>
34
- <Circle
35
- cx={size / 2}
36
- cy={size / 2}
37
- r={radius}
38
- stroke="#3f4e68"
39
- strokeWidth={stroke}
40
- fill="none"
41
- />
42
- <Circle
43
- cx={size / 2}
44
- cy={size / 2}
45
- r={radius}
46
- stroke="#ffb155"
47
- strokeWidth={stroke}
48
- strokeLinecap="round"
49
- strokeDasharray={`${circumference} ${circumference}`}
50
- strokeDashoffset={offset}
51
- fill="none"
52
- transform={`rotate(-90 ${size / 2} ${size / 2})`}
53
- />
54
- </Svg>
55
- );
40
+ const mapRowByTab = (row, tabKey) => {
41
+ if (tabKey === 'faaliyet') {
42
+ return {
43
+ name: row.name,
44
+ value2024: toNumber(row.fk2024),
45
+ value2025: toNumber(row.fk2025),
46
+ change: toNumber(row.changePercent),
47
+ budget2025: toNumber(row.budget2025),
48
+ variance: toNumber(row.budgetVariancePercent),
49
+ ratio2024: null,
50
+ ratio2025: null,
51
+ };
52
+ }
53
+
54
+ if (tabKey === 'fg') {
55
+ const ratio2024 = toNumber(row.ratio2024);
56
+ const ratio2025 = toNumber(row.ratio2025);
57
+ return {
58
+ name: row.name,
59
+ value2024: toNumber(row.gross2024),
60
+ value2025: toNumber(row.gross2025),
61
+ change: ratio2025 - ratio2024,
62
+ budget2025: toNumber(row.fk2025 || row.budget2025),
63
+ variance: ratio2025 - ratio2024,
64
+ ratio2024,
65
+ ratio2025,
66
+ };
67
+ }
68
+
69
+ return {
70
+ name: row.name,
71
+ value2024: toNumber(row.gross2024),
72
+ value2025: toNumber(row.gross2025),
73
+ change: toNumber(row.grossChangePercent),
74
+ budget2025: toNumber(row.budgetGross2025),
75
+ variance: toNumber(row.budgetGrossVariancePercent),
76
+ ratio2024: toNumber(row.ratio2024),
77
+ ratio2025: toNumber(row.ratio2025),
78
+ };
56
79
  };
57
80
 
58
- const Report1ModernScreen = ({ endpoint, token, onBack }) => {
59
- const [rows, setRows] = useState([]);
60
- const [loading, setLoading] = useState(true);
61
- const [viewMode, setViewMode] = useState('yoy');
81
+ const Report1ModernScreen = ({ api, token, onBack }) => {
82
+ const [activeTab, setActiveTab] = useState('kumule');
62
83
  const [sortMode, setSortMode] = useState('growth');
84
+ const [rowsByTab, setRowsByTab] = useState({});
85
+ const [loadedByTab, setLoadedByTab] = useState({});
86
+ const [errorByTab, setErrorByTab] = useState({});
87
+ const [loading, setLoading] = useState(false);
88
+
89
+ const endpointByTab = useMemo(() => ({
90
+ kumule: api?.kumule,
91
+ faaliyet: api?.faaliyetKarZarar,
92
+ fg: api?.fgBrutKarOrani,
93
+ }), [api]);
94
+
95
+ const loadTabData = useCallback(async (tabKey, force = false) => {
96
+ const endpoint = endpointByTab[tabKey];
97
+
98
+ if (!endpoint || !token) {
99
+ setErrorByTab((prev) => ({
100
+ ...prev,
101
+ [tabKey]: 'API endpoint or token missing.',
102
+ }));
103
+ return;
104
+ }
63
105
 
64
- useEffect(() => {
65
106
  setLoading(true);
66
- fetchReport1(endpoint, token)
67
- .then((data) => setRows(data || []))
68
- .catch(() => setRows([]))
69
- .finally(() => setLoading(false));
70
- }, [endpoint, token]);
107
+ setErrorByTab((prev) => ({ ...prev, [tabKey]: null }));
108
+
109
+ try {
110
+ const rows = await fetchReport4(endpoint, token);
111
+ const sortedRows = [...rows].sort(
112
+ (a, b) => (a.sortOrder || 0) - (b.sortOrder || 0),
113
+ );
114
+
115
+ setRowsByTab((prev) => ({ ...prev, [tabKey]: sortedRows }));
116
+ setLoadedByTab((prev) => ({ ...prev, [tabKey]: true }));
117
+ } catch (error) {
118
+ setErrorByTab((prev) => ({
119
+ ...prev,
120
+ [tabKey]: error?.message || 'Failed to load performance data.',
121
+ }));
122
+ } finally {
123
+ setLoading(false);
124
+ }
125
+ }, [endpointByTab, token]);
126
+
127
+ useEffect(() => {
128
+ loadTabData(activeTab);
129
+ }, [activeTab, loadTabData]);
130
+
131
+ const activeRawRows = rowsByTab[activeTab] || [];
132
+ const activeError = errorByTab[activeTab];
133
+ const activeLoaded = loadedByTab[activeTab];
134
+
135
+ const mappedRows = useMemo(() => {
136
+ return activeRawRows.map((row) => mapRowByTab(row, activeTab));
137
+ }, [activeRawRows, activeTab]);
138
+
139
+ const sortedRows = useMemo(() => {
140
+ const copy = [...mappedRows];
141
+ if (sortMode === 'name') {
142
+ return copy.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
143
+ }
144
+ if (sortMode === 'variance') {
145
+ return copy.sort((a, b) => b.variance - a.variance);
146
+ }
147
+ return copy.sort((a, b) => b.change - a.change);
148
+ }, [mappedRows, sortMode]);
71
149
 
72
150
  const totals = useMemo(() => {
73
- const total2024 = rows.reduce((sum, row) => sum + toNumber(row.actual2024), 0);
74
- const total2025 = rows.reduce((sum, row) => sum + toNumber(row.actual2025), 0);
75
- const budget2025 = rows.reduce((sum, row) => sum + toNumber(row.budget2025), 0);
151
+ const total2024 = sortedRows.reduce((sum, row) => sum + row.value2024, 0);
152
+ const total2025 = sortedRows.reduce((sum, row) => sum + row.value2025, 0);
153
+ const budget2025 = sortedRows.reduce((sum, row) => sum + row.budget2025, 0);
154
+ const avgRatio2025 = sortedRows.length
155
+ ? sortedRows.reduce((sum, row) => sum + (row.ratio2025 || 0), 0) / sortedRows.length
156
+ : 0;
157
+
76
158
  return {
77
159
  total2024,
78
160
  total2025,
79
161
  budget2025,
80
162
  yoy: toPercent(total2025, total2024),
163
+ avgRatio2025,
81
164
  };
82
- }, [rows]);
165
+ }, [sortedRows]);
83
166
 
84
- const sortedRows = useMemo(() => {
85
- const copy = [...rows];
86
- if (sortMode === 'variance') {
87
- return copy.sort((a, b) => toNumber(b.budgetVariancePercent) - toNumber(a.budgetVariancePercent));
88
- }
89
- if (sortMode === 'name') {
90
- return copy.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
91
- }
92
- return copy.sort((a, b) => toNumber(b.actualChangePercent) - toNumber(a.actualChangePercent));
93
- }, [rows, sortMode]);
94
-
95
- const topItem = sortedRows[0];
96
-
97
- if (loading) {
98
- return (
99
- <View style={styles.loaderWrap}>
100
- <ActivityIndicator size="large" color="#ffb155" />
101
- </View>
102
- );
103
- }
167
+ const spotlight = sortedRows[0];
104
168
 
105
169
  return (
106
170
  <View style={styles.screen}>
@@ -111,151 +175,185 @@ const Report1ModernScreen = ({ endpoint, token, onBack }) => {
111
175
  <Text style={styles.backIcon}>‹</Text>
112
176
  </TouchableOpacity>
113
177
  <Text style={styles.heroTitle}>Performance Studio</Text>
114
- <Text style={styles.heroSubtitle}>Live operational performance map by activity</Text>
178
+ <Text style={styles.heroSubtitle}>Three-tab live performance monitor</Text>
179
+ </View>
180
+
181
+ <View style={styles.tabsContainer}>
182
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
183
+ {TABS.map((tab) => {
184
+ const isActive = tab.key === activeTab;
185
+ return (
186
+ <TouchableOpacity
187
+ key={tab.key}
188
+ style={[styles.tabButton, isActive && styles.activeTabButton]}
189
+ onPress={() => setActiveTab(tab.key)}
190
+ >
191
+ <Text style={[styles.tabText, isActive && styles.activeTabText]}>
192
+ {tab.label}
193
+ </Text>
194
+ </TouchableOpacity>
195
+ );
196
+ })}
197
+ </ScrollView>
115
198
  </View>
116
199
 
117
200
  <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
118
- <View style={styles.kpiRow}>
119
- <View style={[styles.kpiCard, styles.kpiWarm]}>
120
- <Text style={styles.kpiLabel}>2024 Actual</Text>
121
- <Text style={styles.kpiValue}>{formatNumber(totals.total2024)}</Text>
122
- </View>
123
- <View style={[styles.kpiCard, styles.kpiBlue]}>
124
- <Text style={styles.kpiLabel}>2025 Actual</Text>
125
- <Text style={styles.kpiValue}>{formatNumber(totals.total2025)}</Text>
126
- </View>
127
- </View>
128
- <View style={styles.kpiRow}>
129
- <View style={[styles.kpiCard, styles.kpiIndigo]}>
130
- <Text style={styles.kpiLabel}>2025 Budget</Text>
131
- <Text style={styles.kpiValue}>{formatNumber(totals.budget2025)}</Text>
132
- </View>
133
- <View style={[styles.kpiCard, styles.kpiTeal]}>
134
- <Text style={styles.kpiLabel}>Total YoY</Text>
135
- <Text style={styles.kpiValue}>
136
- {totals.yoy >= 0 ? '+' : ''}
137
- {totals.yoy.toFixed(1)}%
138
- </Text>
139
- </View>
140
- </View>
141
-
142
- {topItem ? (
143
- <View style={styles.spotlightCard}>
144
- <Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
145
- <Text style={styles.spotlightName}>{topItem.name}</Text>
146
- <Text style={styles.spotlightValue}>
147
- {formatNumber(topItem.actual2025)} in 2025 • {toNumber(topItem.actualChangePercent).toFixed(1)}% YoY
148
- </Text>
201
+ {loading && !activeLoaded ? (
202
+ <View style={styles.center}>
203
+ <ActivityIndicator size="large" color="#ffb155" />
149
204
  </View>
150
205
  ) : null}
151
206
 
152
- <View style={styles.controlsWrap}>
153
- <View style={styles.segmentRow}>
154
- <TouchableOpacity
155
- style={[styles.segmentBtn, viewMode === 'yoy' && styles.segmentBtnActive]}
156
- onPress={() => setViewMode('yoy')}
157
- >
158
- <Text style={[styles.segmentText, viewMode === 'yoy' && styles.segmentTextActive]}>YoY</Text>
159
- </TouchableOpacity>
160
- <TouchableOpacity
161
- style={[styles.segmentBtn, viewMode === 'budget' && styles.segmentBtnActive]}
162
- onPress={() => setViewMode('budget')}
163
- >
164
- <Text style={[styles.segmentText, viewMode === 'budget' && styles.segmentTextActive]}>Budget</Text>
165
- </TouchableOpacity>
207
+ {!loading && activeError && !sortedRows.length ? (
208
+ <View style={styles.center}>
209
+ <Text style={styles.errorText}>{activeError}</Text>
166
210
  <TouchableOpacity
167
- style={[styles.segmentBtn, viewMode === 'efficiency' && styles.segmentBtnActive]}
168
- onPress={() => setViewMode('efficiency')}
211
+ style={styles.retryButton}
212
+ onPress={() => loadTabData(activeTab, true)}
169
213
  >
170
- <Text style={[styles.segmentText, viewMode === 'efficiency' && styles.segmentTextActive]}>Efficiency</Text>
214
+ <Text style={styles.retryText}>Retry</Text>
171
215
  </TouchableOpacity>
172
216
  </View>
217
+ ) : null}
173
218
 
174
- <View style={styles.sortRow}>
175
- <TouchableOpacity
176
- style={[styles.sortChip, sortMode === 'growth' && styles.sortChipActive]}
177
- onPress={() => setSortMode('growth')}
178
- >
179
- <Text style={[styles.sortChipText, sortMode === 'growth' && styles.sortChipTextActive]}>Top Growth</Text>
180
- </TouchableOpacity>
181
- <TouchableOpacity
182
- style={[styles.sortChip, sortMode === 'variance' && styles.sortChipActive]}
183
- onPress={() => setSortMode('variance')}
184
- >
185
- <Text style={[styles.sortChipText, sortMode === 'variance' && styles.sortChipTextActive]}>Top Variance</Text>
186
- </TouchableOpacity>
187
- <TouchableOpacity
188
- style={[styles.sortChip, sortMode === 'name' && styles.sortChipActive]}
189
- onPress={() => setSortMode('name')}
190
- >
191
- <Text style={[styles.sortChipText, sortMode === 'name' && styles.sortChipTextActive]}>A-Z</Text>
192
- </TouchableOpacity>
193
- </View>
194
- </View>
195
-
196
- {sortedRows.map((item) => {
197
- const actual2024 = toNumber(item.actual2024);
198
- const actual2025 = toNumber(item.actual2025);
199
- const budget2025 = toNumber(item.budget2025);
200
- const growth = toNumber(item.actualChangePercent);
201
- const variance = toNumber(item.budgetVariancePercent);
202
- const efficiency = toNumber(item.opexToGrossProfitPercent);
203
- const maxForBar = Math.max(1, actual2024, actual2025, budget2025);
204
- const bar2024Width = `${(actual2024 / maxForBar) * 100}%`;
205
- const bar2025Width = `${(actual2025 / maxForBar) * 100}%`;
206
- const budgetWidth = `${(budget2025 / maxForBar) * 100}%`;
207
-
208
- const badgeValue =
209
- viewMode === 'budget' ? variance : viewMode === 'efficiency' ? efficiency : growth;
210
-
211
- return (
212
- <View key={item.name} style={styles.card}>
213
- <View style={styles.cardHeader}>
214
- <Text style={styles.cardTitle}>{item.name}</Text>
215
- <Text style={[styles.badge, badgeValue >= 0 ? styles.badgeUp : styles.badgeDown]}>
216
- {badgeValue >= 0 ? '+' : ''}
217
- {badgeValue.toFixed(1)}%
219
+ {sortedRows.length ? (
220
+ <>
221
+ <View style={styles.kpiRow}>
222
+ <View style={[styles.kpiCard, styles.kpiWarm]}>
223
+ <Text style={styles.kpiLabel}>2024 Value</Text>
224
+ <Text style={styles.kpiValue}>{formatNumber(totals.total2024)}</Text>
225
+ </View>
226
+ <View style={[styles.kpiCard, styles.kpiBlue]}>
227
+ <Text style={styles.kpiLabel}>2025 Value</Text>
228
+ <Text style={styles.kpiValue}>{formatNumber(totals.total2025)}</Text>
229
+ </View>
230
+ </View>
231
+ <View style={styles.kpiRow}>
232
+ <View style={[styles.kpiCard, styles.kpiIndigo]}>
233
+ <Text style={styles.kpiLabel}>2025 Budget</Text>
234
+ <Text style={styles.kpiValue}>{formatNumber(totals.budget2025)}</Text>
235
+ </View>
236
+ <View style={[styles.kpiCard, styles.kpiTeal]}>
237
+ <Text style={styles.kpiLabel}>Total Change</Text>
238
+ <Text style={styles.kpiValue}>
239
+ {totals.yoy >= 0 ? '+' : ''}
240
+ {totals.yoy.toFixed(1)}%
218
241
  </Text>
219
242
  </View>
243
+ </View>
220
244
 
221
- <View style={styles.row}>
222
- <Text style={styles.rowLabel}>2024</Text>
223
- <View style={styles.track}>
224
- <View style={[styles.fill2024, { width: bar2024Width }]} />
225
- </View>
226
- <Text style={styles.rowValue}>{formatNumber(actual2024)}</Text>
245
+ <View style={styles.insightStrip}>
246
+ <View>
247
+ <Text style={styles.insightLabel}>Average Ratio 2025</Text>
248
+ <Text style={styles.insightValue}>{totals.avgRatio2025.toFixed(2)}%</Text>
249
+ </View>
250
+ <View>
251
+ <Text style={styles.insightLabel}>Tab</Text>
252
+ <Text style={styles.insightValue}>{TABS.find((x) => x.key === activeTab)?.label}</Text>
227
253
  </View>
254
+ </View>
228
255
 
229
- <View style={styles.row}>
230
- <Text style={styles.rowLabel}>2025</Text>
231
- <View style={styles.track}>
232
- <View style={[styles.fill2025, { width: bar2025Width }]} />
233
- </View>
234
- <Text style={styles.rowValue}>{formatNumber(actual2025)}</Text>
256
+ {spotlight ? (
257
+ <View style={styles.spotlightCard}>
258
+ <Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
259
+ <Text style={styles.spotlightName}>{spotlight.name}</Text>
260
+ <Text style={styles.spotlightValue}>
261
+ 2025: {formatNumber(spotlight.value2025)} • Change: {spotlight.change >= 0 ? '+' : ''}
262
+ {spotlight.change.toFixed(1)}%
263
+ </Text>
235
264
  </View>
265
+ ) : null}
236
266
 
237
- {viewMode !== 'yoy' ? (
238
- <View style={styles.row}>
239
- <Text style={styles.rowLabel}>Budget</Text>
240
- <View style={styles.track}>
241
- <View style={[styles.fillBudget, { width: budgetWidth }]} />
267
+ <View style={styles.sortRow}>
268
+ <TouchableOpacity
269
+ style={[styles.sortChip, sortMode === 'growth' && styles.sortChipActive]}
270
+ onPress={() => setSortMode('growth')}
271
+ >
272
+ <Text style={[styles.sortChipText, sortMode === 'growth' && styles.sortChipTextActive]}>
273
+ Top Growth
274
+ </Text>
275
+ </TouchableOpacity>
276
+ <TouchableOpacity
277
+ style={[styles.sortChip, sortMode === 'variance' && styles.sortChipActive]}
278
+ onPress={() => setSortMode('variance')}
279
+ >
280
+ <Text style={[styles.sortChipText, sortMode === 'variance' && styles.sortChipTextActive]}>
281
+ Top Variance
282
+ </Text>
283
+ </TouchableOpacity>
284
+ <TouchableOpacity
285
+ style={[styles.sortChip, sortMode === 'name' && styles.sortChipActive]}
286
+ onPress={() => setSortMode('name')}
287
+ >
288
+ <Text style={[styles.sortChipText, sortMode === 'name' && styles.sortChipTextActive]}>
289
+ A-Z
290
+ </Text>
291
+ </TouchableOpacity>
292
+ </View>
293
+
294
+ {sortedRows.map((item) => {
295
+ const maxForBar = Math.max(1, item.value2024, item.value2025, item.budget2025);
296
+ const width2024 = `${(item.value2024 / maxForBar) * 100}%`;
297
+ const width2025 = `${(item.value2025 / maxForBar) * 100}%`;
298
+
299
+ return (
300
+ <View key={item.name} style={styles.card}>
301
+ <View style={styles.cardHeader}>
302
+ <Text style={styles.cardTitle}>{item.name}</Text>
303
+ <Text style={[styles.badge, item.change >= 0 ? styles.badgeUp : styles.badgeDown]}>
304
+ {item.change >= 0 ? '+' : ''}
305
+ {item.change.toFixed(1)}%
306
+ </Text>
242
307
  </View>
243
- <Text style={styles.rowValue}>{formatNumber(budget2025)}</Text>
244
- </View>
245
- ) : null}
246
-
247
- {viewMode === 'efficiency' ? (
248
- <View style={styles.efficiencyRow}>
249
- <RingGauge progress={efficiency} />
250
- <View style={styles.efficiencyTextWrap}>
251
- <Text style={styles.efficiencyLabel}>OPEX / Gross Profit</Text>
252
- <Text style={styles.efficiencyValue}>{efficiency.toFixed(1)}%</Text>
308
+
309
+ <View style={styles.barRow}>
310
+ <Text style={styles.barLabel}>2024</Text>
311
+ <View style={styles.track}>
312
+ <View style={[styles.fill2024, { width: width2024 }]} />
313
+ </View>
314
+ <Text style={styles.barValue}>{formatNumber(item.value2024)}</Text>
315
+ </View>
316
+
317
+ <View style={styles.barRow}>
318
+ <Text style={styles.barLabel}>2025</Text>
319
+ <View style={styles.track}>
320
+ <View style={[styles.fill2025, { width: width2025 }]} />
321
+ </View>
322
+ <Text style={styles.barValue}>{formatNumber(item.value2025)}</Text>
253
323
  </View>
254
324
  </View>
255
- ) : null}
256
- </View>
257
- );
258
- })}
325
+ );
326
+ })}
327
+
328
+ <ModernDataTable
329
+ title="Performance Table"
330
+ freezeFirstColumn
331
+ columns={[
332
+ { key: 'name', label: 'Activity', width: 150 },
333
+ { key: 'value2024', label: '2024', width: 120, align: 'right', render: (row) => formatNumber(row.value2024) },
334
+ { key: 'value2025', label: '2025', width: 120, align: 'right', render: (row) => formatNumber(row.value2025) },
335
+ { key: 'change', label: 'Change %', width: 90, align: 'right', render: (row) => asPercentCell(row.change) },
336
+ { key: 'budget2025', label: 'Budget 2025', width: 130, align: 'right', render: (row) => formatNumber(row.budget2025) },
337
+ { key: 'variance', label: 'Variance %', width: 95, align: 'right', render: (row) => asPercentCell(row.variance) },
338
+ {
339
+ key: 'ratio2024',
340
+ label: 'Ratio 2024',
341
+ width: 92,
342
+ align: 'right',
343
+ render: (row) => (row.ratio2024 === null ? '-' : `${row.ratio2024.toFixed(2)}%`),
344
+ },
345
+ {
346
+ key: 'ratio2025',
347
+ label: 'Ratio 2025',
348
+ width: 92,
349
+ align: 'right',
350
+ render: (row) => (row.ratio2025 === null ? '-' : `${row.ratio2025.toFixed(2)}%`),
351
+ },
352
+ ]}
353
+ rows={sortedRows}
354
+ />
355
+ </>
356
+ ) : null}
259
357
 
260
358
  <View style={styles.footerSpacing} />
261
359
  </ScrollView>
@@ -268,12 +366,6 @@ const styles = StyleSheet.create({
268
366
  flex: 1,
269
367
  backgroundColor: '#0f1726',
270
368
  },
271
- loaderWrap: {
272
- flex: 1,
273
- justifyContent: 'center',
274
- alignItems: 'center',
275
- backgroundColor: '#0f1726',
276
- },
277
369
  hero: {
278
370
  paddingHorizontal: 16,
279
371
  paddingTop: 14,
@@ -325,11 +417,58 @@ const styles = StyleSheet.create({
325
417
  fontSize: 13,
326
418
  fontWeight: '500',
327
419
  },
420
+ tabsContainer: {
421
+ backgroundColor: '#111b2d',
422
+ borderBottomWidth: 1,
423
+ borderBottomColor: '#2f3b57',
424
+ paddingVertical: 8,
425
+ paddingHorizontal: 8,
426
+ },
427
+ tabButton: {
428
+ paddingHorizontal: 14,
429
+ paddingVertical: 9,
430
+ borderRadius: 8,
431
+ marginHorizontal: 4,
432
+ backgroundColor: '#18233a',
433
+ borderWidth: 1,
434
+ borderColor: '#35415e',
435
+ },
436
+ activeTabButton: {
437
+ backgroundColor: '#ffb155',
438
+ borderColor: '#ffb155',
439
+ },
440
+ tabText: {
441
+ fontSize: 12,
442
+ fontWeight: '700',
443
+ color: '#c5d0e8',
444
+ },
445
+ activeTabText: {
446
+ color: '#2e2720',
447
+ },
328
448
  content: {
329
449
  flex: 1,
330
450
  paddingHorizontal: 14,
331
451
  paddingTop: 14,
332
452
  },
453
+ center: {
454
+ paddingVertical: 24,
455
+ alignItems: 'center',
456
+ },
457
+ errorText: {
458
+ color: '#ffb3bb',
459
+ textAlign: 'center',
460
+ marginBottom: 10,
461
+ },
462
+ retryButton: {
463
+ backgroundColor: '#ffb155',
464
+ borderRadius: 8,
465
+ paddingHorizontal: 14,
466
+ paddingVertical: 8,
467
+ },
468
+ retryText: {
469
+ color: '#2e2720',
470
+ fontWeight: '700',
471
+ },
333
472
  kpiRow: {
334
473
  flexDirection: 'row',
335
474
  marginBottom: 10,
@@ -368,6 +507,27 @@ const styles = StyleSheet.create({
368
507
  fontWeight: '800',
369
508
  color: '#1d2738',
370
509
  },
510
+ insightStrip: {
511
+ backgroundColor: '#1a2640',
512
+ borderColor: '#303d58',
513
+ borderWidth: 1,
514
+ borderRadius: 14,
515
+ paddingHorizontal: 12,
516
+ paddingVertical: 10,
517
+ flexDirection: 'row',
518
+ justifyContent: 'space-between',
519
+ marginBottom: 10,
520
+ },
521
+ insightLabel: {
522
+ fontSize: 11,
523
+ color: '#9eb1d3',
524
+ marginBottom: 4,
525
+ },
526
+ insightValue: {
527
+ color: '#fff',
528
+ fontWeight: '800',
529
+ fontSize: 13,
530
+ },
371
531
  spotlightCard: {
372
532
  backgroundColor: '#1a2640',
373
533
  borderColor: '#303d58',
@@ -375,7 +535,7 @@ const styles = StyleSheet.create({
375
535
  borderRadius: 14,
376
536
  paddingHorizontal: 12,
377
537
  paddingVertical: 12,
378
- marginBottom: 12,
538
+ marginBottom: 10,
379
539
  },
380
540
  spotlightLabel: {
381
541
  fontSize: 10,
@@ -394,37 +554,9 @@ const styles = StyleSheet.create({
394
554
  fontSize: 12,
395
555
  color: '#c9d5ed',
396
556
  },
397
- controlsWrap: {
398
- marginBottom: 12,
399
- },
400
- segmentRow: {
401
- flexDirection: 'row',
402
- backgroundColor: '#1a2640',
403
- borderRadius: 12,
404
- borderWidth: 1,
405
- borderColor: '#35415e',
406
- padding: 4,
407
- marginBottom: 8,
408
- },
409
- segmentBtn: {
410
- flex: 1,
411
- alignItems: 'center',
412
- paddingVertical: 9,
413
- borderRadius: 10,
414
- },
415
- segmentBtnActive: {
416
- backgroundColor: '#ffb155',
417
- },
418
- segmentText: {
419
- color: '#c0cee8',
420
- fontWeight: '700',
421
- fontSize: 12,
422
- },
423
- segmentTextActive: {
424
- color: '#2e2720',
425
- },
426
557
  sortRow: {
427
558
  flexDirection: 'row',
559
+ marginBottom: 10,
428
560
  },
429
561
  sortChip: {
430
562
  paddingVertical: 6,
@@ -483,12 +615,12 @@ const styles = StyleSheet.create({
483
615
  backgroundColor: '#522d34',
484
616
  color: '#ff9ca6',
485
617
  },
486
- row: {
618
+ barRow: {
487
619
  flexDirection: 'row',
488
620
  alignItems: 'center',
489
621
  marginBottom: 8,
490
622
  },
491
- rowLabel: {
623
+ barLabel: {
492
624
  width: 54,
493
625
  color: '#bac8e1',
494
626
  fontSize: 11,
@@ -512,37 +644,13 @@ const styles = StyleSheet.create({
512
644
  backgroundColor: '#6db2ff',
513
645
  borderRadius: 999,
514
646
  },
515
- fillBudget: {
516
- height: '100%',
517
- backgroundColor: '#c8cfff',
518
- borderRadius: 999,
519
- },
520
- rowValue: {
521
- width: 86,
647
+ barValue: {
648
+ width: 92,
522
649
  textAlign: 'right',
523
650
  color: '#e4ecff',
524
651
  fontSize: 11,
525
652
  fontWeight: '700',
526
653
  },
527
- efficiencyRow: {
528
- marginTop: 4,
529
- flexDirection: 'row',
530
- alignItems: 'center',
531
- },
532
- efficiencyTextWrap: {
533
- marginLeft: 10,
534
- flex: 1,
535
- },
536
- efficiencyLabel: {
537
- color: '#aebadb',
538
- fontSize: 11,
539
- },
540
- efficiencyValue: {
541
- marginTop: 3,
542
- color: '#ffbf72',
543
- fontSize: 16,
544
- fontWeight: '800',
545
- },
546
654
  footerSpacing: {
547
655
  height: 14,
548
656
  },
@@ -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 ModernDataTable from '../components/ModernDataTable';
16
17
  import { filterChartByMonths } from '../utils/filterChartByMonths';
17
18
  import { formatNumber } from '../utils/formatNumber';
18
19
 
@@ -26,6 +27,14 @@ const toPercent = (current, previous) => {
26
27
  return ((current - previous) / Math.abs(previous)) * 100;
27
28
  };
28
29
 
30
+ const toMonthKey = (value) => {
31
+ return String(value ?? '')
32
+ .trim()
33
+ .toLocaleUpperCase('tr-TR')
34
+ .normalize('NFD')
35
+ .replace(/[\u0300-\u036f]/g, '');
36
+ };
37
+
29
38
  const TrendBadge = ({ value }) => {
30
39
  const positive = value >= 0;
31
40
  return (
@@ -37,6 +46,16 @@ const TrendBadge = ({ value }) => {
37
46
  );
38
47
  };
39
48
 
49
+ const percentCell = (value) => {
50
+ const numeric = toNumber(value);
51
+ const positive = numeric >= 0;
52
+ return {
53
+ text: `${positive ? '+' : ''}${numeric.toFixed(1)}%`,
54
+ color: positive ? '#15724a' : '#b43c44',
55
+ weight: '700',
56
+ };
57
+ };
58
+
40
59
  const KpiCard = ({ label, value, hint, tone }) => {
41
60
  const toneStyle = tone === 'warm' ? styles.kpiWarm : tone === 'mint' ? styles.kpiMint : styles.kpiCool;
42
61
  return (
@@ -90,7 +109,8 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
90
109
  if (!preserveSelection) {
91
110
  return monthLabels;
92
111
  }
93
- const kept = prev.filter((month) => monthLabels.includes(month));
112
+ const monthKeys = new Set(monthLabels.map(toMonthKey));
113
+ const kept = prev.filter((month) => monthKeys.has(toMonthKey(month)));
94
114
  return kept.length ? kept : monthLabels;
95
115
  });
96
116
  },
@@ -136,7 +156,8 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
136
156
 
137
157
  const rows = useMemo(() => {
138
158
  if (!selectedMonths.length) return [];
139
- return baseRows.filter((row) => selectedMonths.includes(row.monthLabel));
159
+ const selectedKeys = new Set(selectedMonths.map(toMonthKey));
160
+ return baseRows.filter((row) => selectedKeys.has(toMonthKey(row.monthLabel)));
140
161
  }, [baseRows, selectedMonths]);
141
162
 
142
163
  const filteredLine = useMemo(() => {
@@ -285,6 +306,23 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
285
306
  </View>
286
307
  ))}
287
308
 
309
+ <ModernDataTable
310
+ title="Gross Profit Table"
311
+ freezeFirstColumn
312
+ columns={[
313
+ { key: 'monthLabel', label: 'Month', width: 110 },
314
+ { key: 'teu2024', label: '2024 TEU', width: 108, align: 'right', render: (row) => formatNumber(row.teu2024) },
315
+ { key: 'teu2025', label: '2025 TEU', width: 108, align: 'right', render: (row) => formatNumber(row.teu2025) },
316
+ { key: 'teuChangePercent', label: 'TEU %', width: 88, align: 'right', render: (row) => percentCell(row.teuChangePercent) },
317
+ { key: 'profitUsd2024', label: '2024 Profit', width: 120, align: 'right', render: (row) => formatNumber(row.profitUsd2024) },
318
+ { key: 'profitUsd2025', label: '2025 Profit', width: 120, align: 'right', render: (row) => formatNumber(row.profitUsd2025) },
319
+ { key: 'profitChangePercent', label: 'Profit %', width: 92, align: 'right', render: (row) => percentCell(row.profitChangePercent) },
320
+ { key: 'budgetProfitUsd2025', label: '2025 Budget', width: 122, align: 'right', render: (row) => formatNumber(row.budgetProfitUsd2025) },
321
+ { key: 'budgetChangePercent', label: 'Budget %', width: 96, align: 'right', render: (row) => percentCell(row.budgetChangePercent) },
322
+ ]}
323
+ rows={rows}
324
+ />
325
+
288
326
  {!rows.length ? (
289
327
  <View style={styles.emptyWrap}>
290
328
  <Text style={styles.emptyText}>No months selected. Please update month filter.</Text>
@@ -17,6 +17,7 @@ import {
17
17
  import MonthFilterModal from '../components/MonthFilterModal';
18
18
  import CompactAxisBarLineChart from '../components/CompactAxisBarLineChart';
19
19
  import CompactAxisLineChart from '../components/CompactAxisLineChart';
20
+ import ModernDataTable from '../components/ModernDataTable';
20
21
  import { filterChartByMonths } from '../utils/filterChartByMonths';
21
22
 
22
23
  const toNumber = (value) => {
@@ -34,6 +35,14 @@ const percentOf = (part, total) => {
34
35
  return (part / total) * 100;
35
36
  };
36
37
 
38
+ const toMonthKey = (value) => {
39
+ return String(value ?? '')
40
+ .trim()
41
+ .toLocaleUpperCase('tr-TR')
42
+ .normalize('NFD')
43
+ .replace(/[\u0300-\u036f]/g, '');
44
+ };
45
+
37
46
  const compactNumber = (value) => {
38
47
  const num = toNumber(value);
39
48
  if (Math.abs(num) >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`;
@@ -50,6 +59,16 @@ const StatTile = ({ label, value, hint, tone }) => (
50
59
  </View>
51
60
  );
52
61
 
62
+ const percentCell = (value) => {
63
+ const numeric = toNumber(value);
64
+ const positive = numeric >= 0;
65
+ return {
66
+ text: `${positive ? '+' : ''}${numeric.toFixed(1)}%`,
67
+ color: positive ? '#15724a' : '#b43c44',
68
+ weight: '700',
69
+ };
70
+ };
71
+
53
72
  const RingGauge = ({ progress }) => {
54
73
  const size = 58;
55
74
  const stroke = 8;
@@ -109,7 +128,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
109
128
  const months = lineRes?.labels || [];
110
129
  setSelectedMonths((prev) => {
111
130
  if (!preserveSelection) return months;
112
- const kept = prev.filter((month) => months.includes(month));
131
+ const monthKeys = new Set(months.map(toMonthKey));
132
+ const kept = prev.filter((month) => monthKeys.has(toMonthKey(month)));
113
133
  return kept.length ? kept : months;
114
134
  });
115
135
  },
@@ -142,7 +162,8 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
142
162
 
143
163
  const rows = useMemo(() => {
144
164
  if (!selectedMonths.length) return [];
145
- return baseRows.filter((row) => selectedMonths.includes(row.monthLabel));
165
+ const selectedKeys = new Set(selectedMonths.map(toMonthKey));
166
+ return baseRows.filter((row) => selectedKeys.has(toMonthKey(row.monthLabel)));
146
167
  }, [baseRows, selectedMonths]);
147
168
 
148
169
  const filteredLine = useMemo(() => {
@@ -355,6 +376,23 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
355
376
  );
356
377
  })}
357
378
 
379
+ <ModernDataTable
380
+ title="Transportation Table"
381
+ freezeFirstColumn
382
+ columns={[
383
+ { key: 'monthLabel', label: 'Month', width: 110 },
384
+ { key: 'loadCount2024', label: '2024 Load', width: 110, align: 'right', render: (row) => compactNumber(row.loadCount2024) },
385
+ { key: 'loadCount2025', label: '2025 Load', width: 110, align: 'right', render: (row) => compactNumber(row.loadCount2025) },
386
+ { key: 'loadCountChangePercent', label: 'Load %', width: 90, align: 'right', render: (row) => percentCell(row.loadCountChangePercent) },
387
+ { key: 'revenueTl2024', label: '2024 Revenue', width: 122, align: 'right', render: (row) => compactNumber(row.revenueTl2024) },
388
+ { key: 'revenueTl2025', label: '2025 Revenue', width: 122, align: 'right', render: (row) => compactNumber(row.revenueTl2025) },
389
+ { key: 'revenueChangePercent', label: 'Revenue %', width: 95, align: 'right', render: (row) => percentCell(row.revenueChangePercent) },
390
+ { key: 'budgetRevenueTl2025', label: '2025 Budget', width: 122, align: 'right', render: (row) => compactNumber(row.budgetRevenueTl2025) },
391
+ { key: 'budgetChangePercent', label: 'Budget %', width: 95, align: 'right', render: (row) => percentCell(row.budgetChangePercent) },
392
+ ]}
393
+ rows={rows}
394
+ />
395
+
358
396
  {!rows.length ? (
359
397
  <View style={styles.emptyWrap}>
360
398
  <Text style={styles.emptyText}>No data for selected month filter.</Text>
@@ -1,8 +1,28 @@
1
1
  export const filterChartByMonths = (chart, selectedMonths) => {
2
- if (!selectedMonths || selectedMonths.length === 0) return chart;
2
+ if (!chart) return chart;
3
+ if (!selectedMonths) return chart;
4
+ if (selectedMonths.length === 0) {
5
+ return {
6
+ ...chart,
7
+ labels: [],
8
+ series: (chart.series || []).map((s) => ({
9
+ ...s,
10
+ data: [],
11
+ })),
12
+ };
13
+ }
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));
3
23
 
4
24
  const indices = chart.labels
5
- .map((label, i) => (selectedMonths.includes(label) ? i : -1))
25
+ .map((label, i) => (selectedKeys.has(toMonthKey(label)) ? i : -1))
6
26
  .filter(i => i !== -1);
7
27
 
8
28
  return {