@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 +1 -1
- package/src/components/CompactAxisBarLineChart.jsx +5 -5
- package/src/components/CompactAxisLineChart.jsx +3 -3
- package/src/components/ModernDataTable.jsx +174 -0
- package/src/index.jsx +1 -1
- package/src/screens/Report1ModernScreen.jsx +367 -256
- package/src/screens/Report2ModernScreen.jsx +27 -0
- package/src/screens/Report3ModernScreen.jsx +27 -0
- package/src/utils/filterChartByMonths.js +12 -1
package/package.json
CHANGED
|
@@ -36,8 +36,8 @@ const CompactAxisBarLineChart = ({
|
|
|
36
36
|
title,
|
|
37
37
|
legend,
|
|
38
38
|
height = 220,
|
|
39
|
-
minGraphWidth =
|
|
40
|
-
groupSpacing =
|
|
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:
|
|
80
|
-
barGap:
|
|
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(-
|
|
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 =
|
|
30
|
-
pointSpacing =
|
|
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
|
-
|
|
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
|
@@ -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
|
|
11
|
-
import
|
|
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
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 = ({
|
|
59
|
-
const [
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 =
|
|
74
|
-
const total2025 =
|
|
75
|
-
const budget2025 =
|
|
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
|
-
}, [
|
|
169
|
+
}, [sortedRows]);
|
|
83
170
|
|
|
84
|
-
const
|
|
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}>
|
|
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
|
-
|
|
119
|
-
<View style={
|
|
120
|
-
<
|
|
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
|
-
|
|
153
|
-
<View style={styles.
|
|
154
|
-
<
|
|
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={
|
|
168
|
-
onPress={() =>
|
|
215
|
+
style={styles.retryButton}
|
|
216
|
+
onPress={() => loadTabData(activeTab, true)}
|
|
169
217
|
>
|
|
170
|
-
<Text style={
|
|
218
|
+
<Text style={styles.retryText}>Retry</Text>
|
|
171
219
|
</TouchableOpacity>
|
|
172
220
|
</View>
|
|
221
|
+
) : null}
|
|
173
222
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
<View style={styles.
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
<
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
)
|
|
256
|
-
|
|
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:
|
|
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
|
-
|
|
621
|
+
barRow: {
|
|
487
622
|
flexDirection: 'row',
|
|
488
623
|
alignItems: 'center',
|
|
489
624
|
marginBottom: 8,
|
|
490
625
|
},
|
|
491
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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 (!
|
|
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))
|