diff --git a/App.tsx b/App.tsx index 4798824..3047a94 100644 --- a/App.tsx +++ b/App.tsx @@ -87,7 +87,7 @@ function AppInner() { // ─── Auto-schedule flight notifications on startup ───────────────────────── useEffect(() => { autoScheduleNotifications().then(count => { - if (count > 0) console.log(`Auto-scheduled ${count} notifications`); + if (count > 0 && __DEV__) console.log(`Auto-scheduled ${count} notifications`); }).catch(() => {}); }, []); diff --git a/src/components/DrawerMenu.tsx b/src/components/DrawerMenu.tsx index 932d200..ef577d6 100644 --- a/src/components/DrawerMenu.tsx +++ b/src/components/DrawerMenu.tsx @@ -1,9 +1,10 @@ +import { version } from '../../package.json'; import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Animated, Modal, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; type DrawerItem = { id: string; @@ -89,7 +90,7 @@ export default function DrawerMenu({ visible, onClose, onSelect }: Props) { activeOpacity={0.7} > - + {item.label} @@ -103,14 +104,14 @@ export default function DrawerMenu({ visible, onClose, onSelect }: Props) { {/* Divider */} - AeroStaff Pro · v1.0 + AeroStaff Pro · v{version} ); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, flexDirection: 'row' }, overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(15,23,42,0.5)' }, diff --git a/src/components/ShiftTimeline.tsx b/src/components/ShiftTimeline.tsx index 902c665..b989dd3 100644 --- a/src/components/ShiftTimeline.tsx +++ b/src/components/ShiftTimeline.tsx @@ -4,7 +4,7 @@ import { ActivityIndicator, Dimensions, LayoutAnimation, Platform, UIManager, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useAirport } from '../context/AirportContext'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; import { fetchAirportScheduleRaw } from '../utils/fr24api'; @@ -300,7 +300,7 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, ); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, sheet: { diff --git a/src/hooks/useDynamicTheme.ts b/src/hooks/useDynamicTheme.ts index b32b0eb..8570ed4 100644 --- a/src/hooks/useDynamicTheme.ts +++ b/src/hooks/useDynamicTheme.ts @@ -118,7 +118,7 @@ async function resolveTheme(): Promise { globalCachedTheme = selected; return selected; } catch (err) { - console.warn('Errore caricamento tema dinamico:', err); + if (__DEV__) console.warn('Errore caricamento tema dinamico:', err); return themes.default; } finally { pendingFetch = null; diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 1266625..ae72df3 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -10,7 +10,7 @@ import * as FileSystem from 'expo-file-system/legacy'; import { WebView } from 'react-native-webview'; import { MaterialIcons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useAirport } from '../context/AirportContext'; import { fetchAirportScheduleRaw } from '../utils/fr24api'; import { @@ -23,7 +23,6 @@ import { type ParsedSchedule, type ParsedEmployee, type ParsedShift, } from '../utils/pdfShiftParser'; -const PRIMARY = '#2563EB'; const STORAGE_KEY = '@shift_import_name'; type ShiftEvent = { @@ -198,7 +197,7 @@ export default function CalendarScreen() { localData[iso].push({ id: e.id, title: e.title, startDate: e.startDate, endDate: e.endDate }); // Lavoro has priority over Riposo for dot color if (e.title.includes('Lavoro') || !dots[iso]) { - dots[iso] = e.title.includes('Riposo') ? '#10b981' : PRIMARY; + dots[iso] = e.title.includes('Riposo') ? '#10b981' : colors.primary; } } }); @@ -206,7 +205,7 @@ export default function CalendarScreen() { setEventsData(localData); setLoading(false); fetchWeatherAndFlights(start, end, localData); - } catch (e) { console.error(e); setLoading(false); } + } catch (e) { if (__DEV__) console.error(e); setLoading(false); } }; const fetchWeatherAndFlights = async (start: Date, end: Date, localData: Record) => { @@ -224,7 +223,7 @@ export default function CalendarScreen() { dict[date] = { weatherText: m.text, weatherIcon: m.icon, flightCount: 0 }; }); } - } catch (e) { console.warn('[calWeather]', e); } + } catch (e) { if (__DEV__) console.warn('[calWeather]', e); } try { const { arrivals, departures } = await fetchAirportScheduleRaw(airportCode); const allF = [...arrivals, ...departures]; @@ -240,7 +239,7 @@ export default function CalendarScreen() { if (dict[iso]) dict[iso].flightCount = cnt; else dict[iso] = { weatherText: 'N/A', weatherIcon: '❓', flightCount: cnt }; } }); - } catch (e) { console.warn('[calFlights]', e); } + } catch (e) { if (__DEV__) console.warn('[calFlights]', e); } setDailyStats(dict); }; @@ -270,7 +269,7 @@ export default function CalendarScreen() { setImportModalVisible(true); setPdfHtml(getPdfExtractorHtml(base64)); } catch (e: any) { - console.error(`Import error at step=${step}:`, e); + if (__DEV__) console.error(`Import error at step=${step}:`, e); Alert.alert('Errore', `Errore (${step}): ${e?.message || e}`); } }; @@ -311,7 +310,7 @@ export default function CalendarScreen() { setImportStep('pickName'); } catch (e) { - console.error(e); + if (__DEV__) console.error(e); Alert.alert('Errore', 'Errore nel parsing del PDF'); setImportModalVisible(false); setImportStep('idle'); @@ -357,7 +356,7 @@ export default function CalendarScreen() { Alert.alert('Importazione completata', `${saved} turni salvati nel calendario`); }, 800); } catch (e) { - console.error(e); + if (__DEV__) console.error(e); Alert.alert('Errore', 'Errore durante il salvataggio'); setImportStep('idle'); } @@ -730,7 +729,7 @@ export default function CalendarScreen() { ); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ pageHeader: { backgroundColor: c.card, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border }, pageTitle: { fontSize: 22, fontWeight: 'bold', color: c.primaryDark }, diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 155fb94..cce1020 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -8,7 +8,7 @@ import * as Calendar from 'expo-calendar'; import * as Notifications from 'expo-notifications'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import { useAirport } from '../context/AirportContext'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; import { fetchAirportScheduleRaw } from '../utils/fr24api'; @@ -34,7 +34,7 @@ try { Notifications.setNotificationHandler({ shouldShowBanner: true, shouldShowList: true, }), -}); } catch (e) { console.warn('[notifHandler]', e); } +}); } catch (e) { if (__DEV__) console.warn('[notifHandler]', e); } function LogoPill({ iataCode, airlineName, color }: { iataCode: string; airlineName: string; color: string }) { @@ -396,7 +396,7 @@ export default function FlightScreen() { await cancelPreviousNotifications(); setScheduledCount(0); } - } catch (e) { console.error('[fetchAll]', e); } finally { setLoading(false); setRefreshing(false); } + } catch (e) { if (__DEV__) console.error('[fetchAll]', e); } finally { setLoading(false); setRefreshing(false); } }, [airportCode, airportLoading]); useEffect(() => { @@ -461,7 +461,7 @@ export default function FlightScreen() { const tab = activeTab; await AsyncStorage.setItem(PINNED_FLIGHT_KEY, JSON.stringify({ ...item, _pinTab: tab, _pinnedAt: Date.now() })); setPinnedFlightId(id); - try { await schedulePinnedNotifications(item, tab); } catch (e) { console.warn('[pinnedNotif]', e); } + try { await schedulePinnedNotifications(item, tab); } catch (e) { if (__DEV__) console.warn('[pinnedNotif]', e); } // Send to watch if (WearDataSender) { const payload = JSON.stringify({ @@ -488,10 +488,10 @@ export default function FlightScreen() { const unpinFlight = useCallback(async () => { try { await AsyncStorage.removeItem(PINNED_FLIGHT_KEY); - try { await cancelPinnedNotifications(); } catch (e) { console.warn('[cancelPinNotif]', e); } + try { await cancelPinnedNotifications(); } catch (e) { if (__DEV__) console.warn('[cancelPinNotif]', e); } setPinnedFlightId(null); if (WearDataSender) WearDataSender.clearPinnedFlight(); - } catch (e) { console.error('[unpin]', e); } + } catch (e) { if (__DEV__) console.error('[unpin]', e); } }, []); const userShift = activeDay === 'today' ? shifts.today : shifts.tomorrow; @@ -726,7 +726,7 @@ export default function FlightScreen() { ); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ pageHeader: { backgroundColor: c.card, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border, flexDirection: 'row', alignItems: 'center' }, notifBtn: { width: 42, height: 42, borderRadius: 21, backgroundColor: c.cardSecondary, justifyContent: 'center', alignItems: 'center' }, diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 466c5ef..696690a 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -10,7 +10,7 @@ import { WebView } from 'react-native-webview'; import * as ImagePicker from 'expo-image-picker'; import * as Calendar from 'expo-calendar'; import * as Location from 'expo-location'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; import ShiftTimeline from '../components/ShiftTimeline'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; @@ -253,7 +253,7 @@ export default function HomeScreen() { const events = await Calendar.getEventsAsync([cal.id], d, dEnd); const shift = events.find(e => e.title.includes('Lavoro') || e.title.includes('Riposo')); setShiftEvent(shift || null); - } catch (e) { console.error('[shift]', e); } finally { setLoadingShift(false); } + } catch (e) { if (__DEV__) console.error('[shift]', e); } finally { setLoadingShift(false); } }; const fetchWeather = async () => { @@ -267,7 +267,7 @@ export default function HomeScreen() { const temp = Math.round(json.current?.temperature_2m ?? 0); const w = weatherMap[code] || { text: 'Sereno', icon: '☀️' }; setWeather({ ...w, temp }); - } catch (e) { console.warn('[weather]', e); } + } catch (e) { if (__DEV__) console.warn('[weather]', e); } }; const pickImage = async () => { @@ -291,7 +291,7 @@ export default function HomeScreen() { true; `); } - } catch (e) { console.error('[imagePicker]', e); setProcessing(false); } + } catch (e) { if (__DEV__) console.error('[imagePicker]', e); setProcessing(false); } }; const handleWebViewMessage = (event: any) => { @@ -299,7 +299,7 @@ export default function HomeScreen() { const r = JSON.parse(event.nativeEvent.data); if (r.success) setOcrText(r.text); else Alert.alert('Errore riconoscimento testo', r.error || 'Prova con un\'immagine più nitida o meglio illuminata.'); - } catch (e) { console.error('[ocrMessage]', e); } finally { setProcessing(false); } + } catch (e) { if (__DEV__) console.error('[ocrMessage]', e); } finally { setProcessing(false); } }; const parseAndSave = async () => { @@ -429,7 +429,7 @@ export default function HomeScreen() { ); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ hiddenWV: { height: 1, width: 1, opacity: 0, position: 'absolute', top: -100 }, topRow: { flexDirection: 'row', gap: 12, padding: 16, paddingBottom: 8 }, diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index f20e5b3..ad9d783 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -5,7 +5,7 @@ import { LayoutAnimation, Platform, UIManager, TextInput, Modal, Alert, KeyboardAvoidingView, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; const STORAGE_KEY = 'manuals_data_v2'; @@ -416,7 +416,7 @@ function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any } // ─── Item component ─────────────────────────────────────────────────────────── -function makeItemStyles(c: any) { +function makeItemStyles(c: ThemeColors) { return StyleSheet.create({ wrapper: { backgroundColor: c.card, @@ -485,7 +485,7 @@ function ManualItemRow({ } // ─── Section component ──────────────────────────────────────────────────────── -function makeSectionStyles(c: any) { +function makeSectionStyles(c: ThemeColors) { return StyleSheet.create({ wrapper: { marginBottom: 12, @@ -583,7 +583,7 @@ const modalStyles = StyleSheet.create({ }); // ─── Main Screen ────────────────────────────────────────────────────────────── -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, backgroundColor: c.bg }, header: { diff --git a/src/screens/NotepadScreen.tsx b/src/screens/NotepadScreen.tsx index 4a71441..f33ea00 100644 --- a/src/screens/NotepadScreen.tsx +++ b/src/screens/NotepadScreen.tsx @@ -5,11 +5,11 @@ import { } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; const STORAGE_KEY = 'aerostaff_notepad_v1'; -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, backgroundColor: c.bg }, toolbar: { @@ -27,7 +27,9 @@ function makeStyles(c: any) { backgroundColor: c.primary, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, }, - saveBtnDim: { backgroundColor: '#93C5FD' }, + // Dims the entire save button (background + icon + label) when content is + // already saved — intentional: the full-button fade signals an inactive state. + saveBtnDim: { opacity: 0.55 }, saveTxt: { color: '#fff', fontWeight: '600', fontSize: 13 }, statusBar: { flexDirection: 'row', alignItems: 'center', gap: 6, diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index 54ed239..cef8710 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -6,7 +6,7 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import * as SecureStore from 'expo-secure-store'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; const PASSWORDS_KEY = 'aerostaff_passwords_v1'; const PIN_KEY = 'aerostaff_pin_v1'; @@ -167,7 +167,7 @@ export default function PasswordScreen() { setPinEnabled(false); await AsyncStorage.setItem(PIN_ENABLED_KEY, 'false'); await deleteSecurePin(); - } catch (e) { console.error('[pin] disable error', e); } + } catch (e) { if (__DEV__) console.error('[pin] disable error', e); } }}, ]); } else { @@ -183,7 +183,7 @@ export default function PasswordScreen() { setPinMode(null); Alert.alert('PIN impostato', 'La schermata password è ora protetta.'); } catch (e) { - console.error('[pin] setup error', e); + if (__DEV__) console.error('[pin] setup error', e); Alert.alert('Errore', 'Impossibile impostare il PIN. Riprova.'); } }, []); @@ -197,7 +197,7 @@ export default function PasswordScreen() { Alert.alert('PIN errato', 'Riprova.'); } } catch (e) { - console.error('[pin] unlock error', e); + if (__DEV__) console.error('[pin] unlock error', e); Alert.alert('Errore', 'Impossibile verificare il PIN. Riprova.'); } }, []); @@ -349,7 +349,7 @@ export default function PasswordScreen() { } // ─── Styles ─────────────────────────────────────────────────────────────────── -function makePinStyles(c: any) { +function makePinStyles(c: ThemeColors) { return StyleSheet.create({ overlay: { flex: 1, backgroundColor: c.bg, justifyContent: 'center', alignItems: 'center' }, box: { alignItems: 'center', padding: 32, width: '100%', maxWidth: 320 }, @@ -364,7 +364,7 @@ function makePinStyles(c: any) { }); } -function makeRowStyles(c: any) { +function makeRowStyles(c: ThemeColors) { return StyleSheet.create({ card: { backgroundColor: c.card, borderRadius: 14, padding: 14, marginBottom: 10, flexDirection: 'row', alignItems: 'flex-start', borderWidth: 1, borderColor: c.border, shadowColor: '#000', shadowOpacity: c.isDark ? 0 : 0.05, shadowRadius: 6, elevation: c.isDark ? 0 : 2 }, cardLeft:{ flex: 1 }, @@ -380,7 +380,7 @@ function makeRowStyles(c: any) { }); } -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, backgroundColor: c.bg }, toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, diff --git a/src/screens/PhonebookScreen.tsx b/src/screens/PhonebookScreen.tsx index 5f2cf27..5770032 100644 --- a/src/screens/PhonebookScreen.tsx +++ b/src/screens/PhonebookScreen.tsx @@ -5,7 +5,7 @@ import { } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; -import { useAppTheme } from '../context/ThemeContext'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; const STORAGE_KEY = 'aerostaff_phonebook_v1'; @@ -40,7 +40,7 @@ interface EditModalProps { onClose: () => void; } -function makeModalStyles(c: any) { +function makeModalStyles(c: ThemeColors) { return StyleSheet.create({ overlay: { flex: 1, justifyContent: 'flex-end', @@ -205,7 +205,7 @@ function EditModal({ visible, contact, onSave, onClose }: EditModalProps) { // ─── Riga contatto ──────────────────────────────────────────────────────────── -function makeRowStyles(c: any) { +function makeRowStyles(c: ThemeColors) { return StyleSheet.create({ card: { flexDirection: 'row', alignItems: 'center', @@ -285,7 +285,7 @@ function ContactRow({ contact, onEdit, onDelete }: ContactRowProps) { // ─── Main Screen ────────────────────────────────────────────────────────────── -function makeStyles(c: any) { +function makeStyles(c: ThemeColors) { return StyleSheet.create({ root: { flex: 1, backgroundColor: c.bg }, header: { diff --git a/src/utils/autoNotifications.ts b/src/utils/autoNotifications.ts index 4c44518..3f0e7f2 100644 --- a/src/utils/autoNotifications.ts +++ b/src/utils/autoNotifications.ts @@ -93,7 +93,7 @@ export async function autoScheduleNotifications(): Promise { }); newIds.push(id); } catch (err) { - console.error('Failed to schedule arrival notification:', err); + if (__DEV__) console.error('Failed to schedule arrival notification:', err); } } @@ -146,7 +146,7 @@ export async function autoScheduleNotifications(): Promise { newIds.push(id); } } catch (err) { - console.error('Failed to schedule departure notification:', err); + if (__DEV__) console.error('Failed to schedule departure notification:', err); } } @@ -170,7 +170,7 @@ export async function autoScheduleNotifications(): Promise { await AsyncStorage.setItem(LAST_SCHEDULE_KEY, todayKey); return newIds.length; } catch (e) { - console.error('autoScheduleNotifications error:', e); + if (__DEV__) console.error('autoScheduleNotifications error:', e); return 0; } } diff --git a/src/utils/devLog.ts b/src/utils/devLog.ts new file mode 100644 index 0000000..8ae1dae --- /dev/null +++ b/src/utils/devLog.ts @@ -0,0 +1,20 @@ +/** + * Development-only logging helpers. + * In production builds Metro eliminates the dead `if (false)` branches, + * so these calls compile away entirely — no performance cost in prod. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const devWarn = (...args: any[]): void => { + if (__DEV__) console.warn(...args); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const devError = (...args: any[]): void => { + if (__DEV__) console.error(...args); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const devLog = (...args: any[]): void => { + if (__DEV__) console.log(...args); +};