Compare commits
2 Commits
3420bee2bf
...
0e0b64f20f
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e0b64f20f | |||
| a1f8c74ec8 |
81
App.js
81
App.js
|
|
@ -8,12 +8,14 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
Switch,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import PlantDetailScreen from './screens/PlantDetailScreen';
|
import PlantDetailScreen from './screens/PlantDetailScreen';
|
||||||
|
import ComparisonScreen from './screens/ComparisonScreen';
|
||||||
|
|
||||||
const API_URL = 'https://solorpower.dadot.net/plants/1';
|
const API_URL = 'https://solorpower.dadot.net/plants/1';
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
@ -49,6 +51,42 @@ function DashboardScreen({ navigation }) {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const handleToggleAlert = async (plant, newValue) => {
|
||||||
|
try {
|
||||||
|
setData((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const newData = { ...prev };
|
||||||
|
newData.data = newData.data.map(p =>
|
||||||
|
p.id === plant.id ? { ...p, alerts_enabled: newValue } : p
|
||||||
|
);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`https://solorpower.dadot.net/plants/${plant.company_id || 1}/${plant.id}/alerts`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ alerts_enabled: newValue }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update alert settings');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setData((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const newData = { ...prev };
|
||||||
|
newData.data = newData.data.map(p =>
|
||||||
|
p.id === plant.id ? { ...p, alerts_enabled: !newValue } : p
|
||||||
|
);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
alert('알림 설정 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onRefresh = useCallback(() => {
|
const onRefresh = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
@ -156,6 +194,12 @@ function DashboardScreen({ navigation }) {
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerTop}>
|
<View style={styles.headerTop}>
|
||||||
<Text style={styles.headerTitle}>☀️ 태양광 발전 현황</Text>
|
<Text style={styles.headerTitle}>☀️ 태양광 발전 현황</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.compareButton}
|
||||||
|
onPress={() => navigation.navigate('Comparison')}
|
||||||
|
>
|
||||||
|
<Text style={styles.compareButtonText}>📊 전체 비교</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.headerSubtitle}>실시간 모니터링 대시보드</Text>
|
<Text style={styles.headerSubtitle}>실시간 모니터링 대시보드</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -211,7 +255,17 @@ function DashboardScreen({ navigation }) {
|
||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.arrowIcon}>›</Text>
|
<View style={styles.alertToggleContainer}>
|
||||||
|
<Text style={styles.alertToggleLabel}>
|
||||||
|
{plant.alerts_enabled !== false ? '알림 ON' : '알림 OFF'}
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
value={plant.alerts_enabled !== false}
|
||||||
|
onValueChange={(value) => handleToggleAlert(plant, value)}
|
||||||
|
trackColor={{ false: '#D1D5DB', true: '#3B82F6' }}
|
||||||
|
thumbColor={'#FFFFFF'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.cardBody}>
|
<View style={styles.cardBody}>
|
||||||
|
|
@ -269,6 +323,7 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="Dashboard" component={DashboardScreen} />
|
<Stack.Screen name="Dashboard" component={DashboardScreen} />
|
||||||
|
<Stack.Screen name="Comparison" component={ComparisonScreen} />
|
||||||
<Stack.Screen name="PlantDetail" component={PlantDetailScreen} />
|
<Stack.Screen name="PlantDetail" component={PlantDetailScreen} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|
@ -449,6 +504,16 @@ const styles = StyleSheet.create({
|
||||||
color: '#9CA3AF',
|
color: '#9CA3AF',
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
},
|
},
|
||||||
|
alertToggleContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
alertToggleLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#6B7280',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
cardBody: {
|
cardBody: {
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
|
|
@ -497,4 +562,18 @@ const styles = StyleSheet.create({
|
||||||
width: 40,
|
width: 40,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
},
|
},
|
||||||
|
compareButton: {
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255,255,255,0.4)',
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
compareButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
432
screens/ComparisonScreen.js
Normal file
432
screens/ComparisonScreen.js
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
|
useWindowDimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { BarChart } from 'react-native-gifted-charts';
|
||||||
|
|
||||||
|
const API_BASE_URL = 'https://solorpower.dadot.net';
|
||||||
|
|
||||||
|
export default function ComparisonScreen({ navigation }) {
|
||||||
|
const [period, setPeriod] = useState('day');
|
||||||
|
const [statsData, setStatsData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentDate(new Date());
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
|
const moveDate = (direction) => {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
if (period === 'day') {
|
||||||
|
newDate.setDate(newDate.getDate() + direction);
|
||||||
|
} else if (period === 'month') {
|
||||||
|
newDate.setMonth(newDate.getMonth() + direction);
|
||||||
|
} else if (period === 'year') {
|
||||||
|
newDate.setFullYear(newDate.getFullYear() + direction);
|
||||||
|
}
|
||||||
|
setCurrentDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateDisplay = () => {
|
||||||
|
const y = currentDate.getFullYear();
|
||||||
|
const m = currentDate.getMonth() + 1;
|
||||||
|
const d = currentDate.getDate();
|
||||||
|
|
||||||
|
if (period === 'day') return `${y}년 ${m}월 ${d}일`;
|
||||||
|
if (period === 'month') return `${y}년 ${m}월`;
|
||||||
|
if (period === 'year') return `${y}년`;
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchStats = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const y = currentDate.getFullYear();
|
||||||
|
const m = currentDate.getMonth() + 1;
|
||||||
|
const d = currentDate.getDate();
|
||||||
|
const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
let query = `period=${period}`;
|
||||||
|
if (period === 'day') {
|
||||||
|
query += `&date=${dateStr}`;
|
||||||
|
} else if (period === 'month') {
|
||||||
|
query += `&year=${y}&month=${m}`;
|
||||||
|
} else if (period === 'year') {
|
||||||
|
query += `&year=${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/plants/stats/comparison?${query}`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
setStatsData(result.data || []);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
setStatsData([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [period, currentDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStats();
|
||||||
|
}, [fetchStats]);
|
||||||
|
|
||||||
|
// Chart Data Preparation
|
||||||
|
const chartData = statsData.map(item => {
|
||||||
|
let label = item.plant_name;
|
||||||
|
const match = label.match(/(\d+호기)/);
|
||||||
|
if (match) {
|
||||||
|
label = match[1];
|
||||||
|
} else {
|
||||||
|
label = label.replace(/태양과바람|발전소/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: item.generation,
|
||||||
|
label: label,
|
||||||
|
frontColor: '#3B82F6',
|
||||||
|
topLabelComponent: () => (
|
||||||
|
<Text style={{ color: '#6B7280', fontSize: 10, marginBottom: 4 }}>
|
||||||
|
{item.generation.toFixed(0)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderTabs = () => (
|
||||||
|
<View style={styles.tabContainer}>
|
||||||
|
{['day', 'month', 'year'].map((p) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p}
|
||||||
|
style={[styles.tab, period === p && styles.tabActive]}
|
||||||
|
onPress={() => setPeriod(p)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tabText, period === p && styles.tabTextActive]}>
|
||||||
|
{p === 'day' ? '일간' : p === 'month' ? '월간' : '연간'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDateControl = () => (
|
||||||
|
<View style={styles.dateControl}>
|
||||||
|
<TouchableOpacity onPress={() => moveDate(-1)} style={styles.arrowButton}>
|
||||||
|
<Text style={styles.arrowText}>◀</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.dateText}>{formatDateDisplay()}</Text>
|
||||||
|
<TouchableOpacity onPress={() => moveDate(1)} style={styles.arrowButton}>
|
||||||
|
<Text style={styles.arrowText}>▶</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTable = () => (
|
||||||
|
<View style={styles.table}>
|
||||||
|
<View style={styles.tableHeader}>
|
||||||
|
<Text style={[styles.columnHeader, { flex: 2 }]}>발전소</Text>
|
||||||
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>발전량</Text>
|
||||||
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>용량</Text>
|
||||||
|
<Text style={[styles.columnHeader, { flex: 1.5 }]}>시간</Text>
|
||||||
|
</View>
|
||||||
|
{statsData.map((item, index) => (
|
||||||
|
<View key={item.plant_id} style={[styles.tableRow, index % 2 === 1 && styles.tableRowAlt]}>
|
||||||
|
<Text style={[styles.cell, { flex: 2, textAlign: 'left' }]}>{item.plant_name}</Text>
|
||||||
|
<Text style={[styles.cell, { flex: 1.5 }]}>{item.generation.toFixed(1)}</Text>
|
||||||
|
<Text style={[styles.cell, { flex: 1.5, color: '#9CA3AF' }]}>{item.capacity}</Text>
|
||||||
|
<Text style={[styles.cell, { flex: 1.5, fontWeight: 'bold', color: '#10B981' }]}>
|
||||||
|
{item.generation_hours.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{statsData.length === 0 && (
|
||||||
|
<View style={styles.emptyRow}>
|
||||||
|
<Text style={styles.emptyText}>데이터가 없습니다</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartWidth = width - 40;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
|
||||||
|
<Text style={styles.backButtonText}>← 뒤로</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>전체 발전소 비교</Text>
|
||||||
|
<View style={{ width: 60 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||||
|
{renderDateControl()}
|
||||||
|
{renderTabs()}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#3B82F6" />
|
||||||
|
<Text style={styles.loadingText}>데이터 불러오는 중...</Text>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>데이터 조회 실패</Text>
|
||||||
|
<Text style={styles.errorDetail}>{error}</Text>
|
||||||
|
<TouchableOpacity style={styles.retryButton} onPress={fetchStats}>
|
||||||
|
<Text style={styles.retryButtonText}>다시 시도</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={styles.chartSection}>
|
||||||
|
<Text style={styles.sectionTitle}>📊 발전량 비교 (kWh)</Text>
|
||||||
|
<View style={styles.chartContainer}>
|
||||||
|
{statsData.length > 0 ? (
|
||||||
|
<BarChart
|
||||||
|
data={chartData}
|
||||||
|
width={chartWidth}
|
||||||
|
height={200}
|
||||||
|
barWidth={20}
|
||||||
|
spacing={15}
|
||||||
|
barBorderRadius={4}
|
||||||
|
frontColor="#3B82F6"
|
||||||
|
yAxisThickness={0}
|
||||||
|
xAxisThickness={1}
|
||||||
|
yAxisTextStyle={{ color: '#9CA3AF', fontSize: 10 }}
|
||||||
|
xAxisLabelTextStyle={{ color: '#6B7280', fontSize: 10 }}
|
||||||
|
hideRules
|
||||||
|
showValuesAsTopLabel
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.noDataText}>표시할 데이터가 없습니다.</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<View style={styles.tableHeaderRow}>
|
||||||
|
<Text style={styles.sectionTitle}>📋 상세 현황</Text>
|
||||||
|
<Text style={styles.unitText}>시간 = 발전량 / 설비용량 {period === 'year' ? '/ 365' : ''}</Text>
|
||||||
|
</View>
|
||||||
|
{renderTable()}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: '#1E40AF',
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
backButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
dateControl: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
arrowButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
arrowText: {
|
||||||
|
fontSize: 20,
|
||||||
|
color: '#4B5563',
|
||||||
|
},
|
||||||
|
dateText: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1F2937',
|
||||||
|
},
|
||||||
|
tabContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 4,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
tabActive: {
|
||||||
|
backgroundColor: '#3B82F6',
|
||||||
|
},
|
||||||
|
tabText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
tabTextActive: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
height: 300,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 12,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
height: 300,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#EF4444',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
errorDetail: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#6B7280',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
backgroundColor: '#3B82F6',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
chartSection: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1F2937',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
chartContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
noDataText: {
|
||||||
|
color: '#9CA3AF',
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
|
tableSection: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
tableHeaderRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
unitText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
tableHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
columnHeader: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#4B5563',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
tableRowAlt: {
|
||||||
|
backgroundColor: '#F9FAFB',
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#1F2937',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
emptyRow: {
|
||||||
|
padding: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: '#9CA3AF',
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user