@dhiraj0720/report1chart 3.0.5 → 3.0.6

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.5",
3
+ "version": "3.0.6",
4
4
  "main": "src/index.jsx",
5
5
  "scripts": {
6
6
  "test": "echo 'No tests'"
package/src/index.jsx CHANGED
@@ -6,6 +6,7 @@ import Report1Screen from './screens/Report1Screen';
6
6
  import Report2Screen from './screens/Report2Screen';
7
7
  import Report2ModernScreen from './screens/Report2ModernScreen';
8
8
  import Report3Screen from './screens/Report3Screen';
9
+ import Report3ModernScreen from './screens/Report3ModernScreen';
9
10
  import Report1AScreen from './screens/Report1AScreen';
10
11
  import Report2AScreen from './screens/Report2AScreen';
11
12
  import Report3AScreen from './screens/Report3AScreen';
@@ -98,6 +99,18 @@ if (active === '2N1') {
98
99
  );
99
100
  }
100
101
 
102
+ if (active === '3N1') {
103
+ return (
104
+ <SafeScreen>
105
+ <Report3ModernScreen
106
+ api={config.report3}
107
+ token={config.token}
108
+ onBack={() => setActive(null)}
109
+ />
110
+ </SafeScreen>
111
+ );
112
+ }
113
+
101
114
 
102
115
  // 👉 REPORT 2
