@dhiraj0720/report1chart 3.0.7 → 3.0.8

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.8",
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,174 @@
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 ModernDataTable = ({ title, columns = [], rows = [], compact = false }) => {
21
+ return (
22
+ <View style={styles.card}>
23
+ <Text style={styles.title}>{title}</Text>
24
+
25
+ {!rows.length ? (
26
+ <View style={styles.emptyWrap}>
27
+ <Text style={styles.emptyText}>No table data for selected filters.</Text>
28
+ </View>
29
+ ) : (
30
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
31
+ <View>
32
+ <View style={styles.headerRow}>
33
+ {columns.map((col) => (
34
+ <View
35
+ key={`header-${col.key}`}
36
+ style={[
37
+ styles.cell,
38
+ styles.headerCell,
39
+ { width: col.width || (compact ? 110 : 126) },
40
+ col.align === 'right' && styles.alignRight,
41
+ ]}
42
+ >
43
+ <Text
44
+ style={[
45
+ styles.headerText,
46
+ col.align === 'right' && styles.textRight,
47
+ col.align === 'center' && styles.textCenter,
48
+ ]}
49
+ >
50
+ {col.label}
51
+ </Text>
52
+ </View>
53
+ ))}
54
+ </View>
55
+
56
+ {rows.map((row, rowIndex) => (
57
+ <View
58
+ key={`row-${rowIndex}`}
59
+ style={[
60
+ styles.dataRow,
61
+ rowIndex % 2 === 0 ? styles.rowEven : styles.rowOdd,
62
+ ]}
63
+ >
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
+ })}
92
+ </View>
93
+ ))}
94
+ </View>
95
+ </ScrollView>
96
+ )}
97
+ </View>
98
+ );
99
+ };
100
+
101
+ const styles = StyleSheet.create({
102
+ card: {
103
+ backgroundColor: '#ffffff',
104
+ borderWidth: 1,
105
+ borderColor: '#d4deed',
106
+ borderRadius: 14,
107
+ padding: 10,
108
+ marginBottom: 12,
109
+ },
110
+ title: {
111
+ fontSize: 13,
112
+ fontWeight: '800',
113
+ color: '#1c2f47',
114
+ marginBottom: 8,
115
+ },
116
+ headerRow: {
117
+ flexDirection: 'row',
118
+ backgroundColor: '#1f385a',
119
+ borderTopLeftRadius: 10,
120
+ borderTopRightRadius: 10,
121
+ overflow: 'hidden',
122
+ },
123
+ headerCell: {
124
+ borderBottomWidth: 1,
125
+ borderBottomColor: '#2d4d78',
126
+ },
127
+ headerText: {
128
+ fontSize: 11,
129
+ color: '#f3f8ff',
130
+ fontWeight: '700',
131
+ },
132
+ dataRow: {
133
+ flexDirection: 'row',
134
+ },
135
+ rowEven: {
136
+ backgroundColor: '#f8fbff',
137
+ },
138
+ rowOdd: {
139
+ backgroundColor: '#ffffff',
140
+ },
141
+ cell: {
142
+ paddingVertical: 10,
143
+ paddingHorizontal: 8,
144
+ borderRightWidth: 1,
145
+ borderRightColor: '#e2ebf7',
146
+ justifyContent: 'center',
147
+ },
148
+ cellText: {
149
+ fontSize: 11.5,
150
+ color: '#2b3850',
151
+ },
152
+ alignRight: {
153
+ alignItems: 'flex-end',
154
+ },
155
+ textRight: {
156
+ textAlign: 'right',
157
+ },
158
+ textCenter: {
159
+ textAlign: 'center',
160
+ },
161
+ emptyWrap: {
162
+ borderWidth: 1,
163
+ borderColor: '#dce5f3',
164
+ borderRadius: 10,
165
+ paddingVertical: 18,
166
+ alignItems: 'center',
167
+ },
168
+ emptyText: {
169
+ color: '#6b7a91',
170
+ fontSize: 12,
171
+ },
172
+ });
173
+
174
+ 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,148 @@ 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
+ }
105
+
106
+ if (!force && loadedByTab[tabKey]) {
107
+ return;
108
+ }
63
109
 
