diff --git a/App.tsx b/App.tsx index 4016578..3e8e843 100644 --- a/App.tsx +++ b/App.tsx @@ -44,6 +44,7 @@ import { useReducedMotionPreference, } from './src/utils/motion'; import { ONBOARDING_SETUP_STORAGE_KEY, shouldShowOnboarding } from './src/utils/appSetup'; +import { SPACING, RADIUS } from './src/theme/spacing'; installGlobalCrashHandler(); @@ -315,11 +316,11 @@ function AppInner() { /> )} {overlay ? ( - + ) : ( - setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection"> + setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yOpenMenu')}> )} @@ -461,13 +462,13 @@ const styles = StyleSheet.create({ appBar: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingBottom: 10, borderBottomWidth: 1, overflow: 'hidden', }, - iconBtn: { padding: 6, borderRadius: 8, marginRight: 6 }, - titleRow: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8 }, + iconBtn: { padding: 6, borderRadius: RADIUS.sm, marginRight: 6 }, + titleRow: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, appBarTitle: { fontSize: 18, fontWeight: '700', letterSpacing: 0.3 }, avatar: { width: 34, height: 34, borderRadius: 17, diff --git a/README.md b/README.md index 8bd0f0a..97f5423 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ npm run github:branches:audit APK files are published in [GitHub Releases](https://github.com/TargetMisser/AeroStaffPro/releases). -Latest stable release: **v2.7.28** +Latest stable release: **v2.7.29** To install the Android app: diff --git a/android/app/build.gradle b/android/app/build.gradle index 3608a19..1116e92 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -105,7 +105,7 @@ android { applicationId 'com.aerostaffpro.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 105 + versionCode 106 versionName "2.7.29" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" diff --git a/package-lock.json b/package-lock.json index 72190a7..6f57e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "dependencies": { "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.3", diff --git a/package.json b/package.json index 04b5628..bdd7ba1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aerostaff-pro", - "version": "2.7.28", + "version": "2.7.29", "main": "index.ts", "scripts": { "start": "expo start", diff --git a/scripts/test-flight-helpers.cjs b/scripts/test-flight-helpers.cjs index 2e4f417..212ca8d 100644 --- a/scripts/test-flight-helpers.cjs +++ b/scripts/test-flight-helpers.cjs @@ -301,6 +301,54 @@ assert(detectedAirlines.includes('transavia'), 'airport airline discovery should assert(!detectedAirlines.some(key => key.startsWith('compagnia')), 'airport airline discovery should drop generic company placeholders'); assert(!['xue', 'si', 'q1', 'ki', 'jt', 'sconosciuta'].some(key => detectedAirlines.includes(key)), 'airport airline discovery should drop raw unknown airline codes'); +const shortNamedAirlines = airportSettings.extractAirportAirlinesFromSchedule(['SAS', 'DHL', 'Scandinavian Airlines']); +assert(shortNamedAirlines.includes('sas'), 'canonical airlines with 3-letter names must survive the raw-code filter'); +assert(shortNamedAirlines.includes('dhl'), 'DHL must survive the raw-code filter'); +assert(shortNamedAirlines.filter(key => key === 'sas').length === 1, 'SAS and Scandinavian Airlines should collapse into one key'); + +// Regressione "voli che non gestisco": una compagnia appena rilevata nello +// schedule non deve mai finire selezionata da sola nel filtro. +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair', 'easyjet'], + previousSelectedAirlines: ['ryanair', 'easyjet'], + nextAirportAirlines: ['ryanair', 'easyjet', 'british airways'], + }) === null, + 'newly detected airlines must not be auto-selected', +); +assert( + JSON.stringify(airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair', 'easyjet'], + previousSelectedAirlines: ['ryanair', 'easyjet'], + nextAirportAirlines: ['ryanair', 'volotea'], + })) === JSON.stringify(['ryanair']), + 'airlines removed from the airport list must be pruned from the selection', +); +assert( + JSON.stringify(airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: [], + previousSelectedAirlines: ['ryanair'], + nextAirportAirlines: ['ryanair'], + })) === JSON.stringify([]), + 'a profile without saved airlines clears the selection', +); +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: [], + previousSelectedAirlines: [], + nextAirportAirlines: ['ryanair'], + }) === null, + 'an already-empty selection needs no reconciliation', +); +assert( + airportSettings.reconcileSelectedAirlines({ + savedProfileAirlines: ['ryanair'], + previousSelectedAirlines: ['ryanair'], + nextAirportAirlines: ['ryanair'], + }) === null, + 'an unchanged selection needs no reconciliation', +); + const merged = adapter.mergeFlightLists([scheduledOnly], [scheduledOnly, delayed], 'departure'); assert(merged.length === 2, 'merge should dedupe cached and fresh flights'); diff --git a/scripts/test-misc-utils.cjs b/scripts/test-misc-utils.cjs index 2312085..b9341c4 100644 --- a/scripts/test-misc-utils.cjs +++ b/scripts/test-misc-utils.cjs @@ -20,6 +20,7 @@ function loadTsModule(relativePath, mocks = {}) { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, + jsx: ts.JsxEmit.React, }, }).outputText; const module = { exports: {} }; @@ -654,8 +655,84 @@ async function testRuntimeDiagnostics() { } } + +async function testWidgetShiftSelfHeal() { + // Regressione: snapshot turni di ieri + turno di oggi presente nel + // calendario di sistema -> il widget deve auto-rigenerare lo snapshot + // invece di mostrare "nessun turno" finche' l'app non viene aperta. + const toIso = date => { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + }; + const today = new Date(); today.setHours(0, 0, 0, 0); + const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); + // Turno 00:00-23:59 così il test non dipende dall'ora in cui gira. + const shiftStart = new Date(today); shiftStart.setHours(0, 0, 0, 0); + const shiftEnd = new Date(today); shiftEnd.setHours(23, 59, 0, 0); + + const store = new Map(); + store.set('widget_shift_v1', JSON.stringify({ + date: toIso(yesterday), + shiftToday: null, + isRestDay: false, + nextShift: null, + })); + + const asyncStorageMock = { + getItem: async key => (store.has(key) ? store.get(key) : null), + setItem: async (key, value) => { store.set(key, value); }, + multiSet: async pairs => { for (const [k, v] of pairs) store.set(k, v); }, + }; + const calendarMock = { + EntityTypes: { EVENT: 'event' }, + getCalendarPermissionsAsync: async () => ({ status: 'granted' }), + getCalendarsAsync: async () => [{ id: 'cal1', allowsModifications: true, isPrimary: true }], + getEventsAsync: async () => [ + { id: 'e1', title: 'Lavoro', startDate: shiftStart.toISOString(), endDate: shiftEnd.toISOString() }, + ], + }; + + const handler = loadTsModule('src/widgets/widgetTaskHandler.tsx', { + '@react-native-async-storage/async-storage': asyncStorageMock, + 'expo-calendar': calendarMock, + 'react-native-android-widget': {}, + './ShiftWidget': { ShiftWidget: () => null }, + './widgetTheme': { getStoredWidgetThemeProps: async () => ({ themeMode: 'light', themeSnapshot: undefined }) }, + '../utils/liveArrivalEta': { applyLiveDepartureStatus: (deps) => deps, fetchAdsbAircraft: async () => [] }, + '../utils/flightProviders/staffMonitorProvider': { staffMonitorProvider: { supports: () => false, fetch: async () => ({ allDepartures: [] }) } }, + react: require('react'), + }); + + const data = await handler.getWidgetData(); + assert( + data.state === 'work' || data.state === 'work_empty', + `stale shift snapshot should self-heal from the calendar, got state=${data.state}`, + ); + + const rewritten = JSON.parse(store.get('widget_shift_v1')); + assert(rewritten.date === toIso(today), 'the shift snapshot should be rewritten with today\'s date'); + assert(rewritten.shiftToday && typeof rewritten.shiftToday.start === 'number', 'the rewritten snapshot should contain today\'s shift window'); + + // Permesso calendario negato -> nessun crash e fallback allo snapshot esistente. + const handlerDenied = loadTsModule('src/widgets/widgetTaskHandler.tsx', { + '@react-native-async-storage/async-storage': asyncStorageMock, + 'expo-calendar': { ...calendarMock, getCalendarPermissionsAsync: async () => ({ status: 'denied' }) }, + 'react-native-android-widget': {}, + './ShiftWidget': { ShiftWidget: () => null }, + './widgetTheme': { getStoredWidgetThemeProps: async () => ({ themeMode: 'light', themeSnapshot: undefined }) }, + '../utils/liveArrivalEta': { applyLiveDepartureStatus: (deps) => deps, fetchAdsbAircraft: async () => [] }, + '../utils/flightProviders/staffMonitorProvider': { staffMonitorProvider: { supports: () => false, fetch: async () => ({ allDepartures: [] }) } }, + react: require('react'), + }); + const denied = await handlerDenied.getWidgetData(); + assert(typeof denied.state === 'string', 'denied calendar permission should still return a widget state'); +} + async function main() { await testDateFormat(); + await testWidgetShiftSelfHeal(); await testThemeMode(); await testSecureWipe(); await testFlightProviderSettings(); diff --git a/src/components/AppTabBar.tsx b/src/components/AppTabBar.tsx index aefb3dd..a3a824e 100644 --- a/src/components/AppTabBar.tsx +++ b/src/components/AppTabBar.tsx @@ -3,6 +3,7 @@ import { Animated, StyleSheet, Text, View } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FrostedSurface from './FrostedSurface'; import TactilePressable from './motion/TactilePressable'; +import { SPACING, RADIUS } from '../theme/spacing'; import { motionDurations, motionEasing, @@ -470,7 +471,7 @@ const styles = StyleSheet.create({ bottom: 0, width: 24, left: 8, - borderRadius: 999, + borderRadius: RADIUS.pill, backgroundColor: 'rgba(255,255,255,0.24)', transform: [{ skewX: '-18deg' }], }, @@ -480,7 +481,7 @@ const styles = StyleSheet.create({ right: 8, bottom: 5, height: 3, - borderRadius: 999, + borderRadius: RADIUS.pill, opacity: 0.72, }, tabPressable: { @@ -505,19 +506,19 @@ const styles = StyleSheet.create({ bottom: 4, width: 18, height: 3, - borderRadius: 999, + borderRadius: RADIUS.pill, }, opsDeck: { flex: 1, paddingHorizontal: 10, - paddingVertical: 8, + paddingVertical: SPACING.sm, gap: 7, }, opsHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 4, + paddingHorizontal: SPACING.xs, }, opsKicker: { color: 'rgba(204,251,241,0.58)', @@ -584,7 +585,7 @@ const styles = StyleSheet.create({ alignSelf: 'flex-start', borderWidth: 1, borderColor: 'rgba(204,251,241,0.14)', - borderRadius: 999, + borderRadius: RADIUS.pill, paddingHorizontal: 5, paddingVertical: 1, }, diff --git a/src/components/DrawerMenuPanel.tsx b/src/components/DrawerMenuPanel.tsx index ba29dc8..9ad1f50 100644 --- a/src/components/DrawerMenuPanel.tsx +++ b/src/components/DrawerMenuPanel.tsx @@ -3,10 +3,12 @@ import { StyleSheet, Text, View } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { type ThemeColors } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; import AeroStaffLogo from './AeroStaffLogo'; import FrostedSurface from './FrostedSurface'; import BoardReveal from './motion/BoardReveal'; import TactilePressable from './motion/TactilePressable'; +import { SPACING, RADIUS } from '../theme/spacing'; export type DrawerItem = { id: string; @@ -113,6 +115,7 @@ export default function DrawerMenuPanel({ onSelect, surfaceVariant = 'app', }: DrawerMenuPanelProps) { + const { t } = useLanguage(); const surface = getDrawerSurface(colors, surfaceVariant); const styles = useMemo(() => makeStyles(colors, surface), [colors, surface]); @@ -136,7 +139,7 @@ export default function DrawerMenuPanel({ Operations - + @@ -148,7 +151,7 @@ export default function DrawerMenuPanel({ style={styles.headerGradient} > - + @@ -225,7 +228,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { opsBrandRow: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: SPACING.md, flex: 1, }, opsLogoBox: { @@ -255,7 +258,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { opsClose: { width: 34, height: 34, - borderRadius: 12, + borderRadius: RADIUS.md, borderWidth: 1, borderColor: 'rgba(45,212,191,0.24)', backgroundColor: 'rgba(2,8,12,0.36)', @@ -266,9 +269,9 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 20, + paddingHorizontal: SPACING.xl, paddingTop: surface.isOperations ? 16 : 20, - paddingBottom: 8, + paddingBottom: SPACING.sm, }, sectionLabel: { fontSize: 10, @@ -313,7 +316,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) { itemCopy: { flex: 1 }, itemLabel: { fontSize: 14, fontWeight: '600', color: c.text }, itemSub: { fontSize: 11, color: c.isDark ? 'rgba(229,233,240,0.70)' : c.textMuted, marginTop: 1 }, - divider: { height: 1, backgroundColor: c.border, marginHorizontal: 18, marginTop: 16 }, + divider: { height: 1, backgroundColor: c.border, marginHorizontal: 18, marginTop: SPACING.lg }, version: { fontSize: 11, color: c.isDark ? 'rgba(229,233,240,0.66)' : c.textMuted, diff --git a/src/components/GlassCard.tsx b/src/components/GlassCard.tsx index 295c2cf..e06bd7f 100644 --- a/src/components/GlassCard.tsx +++ b/src/components/GlassCard.tsx @@ -3,6 +3,7 @@ import { View, StyleSheet, Platform, ViewStyle } from 'react-native'; import { BlurView } from 'expo-blur'; import { LinearGradient } from 'expo-linear-gradient'; import { useAppTheme } from '../context/ThemeContext'; +import { SPACING } from '../theme/spacing'; type Variant = 'default' | 'strong' | 'subtle'; @@ -155,6 +156,6 @@ const styles = StyleSheet.create({ borderWidth: 0.75, }, content: { - padding: 16, + padding: SPACING.lg, }, }); diff --git a/src/components/ProfileSwitcherModal.tsx b/src/components/ProfileSwitcherModal.tsx index 1c61cab..a86c1dc 100644 --- a/src/components/ProfileSwitcherModal.tsx +++ b/src/components/ProfileSwitcherModal.tsx @@ -13,6 +13,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useAirport, type AirportProfile } from '../context/AirportContext'; import { useLanguage } from '../context/LanguageContext'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { AIRPORT_PRESETS, formatAirportSettingLabel, @@ -21,6 +22,7 @@ import { normalizeAirportCode, } from '../utils/airportSettings'; import { AIRLINE_COLORS, AIRLINE_DISPLAY_NAMES } from '../utils/airlineOps'; +import { SPACING, RADIUS } from '../theme/spacing'; type Props = { visible: boolean; @@ -183,7 +185,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { {t('profileTitle')} {t('profileSubtitle')} - + @@ -225,6 +227,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { style={styles.profileAction} onPress={() => openEditor(profile)} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('profileEdit')} > @@ -274,8 +277,8 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { onPress={() => setDraftAirportCode(airport.code)} activeOpacity={0.8} > - {airport.code} - {airport.city} + {airport.code} + {airport.city} ); })} @@ -307,7 +310,7 @@ export default function ProfileSwitcherModal({ visible, onClose }: Props) { activeOpacity={0.85} > - {label} + {label} Voli nel Turno {fmtTime(startSec)} – {fmtTime(endSec)} - + @@ -160,7 +162,7 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, )} {/* Legenda */} - + Check-in @@ -178,14 +180,14 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, ) : error ? ( - Errore nel caricamento + Errore nel caricamento Riprova ) : flights.length === 0 ? ( - + Nessuna partenza nel turno ) : ( @@ -320,20 +322,20 @@ function makeStyles(c: ThemeColors) { }, handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 6 }, handle: { width: 36, height: 4, borderRadius: 2 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 10 }, - title: { fontSize: 18, fontWeight: '800' }, + header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING.xl, paddingBottom: 10 }, + title: { ...TYPE.headline }, subtitle: { fontSize: 12, marginTop: 2 }, - closeBtn: { padding: 8, borderRadius: 20 }, - legend: { flexDirection: 'row', gap: 16, paddingHorizontal: 20, paddingBottom: 12 }, + closeBtn: { padding: SPACING.sm, borderRadius: RADIUS.xl }, + legend: { flexDirection: 'row', gap: SPACING.lg, paddingHorizontal: SPACING.xl, paddingBottom: SPACING.md }, legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6 }, legendDot: { width: 10, height: 10, borderRadius: 5 }, legendText: { fontSize: 11, fontWeight: '600' }, center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - retryBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, + retryBtn: { paddingHorizontal: SPACING.xl, paddingVertical: 10, borderRadius: 10 }, scrollArea: { flex: 1 }, // Righello orizzontale in alto - rulerWrap: { flexDirection: 'row', paddingHorizontal: 12, marginBottom: 4, height: 32 }, + rulerWrap: { flexDirection: 'row', paddingHorizontal: SPACING.md, marginBottom: SPACING.xs, height: 32 }, rulerLabelSpace: { width: 80 }, ruler: { flex: 1, position: 'relative' }, rulerTick: { position: 'absolute', top: 0, alignItems: 'center', transform: [{ translateX: -1 }] }, @@ -344,8 +346,8 @@ function makeStyles(c: ThemeColors) { nowTick: { width: 2, height: 10, backgroundColor: '#EF4444', borderRadius: 1 }, // Righe voli - flightRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: 1 }, - flightLabelWrap: { width: 80, flexDirection: 'row', alignItems: 'center', gap: 4 }, + flightRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, borderBottomWidth: 1 }, + flightLabelWrap: { width: 80, flexDirection: 'row', alignItems: 'center', gap: SPACING.xs }, airlineDot: { width: 6, height: 6, borderRadius: 3, flexShrink: 0 }, flightLabel: { fontSize: 11, fontWeight: '700', flexShrink: 1 }, flightDest: { fontSize: 10, fontWeight: '600' }, @@ -354,16 +356,16 @@ function makeStyles(c: ThemeColors) { ganttArea: { flex: 1, height: 36, position: 'relative', justifyContent: 'center' }, ganttGridLine: { position: 'absolute', top: 0, bottom: 0, width: 1, opacity: 0.25 }, ganttNowLine: { position: 'absolute', top: 0, bottom: 0, width: 2, backgroundColor: '#EF4444', opacity: 0.5, zIndex: 5 }, - ganttBar: { position: 'absolute', height: 14, borderRadius: 3, justifyContent: 'center', paddingHorizontal: 4 }, + ganttBar: { position: 'absolute', height: 14, borderRadius: 3, justifyContent: 'center', paddingHorizontal: SPACING.xs }, ganttBarCI: { backgroundColor: CI_COLOR, top: 2 }, ganttBarGate: { backgroundColor: GATE_COLOR, bottom: 2 }, ganttBarText: { fontSize: 8, fontWeight: '800', color: '#fff' }, depMarker: { position: 'absolute', top: 0, bottom: 0, borderLeftWidth: 2, borderStyle: 'dashed' }, // Card espansa - expandedCard: { borderRadius: 10, padding: 12, marginHorizontal: 12, marginBottom: 4, borderWidth: 1 }, - expandedTitle: { fontSize: 14, fontWeight: '700', marginBottom: 8 }, - expandedRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }, + expandedCard: { borderRadius: 10, padding: SPACING.md, marginHorizontal: SPACING.md, marginBottom: SPACING.xs, borderWidth: 1 }, + expandedTitle: { fontSize: 14, fontWeight: '700', marginBottom: SPACING.sm }, + expandedRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: SPACING.xs }, expandedLabel: { fontSize: 11, fontWeight: '600' }, expandedValue: { fontSize: 11, fontWeight: '700' }, }); diff --git a/src/components/TimeCarouselPicker.tsx b/src/components/TimeCarouselPicker.tsx index 6b41196..d6dbb9e 100644 --- a/src/components/TimeCarouselPicker.tsx +++ b/src/components/TimeCarouselPicker.tsx @@ -9,6 +9,7 @@ import { TouchableOpacity, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; +import { SPACING, RADIUS } from '../theme/spacing'; const ITEM_H = 42; const VISIBLE = 5; @@ -190,10 +191,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - borderRadius: 12, + borderRadius: RADIUS.md, borderWidth: 1, overflow: 'hidden', - paddingHorizontal: 8, + paddingHorizontal: SPACING.sm, marginVertical: 2, }, selectionRect: { @@ -201,7 +202,7 @@ const styles = StyleSheet.create({ left: 3, right: 3, height: ITEM_H, - borderRadius: 8, + borderRadius: RADIUS.sm, borderWidth: 1.5, zIndex: 1, }, @@ -218,7 +219,7 @@ const styles = StyleSheet.create({ colon: { fontSize: 24, fontWeight: '700', - marginHorizontal: 4, + marginHorizontal: SPACING.xs, marginBottom: 1, }, fade: { @@ -233,4 +234,4 @@ const styles = StyleSheet.create({ fadeBottom: { bottom: 0 }, }); -export default TimeCarouselPicker; +export default TimeCarouselPicker; diff --git a/src/components/UpdateModal.tsx b/src/components/UpdateModal.tsx index 64abb97..57b7cd5 100644 --- a/src/components/UpdateModal.tsx +++ b/src/components/UpdateModal.tsx @@ -4,7 +4,9 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { type UpdateInfo, APP_VERSION } from '../utils/updateChecker'; +import { SPACING, RADIUS } from '../theme/spacing'; import { downloadUpdatePackage, getDownloadedUpdateUri, @@ -151,7 +153,7 @@ export default function UpdateModal({ info, onDismiss }: Props) { {/* Header */} - + Aggiornamento disponibile v{APP_VERSION} → v{latestVersionLabel} @@ -210,7 +212,7 @@ export default function UpdateModal({ info, onDismiss }: Props) { activeOpacity={0.8} > - GitHub + GitHub w[0]).join('').toUpperCase(); if (iataCode && !err) { return ( - + setErr(true)} /> ); } return ( - + {initials} ); diff --git a/src/components/flights/FlightFilterModal.tsx b/src/components/flights/FlightFilterModal.tsx index 5eae35b..d923395 100644 --- a/src/components/flights/FlightFilterModal.tsx +++ b/src/components/flights/FlightFilterModal.tsx @@ -57,7 +57,7 @@ export default function FlightFilterModal({ onApplySelectedAirlines(next); }} > - + {allSelected ? t('flightFilterDeselAll') : t('flightFilterSelAll')} diff --git a/src/components/flights/FlightNotificationSettingsModal.tsx b/src/components/flights/FlightNotificationSettingsModal.tsx index e99e130..e74fadb 100644 --- a/src/components/flights/FlightNotificationSettingsModal.tsx +++ b/src/components/flights/FlightNotificationSettingsModal.tsx @@ -143,6 +143,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ arrivalLeadMinutes: clamp(notifSettings.arrivalLeadMinutes - 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yDecrease')} > @@ -152,6 +153,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ arrivalLeadMinutes: clamp(notifSettings.arrivalLeadMinutes + 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yIncrease')} > @@ -166,6 +168,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ departureLeadMinutes: clamp(notifSettings.departureLeadMinutes - 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yDecrease')} > @@ -175,6 +178,7 @@ export default function FlightNotificationSettingsModal({ onPress={() => onUpdateNotificationSettings({ departureLeadMinutes: clamp(notifSettings.departureLeadMinutes + 1, MIN_NOTIF_MINUTES, MAX_NOTIF_MINUTES), }).catch(() => {})} + accessibilityRole="button" accessibilityLabel={t('a11yIncrease')} > diff --git a/src/components/flights/FlightSourceDebugModal.tsx b/src/components/flights/FlightSourceDebugModal.tsx index d3cca80..e426b00 100644 --- a/src/components/flights/FlightSourceDebugModal.tsx +++ b/src/components/flights/FlightSourceDebugModal.tsx @@ -5,6 +5,7 @@ import type { ThemeColors } from '../../context/ThemeContext'; import type { TranslationKey } from '../../i18n/translations'; import type { FlightScheduleProviderStatus } from '../../utils/fr24api'; import { formatFlightSourceLabel } from '../../utils/flightSourceLabel'; +import { SPACING, RADIUS } from '../../theme/spacing'; import { formatFlightCacheAge, formatProviderDiagnostic, @@ -75,7 +76,7 @@ export default function FlightSourceDebugModal({ {t('flightSourceDebugTitle')} {t('flightSourceDebugSub')} - + @@ -172,13 +173,13 @@ const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', - gap: 12, - padding: 16, + gap: SPACING.md, + padding: SPACING.lg, }, headerIcon: { width: 44, height: 44, - borderRadius: 16, + borderRadius: RADIUS.lg, alignItems: 'center', justifyContent: 'center', }, @@ -191,17 +192,17 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - content: { paddingHorizontal: 16, paddingBottom: 18, gap: 12 }, + content: { paddingHorizontal: SPACING.lg, paddingBottom: 18, gap: SPACING.md }, sourcePill: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, alignSelf: 'flex-start', maxWidth: '100%', - borderRadius: 999, + borderRadius: RADIUS.pill, borderWidth: 1, - paddingHorizontal: 12, - paddingVertical: 8, + paddingHorizontal: SPACING.md, + paddingVertical: SPACING.sm, }, sourceText: { flexShrink: 1, fontSize: 12, fontWeight: '900' }, refreshRow: { @@ -210,20 +211,20 @@ const styles = StyleSheet.create({ gap: 10, borderRadius: 14, borderWidth: 1, - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, }, - metaGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - metaBox: { minWidth: '47%', flexGrow: 1, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 10 }, + metaGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + metaBox: { minWidth: '47%', flexGrow: 1, borderRadius: 14, paddingHorizontal: SPACING.md, paddingVertical: 10 }, metaLabel: { fontSize: 10, fontWeight: '900', letterSpacing: 0.7, textTransform: 'uppercase' }, metaValue: { fontSize: 14, fontWeight: '900', marginTop: 3 }, - filterBox: { borderWidth: 1, borderRadius: 16, padding: 12 }, - filterText: { fontSize: 14, fontWeight: '800', marginTop: 4 }, + filterBox: { borderWidth: 1, borderRadius: RADIUS.lg, padding: SPACING.md }, + filterText: { fontSize: 14, fontWeight: '800', marginTop: SPACING.xs }, reasonText: { fontSize: 12, lineHeight: 17, marginTop: 6 }, - sectionTitle: { fontSize: 11, fontWeight: '900', letterSpacing: 1.1, marginTop: 4 }, - providerList: { gap: 8 }, - providerRow: { flexDirection: 'row', gap: 10, borderWidth: 1, borderRadius: 14, padding: 12 }, - statusDot: { width: 10, height: 10, borderRadius: 5, marginTop: 4 }, + sectionTitle: { fontSize: 11, fontWeight: '900', letterSpacing: 1.1, marginTop: SPACING.xs }, + providerList: { gap: SPACING.sm }, + providerRow: { flexDirection: 'row', gap: 10, borderWidth: 1, borderRadius: 14, padding: SPACING.md }, + statusDot: { width: 10, height: 10, borderRadius: 5, marginTop: SPACING.xs }, providerName: { fontSize: 13, fontWeight: '900' }, providerSub: { fontSize: 11, lineHeight: 16, marginTop: 2 }, }); diff --git a/src/components/flights/FlightStates.tsx b/src/components/flights/FlightStates.tsx index 521fc57..9c89cb9 100644 --- a/src/components/flights/FlightStates.tsx +++ b/src/components/flights/FlightStates.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Text, View } from 'react-native'; import type { ThemeColors } from '../../context/ThemeContext'; import type { TranslationKey } from '../../i18n/translations'; import type { FlightScheduleProviderStatus } from '../../utils/fr24api'; +import { SPACING, RADIUS } from '../../theme/spacing'; import { formatProviderDiagnostic, getTomorrowEmptyReason, @@ -50,8 +51,8 @@ export function EmptyFlightState({ return ( {title} {body ? ( - {body} + {body} ) : null} {activeDay === 'tomorrow' ? ( @@ -70,7 +71,7 @@ export function EmptyFlightState({ ) : null} {providerLines.length > 0 ? ( - + {t('flightProviderDebugTitle')} @@ -93,13 +94,13 @@ export function FlightLoadingState({ t: (key: TranslationKey) => string; }) { return ( - + =4.5:1). Per icone e superfici + continuare a usare primary. */ + primaryText: string; // Glass tokens glass: string; glassBorder: string; @@ -58,10 +62,11 @@ const LIGHT: ThemeColors = { cardSecondary: '#F2F2F7', text: '#1C1C1E', textSub: '#48484A', - textMuted: 'rgba(60,60,67,0.45)', + textMuted: 'rgba(60,60,67,0.68)', // alpha 0.45 → 0.68: a 2.39:1 non era leggibile; ora ~4.3:1, come nel tema scuro primary: '#F47B16', primaryDark: '#C2520A', primaryLight: '#FFEDD5', + primaryText: '#C2520A', glass: '#FFFFFF', glassBorder: 'transparent', glassStrong: '#FFFFFF', @@ -69,16 +74,18 @@ const LIGHT: ThemeColors = { appBar: 'rgba(242,242,247,0.85)', tabBar: 'rgba(255,255,255,0.90)', tabIconActive: '#F47B16', - tabIconInactive:'rgba(60,60,67,0.38)', + tabIconInactive:'rgba(60,60,67,0.60)', tabLabelActive: '#F47B16', pillActive: 'rgba(244,123,22,0.14)', - success: '#10B981', + // Fill semantici scuriti per reggere WCAG AA (≥4.5:1) usati come testo su + // card/bg. Gli sfondi chip *Soft restano sulle tinte brillanti originali. + success: '#047857', successSoft: 'rgba(16,185,129,0.13)', - warning: '#F59E0B', + warning: '#B45309', warningSoft: 'rgba(245,158,11,0.13)', - danger: '#EF4444', + danger: '#DC2626', dangerSoft: 'rgba(239,68,68,0.13)', - info: '#3B82F6', + info: '#2563EB', infoSoft: 'rgba(59,130,246,0.13)', neutral: '#64748B', statusBar: 'dark-content', @@ -96,6 +103,7 @@ const DARK: ThemeColors = { primary: '#2DD4BF', primaryDark: '#99F6E4', primaryLight: 'rgba(45,212,191,0.18)', + primaryText: '#2DD4BF', glass: '#111A1F', glassBorder: 'rgba(45,212,191,0.24)', glassStrong: '#19262D', diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index e3750ea..6238088 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -7,6 +7,13 @@ const it = { overlayNotepad: 'Blocco Note', overlayPhonebook: 'Rubrica', overlayPasswords: 'Password', overlayManuals: 'Manuali DCS', overlayArionInbox: 'Arion Inbox', overlaySettings: 'Impostazioni', // Common + // Accessibilità (label per screen reader) + a11yClose: 'Chiudi', a11yBack: 'Indietro', a11yForward: 'Avanti', + a11yRefresh: 'Aggiorna', a11yOpenInBrowser: 'Apri nel browser', + a11yClearSearch: 'Cancella ricerca', a11yEdit: 'Modifica', + a11yShowPassword: 'Mostra password', a11yHidePassword: 'Nascondi password', + a11yBackspace: 'Cancella ultima cifra', a11yCall: 'Chiama', + a11yOpenMenu: 'Apri menu', a11yDecrease: 'Diminuisci', a11yIncrease: 'Aumenta', cancel: 'Annulla', save: 'Salva', delete: 'Elimina', remove: 'Rimuovi', error: 'Errore', confirm: 'Conferma', ok: 'OK', add: 'Aggiungi', yes: 'Sì', no: 'No', profileTitle: 'Profili aeroporto', @@ -137,24 +144,10 @@ const it = { traveldocSub: 'Verifica documenti di viaggio', traveldocLoading: 'Caricamento TravelDoc…', traveldocSlowConn: 'Caricamento lento. Verifica la connessione internet.', - // ShiftScreen - shiftTitle: 'Gestione Turni', - shiftSub: 'Scansiona i turni dal tabellone e sincronizzali nel calendario.', - shiftSyncTitle: 'Sincronizzazione Calendario', - shiftSyncDesc: 'Seleziona gli screenshot del tuo tabellone orari...', - shiftScanBtn: 'Scansiona Screenshot Turni', - shiftExtracting: 'Estrazione del testo in corso...', - shiftExtractedTitle: 'Testo Estratto:', - shiftSyncBtn: 'Sincronizza nel Calendario!', - shiftErrOcrTitle: 'Errore OCR', shiftErrOcrMsg: "Impossibile elaborare l'immagine.", - shiftPermTitle: 'Permesso negato', - shiftPermMsg: "Devi autorizzare l'accesso al calendario del telefono.", - shiftNoCalendar: 'Nessun calendario scrivibile trovato sul dispositivo.', - shiftSyncOkTitle: 'Turni Sincronizzati!', - shiftNoShifts: 'Nessun orario trovato', shiftCalErrTitle: 'Errore Calendario', // Calendar calTitle: 'Gestione Turni', calEditBtn: 'Modifica Turni', calModeCalendar: 'Calendario', calModeWeek: 'Settimana', calModeMonthHours: 'Ore mese', + calPrevWeek: 'Settimana precedente', calNextWeek: 'Settimana successiva', calWeekShiftsCount: 'Turni lavoro: {count}', calMonthTotalHours: 'Totale ore del mese', calMonthShiftsCount: 'Turni lavoro: {count}', calToday: 'Oggi', calWeatherLocal: 'Meteo locale', calShiftWork: 'Turno Lavoro', @@ -308,6 +301,13 @@ const en: typeof it = { overlayNotepad: 'Notepad', overlayPhonebook: 'Phonebook', overlayPasswords: 'Password', overlayManuals: 'DCS Manuals', overlayArionInbox: 'Arion Inbox', overlaySettings: 'Settings', // Common + // Accessibility (screen reader labels) + a11yClose: 'Close', a11yBack: 'Back', a11yForward: 'Forward', + a11yRefresh: 'Refresh', a11yOpenInBrowser: 'Open in browser', + a11yClearSearch: 'Clear search', a11yEdit: 'Edit', + a11yShowPassword: 'Show password', a11yHidePassword: 'Hide password', + a11yBackspace: 'Delete last digit', a11yCall: 'Call', + a11yOpenMenu: 'Open menu', a11yDecrease: 'Decrease', a11yIncrease: 'Increase', cancel: 'Cancel', save: 'Save', delete: 'Delete', remove: 'Remove', error: 'Error', confirm: 'Confirm', ok: 'OK', add: 'Add', yes: 'Yes', no: 'No', profileTitle: 'Airport profiles', @@ -438,24 +438,10 @@ const en: typeof it = { traveldocSub: 'Travel document check', traveldocLoading: 'Loading TravelDoc…', traveldocSlowConn: 'Slow loading. Check your internet connection.', - // ShiftScreen - shiftTitle: 'Shift Manager', - shiftSub: 'Scan shifts from the schedule board and sync them to the calendar.', - shiftSyncTitle: 'Calendar Sync', - shiftSyncDesc: 'Select screenshots of your schedule board...', - shiftScanBtn: 'Scan Shift Screenshots', - shiftExtracting: 'Extracting text...', - shiftExtractedTitle: 'Extracted Text:', - shiftSyncBtn: 'Sync to Calendar!', - shiftErrOcrTitle: 'OCR Error', shiftErrOcrMsg: 'Could not process the image.', - shiftPermTitle: 'Permission denied', - shiftPermMsg: 'You need to grant access to the phone calendar.', - shiftNoCalendar: 'No writable calendar found on the device.', - shiftSyncOkTitle: 'Shifts Synced!', - shiftNoShifts: 'No schedules found', shiftCalErrTitle: 'Calendar Error', // Calendar calTitle: 'Shift Manager', calEditBtn: 'Edit Shifts', calModeCalendar: 'Calendar', calModeWeek: 'Week', calModeMonthHours: 'Month hours', + calPrevWeek: 'Previous week', calNextWeek: 'Next week', calWeekShiftsCount: 'Work shifts: {count}', calMonthTotalHours: 'Total month hours', calMonthShiftsCount: 'Work shifts: {count}', calToday: 'Today', calWeatherLocal: 'Local weather', calShiftWork: 'Work Shift', diff --git a/src/screens/ArionInboxScreen.tsx b/src/screens/ArionInboxScreen.tsx index 4673aae..e6e619a 100644 --- a/src/screens/ArionInboxScreen.tsx +++ b/src/screens/ArionInboxScreen.tsx @@ -3,6 +3,8 @@ import { ActivityIndicator, Linking, StyleSheet, Text, TouchableOpacity, View } import { MaterialIcons } from '@expo/vector-icons'; import { WebView, type WebViewNavigation } from 'react-native-webview'; import { useAppTheme } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; +import { SPACING, RADIUS } from '../theme/spacing'; const ARION_INBOX_URL = 'https://prd-arion-ap.firebaseapp.com/messages/inbox'; @@ -13,6 +15,7 @@ type WebLoadError = { export default function ArionInboxScreen() { const { colors, mode } = useAppTheme(); + const { t } = useLanguage(); const webViewRef = useRef(null); const [progress, setProgress] = useState(0); const [canGoBack, setCanGoBack] = useState(false); @@ -47,6 +50,7 @@ export default function ArionInboxScreen() { disabled={!canGoBack} onPress={() => webViewRef.current?.goBack()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yBack')} > @@ -55,6 +59,7 @@ export default function ArionInboxScreen() { disabled={!canGoForward} onPress={() => webViewRef.current?.goForward()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yForward')} > @@ -62,6 +67,7 @@ export default function ArionInboxScreen() { style={[styles.navButton, { backgroundColor: colors.cardSecondary }]} onPress={() => webViewRef.current?.reload()} activeOpacity={0.8} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -78,6 +84,7 @@ export default function ArionInboxScreen() { style={[styles.externalButton, { borderColor: colors.border, backgroundColor: colors.cardSecondary }]} onPress={openExternal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yOpenInBrowser')} > @@ -109,7 +116,7 @@ export default function ArionInboxScreen() { Riprova - Browser + Browser @@ -167,7 +174,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', gap: 10, - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, borderBottomWidth: 1, }, @@ -175,7 +182,7 @@ const styles = StyleSheet.create({ navButton: { width: 34, height: 34, - borderRadius: 12, + borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center', }, @@ -185,7 +192,7 @@ const styles = StyleSheet.create({ minWidth: 0, flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, }, secureDot: { width: 7, height: 7, borderRadius: 99 }, urlText: { flex: 1, fontSize: 12, fontWeight: '800' }, @@ -212,7 +219,7 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - padding: 24, + padding: SPACING.xxl, }, errorIcon: { width: 64, @@ -220,14 +227,14 @@ const styles = StyleSheet.create({ borderRadius: 22, alignItems: 'center', justifyContent: 'center', - marginBottom: 16, + marginBottom: SPACING.lg, }, errorTitle: { fontSize: 21, fontWeight: '900', textAlign: 'center' }, - errorCopy: { fontSize: 14, lineHeight: 20, textAlign: 'center', marginTop: 8 }, - errorMeta: { fontSize: 11, lineHeight: 16, textAlign: 'center', marginTop: 12 }, + errorCopy: { fontSize: 14, lineHeight: 20, textAlign: 'center', marginTop: SPACING.sm }, + errorMeta: { fontSize: 11, lineHeight: 16, textAlign: 'center', marginTop: SPACING.md }, errorActions: { flexDirection: 'row', gap: 10, marginTop: 18 }, - primaryBtn: { borderRadius: 14, paddingHorizontal: 18, paddingVertical: 12 }, + primaryBtn: { borderRadius: 14, paddingHorizontal: 18, paddingVertical: SPACING.md }, primaryBtnText: { color: '#FFFFFF', fontSize: 14, fontWeight: '900' }, - secondaryBtn: { borderWidth: 1, borderRadius: 14, paddingHorizontal: 18, paddingVertical: 12 }, + secondaryBtn: { borderWidth: 1, borderRadius: 14, paddingHorizontal: 18, paddingVertical: SPACING.md }, secondaryBtnText: { fontSize: 14, fontWeight: '900' }, }); diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 36134d7..6551138 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -32,7 +32,8 @@ import { type ParsedSchedule, type ParsedEmployee, } from '../utils/pdfShiftParser'; import { useLanguage } from '../context/LanguageContext'; -import { TYPE } from '../theme/typography'; +import { TYPE, WEIGHT } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const STORAGE_KEY = '@shift_import_name'; @@ -648,6 +649,18 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole } }; + // Sposta la vista settimana di ±1 settimana; aggiornare visibleMonth quando + // si cambia mese fa ricaricare gli eventi del nuovo intervallo (mese ±7gg). + const goToWeek = (delta: number) => { + const next = fromIsoDate(selectedDay); + next.setDate(next.getDate() + delta * 7); + const iso = toLocalIso(next); + setSelectedDay(iso); + if (!isSameMonth(visibleMonth, iso)) { + setVisibleMonth(new Date(next.getFullYear(), next.getMonth(), 1)); + } + }; + const handleMonthChange = (day: DateData) => { const nextMonth = new Date(day.year, day.month - 1, 1); setVisibleMonth(nextMonth); @@ -874,7 +887,6 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {t('calModeWeek')} - {weekRangeLabel} {weekHoursSummary.totalHours.toFixed(1)} h @@ -884,6 +896,28 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole + + goToWeek(-1)} + activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={t('calPrevWeek')} + > + + + {weekRangeLabel} + goToWeek(1)} + activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={t('calNextWeek')} + > + + + + {selectedWeekDays.map(day => { const selected = day.iso === selectedDay; const dayName = weekDaysShort[day.date.getDay()]; @@ -949,7 +983,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole setEditMenuOpen(false)} /> - Modifica Turni + Modifica Turni { setEditMenuOpen(false); startImport(); }}> @@ -977,9 +1011,9 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {/* Header fisso */} - + {t('calAddShiftTitle')} - setManualModalOpen(false)}> + setManualModalOpen(false)} accessibilityRole="button" accessibilityLabel={t('a11yClose')}> @@ -989,7 +1023,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole keyboardShouldPersistTaps="handled" nestedScrollEnabled showsVerticalScrollIndicator={false} - contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 40 }} + contentContainerStyle={{ paddingHorizontal: SPACING.xxl, paddingBottom: 40 }} > {/* Data */} {t('calDataLabel')} @@ -998,13 +1032,13 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole value={manualDate.split('-').reverse().join('/')} editable={false} /> - + Seleziona un giorno dal calendario per cambiare la data {/* Tipo */} {t('calTypeLabel')} - + {(['Lavoro', 'Riposo'] as const).map(shiftType => ( - {t('calEndTime')} + {t('calEndTime')} )} - + {t('calSaveShift')} @@ -1088,7 +1122,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {t('calImportTitle')} {importStep !== 'saving' && ( - { setImportModalVisible(false); setImportStep('idle'); setImportFileCount(0); }}> + { setImportModalVisible(false); setImportStep('idle'); setImportFileCount(0); }} accessibilityRole="button" accessibilityLabel={t('a11yClose')}> )} @@ -1140,7 +1174,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole {fmtDate(shift.date)} {shift.type === 'work' ? ( - + {shift.start} - {shift.end} @@ -1152,7 +1186,7 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole ))} - + setImportStep('pickName')} @@ -1193,18 +1227,18 @@ export default function CalendarScreen({ isFocused = true }: { isFocused?: boole function makeStyles(c: ThemeColors) { return StyleSheet.create({ - pageHeader: { backgroundColor: c.card, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border }, + pageHeader: { backgroundColor: c.card, paddingHorizontal: SPACING.lg, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: c.border }, pageTitle: { ...TYPE.title, color: c.primaryDark }, pageSub: { fontSize: 11, color: c.textSub, letterSpacing: 1.5, marginTop: 3 }, - importBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10 }, + importBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: SPACING.sm, borderRadius: 10 }, importBtnText: { color: '#fff', fontSize: 14, fontWeight: '600' }, viewModeRow: { flexDirection: 'row', - gap: 8, - marginHorizontal: 16, + gap: SPACING.sm, + marginHorizontal: SPACING.lg, marginTop: 14, backgroundColor: c.card, - borderRadius: 16, + borderRadius: RADIUS.lg, padding: 6, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, @@ -1215,17 +1249,17 @@ function makeStyles(c: ThemeColors) { alignItems: 'center', justifyContent: 'center', gap: 6, - borderRadius: 12, + borderRadius: RADIUS.md, paddingVertical: 10, }, viewModeText: { fontSize: 13, fontWeight: '800' }, calendarCard: { backgroundColor: c.card, - borderRadius: 20, - marginHorizontal: 16, - marginTop: 16, - paddingHorizontal: 12, - paddingTop: 8, + borderRadius: RADIUS.xl, + marginHorizontal: SPACING.lg, + marginTop: SPACING.lg, + paddingHorizontal: SPACING.md, + paddingTop: SPACING.sm, paddingBottom: 14, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, @@ -1234,8 +1268,8 @@ function makeStyles(c: ThemeColors) { borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, }, - monthCalendar: { borderRadius: 16 }, - monthCalendarHeader: { paddingBottom: 8, marginBottom: 6 }, + monthCalendar: { borderRadius: RADIUS.lg }, + monthCalendarHeader: { paddingBottom: SPACING.sm, marginBottom: 6 }, dayCellWrap: { alignItems: 'center', justifyContent: 'center', paddingVertical: 2 }, dayCellInner: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5, borderColor: 'transparent' }, dayCellInnerSelected: { backgroundColor: c.primary }, @@ -1247,22 +1281,22 @@ function makeStyles(c: ThemeColors) { dayCellTextSelected: { color: '#fff' }, dayDotsRow: { minHeight: 10, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 3 }, dayDot: { width: 5, height: 5, borderRadius: 2.5, marginHorizontal: 1.5 }, - calendarLegend: { flexDirection: 'row', flexWrap: 'wrap', gap: 16, paddingHorizontal: 6, paddingTop: 8 }, - legendItem: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + calendarLegend: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.lg, paddingHorizontal: 6, paddingTop: SPACING.sm }, + legendItem: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, legendDot: { width: 8, height: 8, borderRadius: 4 }, legendTodayRing: { width: 12, height: 12, borderRadius: 6, borderWidth: 1.5, borderColor: c.primary, alignItems: 'center', justifyContent: 'center' }, legendTodayCenter: { width: 4, height: 4, borderRadius: 2, backgroundColor: c.primary }, legendText: { ...TYPE.caption, color: c.textSub }, calendarSummary: { marginTop: 14, paddingTop: 14, borderTopWidth: 1, borderTopColor: c.border }, calendarSummaryLabel: { color: c.textSub, fontSize: 12, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase' }, - calendarSummaryValue: { color: c.primary, fontSize: 28, fontWeight: '800', marginTop: 6 }, - calendarSummaryMeta: { ...TYPE.callout, color: c.textSub, marginTop: 4 }, + calendarSummaryValue: { color: c.primaryText, fontSize: 28, fontWeight: '800', marginTop: 6 }, + calendarSummaryMeta: { ...TYPE.callout, color: c.textSub, marginTop: SPACING.xs }, weekCard: { backgroundColor: c.card, - borderRadius: 20, - marginHorizontal: 16, - marginTop: 16, - padding: 16, + borderRadius: RADIUS.xl, + marginHorizontal: SPACING.lg, + marginTop: SPACING.lg, + padding: SPACING.lg, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, @@ -1270,19 +1304,21 @@ function makeStyles(c: ThemeColors) { borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, }, - weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 14 }, + weekHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: SPACING.md, marginBottom: SPACING.sm }, weekTitle: { color: c.primaryDark, fontSize: 20, fontWeight: '900' }, - weekRange: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3, textTransform: 'uppercase' }, - weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, alignItems: 'flex-end' }, - weekTotalValue: { color: c.primary, fontSize: 18, fontWeight: '900' }, + weekNavRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 14 }, + weekNavBtn: { width: 40, height: 36, borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryLight }, + weekNavLabel: { flex: 1, textAlign: 'center', color: c.textSub, fontSize: 12, fontWeight: '700', textTransform: 'uppercase' }, + weekTotalPill: { backgroundColor: c.primaryLight, borderRadius: 14, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, alignItems: 'flex-end' }, + weekTotalValue: { color: c.primaryText, fontSize: 18, fontWeight: '900' }, weekTotalLabel: { ...TYPE.micro, color: c.primaryDark, marginTop: 1 }, weekRow: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: SPACING.md, borderWidth: 1, borderColor: c.border, - borderRadius: 16, + borderRadius: RADIUS.lg, padding: 10, marginTop: 9, backgroundColor: c.bg, @@ -1292,7 +1328,7 @@ function makeStyles(c: ThemeColors) { borderRadius: 14, backgroundColor: c.card, alignItems: 'center', - paddingVertical: 8, + paddingVertical: SPACING.sm, borderWidth: 1, borderColor: c.border, }, @@ -1304,17 +1340,17 @@ function makeStyles(c: ThemeColors) { weekShiftTitle: { fontSize: 16, fontWeight: '900' }, weekShiftMeta: { color: c.text, fontSize: 15, fontWeight: '800', marginTop: 5 }, weekFlightMeta: { color: c.textSub, fontSize: 12, fontWeight: '700', marginTop: 3 }, - weekRestRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + weekRestRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, weekRestText: { color: c.success, fontSize: 16, fontWeight: '900' }, weekEmptyText: { color: c.textSub, fontSize: 14, fontWeight: '700' }, mainCard: { backgroundColor: c.card, borderRadius: 14, - marginHorizontal: 16, marginTop: 16, - padding: 20, + marginHorizontal: SPACING.lg, marginTop: SPACING.lg, + padding: SPACING.xl, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: c.isDark ? 0 : 4, borderWidth: c.isDark ? 1 : 0, borderColor: c.glassBorder, minHeight: 160, }, - selectedDayHeader: { marginBottom: 12, paddingRight: 90 }, + selectedDayHeader: { marginBottom: SPACING.md, paddingRight: 90 }, selectedDayLabel: { color: c.textSub, fontSize: 12, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase' }, weatherBadge: { position: 'absolute', top: 14, right: 14, @@ -1325,50 +1361,50 @@ function makeStyles(c: ThemeColors) { weatherIcon: { marginRight: 2 }, weatherPlace: { fontSize: 10, color: c.textSub, fontWeight: '600' }, weatherText: { ...TYPE.caption, color: c.text }, - shiftTypeRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 14, marginTop: 6 }, - shiftIconBox: { width: 44, height: 44, backgroundColor: c.primaryLight, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + shiftTypeRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, marginBottom: 14, marginTop: 6 }, + shiftIconBox: { width: 44, height: 44, backgroundColor: c.primaryLight, borderRadius: RADIUS.md, justifyContent: 'center', alignItems: 'center' }, shiftTypeName: { ...TYPE.headline, color: c.primaryDark }, timeRow: { flexDirection: 'row', alignItems: 'center' }, - timeText: { ...TYPE.title, color: c.primary }, - flightBadge: { marginTop: 14, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 8, alignSelf: 'flex-start' }, + timeText: { ...TYPE.title, color: c.primaryText }, + flightBadge: { marginTop: 14, backgroundColor: c.primaryLight, borderRadius: 10, paddingHorizontal: 14, paddingVertical: SPACING.sm, alignSelf: 'flex-start' }, flightBadgeRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, - flightBadgeText: { color: c.primary, fontWeight: '700', fontSize: 13 }, + flightBadgeText: { color: c.primaryText, fontWeight: '700', fontSize: 13 }, restRow: { flexDirection: 'row', alignItems: 'center', marginTop: 10 }, - restIconBox: { width: 48, height: 48, borderRadius: 14, backgroundColor: c.successSoft, alignItems: 'center', justifyContent: 'center', marginRight: 12 }, + restIconBox: { width: 48, height: 48, borderRadius: 14, backgroundColor: c.successSoft, alignItems: 'center', justifyContent: 'center', marginRight: SPACING.md }, restText: { ...TYPE.headline, color: c.success }, - emptyText: { ...TYPE.body, textAlign: 'center', color: c.textSub, marginTop: 20 }, + emptyText: { ...TYPE.body, textAlign: 'center', color: c.textSub, marginTop: SPACING.xl }, // Modal modalOverlay: { flex: 1, justifyContent: 'flex-end' }, modalBg: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' }, modalScrollContent: { flex: 1, justifyContent: 'flex-end' }, - modalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 100, maxHeight: '92%' }, + modalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: 100, maxHeight: '92%' }, manualModalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingBottom: 0, maxHeight: '92%' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: SPACING.md }, modalTitle: { ...TYPE.headline }, - centerBox: { alignItems: 'center', paddingVertical: 40, gap: 12 }, + centerBox: { alignItems: 'center', paddingVertical: 40, gap: SPACING.md }, stepText: { fontSize: 16, fontWeight: '600' }, - stepLabel: { ...TYPE.body, marginBottom: 12 }, - nameRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 14, paddingHorizontal: 12, borderBottomWidth: 1, borderRadius: 8, marginBottom: 4 }, + stepLabel: { ...TYPE.body, marginBottom: SPACING.md }, + nameRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 14, paddingHorizontal: SPACING.md, borderBottomWidth: 1, borderRadius: RADIUS.sm, marginBottom: SPACING.xs }, nameText: { fontSize: 15, fontWeight: '500' }, - previewRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 4, borderBottomWidth: 1 }, + previewRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, paddingHorizontal: SPACING.xs, borderBottomWidth: 1 }, previewDate: { fontSize: 14, fontWeight: '600' }, - previewPill: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 8 }, + previewPill: { paddingHorizontal: SPACING.md, paddingVertical: 5, borderRadius: RADIUS.sm }, previewPillText: { fontSize: 13, fontWeight: '700' }, - secondaryBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, alignItems: 'center', borderWidth: 1 }, + secondaryBtn: { flex: 1, paddingVertical: SPACING.md, borderRadius: 10, alignItems: 'center', borderWidth: 1 }, secondaryBtnText: { fontSize: 14, fontWeight: '600' }, - primaryBtn: { flex: 2, paddingVertical: 12, borderRadius: 10, alignItems: 'center' }, - primaryBtnText: { color: '#fff', fontSize: 14, fontWeight: 'bold' }, + primaryBtn: { flex: 2, paddingVertical: SPACING.md, borderRadius: 10, alignItems: 'center' }, + primaryBtnText: { color: '#fff', fontSize: 14, fontWeight: WEIGHT.semibold }, // Edit menu - editMenuContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 40 }, - editMenuOption: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16, borderRadius: 14, marginBottom: 10 }, + editMenuContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: 40 }, + editMenuOption: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: SPACING.lg, borderRadius: 14, marginBottom: 10 }, editMenuLabel: { fontSize: 15, fontWeight: '600' }, editMenuSub: { fontSize: 12, marginTop: 2 }, // Manual entry manualLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 1, marginBottom: 6 }, - manualInput: { borderWidth: 1, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 16, marginBottom: 4 }, + manualInput: { borderWidth: 1, borderRadius: 10, paddingHorizontal: 14, paddingVertical: SPACING.md, fontSize: 16, marginBottom: SPACING.xs }, manualTimeRow: { flexDirection: 'row', gap: 10, marginBottom: 14 }, manualTimeInput: { flex: 1, textAlign: 'center' }, - manualTypeBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1.5, alignItems: 'center' }, + manualTypeBtn: { flex: 1, paddingVertical: SPACING.md, borderRadius: 10, borderWidth: 1.5, alignItems: 'center' }, manualTypeInner: { flexDirection: 'row', alignItems: 'center', gap: 6 }, }); } diff --git a/src/screens/DesignLabScreen.tsx b/src/screens/DesignLabScreen.tsx index e4d06c0..563a32e 100644 --- a/src/screens/DesignLabScreen.tsx +++ b/src/screens/DesignLabScreen.tsx @@ -5,6 +5,7 @@ import { useAppTheme } from '../context/ThemeContext'; import CinematicMotionBoard from '../dev/CinematicMotionBoard'; import DesignDirectionPreview from '../dev/DesignDirectionPreview'; import { DESIGN_DIRECTIONS } from '../dev/designDirections'; +import { SPACING } from '../theme/spacing'; export default function DesignLabScreen() { const { colors } = useAppTheme(); @@ -106,14 +107,14 @@ function makeStyles(isDark: boolean) { paddingVertical: 13, }, selectorText: { fontSize: 14, fontWeight: '900' }, - motionSection: { gap: 5, marginTop: 4 }, + motionSection: { gap: 5, marginTop: SPACING.xs }, motionKicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.7 }, motionTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, motionCopy: { fontSize: 13, lineHeight: 19 }, notesCard: { borderWidth: 1, borderRadius: 24, - padding: 16, + padding: SPACING.lg, gap: 10, shadowColor: '#000', shadowOpacity: isDark ? 0.24 : 0.08, diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index bf70c36..7cbb228 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -25,7 +25,7 @@ import { getAirlineOps, getAirlineColor, getDepartureGateWindow } from '../utils import { statusToToken, delayToToken } from '../utils/statusColors'; import { fetchAirportScheduleRaw, type FlightScheduleProviderStatus } from '../utils/fr24api'; import { fetchStaffMonitorData, normalizeFlightNumber, type StaffMonitorFlight } from '../utils/staffMonitor'; -import { formatAirportHeader, getAirportAirlines, getAirportInfo, getStoredAirportAirlines } from '../utils/airportSettings'; +import { formatAirportHeader, getAirportAirlines, getAirportInfo, getStoredAirportAirlines, reconcileSelectedAirlines } from '../utils/airportSettings'; import { applyLiveArrivalEtas, applyLiveDepartureStatus, applyLiveOriginDepartures, fetchAdsbAircraft } from '../utils/liveArrivalEta'; import { WIDGET_CACHE_KEY, WIDGET_SHIFT_KEY } from '../widgets/widgetTaskHandler'; import type { WidgetData, WidgetFlight, WidgetShiftData } from '../widgets/widgetTaskHandler'; @@ -68,7 +68,6 @@ import { import { clamp, DEFAULT_NOTIFICATION_SETTINGS, - sameAirlineKeys, sanitizeNotificationSettings, type FlightNotificationSettings, } from '../utils/flightNotificationSettings'; @@ -83,7 +82,8 @@ import { schedulePinnedNotifications, scheduleShiftNotifications, } from '../utils/flightNotificationScheduler'; -import { TYPE } from '../theme/typography'; +import { TYPE, WEIGHT } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const WearDataSender = Platform.OS === 'android' ? NativeModules.WearDataSender : null; @@ -559,17 +559,12 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean setSelectedAirlines(next); persistSelectedAirlines(next).catch(() => {}); }, [persistSelectedAirlines]); - const airportAirlinesRef = useRef([]); const selectedAirlinesRef = useRef([]); const notifSettingsRef = useRef(DEFAULT_NOTIFICATION_SETTINGS); const selectedAirlinesNotifSignatureRef = useRef(''); const fetchInFlightRef = useRef(false); const lastFlightRefreshAttemptAtRef = useRef(0); - useEffect(() => { - airportAirlinesRef.current = airportAirlines; - }, [airportAirlines]); - useEffect(() => { selectedAirlinesRef.current = selectedAirlines; }, [selectedAirlines]); @@ -673,19 +668,16 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean const nextAirportAirlines = getAirportAirlines(airportCode); setAirportAirlines(nextAirportAirlines); - const savedProfileAirlines = activeProfile?.airportCode === airportCode ? activeProfile.airlines : []; - const previousAirportAirlines = airportAirlinesRef.current; - const previousSelectedAirlines = selectedAirlinesRef.current; - const hadAllPreviouslySelected = - previousAirportAirlines.length > 0 && - previousAirportAirlines.every(key => previousSelectedAirlines.includes(key)); - - if (savedProfileAirlines.length === 0) { - if (previousSelectedAirlines.length > 0) { - applySelectedAirlines([]); - } - } else if (hadAllPreviouslySelected && !sameAirlineKeys(savedProfileAirlines, nextAirportAirlines)) { - applySelectedAirlines(nextAirportAirlines); + // Le compagnie appena rilevate nello schedule NON vengono mai selezionate + // in automatico: restano deselezionate nel filtro finché l'utente non le + // spunta, così in bacheca non compaiono voli che non gestisce. + const reconciledSelection = reconcileSelectedAirlines({ + savedProfileAirlines: activeProfile?.airportCode === airportCode ? activeProfile.airlines : [], + previousSelectedAirlines: selectedAirlinesRef.current, + nextAirportAirlines, + }); + if (reconciledSelection) { + applySelectedAirlines(reconciledSelection); } // Accumula voli: fonde i dati freschi con quelli in cache e conserva solo // i voli non più vecchi di 1 ora dall'orario migliore disponibile. @@ -1421,7 +1413,7 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean keyExtractor={(item, i) => item.flight?.identification?.id || String(i)} renderItem={renderFlight} contentContainerStyle={{ - paddingHorizontal: 16, + paddingHorizontal: SPACING.lg, paddingTop: isOperations ? 8 : 18, paddingBottom: isOperations ? 176 : 120, }} @@ -1538,18 +1530,18 @@ function makeStyles(c: ThemeColors, isOperations = false) { const operationBorderSoft = isOperations ? 'rgba(45,212,191,0.18)' : c.border; return StyleSheet.create({ - pageHeader: { backgroundColor: isOperations ? 'rgba(2,8,12,0.90)' : c.card, paddingHorizontal: 16, paddingVertical: isOperations ? 12 : 14, borderBottomWidth: 1, borderBottomColor: operationBorderSoft, flexDirection: 'row', alignItems: 'center' }, + pageHeader: { backgroundColor: isOperations ? 'rgba(2,8,12,0.90)' : c.card, paddingHorizontal: SPACING.lg, paddingVertical: isOperations ? 12 : 14, borderBottomWidth: 1, borderBottomColor: operationBorderSoft, flexDirection: 'row', alignItems: 'center' }, notifBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, notifBtnActive: { backgroundColor: c.primary, shadowColor: c.primary, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.35, shadowRadius: 6, elevation: 5 }, - notifBadge: { position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: 8, backgroundColor: c.danger, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, borderColor: c.card }, + notifBadge: { position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: RADIUS.sm, backgroundColor: c.danger, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, borderColor: c.card }, notifBadgeTxt: { fontSize: 9, fontWeight: '800', color: '#fff' }, pageTitle: { ...(isOperations ? TYPE.titleLg : TYPE.title), color: isOperations ? c.text : c.primaryDark, letterSpacing: isOperations ? -0.5 : 0 }, pageSub: { fontSize: 13, color: c.textSub, marginTop: 2, letterSpacing: isOperations ? 0.7 : 0 }, - controlsRow: { flexDirection: 'row', gap: 8, padding: isOperations ? 9 : 12, backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.card, borderBottomWidth: 1, borderBottomColor: operationBorderSoft }, - sourceRow: { flexDirection: 'row', alignItems: 'flex-start', flexWrap: 'wrap', gap: 8, marginTop: isOperations ? 8 : 10, marginBottom: isOperations ? 2 : 8, marginHorizontal: 16 }, - sourceBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', maxWidth: '100%', flexShrink: 1, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: 999, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, borderWidth: 1, borderColor: operationBorder }, + controlsRow: { flexDirection: 'row', gap: SPACING.sm, padding: isOperations ? 9 : 12, backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.card, borderBottomWidth: 1, borderBottomColor: operationBorderSoft }, + sourceRow: { flexDirection: 'row', alignItems: 'flex-start', flexWrap: 'wrap', gap: SPACING.sm, marginTop: isOperations ? 8 : 10, marginBottom: isOperations ? 2 : 8, marginHorizontal: SPACING.lg }, + sourceBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', maxWidth: '100%', flexShrink: 1, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: RADIUS.pill, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, borderWidth: 1, borderColor: operationBorder }, sourceBadgeText: { flexShrink: 1, flexWrap: 'wrap', fontSize: 11, lineHeight: 15, fontWeight: '800', color: c.primaryDark }, - refreshBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: 999, backgroundColor: isOperations ? 'rgba(15,23,42,0.82)' : c.cardSecondary, borderWidth: 1, borderColor: operationBorderSoft }, + refreshBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 7, borderRadius: RADIUS.pill, backgroundColor: isOperations ? 'rgba(15,23,42,0.82)' : c.cardSecondary, borderWidth: 1, borderColor: operationBorderSoft }, refreshBadgeText: { fontSize: 11, fontWeight: '800', color: c.textSub }, segment: { flex: 1, flexDirection: 'row', backgroundColor: isOperations ? 'rgba(2,8,12,0.76)' : c.bg, borderRadius: isOperations ? 14 : 8, padding: 3, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, segBtn: { flex: 1, paddingVertical: isOperations ? 6 : 7, alignItems: 'center', borderRadius: isOperations ? 11 : 6 }, @@ -1558,11 +1550,11 @@ function makeStyles(c: ThemeColors, isOperations = false) { segBtnTextActive: { color: isOperations ? c.primaryDark : c.primary, fontWeight: '800' }, card: { backgroundColor: operationPanel, borderRadius: isOperations ? 18 : 16, marginBottom: 10, overflow: 'hidden', shadowColor: c.primary, shadowOpacity: isOperations || c.isDark ? 0 : 0.08, shadowRadius: 10, elevation: isOperations || c.isDark ? 0 : 3, borderWidth: 1, borderColor: operationBorder, borderLeftWidth: isOperations ? 4 : 1 }, cardShift: { borderWidth: 1.5, borderColor: c.warning }, - shiftBanner: { backgroundColor: c.warning, paddingVertical: 5, paddingHorizontal: 12 }, - shiftBannerText: { color: '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 }, + shiftBanner: { backgroundColor: c.warning, paddingVertical: 5, paddingHorizontal: SPACING.md }, + shiftBannerText: { color: '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, cardPinned: { borderWidth: 2, borderColor: c.warning }, - pinBanner: { backgroundColor: isOperations ? 'rgba(245,158,11,0.18)' : c.warning, paddingVertical: 5, paddingHorizontal: 12, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: 'rgba(245,158,11,0.28)' }, - pinBannerText: { color: isOperations ? '#FBBF24' : '#fff', fontWeight: 'bold', fontSize: 11, letterSpacing: 0.5 }, + pinBanner: { backgroundColor: isOperations ? 'rgba(245,158,11,0.18)' : c.warning, paddingVertical: 5, paddingHorizontal: SPACING.md, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: 'rgba(245,158,11,0.28)' }, + pinBannerText: { color: isOperations ? '#FBBF24' : '#fff', fontWeight: WEIGHT.semibold, fontSize: 11, letterSpacing: 0.5 }, statusPill: { paddingHorizontal: 10, paddingVertical: isOperations ? 3 : 4, borderRadius: isOperations ? 10 : 20, marginTop: isOperations ? 6 : 8, alignSelf: 'flex-end', borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorderSoft : 'transparent' }, statusText: { ...TYPE.micro, letterSpacing: isOperations ? 0.6 : 0 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: isOperations ? 9 : 10, paddingHorizontal: 14, borderBottomWidth: isOperations ? 1 : 0, borderBottomColor: operationBorderSoft }, @@ -1571,7 +1563,7 @@ function makeStyles(c: ThemeColors, isOperations = false) { headerText: { flex: 1, minWidth: 0 }, headerFlightNum: { color: isOperations ? c.primaryDark : '#fff', fontWeight: '900', fontSize: isOperations ? 16 : 15, lineHeight: 18, letterSpacing: isOperations ? 0.6 : 0 }, headerAirlineName: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.8)', fontSize: 10, letterSpacing: isOperations ? 0.5 : 0 }, - headerMetaFlash: { alignItems: 'flex-end', borderRadius: 12, marginRight: -8, paddingHorizontal: 8, paddingVertical: 4, maxWidth: isOperations ? 150 : 142, flexShrink: 0 }, + headerMetaFlash: { alignItems: 'flex-end', borderRadius: RADIUS.md, marginRight: -8, paddingHorizontal: SPACING.sm, paddingVertical: SPACING.xs, maxWidth: isOperations ? 150 : 142, flexShrink: 0 }, headerTime: { color: isOperations ? c.text : '#fff', fontWeight: '900', fontSize: isOperations ? 19 : 18, lineHeight: 20, textAlign: 'right', fontVariant: ['tabular-nums'] }, headerDest: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.8)', fontSize: 10, textAlign: 'right' }, headerAirportCode: { color: isOperations ? c.textSub : 'rgba(255,255,255,0.86)', fontSize: isOperations ? 11 : 10, lineHeight: 13, fontWeight: '900', letterSpacing: isOperations ? 1.1 : 0.8, textAlign: 'right' }, @@ -1579,49 +1571,49 @@ function makeStyles(c: ThemeColors, isOperations = false) { cardBody: { flexDirection: 'column', paddingVertical: isOperations ? 9 : 10, paddingHorizontal: 14, backgroundColor: operationPanel }, bodyInfo: { fontSize: 11, color: c.textSub }, bodyTime: { fontWeight: '700', color: c.text }, - opsRow: { flexDirection: 'row', gap: 8 }, - opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 12 : 10, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, + opsRow: { flexDirection: 'row', gap: SPACING.sm }, + opsBadge: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 12 : 10, paddingHorizontal: 10, paddingVertical: isOperations ? 6 : 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, opsIcon: { fontSize: 16 }, opsLabel: { fontSize: 10, fontWeight: '600', color: c.textSub, letterSpacing: 0.5 }, opsTime: { fontSize: 13, fontWeight: '800', color: c.primaryDark }, pinBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: 'rgba(255,255,255,0.15)', justifyContent: 'center', alignItems: 'center' }, pinBtnActive: { backgroundColor: 'rgba(245,158,11,0.25)' }, - filterBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', marginRight: 8, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, + filterBtn: { width: 42, height: 42, borderRadius: isOperations ? 14 : 21, backgroundColor: operationPanelStrong, justifyContent: 'center', alignItems: 'center', marginRight: SPACING.sm, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, filterBtnActive: { backgroundColor: c.primary, shadowColor: c.primary, shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.35, shadowRadius: 6, elevation: 5 }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'flex-end' }, - alertOverlay: { flex: 1, backgroundColor: 'rgba(2,6,23,0.55)', justifyContent: 'center', alignItems: 'center', padding: 24 }, + alertOverlay: { flex: 1, backgroundColor: 'rgba(2,6,23,0.55)', justifyContent: 'center', alignItems: 'center', padding: SPACING.xxl }, alertCard: { width: '100%', maxWidth: 440, - borderRadius: 20, + borderRadius: RADIUS.xl, padding: 18, backgroundColor: c.card, borderWidth: 1, borderColor: c.glassBorder, }, - alertHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 10 }, + alertHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: SPACING.md, gap: 10 }, alertIconWrap: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' }, alertSuccess: { backgroundColor: c.success }, alertWarning: { backgroundColor: c.warning }, alertInfo: { backgroundColor: c.primary }, alertTitle: { flex: 1, fontSize: 28, fontWeight: '900', color: c.text }, - alertMessage: { fontSize: 17, lineHeight: 24, color: c.textSub, marginBottom: 16 }, - alertBtn: { alignSelf: 'flex-end', paddingHorizontal: 18, paddingVertical: 10, borderRadius: 12, backgroundColor: c.primary }, + alertMessage: { fontSize: 17, lineHeight: 24, color: c.textSub, marginBottom: SPACING.lg }, + alertBtn: { alignSelf: 'flex-end', paddingHorizontal: 18, paddingVertical: 10, borderRadius: RADIUS.md, backgroundColor: c.primary }, alertBtnTxt: { color: '#fff', fontSize: 15, fontWeight: '800' }, - filterSheet: { backgroundColor: isOperations ? '#071414' : c.card, borderTopLeftRadius: isOperations ? 28 : 24, borderTopRightRadius: isOperations ? 28 : 24, padding: 20, paddingBottom: 36, borderTopWidth: isOperations ? 1 : 0, borderColor: operationBorder }, - filterSheetHandle: { width: 36, height: 4, borderRadius: 2, backgroundColor: isOperations ? 'rgba(45,212,191,0.34)' : c.border, alignSelf: 'center', marginBottom: 16 }, - filterSheetTitle: { fontSize: 16, fontWeight: '800', color: isOperations ? c.primaryDark : c.text, marginBottom: 16, textAlign: 'center', letterSpacing: isOperations ? 0.8 : 0 }, - notifSheetSub: { fontSize: 13, color: c.textSub, textAlign: 'center', marginTop: -8, marginBottom: 16 }, - notifRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 10 }, + filterSheet: { backgroundColor: isOperations ? '#071414' : c.card, borderTopLeftRadius: isOperations ? 28 : 24, borderTopRightRadius: isOperations ? 28 : 24, padding: SPACING.xl, paddingBottom: 36, borderTopWidth: isOperations ? 1 : 0, borderColor: operationBorder }, + filterSheetHandle: { width: 36, height: 4, borderRadius: 2, backgroundColor: isOperations ? 'rgba(45,212,191,0.34)' : c.border, alignSelf: 'center', marginBottom: SPACING.lg }, + filterSheetTitle: { fontSize: 16, fontWeight: '800', color: isOperations ? c.primaryDark : c.text, marginBottom: SPACING.lg, textAlign: 'center', letterSpacing: isOperations ? 0.8 : 0 }, + notifSheetSub: { fontSize: 13, color: c.textSub, textAlign: 'center', marginTop: -8, marginBottom: SPACING.lg }, + notifRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, paddingVertical: 10 }, notifRowTextWrap: { flex: 1 }, notifRowTitle: { fontSize: 14, fontWeight: '700', color: c.text }, notifRowSub: { fontSize: 12, color: c.textSub, marginTop: 2 }, notifDivider: { height: 1, backgroundColor: c.border, marginVertical: 10 }, notifMinutesRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10 }, - notifStepper: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.bg, borderRadius: 10, padding: 4 }, - notifStepperBtn: { width: 32, height: 32, borderRadius: 8, alignItems: 'center', justifyContent: 'center', backgroundColor: c.card }, + notifStepper: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.bg, borderRadius: 10, padding: SPACING.xs }, + notifStepperBtn: { width: 32, height: 32, borderRadius: RADIUS.sm, alignItems: 'center', justifyContent: 'center', backgroundColor: c.card }, notifStepperValue: { minWidth: 54, textAlign: 'center', fontSize: 14, fontWeight: '800', color: c.primaryDark }, - filterOption: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14, borderRadius: 14, marginBottom: 8, borderWidth: 1.5 }, + filterOption: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, padding: 14, borderRadius: 14, marginBottom: SPACING.sm, borderWidth: 1.5 }, filterOptionActive: { borderWidth: 1.5, ...filterOptionActiveShadow, @@ -1638,7 +1630,7 @@ function makeStyles(c: ThemeColors, isOperations = false) { }, filterBrandDot: { width: 10, height: 10, borderRadius: 5 }, smFooter: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, paddingHorizontal: 14, paddingBottom: isOperations ? 8 : 10, backgroundColor: operationPanel, borderTopWidth: isOperations ? 1 : 0, borderTopColor: operationBorderSoft }, - smPill: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 10 : 8, paddingHorizontal: 8, paddingVertical: isOperations ? 3 : 4, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, + smPill: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, backgroundColor: isOperations ? 'rgba(45,212,191,0.10)' : c.primaryLight, borderRadius: isOperations ? 10 : 8, paddingHorizontal: SPACING.sm, paddingVertical: isOperations ? 3 : 4, borderWidth: isOperations ? 1 : 0, borderColor: operationBorderSoft }, smPillText: { fontSize: 11, fontWeight: '700', color: c.primaryDark }, }); } diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 64acbb8..5e49ec1 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -41,6 +41,7 @@ import { ShiftWidget } from '../widgets/ShiftWidget'; import { parseOcrShiftText } from '../utils/ocrShiftParser'; import { useLanguage } from '../context/LanguageContext'; import { TYPE } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const GOLD = '#F59E0B'; @@ -124,7 +125,7 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite return ( - + {flightNumber} @@ -156,17 +157,17 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite {/* Body */} - + {tab === 'departures' ? ( - - + + CHECK-IN {fmt(ops.checkInOpen)} – {fmt(ops.checkInClose)} - + GATE @@ -181,10 +182,10 @@ function PinnedFlightCardComponent({ item, colors, isOperations = false }: { ite )} {/* Status row */} - + {statusText} - + {t('homePinned')} @@ -232,31 +233,31 @@ function EasyJetOverlapMonitor({ overlappingFlights, tickerMs, colors, t, locale return ( {/* Header */} - - + + easyJet Overlap Active - + Aggiornato al secondo - + Rilevata fascia oraria con più voli easyJet in arrivo sovrapposti. Monitoraggio in tempo reale attivo. {/* Flight rows */} - + {overlappingFlights.map((item, idx) => { const flightNumber = item.flight?.identification?.number?.default || 'N/A'; const origin = getFlightAirportLabel(item.flight?.airport?.origin, 'N/A'); @@ -274,10 +275,10 @@ function EasyJetOverlapMonitor({ overlappingFlights, tickerMs, colors, t, locale - + {flightNumber} @@ -881,16 +882,16 @@ function makeStyles(c: ThemeColors, isOperations = false) { const operationShadow = isOperations ? 0 : undefined; return StyleSheet.create({ hiddenWV: { height: 1, width: 1, opacity: 0, position: 'absolute', top: -100 }, - topRow: { flexDirection: 'row', gap: 12, padding: 16, paddingBottom: 8 }, - weatherCard: { flex: 1, backgroundColor: operationPanel, borderRadius: isOperations ? 20 : 18, padding: 16, alignItems: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: operationShadow ?? 0.12, shadowRadius: 12, elevation: isOperations ? 0 : 4, borderWidth: 1, borderColor: operationBorder }, - weatherIcon: { marginBottom: 4 }, + topRow: { flexDirection: 'row', gap: SPACING.md, padding: SPACING.lg, paddingBottom: SPACING.sm }, + weatherCard: { flex: 1, backgroundColor: operationPanel, borderRadius: isOperations ? 20 : 18, padding: SPACING.lg, alignItems: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: operationShadow ?? 0.12, shadowRadius: 12, elevation: isOperations ? 0 : 4, borderWidth: 1, borderColor: operationBorder }, + weatherIcon: { marginBottom: SPACING.xs }, weatherTemp: { fontSize: isOperations ? 30 : 28, fontWeight: '800', color: c.primaryDark }, weatherDesc: { fontSize: 11, color: c.textSub, textAlign: 'center', marginTop: 2, letterSpacing: isOperations ? 0.4 : 0 }, dateCard: { width: isOperations ? 96 : 90, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryDark, borderRadius: isOperations ? 20 : 18, padding: 14, alignItems: 'center', justifyContent: 'center', shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.30, shadowRadius: 12, elevation: isOperations ? 0 : 6, borderWidth: isOperations ? 1 : 0, borderColor: operationBorder }, dateToday: { ...TYPE.micro, color: isOperations ? 'rgba(153,246,228,0.72)' : 'rgba(255,255,255,0.6)', letterSpacing: 1.7 }, dateNum: { ...TYPE.display, color: isOperations ? c.primaryDark : '#fff' }, dateMonth: { fontSize: 12, color: isOperations ? c.textSub : 'rgba(255,255,255,0.7)', marginTop: 2 }, - operationalCard: { marginHorizontal: 16, marginTop: 8, backgroundColor: operationPanel, borderRadius: isOperations ? 24 : 20, padding: 16, borderWidth: 1, borderColor: operationBorder, gap: 13, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 12, elevation: isOperations ? 0 : 3 }, + operationalCard: { marginHorizontal: SPACING.lg, marginTop: SPACING.sm, backgroundColor: operationPanel, borderRadius: isOperations ? 24 : 20, padding: SPACING.lg, borderWidth: 1, borderColor: operationBorder, gap: 13, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 12, elevation: isOperations ? 0 : 3 }, operationalHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 }, operationalTitleBlock: { flex: 1, gap: 3 }, operationalKicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.8, color: isOperations ? 'rgba(153,246,228,0.70)' : c.textMuted }, @@ -900,49 +901,49 @@ function makeStyles(c: ThemeColors, isOperations = false) { operationalBeaconActive: { backgroundColor: c.success }, operationalBeaconNext: { backgroundColor: c.primary }, operationalBeaconRest: { backgroundColor: c.info }, - summaryBadgeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - summaryBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, alignSelf: 'flex-start', borderRadius: 999, borderWidth: 1, borderColor: operationBorder, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, paddingHorizontal: 9, paddingVertical: 5 }, + summaryBadgeRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + summaryBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, alignSelf: 'flex-start', borderRadius: RADIUS.pill, borderWidth: 1, borderColor: operationBorder, backgroundColor: isOperations ? 'rgba(45,212,191,0.12)' : c.primaryLight, paddingHorizontal: 9, paddingVertical: 5 }, summaryBadgeText: { fontSize: 11, fontWeight: '900', color: c.primaryDark }, - healthGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, - healthChip: { width: '48%', minWidth: 134, flexGrow: 1, flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 14, borderWidth: 1, paddingHorizontal: 10, paddingVertical: 9 }, + healthGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm }, + healthChip: { width: '48%', minWidth: 134, flexGrow: 1, flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, borderRadius: 14, borderWidth: 1, paddingHorizontal: 10, paddingVertical: 9 }, healthText: { flex: 1, minWidth: 0 }, healthLabel: { ...TYPE.glyph, fontWeight: '900', letterSpacing: 0.8, textTransform: 'uppercase' }, healthValue: { fontSize: 12, fontWeight: '900', marginTop: 1 }, - sectionTitle: { fontSize: 12, fontWeight: '800', color: isOperations ? 'rgba(153,246,228,0.66)' : c.textSub, letterSpacing: isOperations ? 1.6 : 0.5, marginHorizontal: 16, marginTop: 16, marginBottom: 8, textTransform: 'uppercase' }, - shiftCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: 16, padding: isOperations ? 18 : 16, flexDirection: 'row', gap: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.10, shadowRadius: 12, elevation: isOperations ? 0 : 4, minHeight: isOperations ? 104 : 90, borderWidth: 1, borderColor: operationBorder }, - shiftStrip: { width: isOperations ? 5 : 4, borderRadius: 999, backgroundColor: c.primary, marginRight: 2 }, - shiftBadgeRow: { flexDirection: 'row', marginBottom: 8 }, - inProgressBadge: { backgroundColor: isOperations ? 'rgba(45,212,191,0.14)' : '#D1FAE5', paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20, borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorder : 'transparent' }, + sectionTitle: { fontSize: 12, fontWeight: '800', color: isOperations ? 'rgba(153,246,228,0.66)' : c.textSub, letterSpacing: isOperations ? 1.6 : 0.5, marginHorizontal: SPACING.lg, marginTop: SPACING.lg, marginBottom: SPACING.sm, textTransform: 'uppercase' }, + shiftCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: SPACING.lg, padding: isOperations ? 18 : 16, flexDirection: 'row', gap: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.10, shadowRadius: 12, elevation: isOperations ? 0 : 4, minHeight: isOperations ? 104 : 90, borderWidth: 1, borderColor: operationBorder }, + shiftStrip: { width: isOperations ? 5 : 4, borderRadius: RADIUS.pill, backgroundColor: c.primary, marginRight: 2 }, + shiftBadgeRow: { flexDirection: 'row', marginBottom: SPACING.sm }, + inProgressBadge: { backgroundColor: isOperations ? 'rgba(45,212,191,0.14)' : '#D1FAE5', paddingHorizontal: 10, paddingVertical: 3, borderRadius: RADIUS.xl, borderWidth: isOperations ? 1 : 0, borderColor: isOperations ? operationBorder : 'transparent' }, inProgressText: { ...TYPE.micro, color: isOperations ? c.primaryDark : c.success, letterSpacing: isOperations ? 1 : 0 }, - shiftTitle: { ...TYPE.headline, color: isOperations ? c.text : c.primaryDark, marginBottom: 4 }, - shiftTime: { fontSize: isOperations ? 28 : 22, fontWeight: '900', color: isOperations ? c.primaryDark : c.primary, marginBottom: 4, fontVariant: ['tabular-nums'] }, - timelineCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: 16, marginTop: 12, padding: 16, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 10, elevation: isOperations ? 0 : 3, borderWidth: 1, borderColor: operationBorder }, + shiftTitle: { ...TYPE.headline, color: isOperations ? c.text : c.primaryDark, marginBottom: SPACING.xs }, + shiftTime: { fontSize: isOperations ? 28 : 22, fontWeight: '900', color: isOperations ? c.primaryDark : c.primary, marginBottom: SPACING.xs, fontVariant: ['tabular-nums'] }, + timelineCard: { backgroundColor: operationPanel, borderRadius: isOperations ? 22 : 18, marginHorizontal: SPACING.lg, marginTop: SPACING.md, padding: SPACING.lg, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: isOperations ? 0 : 0.08, shadowRadius: 10, elevation: isOperations ? 0 : 3, borderWidth: 1, borderColor: operationBorder }, restRow: { flexDirection: 'row', alignItems: 'center' }, - restIconWrap: { width: 40, height: 40, borderRadius: 12, backgroundColor: c.success + '22', alignItems: 'center', justifyContent: 'center', marginRight: 12 }, + restIconWrap: { width: 40, height: 40, borderRadius: RADIUS.md, backgroundColor: c.success + '22', alignItems: 'center', justifyContent: 'center', marginRight: SPACING.md }, restText: { fontSize: 18, fontWeight: '700', color: c.success }, emptyShift: { ...TYPE.body, color: c.textSub, textAlign: 'center', flex: 1 }, - uploadToggle: { flexDirection: 'row', alignItems: 'center', gap: 10, marginHorizontal: 16, marginTop: 16, backgroundColor: c.card, borderRadius: 18, paddingHorizontal: 16, paddingVertical: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, borderWidth: 1, borderColor: c.glassBorder }, + uploadToggle: { flexDirection: 'row', alignItems: 'center', gap: 10, marginHorizontal: SPACING.lg, marginTop: SPACING.lg, backgroundColor: c.card, borderRadius: 18, paddingHorizontal: SPACING.lg, paddingVertical: 14, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, borderWidth: 1, borderColor: c.glassBorder }, uploadToggleText: { flex: 1, fontSize: 15, fontWeight: '600', color: c.primaryDark }, - uploadSection: { marginHorizontal: 16, backgroundColor: c.card, borderRadius: 18, padding: 16, marginTop: 2, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, borderWidth: 1, borderColor: c.glassBorder }, + uploadSection: { marginHorizontal: SPACING.lg, backgroundColor: c.card, borderRadius: 18, padding: SPACING.lg, marginTop: 2, shadowColor: c.isDark ? '#000000' : c.primary, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, borderWidth: 1, borderColor: c.glassBorder }, uploadDesc: { fontSize: 13, color: c.textSub, lineHeight: 19, marginBottom: 14 }, - scanBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryDark, borderRadius: 12, paddingVertical: 13, paddingHorizontal: 20 }, + scanBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primaryDark, borderRadius: RADIUS.md, paddingVertical: 13, paddingHorizontal: SPACING.xl }, scanBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 }, - imagesRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 12 }, + imagesRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm, marginTop: SPACING.md }, thumb: { width: '47%', height: 120, borderRadius: 10, resizeMode: 'cover' }, - ocrResult: { backgroundColor: c.cardSecondary, borderRadius: 12, padding: 12, marginTop: 12 }, + ocrResult: { backgroundColor: c.cardSecondary, borderRadius: RADIUS.md, padding: SPACING.md, marginTop: SPACING.md }, ocrTitle: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: 6 }, ocrText: { fontSize: 12, color: c.text, lineHeight: 18 }, - syncBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primary, borderRadius: 12, paddingVertical: 13, marginTop: 12 }, + syncBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: c.primary, borderRadius: RADIUS.md, paddingVertical: 13, marginTop: SPACING.md }, syncBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'center', alignItems: 'center', padding: 20 }, - modalContent: { backgroundColor: c.isDark ? c.bg : c.card, width: '100%', borderRadius: 20, padding: 20, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 14, elevation: 8, borderWidth: 1, borderColor: c.glassBorder }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'center', alignItems: 'center', padding: SPACING.xl }, + modalContent: { backgroundColor: c.isDark ? c.bg : c.card, width: '100%', borderRadius: RADIUS.xl, padding: SPACING.xl, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 14, elevation: 8, borderWidth: 1, borderColor: c.glassBorder }, modalTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark, marginBottom: 14 }, - modalLabel: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: 8 }, - modalInput: { borderWidth: 1, borderColor: c.border, borderRadius: 10, padding: 12, marginBottom: 10, fontSize: 14, color: c.text }, + modalLabel: { fontSize: 12, fontWeight: '700', color: c.textSub, marginBottom: SPACING.sm }, + modalInput: { borderWidth: 1, borderColor: c.border, borderRadius: 10, padding: SPACING.md, marginBottom: 10, fontSize: 14, color: c.text }, modalBtn: { flex: 1, padding: 14, borderRadius: 10, alignItems: 'center' }, - typeBtn: { flex: 1, padding: 12, borderRadius: 10, backgroundColor: c.bg, alignItems: 'center' }, - inputLabel: { fontSize: 11, color: c.textSub, fontWeight: '700', marginBottom: 4, letterSpacing: 0.5 }, - modeBtn: { flex: 1, backgroundColor: c.primary, borderRadius: 14, paddingVertical: 20, alignItems: 'center', justifyContent: 'center', gap: 8, shadowColor: c.primary, shadowOpacity: 0.25, shadowRadius: 8, elevation: 4 }, + typeBtn: { flex: 1, padding: SPACING.md, borderRadius: 10, backgroundColor: c.bg, alignItems: 'center' }, + inputLabel: { fontSize: 11, color: c.textSub, fontWeight: '700', marginBottom: SPACING.xs, letterSpacing: 0.5 }, + modeBtn: { flex: 1, backgroundColor: c.primary, borderRadius: 14, paddingVertical: SPACING.xl, alignItems: 'center', justifyContent: 'center', gap: SPACING.sm, shadowColor: c.primary, shadowOpacity: 0.25, shadowRadius: 8, elevation: 4 }, modeBtnText: { color: '#fff', fontWeight: '700', fontSize: 13 }, }); } diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index 0aed1ec..1c0f7c9 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -6,7 +6,10 @@ import { } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { useLanguage } from '../context/LanguageContext'; +import { TYPE } from '../theme/typography'; import { enableLegacyAndroidLayoutAnimation } from '../utils/layoutAnimation'; +import { SPACING, RADIUS } from '../theme/spacing'; const STORAGE_KEY = 'manuals_data_v2'; @@ -329,6 +332,7 @@ function RichBodyText({ text, colors }: { text: string; colors: any }) { // ─── Commands Tab component ────────────────────────────────────────────────── function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any }) { + const { t } = useLanguage(); const [search, setSearch] = useState(''); const lower = search.toLowerCase(); const filtered = lower @@ -340,14 +344,14 @@ function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any return ( {search.length > 0 && ( - setSearch('')}> + setSearch('')} accessibilityRole="button" accessibilityLabel={t('a11yClearSearch')}> )} {categories.map(cat => ( - + {filtered.filter(c => c.category === cat).map((c, i) => ( - + {c.desc} @@ -429,7 +433,7 @@ function makeItemStyles(c: ThemeColors) { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 13, }, - title: { fontSize: 13, fontWeight: '600', color: c.text, flex: 1 }, + title: { ...TYPE.callout, color: c.text, flex: 1 }, body: { paddingHorizontal: 14, paddingBottom: 14, paddingTop: 2, borderTopWidth: 1, borderTopColor: c.cardSecondary, @@ -450,6 +454,7 @@ function ManualItemRow({ editMode: boolean; onEdit: () => void; }) { + const { t } = useLanguage(); const { colors } = useAppTheme(); const itemStyles = useMemo(() => makeItemStyles(colors), [colors]); const [open, setOpen] = useState(false); @@ -469,7 +474,7 @@ function ManualItemRow({ /> {item.title} {editMode && ( - + )} @@ -487,13 +492,13 @@ function ManualItemRow({ function makeSectionStyles(c: ThemeColors) { return StyleSheet.create({ wrapper: { - marginBottom: 12, + marginBottom: SPACING.md, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 10, paddingHorizontal: 4, + paddingVertical: 10, paddingHorizontal: SPACING.xs, borderBottomWidth: 1, borderBottomColor: c.border, - marginBottom: 8, + marginBottom: SPACING.sm, }, title: { fontSize: 12, fontWeight: '700', color: c.textSub, letterSpacing: 0.8 }, body: { paddingLeft: 0 }, @@ -511,6 +516,7 @@ function SectionBlock({ onAddItem: () => void; onEditItem: (itemIdx: number) => void; }) { + const { t } = useLanguage(); const { colors } = useAppTheme(); const sectionStyles = useMemo(() => makeSectionStyles(colors), [colors]); const [open, setOpen] = useState(true); @@ -524,9 +530,9 @@ function SectionBlock({ {section.title} - + {editMode && ( - + )} @@ -548,7 +554,7 @@ function SectionBlock({ ))} {editMode && ( @@ -564,16 +570,16 @@ function SectionBlock({ const modalStyles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, scrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, - sheet: { borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20, paddingBottom: 36, maxHeight: '92%' }, - title: { fontSize: 17, fontWeight: '700', marginBottom: 16 }, - label: { fontSize: 12, fontWeight: '600', marginBottom: 4, marginTop: 12 }, - input: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 9, fontSize: 14 }, + sheet: { borderTopLeftRadius: RADIUS.xl, borderTopRightRadius: RADIUS.xl, padding: SPACING.xl, paddingBottom: 36, maxHeight: '92%' }, + title: { ...TYPE.headline, marginBottom: SPACING.lg }, + label: { ...TYPE.caption, marginBottom: SPACING.xs, marginTop: SPACING.md }, + input: { borderWidth: 1, borderRadius: RADIUS.sm, paddingHorizontal: SPACING.md, paddingVertical: 9, fontSize: 14 }, inputMulti: { minHeight: 100, paddingTop: 9 }, - colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 8 }, + colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: SPACING.sm }, colorDot: { width: 28, height: 28, borderRadius: 14 }, colorDotSelected: { borderWidth: 3, borderColor: '#000', transform: [{ scale: 1.2 }] }, - btnRow: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, marginTop: 20 }, - btn: { paddingHorizontal: 18, paddingVertical: 9, borderRadius: 8 }, + btnRow: { flexDirection: 'row', justifyContent: 'flex-end', gap: SPACING.sm, marginTop: SPACING.xl }, + btn: { paddingHorizontal: 18, paddingVertical: 9, borderRadius: RADIUS.sm }, btnCancel: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#ccc' }, btnSave: {}, btnDanger: { marginRight: 'auto', backgroundColor: '#FEE2E2' }, @@ -587,27 +593,27 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, header: { flexDirection: 'row', alignItems: 'center', gap: 10, - paddingHorizontal: 16, paddingVertical: 13, + paddingHorizontal: SPACING.lg, paddingVertical: 13, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - headerTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, + headerTitle: { ...TYPE.headline, color: c.primaryDark }, airlineBar: { backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, maxHeight: 62, }, airlineBarContent: { - paddingHorizontal: 12, paddingVertical: 10, gap: 8, + paddingHorizontal: SPACING.md, paddingVertical: 10, gap: SPACING.sm, }, airlineChip: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 14, paddingVertical: 7, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, backgroundColor: c.card, }, airlineCode: { fontSize: 11, fontWeight: '800', color: c.textSub }, - airlineName: { fontSize: 12, fontWeight: '600', color: c.textSub }, + airlineName: { ...TYPE.caption, color: c.textSub }, content: { flex: 1 }, contentPad: { padding: 14, paddingBottom: 80 }, banner: { @@ -615,14 +621,14 @@ function makeStyles(c: ThemeColors) { }, bannerCode: { fontSize: 28, fontWeight: '900', letterSpacing: 1 }, bannerName: { fontSize: 15, fontWeight: '600', marginTop: 2 }, - bannerSub: { fontSize: 12, marginTop: 4 }, + bannerSub: { fontSize: 12, marginTop: SPACING.xs }, addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, - paddingVertical: 10, paddingHorizontal: 12, - borderWidth: 1, borderStyle: 'dashed', borderRadius: 8, - marginBottom: 8, + paddingVertical: 10, paddingHorizontal: SPACING.md, + borderWidth: 1, borderStyle: 'dashed', borderRadius: RADIUS.sm, + marginBottom: SPACING.sm, }, - addBtnText: { fontSize: 13, fontWeight: '600' }, + addBtnText: { ...TYPE.callout }, }); } @@ -956,6 +962,7 @@ function ItemModal({ } export default function ManualsScreen() { + const { t } = useLanguage(); const { colors } = useAppTheme(); const s = useMemo(() => makeStyles(colors), [colors]); const [airlines, setAirlines] = useState(DEFAULT_AIRLINES); @@ -997,7 +1004,7 @@ export default function ManualsScreen() { Manuali DCS - setEditMode(v => !v)} style={{ marginLeft: 'auto' }}> + setEditMode(v => !v)} style={{ marginLeft: 'auto' }} accessibilityRole="button" accessibilityLabel={t('a11yEdit')}> setActiveTab(tab)} style={{ - paddingHorizontal: 20, paddingVertical: 8, borderRadius: 8, + paddingHorizontal: SPACING.xl, paddingVertical: SPACING.sm, borderRadius: RADIUS.sm, backgroundColor: activeTab === tab ? colors.primary : 'transparent', borderWidth: activeTab === tab ? 0 : 1, borderColor: colors.border, diff --git a/src/screens/NotepadScreen.tsx b/src/screens/NotepadScreen.tsx index 9114a9e..377081e 100644 --- a/src/screens/NotepadScreen.tsx +++ b/src/screens/NotepadScreen.tsx @@ -6,7 +6,9 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; +import { SPACING } from '../theme/spacing'; const STORAGE_KEY = 'aerostaff_notepad_v1'; @@ -15,26 +17,26 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 16, paddingVertical: 12, + paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - title: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, - actions: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - iconBtn: { padding: 8, borderRadius: 10 }, + titleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, + title: { ...TYPE.headline, color: c.primaryDark }, + actions: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, + iconBtn: { padding: SPACING.sm, borderRadius: 10 }, saveBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, - paddingHorizontal: 14, paddingVertical: 8, + paddingHorizontal: 14, paddingVertical: SPACING.sm, }, // 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 }, + saveTxt: { ...TYPE.callout, color: '#fff' }, statusBar: { flexDirection: 'row', alignItems: 'center', gap: 6, - paddingHorizontal: 16, paddingVertical: 6, + paddingHorizontal: SPACING.lg, paddingVertical: 6, backgroundColor: c.bg, borderBottomWidth: 1, borderBottomColor: c.border, }, @@ -109,7 +111,7 @@ export default function NotepadScreen() { {t('notepadTitle')} - + - SETUP GUIDATO + SETUP GUIDATO Prepara AeroStaff Pro Configura il minimo utile: aeroporto, calendario, fonti voli, notifiche e widget. @@ -187,28 +188,28 @@ export default function OnboardingScreen({ {item.title} - {item.required && richiesto} + {item.required && richiesto} {item.detail} {item.id === 'profile' && ( - Apri + Apri )} {item.id === 'calendar' && item.status !== 'ready' && ( - Consenti + Consenti )} {item.id === 'flightData' && ( - API + API )} {item.id === 'notifications' && item.status !== 'ready' && ( - Attiva + Attiva )} @@ -246,13 +247,13 @@ export default function OnboardingScreen({ const styles = StyleSheet.create({ root: { flex: 1 }, - content: { padding: 16, paddingBottom: 116, gap: 14 }, - heroText: { flex: 1, gap: 4 }, + content: { padding: SPACING.lg, paddingBottom: 116, gap: 14 }, + heroText: { flex: 1, gap: SPACING.xs }, heroIcon: { width: 56, height: 56, borderRadius: 18, alignItems: 'center', justifyContent: 'center' }, kicker: { fontSize: 10, fontWeight: '900', letterSpacing: 1.7 }, title: { fontSize: 28, fontWeight: '900', letterSpacing: -0.7 }, copy: { fontSize: 13, lineHeight: 19 }, - progressCard: { borderWidth: 1, borderRadius: 20, padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12 }, + progressCard: { borderWidth: 1, borderRadius: RADIUS.xl, padding: SPACING.lg, flexDirection: 'row', alignItems: 'center', gap: SPACING.md }, progressTitle: { fontSize: 18, fontWeight: '900' }, progressSub: { fontSize: 12, lineHeight: 17, marginTop: 2 }, steps: { gap: 10 }, @@ -263,15 +264,15 @@ const styles = StyleSheet.create({ stepTitle: { fontSize: 15, fontWeight: '900' }, required: { fontSize: 9, fontWeight: '900', letterSpacing: 0.8, textTransform: 'uppercase' }, stepDetail: { fontSize: 12, lineHeight: 17 }, - stepAction: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 8 }, + stepAction: { borderWidth: 1, borderRadius: RADIUS.md, paddingHorizontal: 10, paddingVertical: SPACING.sm }, stepActionText: { fontSize: 12, fontWeight: '900' }, airportCard: { borderWidth: 1, borderRadius: 18, padding: 14 }, airportLabel: { fontSize: 10, fontWeight: '900', letterSpacing: 1.2, textTransform: 'uppercase' }, airportValue: { fontSize: 15, fontWeight: '900', marginTop: 3 }, footerActions: { flexDirection: 'row', gap: 10 }, - secondaryBtn: { flex: 1, borderWidth: 1, borderRadius: 16, paddingVertical: 14, alignItems: 'center' }, + secondaryBtn: { flex: 1, borderWidth: 1, borderRadius: RADIUS.lg, paddingVertical: 14, alignItems: 'center' }, secondaryText: { fontSize: 14, fontWeight: '900' }, - primaryBtn: { flex: 1.3, borderRadius: 16, paddingVertical: 14, alignItems: 'center' }, + primaryBtn: { flex: 1.3, borderRadius: RADIUS.lg, paddingVertical: 14, alignItems: 'center' }, primaryText: { color: '#fff', fontSize: 14, fontWeight: '900' }, }); @@ -280,7 +281,7 @@ function makeStyles(isOperations: boolean) { hero: { borderWidth: 1, borderRadius: isOperations ? 26 : 22, - padding: 16, + padding: SPACING.lg, flexDirection: 'row', gap: 14, alignItems: 'center', diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index f706c3b..30ca468 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -7,8 +7,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import * as SecureStore from 'expo-secure-store'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; import { secureWipeAsyncStorageItem } from '../utils/secureWipe'; +import { SPACING, RADIUS } from '../theme/spacing'; const PASSWORDS_KEY = 'aerostaff_passwords_v1'; const PIN_KEY = 'aerostaff_pin_v1'; @@ -106,7 +108,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => return ( - + {title} {[0,1,2,3].map(i => ( @@ -117,7 +119,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => {keys.map((k, i) => ( k === '' ? : k === '⌫' ? ( - + ) : ( @@ -128,7 +130,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => ))} {onCancel && ( - + Annulla )} @@ -140,6 +142,7 @@ function PinOverlay({ onUnlock, onCancel, title }: { onUnlock: (pin: string) => // ─── Password Row ───────────────────────────────────────────────────────────── function PasswordRowComponent({ item, onEdit, onDelete }: { item: PasswordEntry; onEdit: () => void; onDelete: () => void }) { const { colors } = useAppTheme(); + const { t } = useLanguage(); const s = useMemo(() => makeRowStyles(colors), [colors]); const [revealed, setRevealed] = useState(false); @@ -150,17 +153,17 @@ function PasswordRowComponent({ item, onEdit, onDelete }: { item: PasswordEntry; {item.username ? {item.username} : null} {revealed ? item.password : '••••••••'} - setRevealed(r => !r)} style={s.eyeBtn}> + setRevealed(r => !r)} style={s.eyeBtn} accessibilityRole="button" accessibilityLabel={t(revealed ? 'a11yHidePassword' : 'a11yShowPassword')}> {item.notes ? {item.notes} : null} - + - + @@ -328,7 +331,7 @@ export default function PasswordScreen() { onDelete={() => deleteEntry(item.id)} /> )} - contentContainerStyle={{ padding: 16, paddingBottom: 96 }} + contentContainerStyle={{ padding: SPACING.lg, paddingBottom: 96 }} ListEmptyComponent={ @@ -367,12 +370,12 @@ export default function PasswordScreen() { secureTextEntry={!showPw} autoCapitalize="none" /> - setShowPw(p => !p)} style={s.eyeModal}> + setShowPw(p => !p)} style={s.eyeModal} accessibilityRole="button" accessibilityLabel={t(showPw ? 'a11yHidePassword' : 'a11yShowPassword')}> - {t('passwordNotesLabel')} + {t('passwordNotesLabel')} setModal(m => ({ ...m, notes: v }))} placeholder="es. scade ogni 90 giorni…" placeholderTextColor={colors.textMuted} multiline numberOfLines={3} textAlignVertical="top" /> @@ -395,12 +398,12 @@ export default function PasswordScreen() { 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 }, - title: { fontSize: 16, fontWeight: '700', color: c.text, marginBottom: 24 }, - dots: { flexDirection: 'row', gap: 16, marginBottom: 32 }, - dot: { width: 16, height: 16, borderRadius: 8, borderWidth: 2, borderColor: c.primary, backgroundColor: 'transparent' }, + box: { alignItems: 'center', padding: SPACING.xxxl, width: '100%', maxWidth: 320 }, + title: { ...TYPE.subhead, color: c.text, marginBottom: SPACING.xxl }, + dots: { flexDirection: 'row', gap: SPACING.lg, marginBottom: SPACING.xxxl }, + dot: { width: 16, height: 16, borderRadius: RADIUS.sm, borderWidth: 2, borderColor: c.primary, backgroundColor: 'transparent' }, dotFilled: { backgroundColor: c.primary }, - grid: { flexDirection: 'row', flexWrap: 'wrap', width: 240, justifyContent: 'center', gap: 12 }, + grid: { flexDirection: 'row', flexWrap: 'wrap', width: 240, justifyContent: 'center', gap: SPACING.md }, key: { width: 64, height: 64, borderRadius: 32, backgroundColor: c.card, borderWidth: 1, borderColor: c.border, justifyContent: 'center', alignItems: 'center' }, keyEmpty:{ width: 64, height: 64 }, keyText: { fontSize: 22, fontWeight: '600', color: c.text }, @@ -409,15 +412,15 @@ function makePinStyles(c: ThemeColors) { function makeRowStyles(c: ThemeColors) { return StyleSheet.create({ - card: { backgroundColor: c.card, borderRadius: 16, padding: 14, marginBottom: 10, flexDirection: 'row', alignItems: 'flex-start', borderWidth: 1, borderColor: c.glassBorder, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 8, elevation: c.isDark ? 0 : 3 }, + card: { backgroundColor: c.card, borderRadius: RADIUS.lg, padding: 14, marginBottom: 10, flexDirection: 'row', alignItems: 'flex-start', borderWidth: 1, borderColor: c.glassBorder, shadowColor: c.primary, shadowOpacity: c.isDark ? 0 : 0.08, shadowRadius: 8, elevation: c.isDark ? 0 : 3 }, cardLeft:{ flex: 1 }, name: { fontSize: 15, fontWeight: '700', color: c.primaryDark, marginBottom: 2 }, - username:{ fontSize: 12, color: c.textSub, marginBottom: 4 }, + username:{ fontSize: 12, color: c.textSub, marginBottom: SPACING.xs }, pwRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 2 }, pw: { fontSize: 13, color: c.text, letterSpacing: 1 }, eyeBtn: { padding: 2 }, - notes: { fontSize: 11, color: c.textMuted, fontStyle: 'italic', marginTop: 4 }, - actions: { flexDirection: 'column', gap: 6, marginLeft: 8 }, + notes: { fontSize: 11, color: c.textMuted, fontStyle: 'italic', marginTop: SPACING.xs }, + actions: { flexDirection: 'column', gap: 6, marginLeft: SPACING.sm }, editBtn: { width: 32, height: 32, borderRadius: 9, backgroundColor: c.primaryLight, justifyContent: 'center', alignItems: 'center' }, delBtn: { width: 32, height: 32, borderRadius: 9, backgroundColor: '#FEF2F2', justifyContent: 'center', alignItems: 'center' }, }); @@ -426,30 +429,30 @@ function makeRowStyles(c: ThemeColors) { 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 }, - titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - title: { fontSize: 17, fontWeight: '700', color: c.primaryDark }, - toolbarActions:{ flexDirection: 'row', alignItems: 'center', gap: 8 }, + toolbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, + titleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, + title: { ...TYPE.headline, color: c.primaryDark }, + toolbarActions:{ flexDirection: 'row', alignItems: 'center', gap: SPACING.sm }, iconBtn: { width: 36, height: 36, borderRadius: 10, backgroundColor: c.cardSecondary, justifyContent: 'center', alignItems: 'center' }, iconBtnActive:{ backgroundColor: c.primary }, - addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8 }, + addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm }, addBtnTxt: { color: '#fff', fontWeight: '600', fontSize: 13 }, - empty: { alignItems: 'center', marginTop: 80, gap: 8 }, + empty: { alignItems: 'center', marginTop: 80, gap: SPACING.sm }, emptyTxt: { fontSize: 16, fontWeight: '600', color: c.textSub }, emptySubTxt: { fontSize: 13, color: c.textMuted }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, modalScrollContent: { flexGrow: 1, justifyContent: 'flex-end' }, - modalBox: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: Platform.OS === 'ios' ? 40 : 24, maxHeight: '92%' }, - modalTitle: { fontSize: 18, fontWeight: '700', color: c.primaryDark, marginBottom: 20 }, - label: { fontSize: 12, fontWeight: '600', color: c.textSub, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 }, + modalBox: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: SPACING.xxl, paddingBottom: Platform.OS === 'ios' ? 40 : 24, maxHeight: '92%' }, + modalTitle: { ...TYPE.headline, color: c.primaryDark, marginBottom: SPACING.xl }, + label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.5 }, input: { backgroundColor: c.bg, borderWidth: 1, borderColor: c.border, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, color: c.text, marginBottom: 14 }, inputMulti: { height: 80, paddingTop: 10 }, - pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 14 }, + pwInputRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 14 }, eyeModal: { padding: 10 }, - modalBtns: { flexDirection: 'row', gap: 10, marginTop: 8 }, - cancelBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, backgroundColor: c.bg, alignItems: 'center', borderWidth: 1, borderColor: c.border }, + modalBtns: { flexDirection: 'row', gap: 10, marginTop: SPACING.sm }, + cancelBtn: { flex: 1, paddingVertical: 14, borderRadius: RADIUS.md, backgroundColor: c.bg, alignItems: 'center', borderWidth: 1, borderColor: c.border }, cancelTxt: { fontSize: 15, fontWeight: '600', color: c.textSub }, - saveBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, backgroundColor: c.primary, alignItems: 'center' }, + saveBtn: { flex: 1, paddingVertical: 14, borderRadius: RADIUS.md, backgroundColor: c.primary, alignItems: 'center' }, saveTxt: { fontSize: 15, fontWeight: '700', color: '#fff' }, }); } diff --git a/src/screens/PhonebookScreen.tsx b/src/screens/PhonebookScreen.tsx index b45efbb..694fdb2 100644 --- a/src/screens/PhonebookScreen.tsx +++ b/src/screens/PhonebookScreen.tsx @@ -6,7 +6,9 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { TYPE } from '../theme/typography'; import { useLanguage } from '../context/LanguageContext'; +import { SPACING, RADIUS } from '../theme/spacing'; const STORAGE_KEY = 'aerostaff_phonebook_v1'; @@ -51,37 +53,37 @@ function makeModalStyles(c: ThemeColors) { sheet: { backgroundColor: c.card, borderTopLeftRadius: 24, borderTopRightRadius: 24, - padding: 20, paddingBottom: 36, maxHeight: '92%', + padding: SPACING.xl, paddingBottom: 36, maxHeight: '92%', }, handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: c.border, alignSelf: 'center', marginBottom: 18, }, - title: { fontSize: 18, fontWeight: '700', color: c.primaryDark, marginBottom: 16 }, - label: { fontSize: 12, fontWeight: '600', color: c.textSub, marginBottom: 6, marginTop: 12 }, + title: { ...TYPE.headline, color: c.primaryDark, marginBottom: SPACING.lg }, + label: { ...TYPE.caption, color: c.textSub, marginBottom: 6, marginTop: SPACING.md }, input: { - borderWidth: 1.5, borderColor: c.border, borderRadius: 12, + borderWidth: 1.5, borderColor: c.border, borderRadius: RADIUS.md, paddingHorizontal: 14, paddingVertical: 11, fontSize: 15, color: c.text, backgroundColor: c.cardSecondary, }, - catRow: { marginBottom: 4 }, + catRow: { marginBottom: SPACING.xs }, catChip: { paddingHorizontal: 14, paddingVertical: 7, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, - marginRight: 8, backgroundColor: c.card, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, + marginRight: SPACING.sm, backgroundColor: c.card, }, - catTxt: { fontSize: 12, fontWeight: '600', color: c.textSub }, - actions: { flexDirection: 'row', gap: 10, marginTop: 20 }, + catTxt: { ...TYPE.caption, color: c.textSub }, + actions: { flexDirection: 'row', gap: 10, marginTop: SPACING.xl }, cancelBtn: { flex: 1, borderWidth: 1.5, borderColor: c.border, - borderRadius: 12, paddingVertical: 13, alignItems: 'center', + borderRadius: RADIUS.md, paddingVertical: 13, alignItems: 'center', }, cancelTxt: { fontSize: 14, fontWeight: '600', color: c.textSub }, saveBtn: { flex: 2, backgroundColor: c.primary, - borderRadius: 12, paddingVertical: 13, - flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, + borderRadius: RADIUS.md, paddingVertical: 13, + flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: SPACING.sm, }, saveTxt: { fontSize: 14, fontWeight: '700', color: '#fff' }, }); @@ -216,19 +218,19 @@ function makeRowStyles(c: ThemeColors) { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: c.isDark ? 0 : 0.05, shadowRadius: 4, elevation: c.isDark ? 0 : 2, borderWidth: c.isDark ? 1 : 0, borderColor: c.border, }, - dot: { width: 4, borderRadius: 2, alignSelf: 'stretch', marginRight: 12 }, + dot: { width: 4, borderRadius: 2, alignSelf: 'stretch', marginRight: SPACING.md }, info: { flex: 1 }, - topRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 3 }, + topRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginBottom: 3 }, name: { fontSize: 14, fontWeight: '700', color: c.text, flex: 1 }, badge: { - paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, + paddingHorizontal: SPACING.sm, paddingVertical: 2, borderRadius: 10, }, badgeTxt: { fontSize: 10, fontWeight: '700' }, number: { fontSize: 13, color: c.textSub, fontWeight: '500' }, note: { fontSize: 11, color: c.textMuted, marginTop: 2 }, callBtn: { width: 36, height: 36, borderRadius: 18, - justifyContent: 'center', alignItems: 'center', marginLeft: 8, + justifyContent: 'center', alignItems: 'center', marginLeft: SPACING.sm, }, editBtn: { padding: 6, marginLeft: 2 }, }); @@ -273,13 +275,13 @@ function ContactRowComponent({ contact, onEdit, onDelete }: ContactRowProps) { {contact.number} {!!contact.note && {contact.note}} - + - onEdit(contact)}> + onEdit(contact)} accessibilityRole="button" accessibilityLabel={t('a11yEdit')}> - + @@ -296,39 +298,39 @@ function makeStyles(c: ThemeColors) { root: { flex: 1, backgroundColor: c.bg }, header: { flexDirection: 'row', alignItems: 'center', gap: 10, - paddingHorizontal: 16, paddingVertical: 12, + paddingHorizontal: SPACING.lg, paddingVertical: SPACING.md, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border, }, - headerTitle: { fontSize: 17, fontWeight: '700', color: c.primaryDark, flex: 1 }, + headerTitle: { ...TYPE.headline, color: c.primaryDark, flex: 1 }, addBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: c.primary, borderRadius: 10, - paddingHorizontal: 12, paddingVertical: 8, + paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, }, addTxt: { color: '#fff', fontWeight: '700', fontSize: 13 }, searchRow: { flexDirection: 'row', alignItems: 'center', gap: 10, - margin: 12, paddingHorizontal: 14, paddingVertical: 10, + margin: SPACING.md, paddingHorizontal: 14, paddingVertical: 10, backgroundColor: c.card, borderRadius: 14, borderWidth: 1.5, borderColor: c.border, }, searchInput: { flex: 1, fontSize: 14, color: c.text }, filterBar: { maxHeight: 50, backgroundColor: c.card, borderBottomWidth: 1, borderBottomColor: c.border }, - filterContent: { paddingHorizontal: 12, paddingVertical: 9, gap: 8 }, + filterContent: { paddingHorizontal: SPACING.md, paddingVertical: 9, gap: SPACING.sm }, filterChip: { paddingHorizontal: 14, paddingVertical: 6, - borderRadius: 20, borderWidth: 1.5, borderColor: c.border, + borderRadius: RADIUS.xl, borderWidth: 1.5, borderColor: c.border, backgroundColor: c.card, }, filterChipActive: { backgroundColor: c.primary, borderColor: c.primary }, - filterTxt: { fontSize: 12, fontWeight: '600', color: c.textSub }, + filterTxt: { ...TYPE.caption, color: c.textSub }, filterTxtActive: { color: '#fff' }, list: { flex: 1 }, - listPad: { padding: 16, paddingBottom: 96 }, + listPad: { padding: SPACING.lg, paddingBottom: 96 }, groupHeader: { - flexDirection: 'row', alignItems: 'center', gap: 8, - marginBottom: 8, marginTop: 6, + flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, + marginBottom: SPACING.sm, marginTop: 6, }, groupDot: { width: 8, height: 8, borderRadius: 4 }, groupLabel: { fontSize: 11, fontWeight: '700', color: c.textSub, letterSpacing: 0.8, flex: 1 }, @@ -421,7 +423,7 @@ export default function PhonebookScreen() { autoCorrect={false} /> {!!search && ( - setSearch('')}> + setSearch('')} accessibilityRole="button" accessibilityLabel={t('a11yClearSearch')}> )} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index aeb4e02..9577ece 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -46,6 +46,7 @@ import { type FlightProviderDiagnosticsSnapshot, } from '../utils/fr24api'; import { formatProviderDiagnostic } from '../utils/flightDiagnostics'; +import { SPACING, RADIUS } from '../theme/spacing'; import { getStaffMonitorDebugColumns, getStaffMonitorDebugFlights, @@ -142,7 +143,7 @@ function ThemeCard({ option, selected, onSelect, activeLabel }: { - + {option.label} {selected && ( @@ -679,9 +680,9 @@ export default function SettingsScreen({ {t('sectionTheme')} {isLoading ? ( - + - + {t('themeLoading')} @@ -787,7 +788,7 @@ export default function SettingsScreen({ {checkingUpdate ? : } - + {checkingUpdate ? 'Controllo…' : 'Controlla'} @@ -991,6 +992,7 @@ export default function SettingsScreen({ style={[styles.providerCloseBtn, { backgroundColor: colors.cardSecondary }]} onPress={closeProviderModal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yClose')} > @@ -1373,6 +1375,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshProviderDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1423,17 +1426,17 @@ export default function SettingsScreen({ )} - + {t('flightDebugStaffMonitorParser')} {staffMonitorDebug || t('flightDebugNoStaffMonitorDebug')} - + {t('flightDebugStaffMonitorRawArrivals')} - + {t('flightDebugStaffMonitorRawArrivalsSub')} { refreshNotificationDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1528,7 +1532,7 @@ export default function SettingsScreen({ : t('notificationDebugNoPending')} - + {t('notificationDebugDuplicateList')} @@ -1540,7 +1544,7 @@ export default function SettingsScreen({ : t('notificationDebugNoDuplicates')} - + {t('notificationDebugLastEvents')} @@ -1593,6 +1597,7 @@ export default function SettingsScreen({ style={[styles.providerCloseBtn, { backgroundColor: colors.cardSecondary }]} onPress={closeDebugModal} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yClose')} > @@ -1677,6 +1682,7 @@ export default function SettingsScreen({ style={[styles.providerRefreshBtn, { backgroundColor: colors.cardSecondary, borderColor: colors.border }]} onPress={() => { refreshProviderDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1727,17 +1733,17 @@ export default function SettingsScreen({ )} - + {t('flightDebugStaffMonitorParser')} {staffMonitorDebug || t('flightDebugNoStaffMonitorDebug')} - + {t('flightDebugStaffMonitorRawArrivals')} - + {t('flightDebugStaffMonitorRawArrivalsSub')} { refreshNotificationDebug().catch(() => {}); }} activeOpacity={0.85} + accessibilityRole="button" accessibilityLabel={t('a11yRefresh')} > @@ -1851,23 +1858,23 @@ export default function SettingsScreen({ const styles = StyleSheet.create({ root: { flex: 1 }, - content: { padding: 16, paddingBottom: 96 }, + content: { padding: SPACING.lg, paddingBottom: 96 }, banner: { flexDirection: 'row', alignItems: 'center', gap: 14, - borderRadius: 16, padding: 18, marginBottom: 20, + borderRadius: RADIUS.lg, padding: 18, marginBottom: SPACING.xl, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.07, shadowRadius: 8, elevation: 3, }, - bannerIcon: { width: 52, height: 52, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + bannerIcon: { width: 52, height: 52, borderRadius: RADIUS.lg, justifyContent: 'center', alignItems: 'center' }, bannerTitle: { ...TYPE.headline }, bannerSub: { fontSize: 12, marginTop: 2 }, - sectionTitle: { ...TYPE.overline, marginBottom: 8, paddingLeft: 4, marginTop: 4 }, + sectionTitle: { ...TYPE.overline, marginBottom: SPACING.sm, paddingLeft: SPACING.xs, marginTop: SPACING.xs }, // Theme grid - themeGrid: { flexDirection: 'row', gap: 10, marginBottom: 20, flexWrap: 'wrap' }, + themeGrid: { flexDirection: 'row', gap: 10, marginBottom: SPACING.xl, flexWrap: 'wrap' }, themeCard: { flex: 1, minWidth: 100, - borderRadius: 16, borderWidth: 2, + borderRadius: RADIUS.lg, borderWidth: 2, overflow: 'hidden', }, themeCardSelected: { @@ -1875,39 +1882,39 @@ const styles = StyleSheet.create({ }, preview: { height: 80, justifyContent: 'space-between' }, previewBar: { height: 12, width: '100%' }, - previewContent: { flex: 1, padding: 6, gap: 4 }, + previewContent: { flex: 1, padding: 6, gap: SPACING.xs }, previewCard: { height: 10, borderRadius: 4, width: '100%' }, previewTab: { height: 14, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }, previewDot: { width: 8, height: 8, borderRadius: 4 }, themeInfo: { padding: 10 }, - themeInfoTop: { flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 3, flexWrap: 'wrap' }, + themeInfoTop: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, marginBottom: 3, flexWrap: 'wrap' }, themeLabel: { fontSize: 13, fontWeight: '700' }, - activeBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8, flexShrink: 0 }, + activeBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: RADIUS.sm, flexShrink: 0 }, activeBadgeTxt: { fontSize: 9, fontWeight: '800', color: '#fff' }, themeSub: { fontSize: 10, lineHeight: 14 }, // Generic rows card: { - borderRadius: 16, marginBottom: 20, + borderRadius: RADIUS.lg, marginBottom: SPACING.xl, shadowColor: '#F47B16', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, overflow: 'hidden', }, divider: { height: 1, marginLeft: 56 }, - row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 13, gap: 12 }, + row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING.lg, paddingVertical: 13, gap: SPACING.md }, iconWrap:{ width: 34, height: 34, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, rowText: { flex: 1 }, rowLabel:{ fontSize: 14, fontWeight: '600' }, rowSub: { fontSize: 12, marginTop: 1 }, - modalOverlay: { flex: 1, justifyContent: 'center', padding: 20, backgroundColor: 'rgba(15,23,42,0.48)' }, - modalCopy: { fontSize: 13, lineHeight: 20, marginBottom: 16 }, - modalLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 0.8, marginBottom: 8 }, + modalOverlay: { flex: 1, justifyContent: 'center', padding: SPACING.xl, backgroundColor: 'rgba(15,23,42,0.48)' }, + modalCopy: { fontSize: 13, lineHeight: 20, marginBottom: SPACING.lg }, + modalLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 0.8, marginBottom: SPACING.sm }, modalInput: { ...TYPE.subhead, borderWidth: 1, borderRadius: 14, paddingHorizontal: 14, - paddingVertical: 12, - marginBottom: 16, + paddingVertical: SPACING.md, + marginBottom: SPACING.lg, }, modalActions: { flexDirection: 'row', gap: 10 }, modalBtn: { flex: 1, borderRadius: 14, paddingVertical: 14, alignItems: 'center' }, @@ -1919,7 +1926,7 @@ const styles = StyleSheet.create({ gap: 14, paddingHorizontal: 18, paddingTop: Platform.OS === 'android' ? 26 : 54, - paddingBottom: 16, + paddingBottom: SPACING.lg, borderBottomWidth: 1, }, providerCloseBtn: { @@ -1932,13 +1939,13 @@ const styles = StyleSheet.create({ providerHeaderText: { flex: 1 }, providerTitle: { fontSize: 22, fontWeight: '900' }, providerSubtitle: { fontSize: 13, lineHeight: 18, marginTop: 2 }, - providerContent: { padding: 16, paddingBottom: 42 }, + providerContent: { padding: SPACING.lg, paddingBottom: 42 }, providerOptions: { gap: 10, marginBottom: 22 }, - providerSectionHelp: { fontSize: 12, lineHeight: 17, marginTop: -2, marginBottom: 12, paddingHorizontal: 4 }, + providerSectionHelp: { fontSize: 12, lineHeight: 17, marginTop: -2, marginBottom: SPACING.md, paddingHorizontal: SPACING.xs }, providerOption: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: SPACING.md, borderRadius: 18, borderWidth: 1, padding: 14, @@ -1954,10 +1961,10 @@ const styles = StyleSheet.create({ providerOptionTitle: { fontSize: 15, fontWeight: '800' }, providerOptionSub: { fontSize: 12, lineHeight: 17, marginTop: 2 }, providerKeyCard: { - borderRadius: 20, + borderRadius: RADIUS.xl, borderWidth: 1, - padding: 16, - marginBottom: 20, + padding: SPACING.lg, + marginBottom: SPACING.xl, }, providerKeyCardFeatured: { borderWidth: 2, @@ -1967,7 +1974,7 @@ const styles = StyleSheet.create({ shadowRadius: 18, elevation: 4, }, - gatewayToggle: { flexDirection: 'row', gap: 8, marginBottom: 16 }, + gatewayToggle: { flexDirection: 'row', gap: SPACING.sm, marginBottom: SPACING.lg }, gatewayBtn: { flex: 1, borderWidth: 1, @@ -1976,9 +1983,9 @@ const styles = StyleSheet.create({ alignItems: 'center', }, gatewayBtnText: { fontSize: 12, fontWeight: '900' }, - providerKeyTop: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 14 }, - providerKeyTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, - providerKeyBadge: { borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3 }, + providerKeyTop: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, marginBottom: 14 }, + providerKeyTitleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, flexWrap: 'wrap' }, + providerKeyBadge: { borderRadius: RADIUS.pill, paddingHorizontal: SPACING.sm, paddingVertical: 3 }, providerKeyBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5, textTransform: 'uppercase' }, providerLinkGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 }, providerLinkBtn: { @@ -1987,7 +1994,7 @@ const styles = StyleSheet.create({ gap: 6, borderRadius: 13, borderWidth: 1, - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, }, providerLinkText: { fontSize: 12, fontWeight: '800' }, @@ -1999,13 +2006,13 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, - debugMetaGrid: { flexDirection: 'row', gap: 8, marginBottom: 12 }, - debugMetaWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 12 }, + debugMetaGrid: { flexDirection: 'row', gap: SPACING.sm, marginBottom: SPACING.md }, + debugMetaWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm, marginBottom: SPACING.md }, debugMetaPill: { flex: 1, borderRadius: 13, paddingHorizontal: 10, paddingVertical: 9 }, debugMetaPillSmall: { minWidth: 92, flexGrow: 1, borderRadius: 13, paddingHorizontal: 10, paddingVertical: 9 }, debugMetaLabel: { ...TYPE.micro, textTransform: 'uppercase' }, debugMetaValue: { fontSize: 15, fontWeight: '900', marginTop: 2 }, - debugProviderList: { gap: 8 }, + debugProviderList: { gap: SPACING.sm }, debugProviderRow: { flexDirection: 'row', alignItems: 'center', @@ -2020,7 +2027,7 @@ const styles = StyleSheet.create({ debugMono: { borderWidth: 1, borderRadius: 14, - padding: 12, + padding: SPACING.md, fontSize: 11, lineHeight: 16, }, @@ -2028,10 +2035,10 @@ const styles = StyleSheet.create({ notificationClearBtn: { flexDirection: 'row', alignItems: 'center', - gap: 8, + gap: SPACING.sm, borderWidth: 1, borderRadius: 13, - paddingHorizontal: 12, + paddingHorizontal: SPACING.md, paddingVertical: 10, }, notificationClearText: { color: '#DC2626', fontSize: 12, fontWeight: '900' }, @@ -2093,31 +2100,31 @@ const styles = StyleSheet.create({ // Update card updateCard: { - borderRadius: 16, marginBottom: 20, padding: 16, + borderRadius: RADIUS.lg, marginBottom: SPACING.xl, padding: SPACING.lg, borderWidth: 1, shadowColor: '#F47B16', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, - updateTop: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 14 }, + updateTop: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md, marginBottom: 14 }, updateIconWrap:{ width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, updateTitle: { fontSize: 15, fontWeight: '700' }, updateSub: { fontSize: 12, marginTop: 2 }, - newBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10 }, + newBadge: { paddingHorizontal: SPACING.sm, paddingVertical: 3, borderRadius: 10 }, newBadgeTxt: { fontSize: 11, fontWeight: '800', color: '#fff' }, updateActions: { flexDirection: 'row', gap: 10 }, updateBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, - borderRadius: 12, paddingVertical: 10, paddingHorizontal: 16, + borderRadius: RADIUS.md, paddingVertical: 10, paddingHorizontal: SPACING.lg, }, updateBtnTxt: { fontSize: 13, fontWeight: '700' }, // Backup tiles - backupRow: { flexDirection: 'row', gap: 12, marginBottom: 20 }, + backupRow: { flexDirection: 'row', gap: SPACING.md, marginBottom: SPACING.xl }, backupTile: { - flex: 1, borderRadius: 16, padding: 18, alignItems: 'center', gap: 8, + flex: 1, borderRadius: RADIUS.lg, padding: 18, alignItems: 'center', gap: SPACING.sm, borderWidth: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2, }, - backupTileIcon: { width: 52, height: 52, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + backupTileIcon: { width: 52, height: 52, borderRadius: RADIUS.lg, justifyContent: 'center', alignItems: 'center' }, backupTileLabel:{ fontSize: 14, fontWeight: '800' }, backupTileSub: { fontSize: 11, textAlign: 'center' }, }); diff --git a/src/screens/ShiftScreen.tsx b/src/screens/ShiftScreen.tsx deleted file mode 100644 index 64e5dc1..0000000 --- a/src/screens/ShiftScreen.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { View, Text, StyleSheet, ActivityIndicator, ScrollView, Alert, TouchableOpacity, Image, Linking } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import * as ImagePicker from 'expo-image-picker'; -import { WebView } from 'react-native-webview'; -import * as Calendar from 'expo-calendar'; -import { useLanguage } from '../context/LanguageContext'; -import { TYPE } from '../theme/typography'; - -const PRIMARY = '#F47B16'; -const DARK_ORANGE = '#C2520A'; -const BG = '#F3F4F6'; - -export default function ShiftScreen() { - const { t } = useLanguage(); - const [imageList, setImageList] = useState([]); - const [ocrText, setOcrText] = useState(''); - const [processing, setProcessing] = useState(false); - const webViewRef = useRef(null); - - const pickImage = async () => { - try { - let result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ['images'], - allowsMultipleSelection: true, - selectionLimit: 0, - orderedSelection: true, - quality: 1, - base64: true, - }); - - if (!result.canceled && result.assets && result.assets.length > 0) { - setImageList(result.assets.map(a => a.uri)); - setProcessing(true); - setOcrText(''); - - const base64List = result.assets - .map(a => a.base64 ? `data:${a.mimeType || 'image/jpeg'};base64,${a.base64}` : null) - .filter((item): item is string => !!item); - if (base64List.length === 0) { - setProcessing(false); - Alert.alert('Errore OCR', 'Nessuna immagine leggibile selezionata.'); - return; - } - const base64Json = JSON.stringify(base64List); - - const jsCode = ` - if (window.runTesseract) { - window.runTesseract(${JSON.stringify(base64Json)}); - } else { - window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: "Motore OCR non pronto." })); - } - true; - `; - webViewRef.current?.injectJavaScript(jsCode); - } - } catch (e) { - Alert.alert("Errore OCR", "Impossibile elaborare l'immagine."); - setProcessing(false); - } - }; - - const handleWebViewMessage = (event: any) => { - const rawData = event.nativeEvent.data; - try { - const result = JSON.parse(rawData); - if (result.success) { - setOcrText(result.text); - } else { - Alert.alert("Errore", "Impossibile analizzare il documento: " + result.error); - } - } catch(e) { - console.error(e); - } finally { - setProcessing(false); - } - }; - - const parseAndSaveShifts = async () => { - const { status, canAskAgain } = await Calendar.requestCalendarPermissionsAsync(); - if (status !== 'granted') { - if (!canAskAgain) { - Alert.alert("Permesso negato", "Abilita l'accesso al calendario nelle impostazioni del dispositivo.", [ - { text: 'Annulla', style: 'cancel' }, - { text: 'Apri Impostazioni', onPress: () => Linking.openSettings() }, - ]); - } else { - Alert.alert("Permesso negato", "Devi autorizzare l'accesso al calendario del telefono."); - } - return; - } - - try { - const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); - // Su iOS isPrimary è comodo, su Android cerchiamo un calendario che accetti scritture - let targetCalendar = calendars.find(c => c.allowsModifications && c.isPrimary); - if (!targetCalendar) { - targetCalendar = calendars.find(c => c.allowsModifications); - } - - if (!targetCalendar) { - Alert.alert('Errore', t('shiftNoCalendar')); - return; - } - // Normalizzazione Estrema OCR globale prima di estrarre - const norText = ocrText.replace(/[OoQ]/g, '0').replace(/[Il|]/g, '1'); - - // Estrai tutte le date in ordine compatto - const dateRegex = /\b(\d{2})[\/\-](\d{2})[\/\-](\d{4})\b/g; - const dates: any[] = []; - let matchDate; - while ((matchDate = dateRegex.exec(norText)) !== null) { - dates.push({ - day: parseInt(matchDate[1], 10), - month: parseInt(matchDate[2], 10) - 1, // JS months are 0-indexed - year: parseInt(matchDate[3], 10), - raw: matchDate[0] - }); - } - - // Nascondiamo gli anni a 4 cifre per evitare che "2026" possa essere letto come l'orario "20:26" dall'OCR - const safeTextForTimes = norText.replace(/\b20\d{2}\b/g, ' ANNO '); - - // Estrai tutti i turni (orari o Riposo) in ordine compatto - // Tolto il flag 'i' per evitare falsi positivi sulla lettera 'r' minuscola (es. o[r]ario, ma[r]tedì, ecc.) - const shiftRegex = /\b([01]?\d|2\d)[.,:]?(\d{2})\s*[-–—_~|]+\s*([01]?\d|2\d)[.,:]?(\d{2})\b|\b(R|RIP|RIP0S0|R1P0S0|R1POSO)\b/g; - const shifts: any[] = []; - let matchShift; - while ((matchShift = shiftRegex.exec(safeTextForTimes)) !== null) { - if (matchShift[5]) { - shifts.push({ isRest: true, raw: matchShift[0] }); - } else { - shifts.push({ - isRest: false, - startH: parseInt(matchShift[1], 10), startM: parseInt(matchShift[2], 10), - endH: parseInt(matchShift[3], 10), endM: parseInt(matchShift[4], 10), - raw: matchShift[0] - }); - } - } - - let savedCount = 0; - // ZIP degli array: Associa la prima data al primo turno, la seconda al secondo, ecc. - // E' perfetto per le estrazioni in colonna! - const iterCount = Math.min(dates.length, shifts.length); - - for (let i = 0; i < iterCount; i++) { - const d = dates[i]; - const s = shifts[i]; - - // --- PREVENZIONE DUPLICATI --- - // Controlliamo l'intera giornata per evitare sovrascritture se l'operazione viene ripetuta - const dayStart = new Date(d.year, d.month, d.day, 0, 0, 0); - const dayEnd = new Date(d.year, d.month, d.day, 23, 59, 59); - const existingEvents = await Calendar.getEventsAsync([targetCalendar.id], dayStart, dayEnd); - - const isDuplicate = existingEvents.some(e => { - if (s.isRest) { - return e.title.includes("Riposo"); - } else { - // Verifica se c'è già un turno di lavoro che inizia alla stessa ora - const eStart = new Date(e.startDate); - return e.title.includes("Lavoro") && eStart.getHours() === s.startH; - } - }); - - if (isDuplicate) { - continue; // Salta alla prossima iterazione senza aggiungere - } - // ------------------------------ - - if (s.isRest) { - const alldayStart = new Date(d.year, d.month, d.day, 12, 0, 0); - const alldayEnd = new Date(d.year, d.month, d.day, 14, 0, 0); - await Calendar.createEventAsync(targetCalendar.id, { - title: 'Riposo', - startDate: alldayStart, - endDate: alldayEnd, - allDay: true, - notes: "Dati estratti: " + d.raw + " -> " + s.raw, - timeZone: 'Europe/Rome', - }); - savedCount++; - } else { - const startDate = new Date(d.year, d.month, d.day); - startDate.setHours(s.startH, s.startM, 0, 0); - - let endDate = new Date(d.year, d.month, d.day); - endDate.setHours(s.endH, s.endM, 0, 0); - - if (endDate <= startDate) { - endDate.setDate(endDate.getDate() + 1); // Notturno - } - - await Calendar.createEventAsync(targetCalendar.id, { - title: 'Turno Lavoro', - startDate: startDate, - endDate: endDate, - notes: "Dati estratti: " + d.raw + " -> " + s.raw, - timeZone: 'Europe/Rome', - }); - savedCount++; - } - } - - if (savedCount > 0) { - Alert.alert( - t('shiftSyncOkTitle'), - `${savedCount} turni salvati nel calendario.` - ); - } else { - Alert.alert("Nessun orario trovato", `Errore estrazione. Date Trovate: ${dates.length}, Orari Trovati: ${shifts.length}. Assicurati di scansionare bene le colonne.`); - } - - } catch (e: any) { - console.error(e); - Alert.alert(t('shiftCalErrTitle'), 'Non è stato possibile salvare: ' + e.message); - } - }; - - const engineHtml = ` - - - - - - - - - - - `; - - return ( - - - - - - {/* Page Header */} - - {t('shiftTitle')} - {t('shiftSub')} - - - - - - {t('shiftSyncTitle')} - - - Seleziona gli screenshot del tuo tabellone orari. Il sistema li leggerà per cercare e salvare automaticamente i voli nel calendario del tuo telefono. - - - - - - - - {t('shiftScanBtn')} - - - - - {imageList.length > 0 && ( - - {imageList.map((uri, index) => ( - - ))} - - )} - - {processing && ( - - - {t('shiftExtracting')} - - )} - - {ocrText ? ( - - {t('shiftExtractedTitle')} - {ocrText} - - - - - {t('shiftSyncBtn')} - - - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - backgroundColor: BG, - paddingBottom: 32, - }, - hiddenWebView: { height: 1, width: 1, opacity: 0, position: 'absolute', top: -100 }, - pageHeader: { - backgroundColor: '#fff', - paddingHorizontal: 16, paddingVertical: 14, - borderBottomWidth: 1, borderBottomColor: '#E5E7EB', - }, - pageTitle: { ...TYPE.title, color: DARK_ORANGE }, - pageSub: { fontSize: 13, color: '#6B7280', marginTop: 4 }, - infoCard: { - backgroundColor: '#fff', - margin: 16, marginBottom: 0, - padding: 16, borderRadius: 14, - borderLeftWidth: 4, borderLeftColor: PRIMARY, - shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 6, elevation: 2, - }, - infoTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 }, - infoTitle: { ...TYPE.subhead, color: PRIMARY }, - infoDesc: { fontSize: 13, color: '#6B7280', lineHeight: 20 }, - buttonsContainer: { margin: 16, marginBottom: 0 }, - button: { - backgroundColor: DARK_ORANGE, - padding: 16, borderRadius: 14, - alignItems: 'center', - shadowColor: DARK_ORANGE, shadowOpacity: 0.3, shadowRadius: 8, elevation: 5, - }, - buttonInner: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - buttonText: { ...TYPE.subhead, color: '#fff' }, - imagesPreview: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', margin: 16, gap: 10 }, - image: { width: '45%', height: 140, resizeMode: 'cover', borderRadius: 10, borderWidth: 1, borderColor: '#E5E7EB' }, - loadingContainer: { marginTop: 24, alignItems: 'center' }, - loadingText: { marginTop: 10, color: '#6B7280', fontWeight: '500' }, - resultContainer: { - margin: 16, padding: 16, - backgroundColor: '#fff', borderRadius: 14, - shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, elevation: 3, - }, - resultTitle: { ...TYPE.subhead, color: DARK_ORANGE, marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 8 }, - resultText: { fontSize: 13, color: '#374151', lineHeight: 20, marginBottom: 16 }, - saveButton: { - backgroundColor: PRIMARY, - padding: 15, borderRadius: 12, alignItems: 'center', - shadowColor: PRIMARY, shadowOpacity: 0.3, shadowRadius: 6, elevation: 4, - }, - saveButtonText: { ...TYPE.subhead, color: '#fff' }, -}); - diff --git a/src/screens/TraveldocScreen.tsx b/src/screens/TraveldocScreen.tsx index f45b7f1..5c8175c 100644 --- a/src/screens/TraveldocScreen.tsx +++ b/src/screens/TraveldocScreen.tsx @@ -5,6 +5,8 @@ import { WebView } from 'react-native-webview'; import { useAppTheme } from '../context/ThemeContext'; import { useLanguage } from '../context/LanguageContext'; import TactilePressable from '../components/motion/TactilePressable'; +import { TYPE, WEIGHT } from '../theme/typography'; +import { SPACING, RADIUS } from '../theme/spacing'; const DARK_CSS_JS = ` (function() { @@ -65,7 +67,7 @@ export default function TraveldocScreen({ isFocused = true }: { isFocused?: bool )} {hasActivated && loadError && !loading && ( - + Caricamento lento o errore di rete. = { + ryanair: ['ryanair', 'fr', 'ryr'], + easyjet: ['easyjet', 'easy jet', 'easyjet europe', 'easyjet switzerland', 'easyjet uk', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], + wizz: ['wizz', 'wizz air', 'wizz air malta', 'wizz air uk', 'wizz air abu dhabi', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], + volotea: ['volotea', 'v7'], + vueling: ['vueling', 'vy'], + transavia: ['transavia', 'transavia france', 'transavia holland', 'transavia airlines', 'hv', 'to', 'tra', 'tvf'], + 'aer lingus': ['aer lingus', 'ei'], + 'british airways': ['british airways', 'ba', 'baw'], + sas: ['sas', 'scandinavian', 'sk'], + scandinavian: ['sas', 'scandinavian', 'sk'], + flydubai: ['flydubai', 'fz', 'fdb'], + aeroitalia: ['aeroitalia', 'xz'], + 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], + 'air arabia': ['air arabia', 'g9', 'abz'], + 'air dolomiti': ['air dolomiti', 'en', 'dla'], + buzz: ['buzz', 'rr', 'rys'], + dhl: ['dhl', 'qy', 'bcs'], + eurowings: ['eurowings', 'ew', 'ewg'], + 'ita airways': ['ita airways', 'ita', 'az', 'ity'], + lufthansa: ['lufthansa', 'lh', 'dlh'], +}; + +export function normalizeAirlineText(value: unknown): string { + if (typeof value !== 'string' && typeof value !== 'number') return ''; + return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); +} + +export function compactAirlineText(value: unknown): string { + return normalizeAirlineText(value).replace(/\s+/g, ''); +} + +/* Gli alias corti (codici IATA/ICAO, max 3 caratteri) combaciano solo come + parola intera o valore esatto, per non far scattare "fr" dentro "france"; + quelli lunghi combaciano come sottostringa compatta ("easyjet" dentro + "easyjet europe"). */ +export function airlineAliasMatches(value: unknown, alias: string): boolean { + const normalizedValue = normalizeAirlineText(value); + const normalizedAlias = normalizeAirlineText(alias); + const compactAlias = compactAirlineText(alias); + if (!normalizedValue || !normalizedAlias || !compactAlias) return false; + + if (compactAlias.length <= 3) { + return normalizedValue.split(' ').includes(compactAlias) || compactAirlineText(value) === compactAlias; + } + + return compactAirlineText(value).includes(compactAlias); +} + +export function canonicalAirlineKey(value: unknown): string { + const normalized = normalizeAirlineText(value); + if (!normalized) return ''; + + for (const [key, aliases] of Object.entries(AIRLINE_ALIASES)) { + if (aliases.some(alias => airlineAliasMatches(normalized, alias))) { + return key; + } + } + + return normalized; +} diff --git a/src/utils/airlineOps.ts b/src/utils/airlineOps.ts index 27cf601..b28aa38 100644 --- a/src/utils/airlineOps.ts +++ b/src/utils/airlineOps.ts @@ -1,3 +1,5 @@ +import { canonicalAirlineKey } from './airlineAliases'; + export type HexColor = `#${string}`; export type AirlineOps = { @@ -29,62 +31,6 @@ export const AIRLINE_OPS: Array<{ key: string; ops: AirlineOps }> = [ { key: 'flydubai', ops: { checkInOpen: 180, checkInClose: 60, gateOpen: 40, gateClose: 20 } }, ]; -const AIRLINE_ALIASES: Record = { - ryanair: ['ryanair', 'fr', 'ryr'], - easyjet: ['easyjet', 'easy jet', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], - wizz: ['wizz', 'wizz air', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], - volotea: ['volotea', 'v7'], - vueling: ['vueling', 'vy'], - transavia: ['transavia', 'transavia france', 'transavia holland', 'hv', 'to', 'tra', 'tvf'], - 'aer lingus': ['aer lingus', 'ei'], - 'british airways': ['british airways', 'ba', 'baw'], - sas: ['sas', 'scandinavian', 'sk'], - flydubai: ['flydubai', 'fz', 'fdb'], - aeroitalia: ['aeroitalia', 'xz'], - 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], - 'air arabia': ['air arabia', 'g9', 'abz'], - 'air dolomiti': ['air dolomiti', 'en', 'dla'], - buzz: ['buzz', 'rr', 'rys'], - dhl: ['dhl', 'qy', 'bcs'], - eurowings: ['eurowings', 'ew', 'ewg'], - 'ita airways': ['ita airways', 'ita', 'az', 'ity'], - lufthansa: ['lufthansa', 'lh', 'dlh'], -}; - -function normalizeAirlineText(value: unknown): string { - if (typeof value !== 'string' && typeof value !== 'number') return ''; - return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); -} - -function compactAirlineText(value: unknown): string { - return normalizeAirlineText(value).replace(/\s+/g, ''); -} - -function airlineAliasMatches(value: string, alias: string): boolean { - const normalizedAlias = normalizeAirlineText(alias); - const compactAlias = compactAirlineText(alias); - if (!value || !normalizedAlias || !compactAlias) return false; - - if (compactAlias.length <= 3) { - return value.split(' ').includes(compactAlias) || compactAirlineText(value) === compactAlias; - } - - return compactAirlineText(value).includes(compactAlias); -} - -function canonicalAirlineKey(value: unknown): string { - const normalized = normalizeAirlineText(value); - if (!normalized) return ''; - - for (const [key, aliases] of Object.entries(AIRLINE_ALIASES)) { - if (aliases.some(alias => airlineAliasMatches(normalized, alias))) { - return key; - } - } - - return normalized; -} - export function getAirlineOps(name: string): AirlineOps { const key = canonicalAirlineKey(name); return AIRLINE_OPS.find(item => item.key === key)?.ops diff --git a/src/utils/airportSettings.ts b/src/utils/airportSettings.ts index beb5bba..175c706 100644 --- a/src/utils/airportSettings.ts +++ b/src/utils/airportSettings.ts @@ -1,5 +1,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { ALLOWED_AIRLINES, AIRLINE_DISPLAY_NAMES } from './airlineOps'; +import { + AIRLINE_ALIASES, + canonicalAirlineKey, + compactAirlineText, + normalizeAirlineText, +} from './airlineAliases'; export type AirportPreset = { code: string; @@ -53,35 +59,13 @@ const airportAirlinesCache: Record = Object.fromEntries( ); function normalizeAirlineKey(value: string | null | undefined): string { - return (value ?? '').trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); + return normalizeAirlineText(value); } function compactAirlineKey(value: string | null | undefined): string { - return normalizeAirlineKey(value).replace(/\s+/g, ''); + return compactAirlineText(value); } -const AIRLINE_CANONICAL_RULES: Array<{ canonical: string; needles: string[] }> = [ - { canonical: 'ryanair', needles: ['ryanair', 'fr', 'ryr'] }, - { canonical: 'easyjet', needles: ['easyjet', 'easy jet', 'easyjet europe', 'easyjet switzerland', 'easyjet uk', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'] }, - { canonical: 'wizz', needles: ['wizz', 'wizz air', 'wizz air malta', 'wizz air uk', 'wizz air abu dhabi', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'] }, - { canonical: 'volotea', needles: ['volotea', 'v7'] }, - { canonical: 'vueling', needles: ['vueling', 'vy'] }, - { canonical: 'transavia', needles: ['transavia france', 'transavia holland', 'transavia airlines', 'transavia', 'hv', 'to', 'tra', 'tvf'] }, - { canonical: 'aer lingus', needles: ['aer lingus', 'ei'] }, - { canonical: 'british airways', needles: ['british airways', 'ba', 'baw'] }, - { canonical: 'sas', needles: ['sas', 'scandinavian', 'sk'] }, - { canonical: 'flydubai', needles: ['flydubai', 'fz', 'fdb'] }, - { canonical: 'aeroitalia', needles: ['aeroitalia', 'xz'] }, - { canonical: 'air arabia maroc', needles: ['air arabia maroc', '3o', 'mac'] }, - { canonical: 'air arabia', needles: ['air arabia', 'g9', 'abz'] }, - { canonical: 'air dolomiti', needles: ['air dolomiti', 'en', 'dla'] }, - { canonical: 'buzz', needles: ['buzz', 'rr', 'rys'] }, - { canonical: 'dhl', needles: ['dhl', 'qy', 'bcs'] }, - { canonical: 'eurowings', needles: ['eurowings', 'ew', 'ewg'] }, - { canonical: 'ita airways', needles: ['ita airways', 'ita', 'az', 'ity'] }, - { canonical: 'lufthansa', needles: ['lufthansa', 'lh', 'dlh'] }, -]; - function isGenericAirlinePlaceholder(value: string): boolean { return value === 'sconosciuta' || value === 'unknown' @@ -94,20 +78,6 @@ function isLikelyRawAirlineCode(value: string): boolean { return /^[a-z0-9]{1,3}$/.test(compactAirlineKey(value)); } -function airlineRuleMatches(value: string, needle: string): boolean { - const normalizedNeedle = normalizeAirlineKey(needle); - const compactNeedle = compactAirlineKey(needle); - if (!normalizedNeedle || !compactNeedle) { - return false; - } - - if (compactNeedle.length <= 3) { - return value.split(' ').includes(compactNeedle) || compactAirlineKey(value) === compactNeedle; - } - - return compactAirlineKey(value).includes(compactNeedle); -} - function canonicalizeAirlineKey(value: string | null | undefined): string { const normalized = normalizeAirlineKey(value); if (!normalized) { @@ -118,10 +88,9 @@ function canonicalizeAirlineKey(value: string | null | undefined): string { return ''; } - for (const rule of AIRLINE_CANONICAL_RULES) { - if (rule.needles.some(needle => airlineRuleMatches(normalized, needle))) { - return rule.canonical; - } + const canonical = canonicalAirlineKey(normalized); + if (AIRLINE_ALIASES[canonical]) { + return canonical; } if (isLikelyRawAirlineCode(normalized)) { @@ -257,6 +226,31 @@ export async function storeDetectedAirportAirlines(code: string | null | undefin return next; } +/*---------------------------------------------------------------------------*\ +| Riconcilia il filtro compagnie quando la lista dell'aeroporto cambia (i | +| provider aggiungono le compagnie rilevate nello schedule, charter inclusi). | +| Regole: | +| - profilo senza compagnie salvate → filtro vuoto (nessun filtro attivo); | +| - chiavi sparite dalla lista aeroporto → tolte dalla selezione; | +| - compagnie NUOVE rilevate → mai selezionate in automatico: comparirebbero | +| in bacheca voli che l'utente non gestisce. | +| Ritorna null quando la selezione corrente può restare invariata. | +\*---------------------------------------------------------------------------*/ +export function reconcileSelectedAirlines(options: { + savedProfileAirlines: string[]; + previousSelectedAirlines: string[]; + nextAirportAirlines: string[]; +}): string[] | null { + const { savedProfileAirlines, previousSelectedAirlines, nextAirportAirlines } = options; + + if (savedProfileAirlines.length === 0) { + return previousSelectedAirlines.length > 0 ? [] : null; + } + + const pruned = previousSelectedAirlines.filter(key => nextAirportAirlines.includes(key)); + return pruned.length === previousSelectedAirlines.length ? null : pruned; +} + export function getAirportAirlines(code: string | null | undefined): string[] { const normalized = isValidAirportCode(code) ? normalizeAirportCode(code) : DEFAULT_AIRPORT_CODE; return airportAirlinesCache[normalized] ?? AIRPORT_AIRLINES[normalized] ?? ALLOWED_AIRLINES; diff --git a/src/utils/flightScheduleAdapter.ts b/src/utils/flightScheduleAdapter.ts index eb68950..a720a5a 100644 --- a/src/utils/flightScheduleAdapter.ts +++ b/src/utils/flightScheduleAdapter.ts @@ -1,3 +1,5 @@ +import { AIRLINE_ALIASES, airlineAliasMatches, normalizeAirlineText } from './airlineAliases'; + export type FlightDirection = 'arrival' | 'departure'; export type FlightTimestampBucket = 'real' | 'estimated' | 'scheduled'; @@ -20,38 +22,6 @@ export function getFlightAirlineName(item: any): string { return String(item?.flight?.airline?.name ?? '').trim(); } -const AIRLINE_MATCH_ALIASES: Record = { - ryanair: ['ryanair', 'fr', 'ryr'], - easyjet: ['easyjet', 'easy jet', 'u2', 'ec', 'ds', 'eju', 'ezy', 'ezs'], - wizz: ['wizz', 'wizz air', 'w6', 'w4', 'w9', 'wzz', 'wmt', 'wuk'], - volotea: ['volotea', 'v7'], - vueling: ['vueling', 'vy'], - transavia: ['transavia', 'transavia france', 'transavia holland', 'hv', 'to', 'tra', 'tvf'], - 'aer lingus': ['aer lingus', 'ei'], - 'british airways': ['british airways', 'ba', 'baw'], - sas: ['sas', 'scandinavian', 'sk', 'sas'], - scandinavian: ['sas', 'scandinavian', 'sk', 'sas'], - flydubai: ['flydubai', 'fz', 'fdb'], - aeroitalia: ['aeroitalia', 'xz'], - 'air arabia maroc': ['air arabia maroc', '3o', 'mac'], - 'air arabia': ['air arabia', 'g9', 'abz'], - 'air dolomiti': ['air dolomiti', 'en', 'dla'], - buzz: ['buzz', 'rr', 'rys'], - dhl: ['dhl', 'qy', 'bcs'], - eurowings: ['eurowings', 'ew', 'ewg'], - 'ita airways': ['ita airways', 'az', 'ity'], - lufthansa: ['lufthansa', 'lh', 'dlh'], -}; - -function normalizeAirlineText(value: unknown): string { - if (typeof value !== 'string' && typeof value !== 'number') return ''; - return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim(); -} - -function compactAirlineText(value: unknown): string { - return normalizeAirlineText(value).replace(/\s+/g, ''); -} - function getFlightNumberAirlinePrefix(item: any): string { const flightNumber = getFlightNumber(item).toUpperCase().replace(/[\s\-_]/g, ''); return flightNumber.match(/^([A-Z0-9]{2,3}?)(?=\d)/)?.[1] ?? ''; @@ -71,28 +41,14 @@ function getFlightAirlineIdentifiers(item: any): string[] { .map(value => String(value)); } -function airlineIdentifierMatchesAlias(identifier: string | number, alias: string): boolean { - const normalizedIdentifier = normalizeAirlineText(identifier); - const compactIdentifier = compactAirlineText(identifier); - const normalizedAlias = normalizeAirlineText(alias); - const compactAlias = compactAirlineText(alias); - if (!normalizedIdentifier || !normalizedAlias || !compactAlias) return false; - - if (compactAlias.length <= 3) { - return normalizedIdentifier.split(' ').includes(compactAlias) || compactIdentifier === compactAlias; - } - - return compactIdentifier.includes(compactAlias); -} - export function isFlightAirlineMatch(item: any, airlineKey: string): boolean { const normalizedKey = normalizeAirlineText(airlineKey); if (!normalizedKey) return false; - const aliases = AIRLINE_MATCH_ALIASES[normalizedKey] ?? [normalizedKey]; + const aliases = AIRLINE_ALIASES[normalizedKey] ?? [normalizedKey]; const identifiers = getFlightAirlineIdentifiers(item); return identifiers.some(identifier => - aliases.some(alias => airlineIdentifierMatchesAlias(identifier, alias)), + aliases.some(alias => airlineAliasMatches(identifier, alias)), ); } diff --git a/src/utils/updateChecker.ts b/src/utils/updateChecker.ts index 90e6373..71485a6 100644 --- a/src/utils/updateChecker.ts +++ b/src/utils/updateChecker.ts @@ -6,7 +6,7 @@ import { nativeApplicationVersion } from 'expo-application'; * (web, tests). Kept in sync with package.json by scripts/bump-version.cjs * and enforced by scripts/release-check.cjs — do not edit by hand. */ -export const FALLBACK_APP_VERSION = '2.7.28'; +export const FALLBACK_APP_VERSION = '2.7.29'; export const APP_VERSION = nativeApplicationVersion ?? FALLBACK_APP_VERSION; const REPO = 'targetmisser/aerostaffpro'; diff --git a/src/widgets/ShiftWidget.tsx b/src/widgets/ShiftWidget.tsx index 2a288c6..14e0d76 100644 --- a/src/widgets/ShiftWidget.tsx +++ b/src/widgets/ShiftWidget.tsx @@ -3,6 +3,7 @@ import { FlexWidget, TextWidget, ListWidget } from 'react-native-android-widget' import type { ThemeMode, ThemeSnapshotColors } from '../utils/themeMode'; import type { WidgetData, WidgetFlight } from './widgetTaskHandler'; import { getWidgetThemePalette, type WidgetThemePalette } from './widgetTheme'; +import { WEIGHT } from '../theme/typography'; const PILL_R = 10; @@ -53,7 +54,7 @@ function FlightRow({ > @@ -90,7 +91,7 @@ function FlightRow({ alignItems: 'center', }} > - + - + @@ -122,7 +123,7 @@ function FlightRow({ alignItems: 'center', }} > - + - + - + @@ -192,7 +193,7 @@ function Header({ label, theme }: { label?: string; theme: WidgetThemePalette }) /> @@ -258,13 +259,13 @@ export function ShiftWidget({ data, themeMode = 'light', themeSnapshot }: ShiftW /> diff --git a/src/widgets/widgetTaskHandler.tsx b/src/widgets/widgetTaskHandler.tsx index 129dda9..7a13817 100644 --- a/src/widgets/widgetTaskHandler.tsx +++ b/src/widgets/widgetTaskHandler.tsx @@ -1,5 +1,6 @@ import React from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as Calendar from 'expo-calendar'; import type { WidgetTaskHandlerProps } from 'react-native-android-widget'; import type { HexColor } from '../utils/airlineOps'; import { getAirlineOps, getAirlineColor } from '../utils/airlineOps'; @@ -104,13 +105,79 @@ function resolveWidgetShift(shiftData: WidgetShiftData): ResolvedWidgetShift | ' return null; } +/*---------------------------------------------------------------------------*\ +| Lo snapshot turni (WIDGET_SHIFT_KEY) è scritto dall'app e copre solo oggi e | +| domani: se l'app non viene aperta per più di un giorno, il widget restava | +| su "nessun turno" fino alla successiva apertura. Qui il task del widget | +| rilegge da solo i turni dal calendario di sistema (stessa logica delle | +| schermate) e riscrive lo snapshot. Solo verifica del permesso, mai prompt: | +| in un contesto headless non c'è UI. In caso di errore si torna al vecchio | +| comportamento basato sullo snapshot esistente. | +\*---------------------------------------------------------------------------*/ +async function refreshShiftSnapshotFromCalendar(): Promise { + try { + const { status } = await Calendar.getCalendarPermissionsAsync(); + if (status !== 'granted') return null; + + const cals = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT); + const cal = cals.find(c => c.allowsModifications && c.isPrimary) || cals.find(c => c.allowsModifications); + if (!cal) return null; + + const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); + const todayEnd = new Date(todayStart); todayEnd.setHours(23, 59, 59, 999); + const tomorrowStart = addDays(todayStart, 1); + const tomorrowEnd = new Date(tomorrowStart); tomorrowEnd.setHours(23, 59, 59, 999); + + let shiftToday: { start: number; end: number } | null = null; + let shiftTomorrow: { start: number; end: number } | null = null; + let isRestDay = false; + const events = await Calendar.getEventsAsync([cal.id], todayStart, tomorrowEnd); + for (const e of events) { + if (e.title.includes('Riposo')) { + const evtDay = new Date(e.startDate); + if (evtDay >= todayStart && evtDay <= todayEnd) isRestDay = true; + continue; + } + if (!e.title.includes('Lavoro')) continue; + const start = new Date(e.startDate).getTime() / 1000; + const end = new Date(e.endDate).getTime() / 1000; + const evtDay = new Date(e.startDate); + if (evtDay >= todayStart && evtDay <= todayEnd) { + shiftToday = { start, end }; + isRestDay = false; + } else if (evtDay >= tomorrowStart && evtDay <= tomorrowEnd) { + shiftTomorrow = { start, end }; + } + } + + const snapshot: WidgetShiftData = { + date: toLocalIso(todayStart), + shiftToday, + isRestDay, + nextShift: shiftTomorrow ? { date: toLocalIso(tomorrowStart), ...shiftTomorrow } : null, + }; + await AsyncStorage.setItem(WIDGET_SHIFT_KEY, JSON.stringify(snapshot)); + return snapshot; + } catch { + return null; + } +} + +async function readShiftSnapshot(): Promise { + const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); + let shiftData: WidgetShiftData | null = shiftRaw ? JSON.parse(shiftRaw) : null; + if (!shiftData || shiftData.date !== toLocalIso()) { + shiftData = (await refreshShiftSnapshotFromCalendar()) ?? shiftData; + } + return shiftData; +} + // ─── Read cached data written by the main app ────────────────────────────────── export async function getWidgetData(): Promise { try { - const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); + const shiftData = await readShiftSnapshot(); - if (shiftRaw) { - const shiftData: WidgetShiftData = JSON.parse(shiftRaw); + if (shiftData) { const resolved = resolveWidgetShift(shiftData); if (resolved === 'rest') return { state: 'rest' }; if (resolved) { @@ -153,10 +220,9 @@ async function renderThemedWidget(props: WidgetTaskHandlerProps, data: WidgetDat // ─── Fetch fresh widget data from the live provider + cached shift key ──────── export async function fetchFreshWidgetData(): Promise { try { - const shiftRaw = await AsyncStorage.getItem(WIDGET_SHIFT_KEY); - if (!shiftRaw) return getWidgetData(); + const shiftData = await readShiftSnapshot(); + if (!shiftData) return getWidgetData(); - const shiftData: WidgetShiftData = JSON.parse(shiftRaw); const resolved = resolveWidgetShift(shiftData); if (resolved === 'rest') return { state: 'rest' }; if (!resolved) return { state: 'no_shift' };