103
116
  if (active === 2) {
@@ -23,6 +23,13 @@ const toPercent = (current, previous) => {
23
23
  return ((current - previous) / Math.abs(previous)) * 100;
24
24
  };
25
25
 
26
+ const compactAmount = (value) => {
27
+ const num = toNumber(value);
28
+ if (Math.abs(num) >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
29
+ if (Math.abs(num) >= 1000) return `${(num / 1000).toFixed(1)}K`;
30
+ return `${num}`;
31
+ };
32
+
26
33
  const TrendBadge = ({ value }) => {
27
34
  const positive = value >= 0;
28
35
  return (
@@ -80,15 +87,35 @@ const ModernBars = ({ rows, metric }) => {
80
87
  const pct = toPercent(value2025, value2024);
81
88
  const bar2024Height = Math.max(6, (value2024 / maxValue) * 122);
82
89
  const bar2025Height = Math.max(6, (value2025 / maxValue) * 122);
90
+ const showInside2024 = bar2024Height > 48;
91
+ const showInside2025 = bar2025Height > 48;
83
92
 
84
93
  return (
85
94
  <View key={row.monthLabel} style={styles.chartGroup}>
86
95
  <View style={styles.barPair}>
87
96
  <View style={styles.barTrack}>
88
- <View style={[styles.barFill2024, { height: bar2024Height }]} />
97
+ {!showInside2024 ? (
98
+ <Text style={styles.valueTopText}>{compactAmount(value2024)}</Text>
99
+ ) : null}
100
+ <View style={[styles.barFill2024, { height: bar2024Height }]}>
101
+ {showInside2024 ? (
102
+ <View style={styles.valueInsideWrap}>
103
+ <Text style={styles.valueInsideText}>{formatNumber(value2024)}</Text>
104
+ </View>
105
+ ) : null}
106
+ </View>
89
107
  </View>
90
108
  <View style={styles.barTrack}>
91
- <View style={[styles.barFill2025, { height: bar2025Height }]} />
109
+ {!showInside2025 ? (
110
+ <Text style={styles.valueTopText}>{compactAmount(value2025)}</Text>
111
+ ) : null}
112
+ <View style={[styles.barFill2025, { height: bar2025Height }]}>
113
+ {showInside2025 ? (
114
+ <View style={styles.valueInsideWrap}>
115
+ <Text style={styles.valueInsideText}>{formatNumber(value2025)}</Text>
116
+ </View>
117
+ ) : null}
118
+ </View>
92
119
  </View>
93
120
  </View>
94
121
  <Text style={styles.chartMonthText}>{row.monthLabel.slice(0, 3)}</Text>
@@ -644,21 +671,46 @@ const styles = StyleSheet.create({
644
671
  marginBottom: 7,
645
672
  },
646
673
  barTrack: {
647
- width: 12,
648
- height: 126,
674
+ width: 14,
675
+ height: 130,
649
676
  borderRadius: 10,
650
677
  justifyContent: 'flex-end',
651
678
  backgroundColor: '#eef3fa',
652
679
  marginHorizontal: 2,
653
- overflow: 'hidden',
680
+ overflow: 'visible',
654
681
  },
655
682
  barFill2024: {
656
683
  backgroundColor: '#f19a54',
657
684
  borderRadius: 10,
685
+ overflow: 'hidden',
658
686
  },
659
687
  barFill2025: {
660
688
  backgroundColor: '#2f7cca',
661
689
  borderRadius: 10,
690
+ overflow: 'hidden',
691
+ },
692
+ valueInsideWrap: {
693
+ ...StyleSheet.absoluteFillObject,
694
+ justifyContent: 'center',
695
+ alignItems: 'center',
696
+ },
697
+ valueInsideText: {
698
+ color: '#fff',
699
+ fontSize: 8,
700
+ fontWeight: '700',
701
+ transform: [{ rotate: '-90deg' }],
702
+ width: 84,
703
+ textAlign: 'center',
704
+ },
705
+ valueTopText: {
706
+ position: 'absolute',
707
+ top: -15,
708
+ left: -18,
709
+ width: 48,
710
+ textAlign: 'center',
711
+ fontSize: 8,
712
+ fontWeight: '700',
713
+ color: '#405268',
662
714
  },
663
715
  chartMonthText: {
664
716
  fontSize: 10,
@@ -0,0 +1,785 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ RefreshControl,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import Svg, { Circle, Line, Polyline } from 'react-native-svg';
12
+ import { fetchReport3Table } from '../api/report3Fetcher';
13
+ import MonthFilterModal from '../components/MonthFilterModal';
14
+ import { formatNumber } from '../utils/formatNumber';
15
+
16
+ const toNumber = (value) => {
17
+ const numeric = Number(value);
18
+ return Number.isFinite(numeric) ? numeric : 0;
19
+ };
20
+
21
+ const percentChange = (current, previous) => {
22
+ if (!previous) return 0;
23
+ return ((current - previous) / Math.abs(previous)) * 100;
24
+ };
25
+
26
+ const percentOf = (part, total) => {
27
+ if (!total) return 0;
28
+ return (part / total) * 100;
29
+ };
30
+
31
+ const compactNumber = (value) => {
32
+ const num = toNumber(value);
33
+ if (Math.abs(num) >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`;
34
+ if (Math.abs(num) >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
35
+ if (Math.abs(num) >= 1000) return `${(num / 1000).toFixed(1)}K`;
36
+ return `${num}`;
37
+ };
38
+
39
+ const StatTile = ({ label, value, hint, tone }) => (
40
+ <View style={[styles.statTile, tone === 'mint' ? styles.statMint : tone === 'sun' ? styles.statSun : styles.statSea]}>
41
+ <Text style={styles.statLabel}>{label}</Text>
42
+ <Text style={styles.statValue}>{value}</Text>
43
+ <Text style={styles.statHint}>{hint}</Text>
44
+ </View>
45
+ );
46
+
47
+ const RingGauge = ({ progress }) => {
48
+ const size = 60;
49
+ const stroke = 8;
50
+ const radius = (size - stroke) / 2;
51
+ const circumference = 2 * Math.PI * radius;
52
+ const clamped = Math.max(0, Math.min(100, progress));
53
+ const offset = circumference - (circumference * clamped) / 100;
54
+
55
+ return (
56
+ <Svg width={size} height={size}>
57
+ <Circle
58
+ cx={size / 2}
59
+ cy={size / 2}
60
+ r={radius}
61
+ stroke="#d8ece8"
62
+ strokeWidth={stroke}
63
+ fill="none"
64
+ />
65
+ <Circle
66
+ cx={size / 2}
67
+ cy={size / 2}
68
+ r={radius}
69
+ stroke="#179b78"
70
+ strokeWidth={stroke}
71
+ strokeLinecap="round"
72
+ strokeDasharray={`${circumference} ${circumference}`}
73
+ strokeDashoffset={offset}
74
+ fill="none"
75
+ transform={`rotate(-90 ${size / 2} ${size / 2})`}
76
+ />
77
+ </Svg>
78
+ );
79
+ };
80
+
81
+ const SignalChart = ({ rows, mode }) => {
82
+ if (!rows.length) return null;
83
+
84
+ const values2024 = rows.map((row) =>
85
+ mode === 'loads' ? toNumber(row.loadCount2024) : toNumber(row.revenueTl2024),
86
+ );
87
+ const values2025 = rows.map((row) =>
88
+ mode === 'loads' ? toNumber(row.loadCount2025) : toNumber(row.revenueTl2025),
89
+ );
90
+
91
+ const width = Math.max(360, rows.length * 72);
92
+ const height = 190;
93
+ const left = 18;
94
+ const right = 12;
95
+ const top = 16;
96
+ const bottom = 38;
97
+ const chartHeight = height - top - bottom;
98
+
99
+ const all = [...values2024, ...values2025];
100
+ const min = Math.min(...all);
101
+ const max = Math.max(...all);
102
+ const pad = Math.max(1, (max - min) * 0.14);
103
+ const yMin = min - pad;
104
+ const yMax = max + pad;
105
+
106
+ const stepX = rows.length > 1 ? (width - left - right) / (rows.length - 1) : width / 2;
107
+ const toY = (value) => top + ((yMax - value) / Math.max(1, yMax - yMin)) * chartHeight;
108
+ const toX = (index) => left + index * stepX;
109
+
110
+ const points2024 = values2024.map((value, i) => `${toX(i)},${toY(value)}`).join(' ');
111
+ const points2025 = values2025.map((value, i) => `${toX(i)},${toY(value)}`).join(' ');
112
+
113
+ return (
114
+ <View style={styles.signalCard}>
115
+ <View style={styles.signalHeader}>
116
+ <Text style={styles.signalTitle}>
117
+ {mode === 'loads' ? 'Load Movement Signal' : 'Revenue Movement Signal'}
118
+ </Text>
119
+ <View style={styles.signalLegend}>
120
+ <Text style={styles.legend2024}>2024</Text>
121
+ <Text style={styles.legend2025}>2025</Text>
122
+ </View>
123
+ </View>
124
+
125
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
126
+ <Svg width={width} height={height}>
127
+ {[0, 0.5, 1].map((ratio) => {
128
+ const y = top + ratio * chartHeight;
129
+ return (
130
+ <Line
131
+ key={ratio}
132
+ x1={left}
133
+ y1={y}
134
+ x2={width - right}
135
+ y2={y}
136
+ stroke="#e7e3d9"
137
+ strokeDasharray="4 5"
138
+ strokeWidth={1}
139
+ />
140
+ );
141
+ })}
142
+
143
+ <Polyline
144
+ points={points2024}
145
+ fill="none"
146
+ stroke="#f59f40"
147
+ strokeWidth={3}
148
+ strokeLinecap="round"
149
+ strokeLinejoin="round"
150
+ strokeDasharray="8 6"
151
+ />
152
+ <Polyline
153
+ points={points2025}
154
+ fill="none"
155
+ stroke="#168a73"
156
+ strokeWidth={3}
157
+ strokeLinecap="round"
158
+ strokeLinejoin="round"
159
+ />
160
+
161
+ {rows.map((row, index) => (
162
+ <React.Fragment key={`${row.monthLabel}-${index}`}>
163
+ <Circle cx={toX(index)} cy={toY(values2024[index])} r={4.8} fill="#f59f40" />
164
+ <Circle cx={toX(index)} cy={toY(values2025[index])} r={4.8} fill="#168a73" />
165
+ </React.Fragment>
166
+ ))}
167
+ </Svg>
168
+ </ScrollView>
169
+
170
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.signalMonthStrip}>
171
+ {rows.map((row) => (
172
+ <View key={`month-${row.monthLabel}`} style={styles.signalMonthChip}>
173
+ <Text style={styles.signalMonthText}>{row.monthLabel}</Text>
174
+ </View>
175
+ ))}
176
+ </ScrollView>
177
+ </View>
178
+ );
179
+ };
180
+
181
+ const Report3ModernScreen = ({ api, token, onBack }) => {
182
+ const [loading, setLoading] = useState(true);
183
+ const [refreshing, setRefreshing] = useState(false);
184
+ const [monthsModal, setMonthsModal] = useState(false);
185
+ const [table, setTable] = useState(null);
186
+ const [selectedMonths, setSelectedMonths] = useState([]);
187
+ const [focusMonth, setFocusMonth] = useState('ALL');
188
+ const [mode, setMode] = useState('loads');
189
+
190
+ const loadData = useCallback(async () => {
191
+ const response = await fetchReport3Table(api.table, token);
192
+ setTable(response);
193
+ const months = (response?.rows || [])
194
+ .filter((row) => row.month !== 99 && row.monthLabel !== 'Total')
195
+ .map((row) => row.monthLabel);
196
+ setSelectedMonths(months);
197
+ setFocusMonth('ALL');
198
+ }, [api.table, token]);
199
+
200
+ useEffect(() => {
201
+ setLoading(true);
202
+ loadData()
203
+ .catch(() => {})
204
+ .finally(() => setLoading(false));
205
+ }, [loadData]);
206
+
207
+ const onRefresh = useCallback(async () => {
208
+ setRefreshing(true);
209
+ try {
210
+ await loadData();
211
+ } finally {
212
+ setRefreshing(false);
213
+ }
214
+ }, [loadData]);
215
+
216
+ const baseRows = useMemo(() => {
217
+ return (table?.rows || []).filter((row) => row.month !== 99 && row.monthLabel !== 'Total');
218
+ }, [table]);
219
+
220
+ const rows = useMemo(() => {
221
+ let filtered = baseRows;
222
+ if (selectedMonths.length) {
223
+ filtered = filtered.filter((row) => selectedMonths.includes(row.monthLabel));
224
+ }
225
+ if (focusMonth !== 'ALL') {
226
+ filtered = filtered.filter((row) => row.monthLabel === focusMonth);
227
+ }
228
+ return filtered;
229
+ }, [baseRows, selectedMonths, focusMonth]);
230
+
231
+ const quickMonths = useMemo(() => ['ALL', ...selectedMonths.slice(0, 6)], [selectedMonths]);
232
+
233
+ const load2024Total = useMemo(
234
+ () => rows.reduce((sum, row) => sum + toNumber(row.loadCount2024), 0),
235
+ [rows],
236
+ );
237
+ const load2025Total = useMemo(
238
+ () => rows.reduce((sum, row) => sum + toNumber(row.loadCount2025), 0),
239
+ [rows],
240
+ );
241
+ const revenue2024Total = useMemo(
242
+ () => rows.reduce((sum, row) => sum + toNumber(row.revenueTl2024), 0),
243
+ [rows],
244
+ );
245
+ const revenue2025Total = useMemo(
246
+ () => rows.reduce((sum, row) => sum + toNumber(row.revenueTl2025), 0),
247
+ [rows],
248
+ );
249
+ const budget2025Total = useMemo(
250
+ () => rows.reduce((sum, row) => sum + toNumber(row.budgetRevenueTl2025), 0),
251
+ [rows],
252
+ );
253
+
254
+ const loadYoY = percentChange(load2025Total, load2024Total);
255
+ const revenueYoY = percentChange(revenue2025Total, revenue2024Total);
256
+ const budgetCoverage = percentOf(revenue2025Total, budget2025Total);
257
+
258
+ const topMonth = useMemo(() => {
259
+ if (!rows.length) return null;
260
+ return rows.reduce((best, row) => {
261
+ if (!best) return row;
262
+ const bestValue = mode === 'loads' ? toNumber(best.loadCount2025) : toNumber(best.revenueTl2025);
263
+ const rowValue = mode === 'loads' ? toNumber(row.loadCount2025) : toNumber(row.revenueTl2025);
264
+ return rowValue > bestValue ? row : best;
265
+ }, null);
266
+ }, [rows, mode]);
267
+
268
+ const maxLaneValue = useMemo(() => {
269
+ if (!rows.length) return 1;
270
+ const values = rows.flatMap((row) => (
271
+ mode === 'loads'
272
+ ? [toNumber(row.loadCount2024), toNumber(row.loadCount2025)]
273
+ : [toNumber(row.revenueTl2024), toNumber(row.revenueTl2025), toNumber(row.budgetRevenueTl2025)]
274
+ ));
275
+ return Math.max(1, ...values);
276
+ }, [rows, mode]);
277
+
278
+ if (loading && !table) {
279
+ return (
280
+ <View style={styles.loaderWrap}>
281
+ <ActivityIndicator size="large" color="#13866f" />
282
+ </View>
283
+ );
284
+ }
285
+
286
+ return (
287
+ <View style={styles.screen}>
288
+ <View style={styles.hero}>
289
+ <View style={styles.heroBlobLeft} />
290
+ <View style={styles.heroBlobRight} />
291
+ <TouchableOpacity style={styles.backButton} onPress={onBack}>
292
+ <Text style={styles.backIcon}>‹</Text>
293
+ </TouchableOpacity>
294
+ <Text style={styles.heroTitle}>Transportation Command Center</Text>
295
+ <Text style={styles.heroSubtitle}>Operational pulse for load and revenue movement</Text>
296
+ </View>
297
+
298
+ <ScrollView
299
+ style={styles.content}
300
+ showsVerticalScrollIndicator={false}
301
+ refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
302
+ >
303
+ <View style={styles.topControlRow}>
304
+ <TouchableOpacity
305
+ style={styles.controlChip}
306
+ onPress={() => setMonthsModal(true)}
307
+ activeOpacity={0.9}
308
+ >
309
+ <Text style={styles.controlChipLabel}>Months</Text>
310
+ <Text style={styles.controlChipValue}>
311
+ {selectedMonths.length}/{baseRows.length || selectedMonths.length}
312
+ </Text>
313
+ </TouchableOpacity>
314
+
315
+ <View style={styles.modeToggle}>
316
+ <TouchableOpacity
317
+ style={[styles.modeButton, mode === 'loads' && styles.modeButtonActive]}
318
+ onPress={() => setMode('loads')}
319
+ >
320
+ <Text style={[styles.modeButtonText, mode === 'loads' && styles.modeButtonTextActive]}>
321
+ Loads
322
+ </Text>
323
+ </TouchableOpacity>
324
+ <TouchableOpacity
325
+ style={[styles.modeButton, mode === 'revenue' && styles.modeButtonActive]}
326
+ onPress={() => setMode('revenue')}
327
+ >
328
+ <Text style={[styles.modeButtonText, mode === 'revenue' && styles.modeButtonTextActive]}>
329
+ Revenue
330
+ </Text>
331
+ </TouchableOpacity>
332
+ </View>
333
+ </View>
334
+
335
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.quickMonthRow}>
336
+ {quickMonths.map((month) => (
337
+ <TouchableOpacity
338
+ key={month}
339
+ style={[styles.monthChip, focusMonth === month && styles.monthChipActive]}
340
+ onPress={() => setFocusMonth(month)}
341
+ >
342
+ <Text style={[styles.monthChipText, focusMonth === month && styles.monthChipTextActive]}>
343
+ {month}
344
+ </Text>
345
+ </TouchableOpacity>
346
+ ))}
347
+ </ScrollView>
348
+
349
+ <View style={styles.statRow}>
350
+ <StatTile
351
+ label="Load YoY"
352
+ value={`${loadYoY >= 0 ? '+' : ''}${loadYoY.toFixed(1)}%`}
353
+ hint={`${compactNumber(load2025Total)} vs ${compactNumber(load2024Total)}`}
354
+ tone="mint"
355
+ />
356
+ <StatTile
357
+ label="Revenue YoY"
358
+ value={`${revenueYoY >= 0 ? '+' : ''}${revenueYoY.toFixed(1)}%`}
359
+ hint={`${compactNumber(revenue2025Total)} vs ${compactNumber(revenue2024Total)}`}
360
+ tone="sun"
361
+ />
362
+ </View>
363
+ <View style={styles.statRow}>
364
+ <StatTile
365
+ label="Budget Coverage"
366
+ value={`${budgetCoverage.toFixed(1)}%`}
367
+ hint={`${compactNumber(revenue2025Total)} / ${compactNumber(budget2025Total)}`}
368
+ tone="sea"
369
+ />
370
+ <StatTile
371
+ label="Peak Month"
372
+ value={topMonth?.monthLabel || '-'}
373
+ hint={mode === 'loads' ? compactNumber(topMonth?.loadCount2025) : compactNumber(topMonth?.revenueTl2025)}
374
+ tone="mint"
375
+ />
376
+ </View>
377
+
378
+ <SignalChart rows={rows} mode={mode} />
379
+
380
+ <Text style={styles.sectionTitle}>Operational lanes</Text>
381
+ {rows.map((row) => {
382
+ const current = mode === 'loads' ? toNumber(row.loadCount2025) : toNumber(row.revenueTl2025);
383
+ const previous = mode === 'loads' ? toNumber(row.loadCount2024) : toNumber(row.revenueTl2024);
384
+ const target = mode === 'loads' ? previous : toNumber(row.budgetRevenueTl2025);
385
+ const delta = percentChange(current, previous);
386
+ const progress = percentOf(current, Math.max(1, target));
387
+ const previousWidth = `${Math.min(100, (previous / maxLaneValue) * 100)}%`;
388
+ const currentWidth = `${Math.min(100, (current / maxLaneValue) * 100)}%`;
389
+
390
+ return (
391
+ <View key={`lane-${row.monthLabel}`} style={styles.laneCard}>
392
+ <View style={styles.laneHeader}>
393
+ <Text style={styles.laneTitle}>{row.monthLabel}</Text>
394
+ <Text style={[styles.deltaTag, delta >= 0 ? styles.deltaUp : styles.deltaDown]}>
395
+ {delta >= 0 ? '+' : ''}
396
+ {delta.toFixed(1)}%
397
+ </Text>
398
+ </View>
399
+
400
+ <View style={styles.laneBarsArea}>
401
+ <View style={styles.laneBars}>
402
+ <Text style={styles.laneLabel}>2024</Text>
403
+ <View style={styles.track}>
404
+ <View style={[styles.bar2024, { width: previousWidth }]} />
405
+ </View>
406
+ <Text style={styles.laneValue}>{compactNumber(previous)}</Text>
407
+ </View>
408
+ <View style={styles.laneBars}>
409
+ <Text style={styles.laneLabel}>2025</Text>
410
+ <View style={styles.track}>
411
+ <View style={[styles.bar2025, { width: currentWidth }]} />
412
+ </View>
413
+ <Text style={styles.laneValue}>{compactNumber(current)}</Text>
414
+ </View>
415
+ </View>
416
+
417
+ <View style={styles.gaugeRow}>
418
+ <RingGauge progress={progress} />
419
+ <View style={styles.gaugeTextWrap}>
420
+ <Text style={styles.gaugeTitle}>
421
+ {mode === 'loads' ? '2025 vs 2024 efficiency' : '2025 budget delivery'}
422
+ </Text>
423
+ <Text style={styles.gaugeValue}>{progress.toFixed(1)}%</Text>
424
+ </View>
425
+ </View>
426
+ </View>
427
+ );
428
+ })}
429
+
430
+ {!rows.length ? (
431
+ <View style={styles.emptyWrap}>
432
+ <Text style={styles.emptyText}>No data for selected filters.</Text>
433
+ </View>
434
+ ) : null}
435
+ </ScrollView>
436
+
437
+ <MonthFilterModal
438
+ visible={monthsModal}
439
+ months={baseRows.map((row) => row.monthLabel)}
440
+ selected={selectedMonths}
441
+ onApply={setSelectedMonths}
442
+ onClose={() => setMonthsModal(false)}
443
+ />
444
+ </View>
445
+ );
446
+ };
447
+
448
+ const styles = StyleSheet.create({
449
+ screen: {
450
+ flex: 1,
451
+ backgroundColor: '#faf7ef',
452
+ },
453
+ loaderWrap: {
454
+ flex: 1,
455
+ justifyContent: 'center',
456
+ alignItems: 'center',
457
+ backgroundColor: '#faf7ef',
458
+ },
459
+ hero: {
460
+ backgroundColor: '#0b7a61',
461
+ paddingHorizontal: 16,
462
+ paddingTop: 14,
463
+ paddingBottom: 18,
464
+ overflow: 'hidden',
465
+ },
466
+ heroBlobLeft: {
467
+ position: 'absolute',
468
+ width: 160,
469
+ height: 160,
470
+ borderRadius: 80,
471
+ left: -55,
472
+ top: -90,
473
+ backgroundColor: '#12a07f',
474
+ },
475
+ heroBlobRight: {
476
+ position: 'absolute',
477
+ width: 180,
478
+ height: 180,
479
+ borderRadius: 90,
480
+ right: -70,
481
+ top: -40,
482
+ backgroundColor: '#0f8d70',
483
+ },
484
+ backButton: {
485
+ width: 42,
486
+ height: 42,
487
+ borderRadius: 22,
488
+ justifyContent: 'center',
489
+ alignItems: 'center',
490
+ backgroundColor: 'rgba(255,255,255,0.2)',
491
+ },
492
+ backIcon: {
493
+ fontSize: 28,
494
+ fontWeight: '700',
495
+ color: '#fff',
496
+ marginTop: -2,
497
+ },
498
+ heroTitle: {
499
+ marginTop: 10,
500
+ color: '#fff',
501
+ fontSize: 22,
502
+ fontWeight: '800',
503
+ },
504
+ heroSubtitle: {
505
+ marginTop: 5,
506
+ fontSize: 13,
507
+ color: '#d4fff4',
508
+ fontWeight: '500',
509
+ },
510
+ content: {
511
+ flex: 1,
512
+ paddingHorizontal: 14,
513
+ paddingTop: 14,
514
+ },
515
+ topControlRow: {
516
+ flexDirection: 'row',
517
+ marginBottom: 10,
518
+ },
519
+ controlChip: {
520
+ width: 118,
521
+ borderRadius: 14,
522
+ paddingVertical: 10,
523
+ paddingHorizontal: 12,
524
+ backgroundColor: '#fffdf8',
525
+ borderWidth: 1,
526
+ borderColor: '#dfd6c3',
527
+ marginRight: 10,
528
+ },
529
+ controlChipLabel: {
530
+ fontSize: 11,
531
+ color: '#736b57',
532
+ marginBottom: 2,
533
+ },
534
+ controlChipValue: {
535
+ fontSize: 14,
536
+ fontWeight: '700',
537
+ color: '#2e2a21',
538
+ },
539
+ modeToggle: {
540
+ flex: 1,
541
+ flexDirection: 'row',
542
+ backgroundColor: '#ede6d8',
543
+ borderRadius: 12,
544
+ padding: 4,
545
+ borderWidth: 1,
546
+ borderColor: '#d8cfbf',
547
+ },
548
+ modeButton: {
549
+ flex: 1,
550
+ borderRadius: 10,
551
+ alignItems: 'center',
552
+ justifyContent: 'center',
553
+ paddingVertical: 9,
554
+ },
555
+ modeButtonActive: {
556
+ backgroundColor: '#1c6f5a',
557
+ },
558
+ modeButtonText: {
559
+ fontSize: 13,
560
+ color: '#544b38',
561
+ fontWeight: '700',
562
+ },
563
+ modeButtonTextActive: {
564
+ color: '#fff',
565
+ },
566
+ quickMonthRow: {
567
+ marginBottom: 12,
568
+ },
569
+ monthChip: {
570
+ backgroundColor: '#fffdf8',
571
+ borderWidth: 1,
572
+ borderColor: '#ddd2be',
573
+ borderRadius: 999,
574
+ paddingVertical: 7,
575
+ paddingHorizontal: 12,
576
+ marginRight: 8,
577
+ },
578
+ monthChipActive: {
579
+ backgroundColor: '#e3912f',
580
+ borderColor: '#e3912f',
581
+ },
582
+ monthChipText: {
583
+ fontSize: 12,
584
+ color: '#635640',
585
+ fontWeight: '700',
586
+ },
587
+ monthChipTextActive: {
588
+ color: '#fff',
589
+ },
590
+ statRow: {
591
+ flexDirection: 'row',
592
+ marginBottom: 10,
593
+ },
594
+ statTile: {
595
+ flex: 1,
596
+ borderRadius: 15,
597
+ paddingHorizontal: 12,
598
+ paddingVertical: 11,
599
+ borderWidth: 1,
600
+ marginRight: 10,
601
+ },
602
+ statMint: {
603
+ backgroundColor: '#edfdf7',
604
+ borderColor: '#bcefe0',
605
+ },
606
+ statSun: {
607
+ backgroundColor: '#fff6eb',
608
+ borderColor: '#ffd6a8',
609
+ },
610
+ statSea: {
611
+ backgroundColor: '#edf8ff',
612
+ borderColor: '#b8dcf5',
613
+ },
614
+ statLabel: {
615
+ fontSize: 11,
616
+ color: '#5d5a52',
617
+ },
618
+ statValue: {
619
+ marginTop: 6,
620
+ fontSize: 17,
621
+ color: '#1f2f2d',
622
+ fontWeight: '800',
623
+ },
624
+ statHint: {
625
+ marginTop: 4,
626
+ fontSize: 11,
627
+ color: '#6b6860',
628
+ },
629
+ signalCard: {
630
+ backgroundColor: '#fffdf8',
631
+ borderWidth: 1,
632
+ borderColor: '#e0d7c6',
633
+ borderRadius: 16,
634
+ padding: 12,
635
+ marginBottom: 12,
636
+ },
637
+ signalHeader: {
638
+ flexDirection: 'row',
639
+ justifyContent: 'space-between',
640
+ alignItems: 'center',
641
+ marginBottom: 8,
642
+ },
643
+ signalTitle: {
644
+ fontSize: 14,
645
+ fontWeight: '800',
646
+ color: '#2c2a22',
647
+ },
648
+ signalLegend: {
649
+ flexDirection: 'row',
650
+ },
651
+ legend2024: {
652
+ marginRight: 10,
653
+ color: '#d78835',
654
+ fontWeight: '700',
655
+ fontSize: 12,
656
+ },
657
+ legend2025: {
658
+ color: '#15896f',
659
+ fontWeight: '700',
660
+ fontSize: 12,
661
+ },
662
+ signalMonthStrip: {
663
+ marginTop: 6,
664
+ },
665
+ signalMonthChip: {
666
+ backgroundColor: '#f5efe2',
667
+ borderRadius: 999,
668
+ paddingVertical: 5,
669
+ paddingHorizontal: 10,
670
+ marginRight: 6,
671
+ },
672
+ signalMonthText: {
673
+ color: '#635742',
674
+ fontSize: 11,
675
+ fontWeight: '700',
676
+ },
677
+ sectionTitle: {
678
+ fontSize: 15,
679
+ fontWeight: '800',
680
+ color: '#2b2a22',
681
+ marginBottom: 8,
682
+ },
683
+ laneCard: {
684
+ backgroundColor: '#fffdf8',
685
+ borderRadius: 16,
686
+ borderWidth: 1,
687
+ borderColor: '#e0d7c6',
688
+ padding: 12,
689
+ marginBottom: 10,
690
+ },
691
+ laneHeader: {
692
+ flexDirection: 'row',
693
+ justifyContent: 'space-between',
694
+ marginBottom: 10,
695
+ alignItems: 'center',
696
+ },
697
+ laneTitle: {
698
+ fontSize: 15,
699
+ fontWeight: '800',
700
+ color: '#28261f',
701
+ },
702
+ deltaTag: {
703
+ fontSize: 12,
704
+ fontWeight: '700',
705
+ paddingHorizontal: 8,
706
+ paddingVertical: 4,
707
+ borderRadius: 999,
708
+ },
709
+ deltaUp: {
710
+ color: '#0f8a6d',
711
+ backgroundColor: '#ddf8ef',
712
+ },
713
+ deltaDown: {
714
+ color: '#b6494c',
715
+ backgroundColor: '#ffe7e7',
716
+ },
717
+ laneBarsArea: {
718
+ marginBottom: 10,
719
+ },
720
+ laneBars: {
721
+ flexDirection: 'row',
722
+ alignItems: 'center',
723
+ marginBottom: 8,
724
+ },
725
+ laneLabel: {
726
+ width: 34,
727
+ fontSize: 11,
728
+ fontWeight: '700',
729
+ color: '#675a43',
730
+ },
731
+ track: {
732
+ flex: 1,
733
+ height: 10,
734
+ borderRadius: 999,
735
+ overflow: 'hidden',
736
+ backgroundColor: '#efe8da',
737
+ marginHorizontal: 8,
738
+ },
739
+ bar2024: {
740
+ height: '100%',
741
+ backgroundColor: '#f4a04a',
742
+ borderRadius: 999,
743
+ },
744
+ bar2025: {
745
+ height: '100%',
746
+ backgroundColor: '#179a79',
747
+ borderRadius: 999,
748
+ },
749
+ laneValue: {
750
+ width: 74,
751
+ textAlign: 'right',
752
+ fontSize: 11,
753
+ fontWeight: '700',
754
+ color: '#464238',
755
+ },
756
+ gaugeRow: {
757
+ flexDirection: 'row',
758
+ alignItems: 'center',
759
+ marginTop: 2,
760
+ },
761
+ gaugeTextWrap: {
762
+ marginLeft: 10,
763
+ flex: 1,
764
+ },
765
+ gaugeTitle: {
766
+ fontSize: 11,
767
+ color: '#686152',
768
+ marginBottom: 3,
769
+ },
770
+ gaugeValue: {
771
+ fontSize: 17,
772
+ fontWeight: '800',
773
+ color: '#165a4a',
774
+ },
775
+ emptyWrap: {
776
+ paddingVertical: 24,
777
+ alignItems: 'center',
778
+ },
779
+ emptyText: {
780
+ fontSize: 13,
781
+ color: '#6d675a',
782
+ },
783
+ });
784
+
785
+ export default Report3ModernScreen;
@@ -51,6 +51,11 @@ const NEW_REPORTS = [
51
51
  title: 'Gross Profit by Company & Division',
52
52
  desc: 'Modern interactive layout',
53
53
  },
54
+ {
55
+ id: '3N1',
56
+ title: 'Transportation Business Analysis',
57
+ desc: 'Modern command center layout',
58
+ },
54
59
  ];
55
60
 
56
61
  const ReportListScreen = ({ onSelect, onExit }) => {