64
- useEffect(() => {
65
110
  setLoading(true);
66
- fetchReport1(endpoint, token)
67
- .then((data) => setRows(data || []))
68
- .catch(() => setRows([]))
69
- .finally(() => setLoading(false));
70
- }, [endpoint, token]);
111
+ setErrorByTab((prev) => ({ ...prev, [tabKey]: null }));
112
+
113
+ try {
114
+ const rows = await fetchReport4(endpoint, token);
115
+ const sortedRows = [...rows].sort(
116
+ (a, b) => (a.sortOrder || 0) - (b.sortOrder || 0),
117
+ );
118
+
119
+ setRowsByTab((prev) => ({ ...prev, [tabKey]: sortedRows }));
120
+ setLoadedByTab((prev) => ({ ...prev, [tabKey]: true }));
121
+ } catch (error) {
122
+ setErrorByTab((prev) => ({
123
+ ...prev,
124
+ [tabKey]: error?.message || 'Failed to load performance data.',
125
+ }));
126
+ } finally {
127
+ setLoading(false);
128
+ }
129
+ }, [endpointByTab, loadedByTab, token]);
130
+
131
+ useEffect(() => {
132
+ loadTabData(activeTab);
133
+ }, [activeTab, loadTabData]);
134
+
135
+ const activeRawRows = rowsByTab[activeTab] || [];
136
+ const activeError = errorByTab[activeTab];
137
+ const activeLoaded = loadedByTab[activeTab];
138
+
139
+ const mappedRows = useMemo(() => {
140
+ return activeRawRows.map((row) => mapRowByTab(row, activeTab));
141
+ }, [activeRawRows, activeTab]);
142
+
143
+ const sortedRows = useMemo(() => {
144
+ const copy = [...mappedRows];
145
+ if (sortMode === 'name') {
146
+ return copy.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
147
+ }
148
+ if (sortMode === 'variance') {
149
+ return copy.sort((a, b) => b.variance - a.variance);
150
+ }
151
+ return copy.sort((a, b) => b.change - a.change);
152
+ }, [mappedRows, sortMode]);
71
153
 
72
154
  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);
155
+ const total2024 = sortedRows.reduce((sum, row) => sum + row.value2024, 0);
156
+ const total2025 = sortedRows.reduce((sum, row) => sum + row.value2025, 0);
157
+ const budget2025 = sortedRows.reduce((sum, row) => sum + row.budget2025, 0);
158
+ const avgRatio2025 = sortedRows.length
159
+ ? sortedRows.reduce((sum, row) => sum + (row.ratio2025 || 0), 0) / sortedRows.length
160
+ : 0;
161
+
76
162
  return {
77
163
  total2024,
78
164
  total2025,
79
165
  budget2025,
80
166
  yoy: toPercent(total2025, total2024),
167
+ avgRatio2025,
81
168
  };
82
- }, [rows]);
169
+ }, [sortedRows]);
83
170
 
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
- }
171
+ const spotlight = sortedRows[0];
104
172
 
