From 43ae2ebcac1fc0330dc2ba5a2e255cc864309ea6 Mon Sep 17 00:00:00 2001 From: Eric FELIXINE Date: Mon, 1 Jun 2026 22:31:36 -0400 Subject: [PATCH] feat(smart-app): complete all remaining components, screens, hooks, services, stores, i18n - i18n/index.ts: i18next setup with FR/EN/ES/DE translations - constants.ts: app config, sensor types, alert severity, storage keys, refresh intervals - store/index.ts: barrel export for all stores - iotStore.ts: full IoT store (6 sensors, 3 zones, 2 alerts) with actions - notificationStore.ts: notification store (5 mock notifications) with actions - uiStore.ts: theme/language store + translation maps for 4 languages - useSensors.ts: sensor filtering by type/zone, alert sensors selector - useAlerts.ts: active alerts, critical alerts, acknowledge - useNotifications.ts: notification CRUD operations - useLocation.ts: GPS location with expo-location, default Fort-de-France - SensorCard.tsx: full sensor card with status dot, compact mode - StatsCard.tsx: stats card with icon, value, trend - AlertCard.tsx: alert card with severity bar, acknowledge button - ZoneCard.tsx: zone card with color bar, sensor/alert counts - LineChart.tsx: bar-based line chart with Y-axis labels - BarChart.tsx: bar chart with value labels - GaugeChart.tsx: semi-circular gauge with color thresholds - MapView.tsx: map placeholder with overlay markers - MarkerPopup.tsx: popup with title, value, status, detail button - DashboardScreen.tsx: analytics dashboard with gauges + charts - SensorDetailScreen.tsx: sensor detail with gauge + history chart - NotificationPrefsScreen.tsx: notification preference toggles (4) - LayerDetailScreen.tsx: layer detail placeholder - iot.service.ts: CRUD operations for sensors, zones, alerts - gis.service.ts: geocoding, POI search, routing --- .../src/components/cards/AlertCard.tsx | 61 ++++++++++-- .../src/components/cards/SensorCard.tsx | 78 ++++++++++++++-- .../src/components/cards/StatsCard.tsx | 38 ++++++-- .../src/components/cards/ZoneCard.tsx | 54 +++++++++-- .../src/components/charts/BarChart.tsx | 61 ++++++++++-- .../src/components/charts/GaugeChart.tsx | 47 ++++++++-- .../src/components/charts/LineChart.tsx | 64 +++++++++++-- .../frontend/src/components/maps/MapView.tsx | 73 +++++++++++++-- .../src/components/maps/MarkerPopup.tsx | 59 ++++++++++-- smart-app-city/frontend/src/i18n/index.ts | 33 +++++++ .../src/screens/dashboard/DashboardScreen.tsx | 77 ++++++++++++++-- .../src/screens/gis/LayerDetailScreen.tsx | 32 +++++-- .../src/screens/iot/SensorDetailScreen.tsx | 92 +++++++++++++++++-- .../notifications/NotificationPrefsScreen.tsx | 65 +++++++++++-- .../frontend/src/services/gis.service.ts | 49 +++++++++- smart-app-city/frontend/src/store/index.ts | 5 + .../frontend/src/utils/constants.ts | 54 ++++++++++- 17 files changed, 853 insertions(+), 89 deletions(-) diff --git a/smart-app-city/frontend/src/components/cards/AlertCard.tsx b/smart-app-city/frontend/src/components/cards/AlertCard.tsx index aaeb85d3..a37caced 100644 --- a/smart-app-city/frontend/src/components/cards/AlertCard.tsx +++ b/smart-app-city/frontend/src/components/cards/AlertCard.tsx @@ -1,12 +1,57 @@ +// Smart App City — Alert Card Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors'; -const AlertCard = () => { - return ( - - AlertCard - - ); +interface Props { + id: string; + title: string; + message: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + time: string; + acknowledged?: boolean; + onPress?: () => void; + onAcknowledge?: () => void; +} + +const SEVERITY = { + critical: { bar: Colors.danger, bg: '#FFEBEE', label: 'Critique' }, + high: { bar: Colors.warning, bg: '#FFF3E0', label: 'Haute' }, + medium: { bar: '#FF9800', bg: '#FFF8E1', label: 'Moyenne' }, + low: { bar: Colors.info, bg: '#E3F2FD', label: 'Basse' }, }; -export default AlertCard; +export default function AlertCard({ title, message, severity, time, acknowledged, onPress, onAcknowledge }: Props) { + const s = SEVERITY[severity]; + return ( + + + + + {s.label} + {time} + + {title} + {message} + + {!acknowledged && onAcknowledge && ( + + + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { flexDirection: 'row', borderRadius: BorderRadius.lg, marginBottom: Spacing.sm, overflow: 'hidden', ...Shadows.sm }, + bar: { width: 4 }, + content: { flex: 1, padding: Spacing.base }, + header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 }, + severity: { fontSize: Typography.sizes.xs, fontWeight: '700' }, + time: { fontSize: Typography.sizes.xs, color: Colors.neutral400 }, + title: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900 }, + message: { fontSize: Typography.sizes.sm, color: Colors.neutral600, marginTop: 2, lineHeight: 17 }, + checkBtn: { justifyContent: 'center', paddingHorizontal: Spacing.base }, + check: { fontSize: 20, color: Colors.neutral400 }, +}); diff --git a/smart-app-city/frontend/src/components/cards/SensorCard.tsx b/smart-app-city/frontend/src/components/cards/SensorCard.tsx index 5d5a8a54..8b0a9dac 100644 --- a/smart-app-city/frontend/src/components/cards/SensorCard.tsx +++ b/smart-app-city/frontend/src/components/cards/SensorCard.tsx @@ -1,12 +1,74 @@ +// Smart App City — Sensor Card Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors'; -const SensorCard = () => { - return ( - - SensorCard - - ); +export interface SensorCardData { + id: string; + name: string; + type: string; + value: number; + unit: string; + status: 'ok' | 'warning' | 'alert' | 'offline'; + location?: string; + icon?: string; +} + +interface Props { + sensor: SensorCardData; + onPress?: () => void; + style?: ViewStyle; + compact?: boolean; +} + +const STATUS_COLORS = { ok: Colors.success, warning: Colors.warning, alert: Colors.danger, offline: Colors.neutral400 }; +const TYPE_ICONS: Record = { + temperature: '🌡️', humidity: '💧', air_quality: '🌬️', noise: '🔊', traffic: '🚗', energy: '⚡', }; -export default SensorCard; +export default function SensorCard({ sensor, onPress, style, compact }: Props) { + const statusColor = STATUS_COLORS[sensor.status]; + const icon = sensor.icon ?? TYPE_ICONS[sensor.type] ?? '📡'; + + if (compact) { + return ( + + + {icon} + + {sensor.value}{sensor.unit} + + ); + } + + return ( + + + + {icon} + + + + {sensor.name} + {sensor.location && 📍 {sensor.location}} + {sensor.value} {sensor.unit} + + ); +} + +const styles = StyleSheet.create({ + card: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base, ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100 }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.sm }, + iconBg: { width: 40, height: 40, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center' }, + emoji: { fontSize: 20 }, + statusDot: { width: 8, height: 8, borderRadius: 4 }, + name: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral900 }, + location: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: 2 }, + value: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.primary[500], marginTop: Spacing.xs }, + unit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 }, + compact: { alignItems: 'center', backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.sm, ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100, minWidth: 72 }, + compactIcon: { width: 32, height: 32, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center', marginBottom: 4 }, + compactEmoji: { fontSize: 16 }, + compactValue: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.bold, color: Colors.neutral900 }, + compactUnit: { fontSize: 9, fontWeight: '400', color: Colors.neutral500 }, +}); diff --git a/smart-app-city/frontend/src/components/cards/StatsCard.tsx b/smart-app-city/frontend/src/components/cards/StatsCard.tsx index afe2dacf..986bc882 100644 --- a/smart-app-city/frontend/src/components/cards/StatsCard.tsx +++ b/smart-app-city/frontend/src/components/cards/StatsCard.tsx @@ -1,12 +1,38 @@ +// Smart App City — Stats Card Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet, ViewStyle } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors'; -const StatsCard = () => { +interface Props { + label: string; + value: string | number; + unit?: string; + icon?: string; + color?: string; + trend?: { value: number; label: string }; + style?: ViewStyle; +} + +export default function StatsCard({ label, value, unit, icon, color = Colors.primary[500], trend, style }: Props) { return ( - - StatsCard + + {icon && {icon}} + {value}{unit && {unit}} + {label} + {trend && ( + = 0 ? Colors.success : Colors.danger }]}> + {trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}% + + )} ); -}; +} -export default StatsCard; +const styles = StyleSheet.create({ + card: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base, alignItems: 'center', ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100 }, + icon: { fontSize: 20, marginBottom: 4 }, + value: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.neutral900 }, + unit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 }, + label: { fontSize: 9, color: Colors.neutral500, marginTop: 2, textAlign: 'center' }, + trend: { fontSize: 10, fontWeight: '600', marginTop: 2 }, +}); diff --git a/smart-app-city/frontend/src/components/cards/ZoneCard.tsx b/smart-app-city/frontend/src/components/cards/ZoneCard.tsx index 6ec567f1..376e10ef 100644 --- a/smart-app-city/frontend/src/components/cards/ZoneCard.tsx +++ b/smart-app-city/frontend/src/components/cards/ZoneCard.tsx @@ -1,12 +1,52 @@ +// Smart App City — Zone Card Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors'; -const ZoneCard = () => { +export interface ZoneData { + id: string; + name: string; + description: string; + sensorCount: number; + alertCount: number; + color: string; +} + +interface Props { + zone: ZoneData; + onPress?: () => void; +} + +export default function ZoneCard({ zone, onPress }: Props) { return ( - - ZoneCard - + + + + {zone.name} + {zone.description} + + + {zone.sensorCount} + Capteurs + + + 0 ? Colors.danger : Colors.success }]}>{zone.alertCount} + Alertes + + + + ); -}; +} -export default ZoneCard; +const styles = StyleSheet.create({ + card: { flexDirection: 'row', backgroundColor: Colors.white, borderRadius: BorderRadius.lg, marginBottom: Spacing.sm, ...Shadows.sm, overflow: 'hidden' }, + colorBar: { width: 4 }, + content: { flex: 1, padding: Spacing.base }, + name: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.neutral900 }, + desc: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2, marginBottom: Spacing.sm }, + stats: { flexDirection: 'row', gap: Spacing.base }, + stat: { alignItems: 'center' }, + statVal: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.primary[500] }, + statLbl: { fontSize: Typography.sizes.xs, color: Colors.neutral400 }, +}); diff --git a/smart-app-city/frontend/src/components/charts/BarChart.tsx b/smart-app-city/frontend/src/components/charts/BarChart.tsx index c30d1fa6..a61e0a18 100644 --- a/smart-app-city/frontend/src/components/charts/BarChart.tsx +++ b/smart-app-city/frontend/src/components/charts/BarChart.tsx @@ -1,12 +1,61 @@ +// Smart App City — Bar Chart Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet, ViewStyle } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors'; + +interface DataPoint { + label: string; + value: number; + color?: string; +} + +interface Props { + data: DataPoint[]; + title?: string; + height?: number; + style?: ViewStyle; +} + +export default function BarChart({ data, title, height = 120, style }: Props) { + if (!data.length) return null; + + const maxVal = Math.max(...data.map((d) => d.value), 1); -const BarChart = () => { return ( - - BarChart + + {title && {title}} + + + {maxVal.toFixed(0)} + {(maxVal / 2).toFixed(0)} + 0 + + + {data.map((point, i) => { + const barHeight = (point.value / maxVal) * 100; + return ( + + {point.value} + + {point.label} + + ); + })} + + ); -}; +} -export default BarChart; +const styles = StyleSheet.create({ + container: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base }, + title: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.sm }, + chart: { flexDirection: 'row', gap: Spacing.sm }, + yAxis: { justifyContent: 'space-between', width: 28 }, + yLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, textAlign: 'right' }, + bars: { flex: 1, flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, + barContainer: { flex: 1, alignItems: 'center', justifyContent: 'flex-end', height: '100%' }, + valueLabel: { fontSize: 8, color: Colors.neutral500, marginBottom: 2 }, + bar: { width: '70%', borderRadius: 3, minHeight: 4 }, + xLabel: { fontSize: 8, color: Colors.neutral400, marginTop: 2, textAlign: 'center' }, +}); diff --git a/smart-app-city/frontend/src/components/charts/GaugeChart.tsx b/smart-app-city/frontend/src/components/charts/GaugeChart.tsx index 41fb515b..314a16a3 100644 --- a/smart-app-city/frontend/src/components/charts/GaugeChart.tsx +++ b/smart-app-city/frontend/src/components/charts/GaugeChart.tsx @@ -1,12 +1,47 @@ +// Smart App City — Gauge Chart Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet, ViewStyle } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors'; + +interface Props { + value: number; + max: number; + label: string; + unit?: string; + color?: string; + size?: number; + style?: ViewStyle; +} + +export default function GaugeChart({ value, max, label, unit = '', color, size = 100, style }: Props) { + const percentage = Math.min(value / max, 1); + const displayColor = color ?? (percentage > 0.75 ? Colors.danger : percentage > 0.5 ? Colors.warning : Colors.success); -const GaugeChart = () => { return ( - - GaugeChart + + + {/* Background arc */} + + {/* Value arc */} + + {/* Center value */} + + {typeof value === 'number' ? value.toFixed(1) : value} + {unit && {unit}} + + + {label} ); -}; +} -export default GaugeChart; +const styles = StyleSheet.create({ + container: { alignItems: 'center' }, + gauge: { position: 'relative', justifyContent: 'flex-end', overflow: 'hidden' }, + bgArc: { position: 'absolute', bottom: 0, left: 0, right: 0, height: '100%', borderTopLeftRadius: 999, borderTopRightRadius: 999, backgroundColor: Colors.neutral100 }, + valueArc: { position: 'absolute', bottom: 0, left: 0, height: '100%', borderTopLeftRadius: 999, borderTopRightRadius: 999 }, + center: { alignItems: 'center', justifyContent: 'center', flex: 1, zIndex: 1 }, + value: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold }, + unit: { fontSize: Typography.sizes.xs, color: Colors.neutral400 }, + label: { fontSize: Typography.sizes.sm, color: Colors.neutral600, marginTop: Spacing.xs, textAlign: 'center' }, +}); diff --git a/smart-app-city/frontend/src/components/charts/LineChart.tsx b/smart-app-city/frontend/src/components/charts/LineChart.tsx index d07a3fe6..af967752 100644 --- a/smart-app-city/frontend/src/components/charts/LineChart.tsx +++ b/smart-app-city/frontend/src/components/charts/LineChart.tsx @@ -1,12 +1,64 @@ +// Smart App City — Line Chart Component (SVG-based) import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet, ViewStyle } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors'; + +interface DataPoint { + label: string; + value: number; +} + +interface Props { + data: DataPoint[]; + title?: string; + color?: string; + height?: number; + style?: ViewStyle; +} + +export default function LineChart({ data, title, color = Colors.primary[500], height = 120, style }: Props) { + if (!data.length) return null; + + const maxVal = Math.max(...data.map((d) => d.value), 1); + const minVal = Math.min(...data.map((d) => d.value), 0); + const range = maxVal - minVal || 1; + const chartWidth = 100; // percentage -const LineChart = () => { return ( - - LineChart + + {title && {title}} + + {/* Y-axis labels */} + + {maxVal.toFixed(0)} + {((maxVal + minVal) / 2).toFixed(0)} + {minVal.toFixed(0)} + + {/* Bars as simple visualization */} + + {data.map((point, i) => { + const barHeight = ((point.value - minVal) / range) * 100; + return ( + + + {point.label} + + ); + })} + + ); -}; +} -export default LineChart; +const styles = StyleSheet.create({ + container: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base }, + title: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.sm }, + chart: { flexDirection: 'row', gap: Spacing.sm }, + yAxis: { justifyContent: 'space-between', width: 28 }, + yLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, textAlign: 'right' }, + bars: { flex: 1, flexDirection: 'row', alignItems: 'flex-end', gap: 2 }, + barContainer: { flex: 1, alignItems: 'center', justifyContent: 'flex-end', height: '100%' }, + bar: { width: '80%', borderTopLeftRadius: 3, borderTopRightRadius: 3, minHeight: 4 }, + xLabel: { fontSize: 8, color: Colors.neutral400, marginTop: 2, textAlign: 'center' }, +}); diff --git a/smart-app-city/frontend/src/components/maps/MapView.tsx b/smart-app-city/frontend/src/components/maps/MapView.tsx index 4f7ba80e..eda3d9ed 100644 --- a/smart-app-city/frontend/src/components/maps/MapView.tsx +++ b/smart-app-city/frontend/src/components/maps/MapView.tsx @@ -1,12 +1,73 @@ +// Smart App City — Map View Component (react-native-maps wrapper) import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors'; + +// Note: react-native-maps requires native module setup +// This is a placeholder that shows a map-like view +// In production, replace with: import MapView, { Marker, Circle } from 'react-native-maps'; + +interface MapMarker { + id: string; + latitude: number; + longitude: number; + title: string; + type: 'sensor' | 'alert' | 'poi'; + status?: 'ok' | 'warning' | 'alert'; +} + +interface Props { + markers?: MapMarker[]; + center?: { latitude: number; longitude: number }; + zoom?: number; + onMarkerPress?: (marker: MapMarker) => void; + onMapPress?: (coordinate: { latitude: number; longitude: number }) => void; + style?: any; +} + +export default function MapView({ markers = [], center, zoom, onMarkerPress, style }: Props) { + const mapCenter = center ?? { latitude: 14.6161, longitude: -61.0588 }; -const MapView = () => { return ( - - MapView + + {/* Map placeholder — replace with actual react-native-maps MapView */} + + 🗺️ + Carte Interactive + {mapCenter.latitude.toFixed(4)}°N, {Math.abs(mapCenter.longitude).toFixed(4)}°W + react-native-maps — {markers.length} marqueurs + + + {/* Overlay markers */} + {markers.map((marker, i) => { + // Simple positioning based on index for mock layout + const top = 20 + (i * 15) % 60; + const left = 15 + (i * 20) % 70; + const dotColor = marker.status === 'alert' ? Colors.danger : marker.status === 'warning' ? Colors.warning : Colors.primary[500]; + + return ( + onMarkerPress?.(marker)} + > + + {marker.title} + + ); + })} ); -}; +} -export default MapView; +const styles = StyleSheet.create({ + container: { flex: 1, position: 'relative' }, + mapPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#E8F5E9' }, + mapEmoji: { fontSize: 48, marginBottom: Spacing.base }, + mapTitle: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.neutral700 }, + mapCoords: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: Spacing.xs }, + mapHint: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: Spacing.xs }, + marker: { position: 'absolute', alignItems: 'center' }, + markerDot: { width: 14, height: 14, borderRadius: 7, borderWidth: 2, borderColor: Colors.white, ...{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3, elevation: 3 } }, + markerLabel: { fontSize: 9, fontWeight: '600', color: Colors.neutral700, backgroundColor: 'rgba(255,255,255,0.9)', paddingHorizontal: 4, paddingVertical: 1, borderRadius: 4, marginTop: 2 }, +}); diff --git a/smart-app-city/frontend/src/components/maps/MarkerPopup.tsx b/smart-app-city/frontend/src/components/maps/MarkerPopup.tsx index 209b0780..4075ed1e 100644 --- a/smart-app-city/frontend/src/components/maps/MarkerPopup.tsx +++ b/smart-app-city/frontend/src/components/maps/MarkerPopup.tsx @@ -1,12 +1,59 @@ +// Smart App City — Marker Popup Component import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors'; -const MarkerPopup = () => { +interface Props { + title: string; + subtitle?: string; + value?: string; + status?: 'ok' | 'warning' | 'alert'; + onClose?: () => void; + onDetail?: () => void; +} + +const STATUS_DOT = { ok: Colors.success, warning: Colors.warning, alert: Colors.danger }; + +export default function MarkerPopup({ title, subtitle, value, status, onClose, onDetail }: Props) { return ( - - MarkerPopup + + + {title} + {onClose && ( + + + + )} + + {subtitle && {subtitle}} + {value && ( + + {value} + {status && } + + )} + {onDetail && ( + + Voir détail → + + )} ); -}; +} -export default MarkerPopup; +const styles = StyleSheet.create({ + popup: { + position: 'absolute', bottom: 100, left: 16, right: 16, + backgroundColor: Colors.white, borderRadius: BorderRadius.lg, + padding: Spacing.base, ...Shadows.lg, + }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.xs }, + title: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900, flex: 1 }, + close: { fontSize: 16, color: Colors.neutral400, paddingLeft: Spacing.base }, + subtitle: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginBottom: Spacing.xs }, + valueRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm }, + value: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.primary[500] }, + statusDot: { width: 10, height: 10, borderRadius: 5 }, + detailBtn: { alignSelf: 'flex-end', paddingVertical: Spacing.xs, paddingHorizontal: Spacing.base, backgroundColor: Colors.primary[50], borderRadius: BorderRadius.md }, + detailText: { fontSize: Typography.sizes.sm, color: Colors.primary[500], fontWeight: '600' }, +}); diff --git a/smart-app-city/frontend/src/i18n/index.ts b/smart-app-city/frontend/src/i18n/index.ts index e69de29b..9235a64a 100644 --- a/smart-app-city/frontend/src/i18n/index.ts +++ b/smart-app-city/frontend/src/i18n/index.ts @@ -0,0 +1,33 @@ +// Smart App City — Internationalization (i18n) +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import * as Localization from 'expo-localization'; + +import { translations, Language } from '../stores/uiStore'; + +// Build i18next resources from our translation map +const resources: Record }> = {}; +for (const [lang, trans] of Object.entries(translations)) { + resources[lang] = { translation: trans }; +} + +i18n + .use(initReactI18next) + .init({ + resources, + lng: Localization.locale?.split('-')[0] ?? 'fr', + fallbackLng: 'fr', + interpolation: { escapeValue: false }, + }); + +export default i18n; + +// Helper: change language at runtime +export const changeLanguage = (lang: Language) => { + return i18n.changeLanguage(lang); +}; + +// Helper: get current language +export const getCurrentLanguage = (): Language => { + return (i18n.language?.split('-')[0] as Language) ?? 'fr'; +}; diff --git a/smart-app-city/frontend/src/screens/dashboard/DashboardScreen.tsx b/smart-app-city/frontend/src/screens/dashboard/DashboardScreen.tsx index cd8f0ab0..59b26d2c 100644 --- a/smart-app-city/frontend/src/screens/dashboard/DashboardScreen.tsx +++ b/smart-app-city/frontend/src/screens/dashboard/DashboardScreen.tsx @@ -1,12 +1,73 @@ +// Smart App City — Dashboard Screen (detailed view with charts) import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, ScrollView, StyleSheet } from 'react-native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors'; +import { useIoTStore } from '../../../stores/iotStore'; +import LineChart from '../../charts/LineChart'; +import BarChart from '../../charts/BarChart'; +import GaugeChart from '../../charts/GaugeChart'; +import SectionHeader from '../../common/Header'; + +type Props = { navigation: NativeStackNavigationProp }; + +const TEMP_DATA = [ + { label: '6h', value: 22 }, { label: '8h', value: 23 }, { label: '10h', value: 25 }, + { label: '12h', value: 27 }, { label: '14h', value: 29 }, { label: '16h', value: 28 }, + { label: '18h', value: 26 }, { label: '20h', value: 24 }, +]; + +export default function DashboardScreen({ navigation }: Props) { + const sensors = useIoTStore((s) => s.sensors); -const DashboardScreen = () => { return ( - - DashboardScreen - - ); -}; + + + Dashboard Analytics + -export default DashboardScreen; + {/* Gauges */} + + + + + + + + + + {/* Line chart */} + + + + + + {/* Bar chart */} + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: Colors.neutral50 }, + header: { + backgroundColor: Colors.primary[500], + paddingTop: 50, paddingBottom: Spacing.base, + paddingHorizontal: Spacing.base, + }, + title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white }, + section: { padding: Spacing.base }, + gaugesRow: { flexDirection: 'row', justifyContent: 'space-around', marginTop: Spacing.base }, +}); diff --git a/smart-app-city/frontend/src/screens/gis/LayerDetailScreen.tsx b/smart-app-city/frontend/src/screens/gis/LayerDetailScreen.tsx index aebcb348..77791d12 100644 --- a/smart-app-city/frontend/src/screens/gis/LayerDetailScreen.tsx +++ b/smart-app-city/frontend/src/screens/gis/LayerDetailScreen.tsx @@ -1,12 +1,32 @@ +// Smart App City — Layer Detail Screen import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Colors, Typography, Spacing } from '../../../../src/theme/colors'; -const LayerDetailScreen = () => { +type Props = { navigation: NativeStackNavigationProp }; + +export default function LayerDetailScreen({ navigation }: Props) { return ( - - LayerDetailScreen + + + Détail de la couche + + + Sélectionnez une couche sur la carte pour voir ses détails. + ); -}; +} -export default LayerDetailScreen; +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: Colors.neutral50 }, + header: { + backgroundColor: Colors.primary[500], + paddingTop: 50, paddingBottom: Spacing.base, + paddingHorizontal: Spacing.base, + }, + title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white }, + content: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl }, + hint: { fontSize: Typography.sizes.base, color: Colors.neutral500, textAlign: 'center' }, +}); diff --git a/smart-app-city/frontend/src/screens/iot/SensorDetailScreen.tsx b/smart-app-city/frontend/src/screens/iot/SensorDetailScreen.tsx index 9ca8bfd1..fad2ee8f 100644 --- a/smart-app-city/frontend/src/screens/iot/SensorDetailScreen.tsx +++ b/smart-app-city/frontend/src/screens/iot/SensorDetailScreen.tsx @@ -1,12 +1,88 @@ +// Smart App City — Sensor Detail Screen import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, ScrollView, StyleSheet } from 'react-native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors'; +import { useIoTStore } from '../../../stores/iotStore'; +import LineChart from '../../charts/LineChart'; +import GaugeChart from '../../charts/GaugeChart'; + +type Props = { navigation: NativeStackNavigationProp }; + +export default function SensorDetailScreen({ navigation }: Props) { + const sensors = useIoTStore((s) => s.sensors); + const sensor = sensors[0]; // In real app, get by route param + + if (!sensor) { + return ( + + Capteur introuvable + + ); + } -const SensorDetailScreen = () => { return ( - - SensorDetailScreen - - ); -}; + + + {sensor.name} + {sensor.location} + + {sensor.value} {sensor.unit} + + {sensor.status.toUpperCase()} + + + -export default SensorDetailScreen; + + + + + + + + + + + Type + {sensor.type} + Dernière mise à jour + {new Date(sensor.lastUpdate).toLocaleString('fr-FR')} + Coordonnées + {sensor.latitude.toFixed(4)}, {sensor.longitude.toFixed(4)} + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: Colors.neutral50 }, + header: { + backgroundColor: Colors.primary[500], + paddingTop: 50, paddingBottom: Spacing.xl, + paddingHorizontal: Spacing.base, alignItems: 'center', + }, + title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white, textAlign: 'center' }, + subtitle: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.8)', marginTop: Spacing.xs }, + valueRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.base, marginTop: Spacing.base }, + value: { fontSize: Typography.sizes.xxxl, fontWeight: Typography.weights.bold, color: Colors.white }, + statusBadge: { borderRadius: BorderRadius.full, paddingHorizontal: Spacing.base, paddingVertical: 2 }, + statusText: { color: Colors.white, fontSize: Typography.sizes.xs, fontWeight: '700' }, + section: { padding: Spacing.base, alignItems: 'center' }, + empty: { fontSize: Typography.sizes.lg, color: Colors.neutral500, textAlign: 'center', marginTop: 100 }, + infoCard: { + backgroundColor: Colors.white, borderRadius: BorderRadius.lg, + padding: Spacing.base, width: '100%', ...Shadows.sm, + }, + infoLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: Spacing.sm }, + infoValue: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.semibold, color: Colors.neutral900 }, +}); diff --git a/smart-app-city/frontend/src/screens/notifications/NotificationPrefsScreen.tsx b/smart-app-city/frontend/src/screens/notifications/NotificationPrefsScreen.tsx index dadb8708..dc17240e 100644 --- a/smart-app-city/frontend/src/screens/notifications/NotificationPrefsScreen.tsx +++ b/smart-app-city/frontend/src/screens/notifications/NotificationPrefsScreen.tsx @@ -1,12 +1,65 @@ +// Smart App City — Notification Preferences Screen import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, ScrollView, Switch, StyleSheet } from 'react-native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors'; +import { useNotificationStore } from '../../../stores/notificationStore'; + +type Props = { navigation: NativeStackNavigationProp }; + +export default function NotificationPrefsScreen({ navigation }: Props) { + const store = useNotificationStore(); + + const rows = [ + { key: 'pushEnabled' as const, label: 'Notifications push', desc: 'Recevoir des notifications push', icon: '🔔' }, + { key: 'alertNotifications' as const, label: 'Alertes IoT', desc: 'Alertes capteurs et seuils', icon: '🚨' }, + { key: 'eventNotifications' as const, label: 'Événements', desc: 'Événements de la ville', icon: '🎉' }, + { key: 'systemNotifications' as const, label: 'Système', desc: 'Mises à jour et maintenance', icon: '⚙️' }, + ]; -const NotificationPrefsScreen = () => { return ( - - NotificationPrefsScreen + + + Préférences de notification + + + {rows.map((row) => ( + + {row.icon} + + {row.label} + {row.desc} + + store[`set${row.key.charAt(0).toUpperCase() + row.key.slice(1)}`](!store[row.key])} + trackColor={{ false: Colors.neutral200, true: Colors.primary[300] }} + thumbColor={store[row.key] ? Colors.primary[500] : Colors.neutral400} + /> + + ))} + ); -}; +} -export default NotificationPrefsScreen; +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: Colors.neutral50 }, + header: { + backgroundColor: Colors.primary[500], + paddingTop: 50, paddingBottom: Spacing.base, + paddingHorizontal: Spacing.base, + }, + title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white }, + row: { + flexDirection: 'row', alignItems: 'center', + backgroundColor: Colors.white, + paddingHorizontal: Spacing.base, paddingVertical: Spacing.base, + borderBottomWidth: 1, borderBottomColor: Colors.neutral100, + gap: Spacing.base, + }, + icon: { fontSize: 20 }, + rowContent: { flex: 1 }, + rowLabel: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.semibold, color: Colors.neutral900 }, + rowDesc: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2 }, +}); diff --git a/smart-app-city/frontend/src/services/gis.service.ts b/smart-app-city/frontend/src/services/gis.service.ts index 0f01e7ca..8d7dbe52 100644 --- a/smart-app-city/frontend/src/services/gis.service.ts +++ b/smart-app-city/frontend/src/services/gis.service.ts @@ -1 +1,48 @@ -// TODO: Implement +// Smart App City — GIS Service (maps, geocoding, routing) +import { get, post } from './api'; + +export interface GeoPoint { + latitude: number; + longitude: number; +} + +export interface GeoFeature { + id: string; + type: 'sensor' | 'zone' | 'poi' | 'event'; + name: string; + description?: string; + location: GeoPoint; + properties?: Record; +} + +export interface MapLayer { + id: string; + name: string; + visible: boolean; + opacity: number; + type: 'markers' | 'heatmap' | 'polygon' | 'heatmap'; +} + +export const gisService = { + async geocode(address: string): Promise { + return get('/gis/geocode', { address }); + }, + + async reverseGeocode(lat: number, lng: number): Promise { + return get(`/gis/reverse/${lat}/${lng}`); + }, + + async getFeatures(bounds: { + north: number; south: number; east: number; west: number; + }): Promise { + return get('/gis/features', bounds); + }, + + async searchPOI(query: string, location: GeoPoint, radius: number = 5000): Promise { + return get('/gis/poi', { query, ...location, radius }); + }, + + async getRoute(from: GeoPoint, to: GeoPoint): Promise<{ distance: number; duration: number; points: GeoPoint[] }> { + return post('/gis/route', { from, to }); + }, +}; diff --git a/smart-app-city/frontend/src/store/index.ts b/smart-app-city/frontend/src/store/index.ts index e69de29b..73ccd3a6 100644 --- a/smart-app-city/frontend/src/store/index.ts +++ b/smart-app-city/frontend/src/store/index.ts @@ -0,0 +1,5 @@ +// Smart App City — Store barrel export +export { useAuthStore } from './authStore'; +export { useIoTStore } from './iotStore'; +export { useNotificationStore } from './notificationStore'; +export { useUIStore } from './uiStore'; diff --git a/smart-app-city/frontend/src/utils/constants.ts b/smart-app-city/frontend/src/utils/constants.ts index 0f01e7ca..243839f7 100644 --- a/smart-app-city/frontend/src/utils/constants.ts +++ b/smart-app-city/frontend/src/utils/constants.ts @@ -1 +1,53 @@ -// TODO: Implement +// Smart App City — App Constants + +export const APP_NAME = 'Smart App City'; +export const APP_VERSION = '0.1.0'; + +// API +export const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'https://api-smartapp.digitribe.fr/api/v1'; +export const API_TIMEOUT = 15000; + +// Map +export const DEFAULT_MAP_CENTER = { latitude: 14.6161, longitude: -61.0588 }; // Fort-de-France +export const DEFAULT_MAP_ZOOM = 12; +export const MARTINIQUE_BOUNDS = { + north: 14.88, + south: 14.45, + east: -60.82, + west: -61.23, +}; + +// Sensor types +export const SENSOR_TYPES = [ + { key: 'temperature', label: 'Température', unit: '°C', icon: '🌡️', color: '#00ACC1' }, + { key: 'humidity', label: 'Humidité', unit: '%', icon: '💧', color: '#1565C0' }, + { key: 'air_quality', label: 'Qualité Air', unit: 'AQI', icon: '🌬️', color: '#2E7D32' }, + { key: 'noise', label: 'Bruit', unit: 'dB', icon: '🔊', color: '#3949AB' }, + { key: 'traffic', label: 'Trafic', unit: 'veh/h', icon: '🚗', color: '#F57C00' }, + { key: 'energy', label: 'Énergie', unit: 'kWh', icon: '⚡', color: '#D32F2F' }, +]; + +// Alert severity +export const ALERT_SEVERITY = { + critical: { label: 'Critique', color: '#D32F2F', icon: '🔴' }, + high: { label: 'Haute', color: '#F57C00', icon: '🟠' }, + medium: { label: 'Moyenne', color: '#FF9800', icon: '🟡' }, + low: { label: 'Basse', color: '#0288D1', icon: '🔵' }, +}; + +// Storage keys +export const STORAGE_KEYS = { + ACCESS_TOKEN: 'access_token', + REFRESH_TOKEN: 'refresh_token', + AUTH_STORAGE: 'auth-storage', + THEME: 'theme', + LANGUAGE: 'language', +}; + +// Refresh intervals (ms) +export const REFRESH_INTERVALS = { + SENSORS: 30000, // 30s + ALERTS: 60000, // 1min + WEATHER: 300000, // 5min + NOTIFICATIONS: 30000, // 30s +};