Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -315,11 +316,11 @@ function AppInner() {
/>
)}
{overlay ? (
<TactilePressable onPress={handleBack} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection">
<TactilePressable onPress={handleBack} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yBack')}>
<MaterialIcons name="arrow-back" size={22} color={colors.primaryDark} />
</TactilePressable>
) : (
<TactilePressable onPress={() => setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection">
<TactilePressable onPress={() => setDrawerOpen(true)} animatedStyle={styles.iconBtn} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yOpenMenu')}>
<MaterialIcons name="menu" size={24} color={colors.primaryDark} />
</TactilePressable>
)}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'}\""
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aerostaff-pro",
"version": "2.7.28",
"version": "2.7.29",
"main": "index.ts",
"scripts": {
"start": "expo start",
Expand Down
48 changes: 48 additions & 0 deletions scripts/test-flight-helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
77 changes: 77 additions & 0 deletions scripts/test-misc-utils.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} };
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 7 additions & 6 deletions src/components/AppTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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' }],
},
Expand All @@ -480,7 +481,7 @@ const styles = StyleSheet.create({
right: 8,
bottom: 5,
height: 3,
borderRadius: 999,
borderRadius: RADIUS.pill,
opacity: 0.72,
},
tabPressable: {
Expand All @@ -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)',
Expand Down Expand Up @@ -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,
},
Expand Down
17 changes: 10 additions & 7 deletions src/components/DrawerMenuPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);

Expand All @@ -136,7 +139,7 @@ export default function DrawerMenuPanel({
<Text style={styles.opsTitle}>Operations</Text>
</View>
</View>
<TactilePressable onPress={onClose} animatedStyle={styles.opsClose} depth={2} pressedScale={0.94} haptic="selection">
<TactilePressable onPress={onClose} animatedStyle={styles.opsClose} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yClose')}>
<MaterialIcons name="close" size={18} color="rgba(204,251,241,0.72)" />
</TactilePressable>
</View>
Expand All @@ -148,7 +151,7 @@ export default function DrawerMenuPanel({
style={styles.headerGradient}
>
<AeroStaffLogo variant="large" monochrome />
<TactilePressable onPress={onClose} animatedStyle={styles.closeIconBtn} depth={2} pressedScale={0.94} haptic="selection">
<TactilePressable onPress={onClose} animatedStyle={styles.closeIconBtn} depth={2} pressedScale={0.94} haptic="selection" accessibilityRole="button" accessibilityLabel={t('a11yClose')}>
<MaterialIcons name="close" size={20} color="rgba(255,255,255,0.72)" />
</TactilePressable>
</LinearGradient>
Expand Down Expand Up @@ -225,7 +228,7 @@ function makeStyles(c: ThemeColors, surface: DrawerSurfaceConfig) {
opsBrandRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
gap: SPACING.md,
flex: 1,
},
opsLogoBox: {
Expand Down Expand Up @@ -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)',
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/components/GlassCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -155,6 +156,6 @@ const styles = StyleSheet.create({
borderWidth: 0.75,
},
content: {
padding: 16,
padding: SPACING.lg,
},
});
Loading
Loading