105
173
  return (
106
174
  <View style={styles.screen}>
@@ -111,151 +179,184 @@ const Report1ModernScreen = ({ endpoint, token, onBack }) => {
111
179
  <Text style={styles.backIcon}>‹</Text>
112
180
  </TouchableOpacity>
113
181
  <Text style={styles.heroTitle}>Performance Studio</Text>
114
- <Text style={styles.heroSubtitle}>Live operational performance map by activity</Text>
182
+ <Text style={styles.heroSubtitle}>Three-tab live performance monitor</Text>
183
+ </View>
184
+
185
+ <View style={styles.tabsContainer}>
186
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
187
+ {TABS.map((tab) => {
188
+ const isActive = tab.key === activeTab;
189
+ return (
190
+ <TouchableOpacity
191
+ key={tab.key}
192
+ style={[styles.tabButton, isActive && styles.activeTabButton]}
193
+ onPress={() => setActiveTab(tab.key)}
194
+ >
195
+ <Text style={[styles.tabText, isActive && styles.activeTabText]}>
196
+ {tab.label}
197
+ </Text>
198
+ </TouchableOpacity>
199
+ );
200
+ })}
201
+ </ScrollView>
115
202
  </View>
116
203
 
117
204
  <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>
205
+ {loading && !activeLoaded ? (
206
+ <View style={styles.center}>
207
+ <ActivityIndicator size="large" color="#ffb155" />
149
208
  </View>
150
209
  ) : null}
151
210
 
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>
211
+ {!loading && activeError && !sortedRows.length ? (
212
+ <View style={styles.center}>
213
+ <Text style={styles.errorText}>{activeError}</Text>
166
214
  <TouchableOpacity
167
- style={[styles.segmentBtn, viewMode === 'efficiency' && styles.segmentBtnActive]}
168
- onPress={() => setViewMode('efficiency')}
215
+ style={styles.retryButton}
216
+ onPress={() => loadTabData(activeTab, true)}
169
217
  >
170
- <Text style={[styles.segmentText, viewMode === 'efficiency' && styles.segmentTextActive]}>Efficiency</Text>
218
+ <Text style={styles.retryText}>Retry</Text>
171
219
  </TouchableOpacity>
172
220
  </View>
221
+ ) : null}
173
222
 
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)}%
223
+ {sortedRows.length ? (
224
+ <>
225
+ <View style={styles.kpiRow}>
226
+ <View style={[styles.kpiCard, styles.kpiWarm]}>
227
+ <Text style={styles.kpiLabel}>2024 Value</Text>
228
+ <Text style={styles.kpiValue}>{formatNumber(totals.total2024)}</Text>
229
+ </View>
230
+ <View style={[styles.kpiCard, styles.kpiBlue]}>
231
+ <Text style={styles.kpiLabel}>2025 Value</Text>
232
+ <Text style={styles.kpiValue}>{formatNumber(totals.total2025)}</Text>
233
+ </View>
234
+ </View>
235
+ <View style={styles.kpiRow}>
236
+ <View style={[styles.kpiCard, styles.kpiIndigo]}>
237
+ <Text style={styles.kpiLabel}>2025 Budget</Text>
238
+ <Text style={styles.kpiValue}>{formatNumber(totals.budget2025)}</Text>
239
+ </View>
240
+ <View style={[styles.kpiCard, styles.kpiTeal]}>
241
+ <Text style={styles.kpiLabel}>Total Change</Text>
242
+ <Text style={styles.kpiValue}>
243
+ {totals.yoy >= 0 ? '+' : ''}
244
+ {totals.yoy.toFixed(1)}%
218
245
  </Text>
219
246
  </View>
247
+ </View>
220
248
 
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>
249
+ <View style={styles.insightStrip}>
250
+ <View>
251
+ <Text style={styles.insightLabel}>Average Ratio 2025</Text>
252
+ <Text style={styles.insightValue}>{totals.avgRatio2025.toFixed(2)}%</Text>
253
+ </View>
254
+ <View>
255
+ <Text style={styles.insightLabel}>Tab</Text>
256
+ <Text style={styles.insightValue}>{TABS.find((x) => x.key === activeTab)?.label}</Text>
227
257
  </View>
258
+ </View>
228
259
 
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>
260
+ {spotlight ? (
261
+ <View style={styles.spotlightCard}>
262
+ <Text style={styles.spotlightLabel}>SPOTLIGHT</Text>
263
+ <Text style={styles.spotlightName}>{spotlight.name}</Text>
264
+ <Text style={styles.spotlightValue}>
265
+ 2025: {formatNumber(spotlight.value2025)} • Change: {spotlight.change >= 0 ? '+' : ''}
266
+ {spotlight.change.toFixed(1)}%
267
+ </Text>
235
268
  </View>
269
+ ) : null}
270
+
271
+ <View style={styles.sortRow}>
272
+ <TouchableOpacity
273
+ style={[styles.sortChip, sortMode === 'growth' && styles.sortChipActive]}
274
+ onPress={() => setSortMode('growth')}
275
+ >
276
+ <Text style={[styles.sortChipText, sortMode === 'growth' && styles.sortChipTextActive]}>
277
+ Top Growth
278
+ </Text>
279
+ </TouchableOpacity>
280
+ <TouchableOpacity
281
+ style={[styles.sortChip, sortMode === 'variance' && styles.sortChipActive]}
282
+ onPress={() => setSortMode('variance')}
283
+ >
284
+ <Text style={[styles.sortChipText, sortMode === 'variance' && styles.sortChipTextActive]}>
285
+ Top Variance
286
+ </Text>
287
+ </TouchableOpacity>
288
+ <TouchableOpacity
289
+ style={[styles.sortChip, sortMode === 'name' && styles.sortChipActive]}
290
+ onPress={() => setSortMode('name')}
291
+ >
292
+ <Text style={[styles.sortChipText, sortMode === 'name' && styles.sortChipTextActive]}>
293
+ A-Z
294
+ </Text>
295
+ </TouchableOpacity>
296
+ </View>
297
+
298
+ {sortedRows.map((item) => {
299
+ const maxForBar = Math.max(1, item.value2024, item.value2025, item.budget2025);
300
+ const width2024 = `${(item.value2024 / maxForBar) * 100}%`;
301
+ const width2025 = `${(item.value2025 / maxForBar) * 100}%`;
236
302
 
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 }]} />
303
+ return (
304
+ <View key={item.name} style={styles.card}>
305
+ <View style={styles.cardHeader}>
306
+ <Text style={styles.cardTitle}>{item.name}</Text>
307
+ <Text style={[styles.badge, item.change >= 0 ? styles.badgeUp : styles.badgeDown]}>
308
+ {item.change >= 0 ? '+' : ''}
309
+ {item.change.toFixed(1)}%
310
+ </Text>
242
311
  </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>
312
+
313
+ <View style={styles.barRow}>
314
+ <Text style={styles.barLabel}>2024</Text>
315
+ <View style={styles.track}>
316
+ <View style={[styles.fill2024, { width: width2024 }]} />
317
+ </View>
318
+ <Text style={styles.barValue}>{formatNumber(item.value2024)}</Text>
319
+ </View>
320
+
321
+ <View style={styles.barRow}>
322
+ <Text style={styles.barLabel}>2025</Text>
323
+ <View style={styles.track}>
324
+ <View style={[styles.fill2025, { width: width2025 }]} />
325
+ </View>
326
+ <Text style={styles.barValue}>{formatNumber(item.value2025)}</Text>
253
327
  </View>
254
328
  </View>
255
- ) : null}
256
- </View>
257
- );
258
- })}
329
+ );
330
+ })}
331
+
332
+ <ModernDataTable
333
+ title="Performance Table"
334
+ columns={[
335
+ { key: 'name', label: 'Activity', width: 150 },
336
+ { key: 'value2024', label: '2024', width: 120, align: 'right', render: (row) => formatNumber(row.value2024) },
337
+ { key: 'value2025', label: '2025', width: 120, align: 'right', render: (row) => formatNumber(row.value2025) },
338
+ { key: 'change', label: 'Change %', width: 90, align: 'right', render: (row) => asPercentCell(row.change) },
339
+ { key: 'budget2025', label: 'Budget 2025', width: 130, align: 'right', render: (row) => formatNumber(row.budget2025) },
340
+ { key: 'variance', label: 'Variance %', width: 95, align: 'right', render: (row) => asPercentCell(row.variance) },
341
+ {
342
+ key: 'ratio2024',
343
+ label: 'Ratio 2024',
344
+ width: 92,
345
+ align: 'right',
346
+ render: (row) => (row.ratio2024 === null ? '-' : `${row.ratio2024.toFixed(2)}%`),
347
+ },
348
+ {
349
+ key: 'ratio2025',
350
+ label: 'Ratio 2025',
351
+ width: 92,
352
+ align: 'right',
353
+ render: (row) => (row.ratio2025 === null ? '-' : `${row.ratio2025.toFixed(2)}%`),
354
+ },
355
+ ]}
356
+ rows={sortedRows}
357
+ />
358
+ </>
359
+ ) : null}
259
360
 
260
361
  <View style={styles.footerSpacing} />
261
362
  </ScrollView>
@@ -268,12 +369,6 @@ const styles = StyleSheet.create({
268
369
  flex: 1,
269
370
  backgroundColor: '#0f1726',
270
371
  },
271
- loaderWrap: {
272
- flex: 1,
273
- justifyContent: 'center',
274
- alignItems: 'center',
275
- backgroundColor: '#0f1726',
276
- },
277
372
  hero: {
278
373
  paddingHorizontal: 16,
279
374
  paddingTop: 14,
@@ -325,11 +420,58 @@ const styles = StyleSheet.create({
325
420
  fontSize: 13,
326
421
  fontWeight: '500',
327
422
  },
423
+ tabsContainer: {
424
+ backgroundColor: '#111b2d',
425
+ borderBottomWidth: 1,
426
+ borderBottomColor: '#2f3b57',
427
+ paddingVertical: 8,
428
+ paddingHorizontal: 8,
429
+ },
430
+ tabButton: {
431
+ paddingHorizontal: 14,
432
+ paddingVertical: 9,
433
+ borderRadius: 8,
434
+ marginHorizontal: 4,
435
+ backgroundColor: '#18233a',
436
+ borderWidth: 1,
437
+ borderColor: '#35415e',
438
+ },
439
+ activeTabButton: {
440
+ backgroundColor: '#ffb155',
441
+ borderColor: '#ffb155',
442
+ },
443
+ tabText: {
444
+ fontSize: 12,
445
+ fontWeight: '700',
446
+ color: '#c5d0e8',
447
+ },
448
+ activeTabText: {
449
+ color: '#2e2720',
450
+ },
328
451
  content: {
329
452
  flex: 1,
330
453
  paddingHorizontal: 14,
331
454
  paddingTop: 14,
332
455
  },
456
+ center: {
457
+ paddingVertical: 24,
458
+ alignItems: 'center',
459
+ },
460
+ errorText: {
461
+ color: '#ffb3bb',
462
+ textAlign: 'center',
463
+ marginBottom: 10,
464
+ },
465
+ retryButton: {
466
+ backgroundColor: '#ffb155',
467
+ borderRadius: 8,
468
+ paddingHorizontal: 14,
469
+ paddingVertical: 8,
470
+ },
471
+ retryText: {
472
+ color: '#2e2720',
473
+ fontWeight: '700',
474
+ },
333
475
  kpiRow: {
334
476
  flexDirection: 'row',
335
477
  marginBottom: 10,
@@ -368,6 +510,27 @@ const styles = StyleSheet.create({
368
510
  fontWeight: '800',
369
511
  color: '#1d2738',
370
512
  },
513
+ insightStrip: {
514
+ backgroundColor: '#1a2640',
515
+ borderColor: '#303d58',
516
+ borderWidth: 1,
517
+ borderRadius: 14,
518
+ paddingHorizontal: 12,
519
+ paddingVertical: 10,
520
+ flexDirection: 'row',
521
+ justifyContent: 'space-between',
522
+ marginBottom: 10,
523
+ },
524
+ insightLabel: {
525
+ fontSize: 11,
526
+ color: '#9eb1d3',
527
+ marginBottom: 4,
528
+ },
529
+ insightValue: {
530
+ color: '#fff',
531
+ fontWeight: '800',
532
+ fontSize: 13,
533
+ },
371
534
  spotlightCard: {
372
535
  backgroundColor: '#1a2640',
373
536
  borderColor: '#303d58',
@@ -375,7 +538,7 @@ const styles = StyleSheet.create({
375
538
  borderRadius: 14,
376
539
  paddingHorizontal: 12,
377
540
  paddingVertical: 12,
378
- marginBottom: 12,
541
+ marginBottom: 10,
379
542
  },
380
543
  spotlightLabel: {
381
544
  fontSize: 10,
@@ -394,37 +557,9 @@ const styles = StyleSheet.create({
394
557
  fontSize: 12,
395
558
  color: '#c9d5ed',
396
559
  },
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
560
  sortRow: {
427
561
  flexDirection: 'row',
562
+ marginBottom: 10,
428
563
  },
429
564
  sortChip: {
430
565
  paddingVertical: 6,
@@ -483,12 +618,12 @@ const styles = StyleSheet.create({
483
618
  backgroundColor: '#522d34',
484
619
  color: '#ff9ca6',
485
620
  },
486
- row: {
621
+ barRow: {
487
622
  flexDirection: 'row',
488
623
  alignItems: 'center',
489
624
  marginBottom: 8,
490
625
  },
491
- rowLabel: {
626
+ barLabel: {
492
627
  width: 54,
493
628
  color: '#bac8e1',
494
629
  fontSize: 11,
@@ -512,37 +647,13 @@ const styles = StyleSheet.create({
512
647
  backgroundColor: '#6db2ff',
513
648
  borderRadius: 999,
514
649
  },
515
- fillBudget: {
516
- height: '100%',
517
- backgroundColor: '#c8cfff',
518
- borderRadius: 999,
519
- },
520
- rowValue: {
521
- width: 86,
650
+ barValue: {
651
+ width: 92,
522
652
  textAlign: 'right',
523
653
  color: '#e4ecff',
524
654
  fontSize: 11,
525
655
  fontWeight: '700',
526
656
  },
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
657
  footerSpacing: {
547
658
  height: 14,
548
659
  },
@@ -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
 
@@ -37,6 +38,16 @@ const TrendBadge = ({ value }) => {
37
38
  );
38
39
  };
39
40
 
41
+ const percentCell = (value) => {
42
+ const numeric = toNumber(value);
43
+ const positive = numeric >= 0;
44
+ return {
45
+ text: `${positive ? '+' : ''}${numeric.toFixed(1)}%`,
46
+ color: positive ? '#15724a' : '#b43c44',
47
+ weight: '700',
48
+ };
49
+ };
50
+
40
51
  const KpiCard = ({ label, value, hint, tone }) => {
41
52
  const toneStyle = tone === 'warm' ? styles.kpiWarm : tone === 'mint' ? styles.kpiMint : styles.kpiCool;
42
53
  return (
@@ -285,6 +296,22 @@ const Report2ModernScreen = ({ api, token, onBack }) => {
285
296
  </View>
286
297
  ))}
287
298
 
299
+ <ModernDataTable
300
+ title="Gross Profit Table"
301
+ columns={[
302
+ { key: 'monthLabel', label: 'Month', width: 110 },
303
+ { key: 'teu2024', label: '2024 TEU', width: 108, align: 'right', render: (row) => formatNumber(row.teu2024) },
304
+ { key: 'teu2025', label: '2025 TEU', width: 108, align: 'right', render: (row) => formatNumber(row.teu2025) },
305
+ { key: 'teuChangePercent', label: 'TEU %', width: 88, align: 'right', render: (row) => percentCell(row.teuChangePercent) },
306
+ { key: 'profitUsd2024', label: '2024 Profit', width: 120, align: 'right', render: (row) => formatNumber(row.profitUsd2024) },
307
+ { key: 'profitUsd2025', label: '2025 Profit', width: 120, align: 'right', render: (row) => formatNumber(row.profitUsd2025) },
308
+ { key: 'profitChangePercent', label: 'Profit %', width: 92, align: 'right', render: (row) => percentCell(row.profitChangePercent) },
309
+ { key: 'budgetProfitUsd2025', label: '2025 Budget', width: 122, align: 'right', render: (row) => formatNumber(row.budgetProfitUsd2025) },
310
+ { key: 'budgetChangePercent', label: 'Budget %', width: 96, align: 'right', render: (row) => percentCell(row.budgetChangePercent) },
311
+ ]}
312
+ rows={rows}
313
+ />
314
+
288
315
  {!rows.length ? (
289
316
  <View style={styles.emptyWrap}>
290
317
  <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) => {
@@ -50,6 +51,16 @@ const StatTile = ({ label, value, hint, tone }) => (
50
51
  </View>
51
52
  );
52
53
 
54
+ const percentCell = (value) => {
55
+ const numeric = toNumber(value);
56
+ const positive = numeric >= 0;
57
+ return {
58
+ text: `${positive ? '+' : ''}${numeric.toFixed(1)}%`,
59
+ color: positive ? '#15724a' : '#b43c44',
60
+ weight: '700',
61
+ };
62
+ };
63
+
53
64
  const RingGauge = ({ progress }) => {
54
65
  const size = 58;
55
66
  const stroke = 8;
@@ -355,6 +366,22 @@ const Report3ModernScreen = ({ api, token, onBack }) => {
355
366
  );
356
367
  })}
357
368
 
369
+ <ModernDataTable
370
+ title="Transportation Table"
371
+ columns={[
372
+ { key: 'monthLabel', label: 'Month', width: 110 },
373
+ { key: 'loadCount2024', label: '2024 Load', width: 110, align: 'right', render: (row) => compactNumber(row.loadCount2024) },
374
+ { key: 'loadCount2025', label: '2025 Load', width: 110, align: 'right', render: (row) => compactNumber(row.loadCount2025) },
375
+ { key: 'loadCountChangePercent', label: 'Load %', width: 90, align: 'right', render: (row) => percentCell(row.loadCountChangePercent) },
376
+ { key: 'revenueTl2024', label: '2024 Revenue', width: 122, align: 'right', render: (row) => compactNumber(row.revenueTl2024) },
377
+ { key: 'revenueTl2025', label: '2025 Revenue', width: 122, align: 'right', render: (row) => compactNumber(row.revenueTl2025) },
378
+ { key: 'revenueChangePercent', label: 'Revenue %', width: 95, align: 'right', render: (row) => percentCell(row.revenueChangePercent) },
379
+ { key: 'budgetRevenueTl2025', label: '2025 Budget', width: 122, align: 'right', render: (row) => compactNumber(row.budgetRevenueTl2025) },
380
+ { key: 'budgetChangePercent', label: 'Budget %', width: 95, align: 'right', render: (row) => percentCell(row.budgetChangePercent) },
381
+ ]}
382
+ rows={rows}
383
+ />
384
+
358
385
  {!rows.length ? (
359
386
  <View style={styles.emptyWrap}>
360
387
  <Text style={styles.emptyText}>No data for selected month filter.</Text>
@@ -1,5 +1,16 @@
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
+ }
3
14
 
4
15
  const indices = chart.labels
5
16
  .map((label, i) => (selectedMonths.includes(label) ? i : -1))