diff --git a/package-lock.json b/package-lock.json index a3bbe99..9b54474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-picker/picker": "2.11.1", + "@react-native-picker/picker": "2.11.4", "@types/tesseract.js": "^0.0.2", "expo": "~54.0.0", "expo-blur": "~15.0.8", @@ -23,18 +23,19 @@ "expo-linear-gradient": "~15.0.8", "expo-location": "~19.0.8", "expo-notifications": "~0.32.16", + "expo-secure-store": "~15.0.5", "expo-status-bar": "~3.0.9", "react": "19.1.0", "react-native": "0.81.5", "react-native-android-widget": "^0.20.1", "react-native-calendars": "^1.1314.0", - "react-native-webview": "13.15.0", + "react-native-webview": "13.16.1", "tesseract.js": "^7.0.0" }, "devDependencies": { "@react-native-community/cli": "^20.1.3", "@types/react": "~19.1.10", - "pdfjs-dist": "^5.5.207", + "pdfjs-dist": "^5.6.205", "typescript": "~5.9.2" } }, @@ -80,7 +81,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1492,7 +1492,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -2431,7 +2430,6 @@ "integrity": "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@react-native-community/cli-clean": "20.1.3", "@react-native-community/cli-config": "20.1.3", @@ -2996,9 +2994,9 @@ } }, "node_modules/@react-native-picker/picker": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", - "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz", + "integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==", "license": "MIT", "workspaces": [ "example" @@ -3399,7 +3397,6 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4109,7 +4106,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5014,7 +5010,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.23", @@ -5130,7 +5125,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -5239,6 +5233,15 @@ "react-native": "*" } }, + "node_modules/expo-secure-store": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz", + "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-server": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", @@ -8412,16 +8415,16 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.5.207", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", - "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", + "version": "5.6.205", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.6.205.tgz", + "integrity": "sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=20.19.0 || >=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.95", + "@napi-rs/canvas": "^0.1.96", "node-readable-to-web-readable-stream": "^0.4.2" } }, @@ -8436,7 +8439,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8716,7 +8718,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8742,7 +8743,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -8849,11 +8849,10 @@ "license": "MIT" }, "node_modules/react-native-webview": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", - "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", + "version": "13.16.1", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz", + "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==", "license": "MIT", - "peer": true, "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" @@ -8952,7 +8951,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10211,9 +10209,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/components/ManualsComponents.tsx b/src/components/ManualsComponents.tsx new file mode 100644 index 0000000..1a897b1 --- /dev/null +++ b/src/components/ManualsComponents.tsx @@ -0,0 +1,265 @@ +import React, { useState, useMemo } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, TextInput, Platform, LayoutAnimation } from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; +import { DCSCommand, ManualItem, Section } from '../utils/manualsData'; + +const CMD_REGEX = /(`[^`]+`)/g; + +export function RichBodyText({ text, colors }: { text: string; colors: any }) { + const parts = text.split(CMD_REGEX); + return ( + + {parts.map((part, i) => { + if (part.startsWith('`') && part.endsWith('`')) { + const cmd = part.slice(1, -1); + return ( + + {cmd} + + ); + } + return {part}; + })} + + ); +} + +export function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any }) { + const [search, setSearch] = useState(''); + const lower = search.toLowerCase(); + const filtered = lower + ? commands.filter(c => c.cmd.toLowerCase().includes(lower) || c.desc.toLowerCase().includes(lower)) + : commands; + + const categories = [...new Set(filtered.map(c => c.category))]; + + return ( + + + + + {search.length > 0 && ( + setSearch('')}> + + + )} + + + {categories.map(cat => ( + + + + {cat} + + + {filtered.filter(c => c.category === cat).map((c, i) => ( + + + {c.cmd.split(/(\[[^\]]+\])/).map((part, j) => + part.startsWith('[') ? ( + {part} + ) : ( + {part} + ) + )} + + + {c.desc} + + + ))} + + ))} + + {filtered.length === 0 && ( + + Nessun comando trovato + + )} + + ); +} + +function makeItemStyles(c: ThemeColors) { + return StyleSheet.create({ + wrapper: { + backgroundColor: c.card, + borderRadius: 10, + marginBottom: 6, + overflow: 'hidden', + borderWidth: 1, + borderColor: c.border, + }, + header: { + flexDirection: 'row', alignItems: 'center', gap: 10, + padding: 13, + }, + title: { fontSize: 13, fontWeight: '600', color: c.text, flex: 1 }, + body: { + paddingHorizontal: 14, paddingBottom: 14, paddingTop: 2, + borderTopWidth: 1, borderTopColor: c.cardSecondary, + }, + }); +} + +export function ManualItemRow({ + item, itemIdx, sectionIdx, airlineId, editMode, onEdit, +}: { + item: ManualItem; + itemIdx: number; + sectionIdx: number; + airlineId: string; + editMode: boolean; + onEdit: () => void; +}) { + const { colors } = useAppTheme(); + const itemStyles = useMemo(() => makeItemStyles(colors), [colors]); + const [open, setOpen] = useState(false); + + const toggle = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setOpen(v => !v); + }; + + return ( + + + + {item.title} + {editMode && ( + + + + )} + + {open && ( + + + + )} + + ); +} + +function makeSectionStyles(c: ThemeColors) { + return StyleSheet.create({ + wrapper: { + marginBottom: 12, + }, + header: { + flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', + paddingVertical: 10, paddingHorizontal: 4, + borderBottomWidth: 1, borderBottomColor: c.border, + marginBottom: 8, + }, + title: { fontSize: 12, fontWeight: '700', color: c.textSub, letterSpacing: 0.8 }, + body: { paddingLeft: 0 }, + }); +} + +export function SectionBlock({ + section, sectionIdx, airlineId, editMode, onEdit, onAddItem, onEditItem, +}: { + section: Section; + sectionIdx: number; + airlineId: string; + editMode: boolean; + onEdit: () => void; + onAddItem: () => void; + onEditItem: (itemIdx: number) => void; +}) { + const { colors } = useAppTheme(); + const sectionStyles = useMemo(() => makeSectionStyles(colors), [colors]); + const [open, setOpen] = useState(true); + + const toggle = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setOpen(v => !v); + }; + + return ( + + + {section.title} + + {editMode && ( + + + + )} + + + + {open && ( + + {section.items.map((item, i) => ( + onEditItem(i)} + /> + ))} + {editMode && ( + + + Aggiungi voce + + )} + + )} + + ); +} diff --git a/src/components/ManualsModals.tsx b/src/components/ManualsModals.tsx new file mode 100644 index 0000000..867c3c3 --- /dev/null +++ b/src/components/ManualsModals.tsx @@ -0,0 +1,353 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, TextInput, Modal, Alert, KeyboardAvoidingView, ScrollView, Platform } from 'react-native'; +import { useAppTheme } from '../context/ThemeContext'; +import { Airline, ModalState, AIRLINE_COLORS } from '../utils/manualsData'; + +export 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 }, + inputMulti: { minHeight: 100, paddingTop: 9 }, + colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 8 }, + 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 }, + btnCancel: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#ccc' }, + btnSave: {}, + btnDanger: { marginRight: 'auto', backgroundColor: '#FEE2E2' }, + btnText: { fontSize: 14, fontWeight: '600' }, + btnDangerText: { fontSize: 14, fontWeight: '600', color: '#DC2626' }, +}); + +export function AirlineModal({ + modal, airlines, persist, closeModal, +}: { + modal: ModalState; + airlines: Airline[]; + persist: (a: Airline[]) => void; + closeModal: () => void; +}) { + const { colors } = useAppTheme(); + const isEdit = modal.kind === 'airline_edit'; + const existing = isEdit ? airlines.find(a => a.id === (modal as any).airlineId) : undefined; + + const [name, setName] = useState(existing?.name ?? ''); + const [code, setCode] = useState(existing?.code ?? ''); + const [colorIdx, setColorIdx] = useState(() => { + if (!existing) return 0; + const idx = AIRLINE_COLORS.findIndex(c => c.color === existing.color); + return idx >= 0 ? idx : 0; + }); + + const visible = modal.kind === 'airline_add' || modal.kind === 'airline_edit'; + + const save = () => { + if (!name.trim() || !code.trim()) return; + const chosen = AIRLINE_COLORS[colorIdx] ?? AIRLINE_COLORS[0]; + if (isEdit) { + const updated = airlines.map(a => + a.id === (modal as any).airlineId + ? { ...a, name: name.trim(), code: code.trim().toUpperCase(), color: chosen.color, textColor: chosen.textColor } + : a + ); + persist(updated); + } else { + const newAirline: Airline = { + id: Date.now().toString(), + name: name.trim(), + code: code.trim().toUpperCase(), + color: chosen.color, + textColor: chosen.textColor, + sections: [], + }; + persist([...airlines, newAirline]); + } + closeModal(); + }; + + const del = () => { + Alert.alert( + 'Elimina compagnia', + `Eliminare "${existing?.name}" e tutti i suoi contenuti?`, + [ + { text: 'Annulla', style: 'cancel' }, + { + text: 'Elimina', style: 'destructive', + onPress: () => { + persist(airlines.filter(a => a.id !== (modal as any).airlineId)); + closeModal(); + }, + }, + ] + ); + }; + + return ( + + + + + + {isEdit ? 'Modifica compagnia' : 'Nuova compagnia'} + + Nome + + Codice IATA + + Colore + + {AIRLINE_COLORS.map((c, i) => ( + setColorIdx(i)} + /> + ))} + + + {isEdit && ( + + Elimina + + )} + + Annulla + + + Salva + + + + + + + ); +} + +export function SectionModal({ + modal, airlines, persist, closeModal, +}: { + modal: ModalState; + airlines: Airline[]; + persist: (a: Airline[]) => void; + closeModal: () => void; +}) { + const { colors } = useAppTheme(); + const isEdit = modal.kind === 'section_edit'; + const airlineId = (modal as any).airlineId as string | undefined; + const sectionIdx = (modal as any).sectionIdx as number | undefined; + const existingTitle = isEdit && airlineId !== undefined && sectionIdx !== undefined + ? airlines.find(a => a.id === airlineId)?.sections[sectionIdx]?.title ?? '' + : ''; + + const [title, setTitle] = useState(existingTitle); + const visible = modal.kind === 'section_add' || modal.kind === 'section_edit'; + + const save = () => { + if (!title.trim() || !airlineId) return; + const updated = airlines.map(a => { + if (a.id !== airlineId) return a; + if (isEdit && sectionIdx !== undefined) { + const sections = a.sections.map((s, i) => + i === sectionIdx ? { ...s, title: title.trim() } : s + ); + return { ...a, sections }; + } else { + return { ...a, sections: [...a.sections, { title: title.trim(), items: [] }] }; + } + }); + persist(updated); + closeModal(); + }; + + const del = () => { + const airline = airlines.find(a => a.id === airlineId); + const sectionTitle = sectionIdx !== undefined ? airline?.sections[sectionIdx]?.title : ''; + Alert.alert( + 'Elimina sezione', + `Eliminare la sezione "${sectionTitle}" e tutte le sue voci?`, + [ + { text: 'Annulla', style: 'cancel' }, + { + text: 'Elimina', style: 'destructive', + onPress: () => { + const updated = airlines.map(a => { + if (a.id !== airlineId) return a; + return { ...a, sections: a.sections.filter((_, i) => i !== sectionIdx) }; + }); + persist(updated); + closeModal(); + }, + }, + ] + ); + }; + + return ( + + + + + + {isEdit ? 'Modifica sezione' : 'Nuova sezione'} + + Titolo + + + {isEdit && ( + + Elimina + + )} + + Annulla + + + Salva + + + + + + + ); +} + +export function ItemModal({ + modal, airlines, persist, closeModal, +}: { + modal: ModalState; + airlines: Airline[]; + persist: (a: Airline[]) => void; + closeModal: () => void; +}) { + const { colors } = useAppTheme(); + const isEdit = modal.kind === 'item_edit'; + const airlineId = (modal as any).airlineId as string | undefined; + const sectionIdx = (modal as any).sectionIdx as number | undefined; + const itemIdx = (modal as any).itemIdx as number | undefined; + + const existing = isEdit && airlineId && sectionIdx !== undefined && itemIdx !== undefined + ? airlines.find(a => a.id === airlineId)?.sections[sectionIdx]?.items[itemIdx] + : undefined; + + const [title, setTitle] = useState(existing?.title ?? ''); + const [body, setBody] = useState(existing?.body ?? ''); + const visible = modal.kind === 'item_add' || modal.kind === 'item_edit'; + + const save = () => { + if (!title.trim() || !airlineId || sectionIdx === undefined) return; + const updated = airlines.map(a => { + if (a.id !== airlineId) return a; + const sections = a.sections.map((s, si) => { + if (si !== sectionIdx) return s; + if (isEdit && itemIdx !== undefined) { + const items = s.items.map((it, ii) => + ii === itemIdx ? { title: title.trim(), body: body.trim() } : it + ); + return { ...s, items }; + } else { + return { ...s, items: [...s.items, { title: title.trim(), body: body.trim() }] }; + } + }); + return { ...a, sections }; + }); + persist(updated); + closeModal(); + }; + + const del = () => { + Alert.alert( + 'Elimina voce', + `Eliminare "${existing?.title}"?`, + [ + { text: 'Annulla', style: 'cancel' }, + { + text: 'Elimina', style: 'destructive', + onPress: () => { + const updated = airlines.map(a => { + if (a.id !== airlineId) return a; + const sections = a.sections.map((s, si) => { + if (si !== sectionIdx) return s; + return { ...s, items: s.items.filter((_, ii) => ii !== itemIdx) }; + }); + return { ...a, sections }; + }); + persist(updated); + closeModal(); + }, + }, + ] + ); + }; + + return ( + + + + + + {isEdit ? 'Modifica voce' : 'Nuova voce'} + + Titolo + + Contenuto + + + {isEdit && ( + + Elimina + + )} + + Annulla + + + Salva + + + + + + + ); +} diff --git a/src/screens/ManualsScreen.tsx b/src/screens/ManualsScreen.tsx index ad9d783..3e2d8d5 100644 --- a/src/screens/ManualsScreen.tsx +++ b/src/screens/ManualsScreen.tsx @@ -2,586 +2,18 @@ import React, { useState, useMemo, useEffect } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, - LayoutAnimation, Platform, UIManager, TextInput, Modal, Alert, KeyboardAvoidingView, + Platform, UIManager, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useAppTheme, type ThemeColors } from '../context/ThemeContext'; - -const STORAGE_KEY = 'manuals_data_v2'; - -const AIRLINE_COLORS = [ - { color: '#FF6600', textColor: '#fff' }, - { color: '#C01380', textColor: '#fff' }, - { color: '#003580', textColor: '#fff' }, - { color: '#F7C800', textColor: '#1a1a1a' }, - { color: '#006DBF', textColor: '#fff' }, - { color: '#2E7D32', textColor: '#fff' }, - { color: '#B71C1C', textColor: '#fff' }, - { color: '#4A148C', textColor: '#fff' }, - { color: '#E65100', textColor: '#fff' }, - { color: '#37474F', textColor: '#fff' }, -]; +import { Airline, ModalState, DEFAULT_AIRLINES, STORAGE_KEY } from '../utils/manualsData'; +import { CommandsTab, SectionBlock } from '../components/ManualsComponents'; +import { AirlineModal, SectionModal, ItemModal } from '../components/ManualsModals'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } -// ─── Types ─────────────────────────────────────────────────────────────────── -type ManualItem = { title: string; body: string }; -type Section = { title: string; items: ManualItem[] }; -type DCSCommand = { cmd: string; desc: string; category: string }; -type Airline = { id: string; name: string; code: string; color: string; textColor: string; sections: Section[]; commands?: DCSCommand[] }; - -type ModalState = - | { kind: 'none' } - | { kind: 'airline_add' } - | { kind: 'airline_edit'; airlineId: string } - | { kind: 'section_add'; airlineId: string } - | { kind: 'section_edit'; airlineId: string; sectionIdx: number } - | { kind: 'item_add'; airlineId: string; sectionIdx: number } - | { kind: 'item_edit'; airlineId: string; sectionIdx: number; itemIdx: number }; - -// ─── Data ──────────────────────────────────────────────────────────────────── -const DEFAULT_AIRLINES: Airline[] = [ - { - id: 'easyjet', name: 'easyJet', code: 'EZY', - color: '#FF6600', textColor: '#fff', - commands: [ - { cmd: 'L', desc: 'Lista voli del giorno', category: 'Flight Ops' }, - { cmd: 'OF/[volo]', desc: 'Apri volo per check-in', category: 'Flight Ops' }, - { cmd: 'CF/[volo]', desc: 'Chiudi volo', category: 'Flight Ops' }, - { cmd: 'LOF', desc: 'Lista voli aperti', category: 'Flight Ops' }, - { cmd: 'GC/[gate]', desc: 'Cambia gate', category: 'Flight Ops' }, - { cmd: 'F[cognome]/[nome]', desc: 'Cerca passeggero per nome', category: 'Search' }, - { cmd: '.[booking ref]', desc: 'Cerca per codice prenotazione', category: 'Search' }, - { cmd: 'C[n]', desc: 'Check-in passeggero linea n', category: 'Check-in' }, - { cmd: 'C[n]-[m]', desc: 'Check-in gruppo (linee n-m)', category: 'Check-in' }, - { cmd: 'AB[n]', desc: 'Aggiungi bagaglio a passeggero', category: 'Baggage' }, - { cmd: '.DSP[n]', desc: 'Mostra/cambia seat plan', category: 'Seating' }, - { cmd: '.SS/[volo]/[s1]-[s2]', desc: 'Seat swap', category: 'Seating' }, - { cmd: '.SAG[volo]', desc: 'Lista Seat at Gate', category: 'Seating' }, - { cmd: 'DSP1', desc: 'Seat plan per SAG customer 1', category: 'Seating' }, - { cmd: '.COM[n]', desc: 'Visualizza commenti booking', category: 'Customer' }, - { cmd: 'COM[n]', desc: 'Aggiungi commento a booking', category: 'Customer' }, - { cmd: 'TM[n]', desc: 'Cambia titolo a Male', category: 'Customer' }, - { cmd: 'TF[n]', desc: 'Cambia titolo a Female', category: 'Customer' }, - { cmd: 'J[n]', desc: 'Cambia titolo a Child', category: 'Customer' }, - { cmd: 'EX', desc: 'Esci / logout', category: 'System' }, - ], - sections: [ - { - title: 'Check-in DCS', - items: [ - { - title: 'Apertura volo', - body: 'Accedere al DCS (opzione 16). Digitare `L` per lista voli.\n\nPer aprire il volo: `OF/[numero volo]` e confermare con Y.\n\nImpostare boarding card e bag tag su A (auto).\nInserire gate, orario boarding e capacità.\n\nVerificare:\n• Numero volo e data corretti\n• Rotta e gate assegnati\n• Segmenti collegati (connessioni)\n• Restrizioni eventuali sul volo', - }, - { - title: 'Check-in passeggeri', - body: 'Cercare passeggero: `F[cognome]/[nome]` oppure `.[booking ref]`.\n\nCheck-in: `C[n]` per singolo, `C[n]-[m]` per gruppo.\n\nRichiedere documento d\'identità valido.\n\nDocumenti accettati (UE):\n• Carta d\'identità\n• Passaporto\n\nVerificare: validità, nome uguale al biglietto, destinazione.\n\nAssegnare posto con `.DSP[n]`.', - }, - { - title: 'Bagaglio a mano', - body: 'Misure max bagaglio cabina:\n• Standard: 56×45×25 cm (inclusa impugnatura e ruote)\n• Borsa aggiuntiva: 45×36×20 cm\n\nSe eccede → addebitare hold baggage fee.\nSe il volo è pieno → imbarchare gratis in stiva.', - }, - { - title: 'Overbooking', - body: 'Procedura overbooking:\n1. Verificare numero di passeggeri in eccesso\n2. Cercare volontari (offrire voucher + volo alternativo)\n3. Se nessun volontario → denied boarding involontario\n\nAggiungere commento al booking con `COM[n]` selezionando il motivo appropriato.\n\nRegolamento EU 261/2004:\n• Voli ≤1.500 km → €250\n• Voli 1.500–3.500 km → €400\n• Voli >3.500 km → €600\n\nOltre a rimborso o volo alternativo.', - }, - ], - }, - { - title: 'Gate e Imbarco', - items: [ - { - title: 'Apertura gate', - body: 'Prima di aprire il gate:\n• Attivare gate nel sistema DCS\n• Per cambiare gate: `GC/[numero gate]`\n• Verificare lista SPEQ comunicata al capo cabina\n• Controllare info gate board\n• Comunicare ritardi se presenti\n\nPer lista SAG: `.SAG[numero volo]`', - }, - { - title: 'Sequenza boarding', - body: 'Ordine imbarco easyJet:\n1. Passeggeri speciali (WCHR, WCHC, famiglie con bambini <5 anni)\n2. easyJet Plus + posti Upfront & Extra Legroom\n3. Tutti gli altri passeggeri\n\nVerificare sempre boarding pass + documento.\n\nPer seat swap operativo: `.SS/[volo]/[posto1]-[posto2]`', - }, - { - title: 'Chiusura volo', - body: 'Procedura chiusura:\n1. Scansione ultimi passeggeri (T-15 min)\n2. Riconciliare no-show nel DCS\n3. Last-minute changes comunicati al capo volo\n4. Invio load sheet definitiva\n5. Chiusura volo: `CF/[numero volo]`\n6. Comunicare al gate supervisore\n\nPer verificare voli aperti: `LOF`', - }, - ], - }, - { - title: 'Passeggeri Speciali', - items: [ - { - title: 'Codici WCHR / WCHC / WCHS', - body: 'WCHR → può camminare, ha difficoltà sulle scale.\nWCHC → non può camminare, necessita assistenza totale.\nWCHS → non può fare scale (usa pontile o bus).\n\nSempre:\n• Pre-imbarcare\n• Notificare capo cabina e handling\n• Verificare posto (non uscita emergenza)', - }, - { - title: 'UM – Unaccompanied Minor', - body: 'Minori non accompagnati (5–14 anni):\n• Servizio UM obbligatorio\n• Modulo UM compilato da genitore/tutore\n• Accettare solo con form completo\n• Notificare capo cabina\n• Contatti ritirante a destinazione\n• Non lasciare mai il minore da solo', - }, - { - title: 'MEDA – Passeggero Medico', - body: 'Passeggero con condizione medica rilevante:\n• Richiede MEDA clearance pre-volo\n• MEDA form compilato dal medico curante\n• Eventuale accompagnatore medico (MEDA + 1)\n• Comunicare al Comandante\n• Equipaggiamento medico a bordo verificato', - }, - { - title: 'Documenti di viaggio', - body: 'Verificare sempre:\n• Validità documento (min. 3 mesi oltre la data di rientro per alcuni paesi)\n• Visto se necessario (TIMATIC nel DCS)\n• Per viaggi extra-Schengen: passaporto obbligatorio\n• APIS: inserire dati documento nel sistema se richiesto dalla rotta', - }, - ], - }, - { - title: 'Tariffe Aeroportuali', - items: [ - { - title: 'Bagaglio irregolare al gate', - body: 'Bagaglio cabina fuori misura o non conforme al gate: €58.\n\nViene imbarcato in stiva.\n\nMisure consentite:\n• Standard: 45×36×20 cm\n• Grande (Plus/Up Front/Extra Legroom): 56×45×25 cm', - }, - { - title: 'Bagaglio da aggiungere', - body: 'Bagaglio stiva da aggiungere in aeroporto: €65.\n\nBagaglio stiva 23kg prenotato online: da €11.99 (varia per rotta/periodo).', - }, - { - title: 'Sovrappeso bagaglio', - body: 'Bagaglio che supera il peso acquistato:\n• €15 per ogni kg in eccesso\n• Peso massimo consentito: 32kg\n\nPesare sempre il bagaglio e addebitare la differenza.', - }, - ], - }, - ], - }, - { - id: 'wizzair', name: 'Wizz Air', code: 'W6', - color: '#C01380', textColor: '#fff', - sections: [ - { - title: 'Check-in DCS', - items: [ - { - title: 'Apertura check-in', - body: 'Accedere al sistema DCS Wizz Air con credenziali personali.\n\nSelezionare il volo → verificare rotta, data e orario → avviare check-in.\n\nCheck-in online disponibile da 30 giorni a 3 ore prima del volo.', - }, - { - title: 'Documenti accettati', - body: 'Per TUTTI i voli:\n• Passaporto valido\n\nVoli intra-Schengen (paesi selezionati):\n• Carta d\'identità\n\nVerificare sempre validità e destinazione tramite TIMATIC.', - }, - { - title: 'WIZZ Priority', - body: 'Passeggeri WIZZ Priority:\n• Corsia dedicata al check-in\n• Imbarco prioritario\n• Bagaglio cabina grande (40×30×20 cm + borsa)\n• Verificare tariffa Priority sulla prenotazione o Wizz+ membership\n\nSenza Priority: solo borsa piccola 40×30×20 cm.', - }, - ], - }, - { - title: 'Bagaglio', - items: [ - { - title: 'Politica bagagli', - body: 'Bagaglio incluso in tutti i biglietti:\n• 1 borsa piccola 40×30×20 cm (sotto sedile) – GRATIS\n\nA pagamento (prenotato online o al banco):\n• Bagaglio cabina grande 55×40×23 cm\n\nBagaglio stiva disponibile: 10 kg / 20 kg / 26 kg / 32 kg\nVerificare la prenotazione nel DCS.', - }, - { - title: 'Eccesso bagaglio al banco', - body: 'Se il bagaglio non è prenotato o supera i limiti:\n1. Verificare nel DCS la prenotazione bagagli\n2. Addebitare la tariffa airport fee (superiore al prezzo online)\n3. Registrare il pagamento nel sistema\n4. Emettere ricevuta\n5. Attaccare tag bagaglio correttamente', - }, - ], - }, - { - title: 'Gate e Imbarco', - items: [ - { - title: 'Sequenza boarding', - body: 'Ordine imbarco Wizz Air:\n1. Passeggeri speciali + WCHR + famiglie con bambini piccoli\n2. WIZZ Priority\n3. Tutti gli altri\n\nVerificare boarding pass (app o cartaceo) + documento d\'identità ad ogni imbarco.', - }, - { - title: 'Chiusura volo', - body: 'Procedura chiusura volo:\n1. Scansione ultimi passeggeri\n2. Riconciliare no-show nel sistema\n3. Bags reconcile (bagagli senza passeggero → scaricare)\n4. Trasmettere dati finali al vettore\n5. Chiudere gate nel DCS\n6. Notificare supervisore', - }, - ], - }, - { - title: 'Tariffe Aeroportuali', - items: [ - { - title: 'Check-in aeroportuale', - body: 'Check-in al banco aeroporto: €40 a passeggero.\n\nIl passeggero deve aver fatto check-in online (da 30 giorni a 3h prima). Se non l\'ha fatto, addebitare la tariffa.', - }, - { - title: 'Bagaglio irregolare al gate', - body: 'Bagaglio cabina fuori misura o non conforme al gate: €58.\n\nViene imbarcato in stiva.\n\nMisure consentite senza Priority: 40×30×20 cm.\nCon WIZZ Priority: 55×40×23 cm inclusa.', - }, - { - title: 'Bagaglio da aggiungere', - body: 'Bagaglio stiva da aggiungere in aeroporto: €65.\n\nBagaglio stiva disponibile online: 10kg / 20kg / 26kg / 32kg.\nVerificare sempre la prenotazione nel DCS.', - }, - { - title: 'Sovrappeso bagaglio', - body: 'Bagaglio che supera il peso acquistato:\n• €15 per ogni kg in eccesso\n\nPesare sempre il bagaglio e addebitare la differenza.', - }, - ], - }, - ], - }, - { - id: 'ryanair', name: 'Ryanair', code: 'FR', - color: '#003580', textColor: '#fff', - sections: [ - { - title: 'Check-in DCS', - items: [ - { - title: 'Check-in online obbligatorio', - body: 'Ryanair richiede check-in online obbligatorio (2–30 giorni prima).\n\nAl banco: solo per casi speciali (SPEQ, documenti da verificare, bagagli).\n\nVerificare boarding pass su app Ryanair o PDF stampato.', - }, - { - title: 'Priority Boarding', - body: 'Passeggeri con Priority:\n• Boarding prioritario\n• Bagaglio cabina grande 55×40×20 cm incluso\n• Verificare tariffa (Regular Plus, Family Plus) o Ryanair+ membership\n\nSenza Priority:\n• Solo borsa 40×20×25 cm (sotto sedile) – GRATIS\n• Bagaglio cabina non incluso', - }, - { - title: 'Gestione bagagli non-Priority', - body: 'Se passeggero senza Priority porta bagaglio grande:\n• Al gate: imbarcare in stiva GRATIS (se volo non pieno)\n• Addebitare tariffa se supera i limiti\n\nAl banco check-in:\n• Addebitare bagaglio cabina se non incluso nel biglietto\n• Registrare nel DCS e emettere ricevuta', - }, - ], - }, - { - title: 'Gate Operations', - items: [ - { - title: 'Scan boarding pass', - body: 'Scansione boarding pass:\n• App Ryanair (QR code)\n• PDF stampato (con QR code leggibile)\n\nIn caso di errore lettura:\n1. Verificare volo e data\n2. Controllare documento d\'identità\n3. Verificare manualmente nel DCS\n4. Contattare supervisore se necessario', - }, - { - title: 'Eccesso bagaglio al gate', - body: 'Al gate – bagaglio fuori misura:\n1. Misurare con il gauge\n2. Addebitare gate fee (tariffa più alta)\n3. Imbarcare in stiva\n4. Emettere ricevuta\n\nNote: il gate fee è significativamente più alto della tariffa online.', - }, - { - title: 'Chiusura e DEP', - body: 'Chiusura volo Ryanair:\n• Chiudere scansione 15 min prima della partenza\n• Riconciliare no-show\n• Bags off per no-show (sicurezza)\n• Trasmettere DEP (Departure Message)\n• Comunicare via radio/telefono al supervisore operativo', - }, - ], - }, - ], - }, - { - id: 'vueling', name: 'Vueling', code: 'VY', - color: '#F7C800', textColor: '#1a1a1a', - sections: [ - { - title: 'Check-in Amadeus Altéa', - items: [ - { - title: 'Apertura volo', - body: 'DCS Vueling – Amadeus Altéa DCS:\n\nLogin → selezionare volo → verificare rotta e data → aprire check-in.\n\nVerificare eventuali restrizioni operative sul volo.', - }, - { - title: 'Tariffe e bagagli', - body: 'Tariffa Basic:\n• 1 borsa 40×20×30 cm (gratis, sotto sedile)\n\nTariffa Optima / TimeFlex:\n• Bagaglio cabina 55×35×25 cm incluso\n• Possibile bagaglio stiva incluso\n\nVerificare sempre la tariffa nella prenotazione.', - }, - { - title: 'Documenti e TIMATIC', - body: 'Consultare TIMATIC in Altéa per requisiti documenti.\n\nVoli intra-UE: carta d\'identità o passaporto.\nVoli extra-UE: passaporto + eventuale visto.\n\nVerificare validità documento rispetto alla data di rientro.', - }, - ], - }, - { - title: 'Imbarco', - items: [ - { - title: 'Sequenza imbarco', - body: 'Ordine imbarco Vueling:\n1. Passeggeri speciali\n2. Vueling Club Platinum / Gold\n3. Biglietti Excellence\n4. Tutti gli altri\n\nAnnuncio imbarco via interfono o display gate.', - }, - { - title: 'Chiusura e APIS', - body: 'Chiusura volo:\n1. Scansione passeggeri completata\n2. Riconciliare no-show in Altéa\n3. Trasmissione APIS se rotta lo richiede\n4. Load sheet definitiva approvata\n5. Chiusura volo nel sistema', - }, - ], - }, - ], - }, -]; - -// ─── Inline command highlighting ────────────────────────────────────────────── -const CMD_REGEX = /(`[^`]+`)/g; - -function RichBodyText({ text, colors }: { text: string; colors: any }) { - const parts = text.split(CMD_REGEX); - return ( - - {parts.map((part, i) => { - if (part.startsWith('`') && part.endsWith('`')) { - const cmd = part.slice(1, -1); - return ( - - {cmd} - - ); - } - return {part}; - })} - - ); -} - -// ─── Commands Tab component ────────────────────────────────────────────────── -function CommandsTab({ commands, colors }: { commands: DCSCommand[]; colors: any }) { - const [search, setSearch] = useState(''); - const lower = search.toLowerCase(); - const filtered = lower - ? commands.filter(c => c.cmd.toLowerCase().includes(lower) || c.desc.toLowerCase().includes(lower)) - : commands; - - const categories = [...new Set(filtered.map(c => c.category))]; - - return ( - - - - - {search.length > 0 && ( - setSearch('')}> - - - )} - - - {categories.map(cat => ( - - - - {cat} - - - {filtered.filter(c => c.category === cat).map((c, i) => ( - - - {c.cmd.split(/(\[[^\]]+\])/).map((part, j) => - part.startsWith('[') ? ( - {part} - ) : ( - {part} - ) - )} - - - {c.desc} - - - ))} - - ))} - - {filtered.length === 0 && ( - - Nessun comando trovato - - )} - - ); -} - -// ─── Item component ─────────────────────────────────────────────────────────── -function makeItemStyles(c: ThemeColors) { - return StyleSheet.create({ - wrapper: { - backgroundColor: c.card, - borderRadius: 10, - marginBottom: 6, - overflow: 'hidden', - borderWidth: 1, - borderColor: c.border, - }, - header: { - flexDirection: 'row', alignItems: 'center', gap: 10, - padding: 13, - }, - title: { fontSize: 13, fontWeight: '600', color: c.text, flex: 1 }, - body: { - paddingHorizontal: 14, paddingBottom: 14, paddingTop: 2, - borderTopWidth: 1, borderTopColor: c.cardSecondary, - }, - bodyText: { - fontSize: 13, color: c.textSub, lineHeight: 20, - }, - }); -} - -function ManualItemRow({ - item, itemIdx, sectionIdx, airlineId, editMode, onEdit, -}: { - item: ManualItem; - itemIdx: number; - sectionIdx: number; - airlineId: string; - editMode: boolean; - onEdit: () => void; -}) { - const { colors } = useAppTheme(); - const itemStyles = useMemo(() => makeItemStyles(colors), [colors]); - const [open, setOpen] = useState(false); - - const toggle = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setOpen(v => !v); - }; - - return ( - - - - {item.title} - {editMode && ( - - - - )} - - {open && ( - - - - )} - - ); -} - -// ─── Section component ──────────────────────────────────────────────────────── -function makeSectionStyles(c: ThemeColors) { - return StyleSheet.create({ - wrapper: { - marginBottom: 12, - }, - header: { - flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 10, paddingHorizontal: 4, - borderBottomWidth: 1, borderBottomColor: c.border, - marginBottom: 8, - }, - title: { fontSize: 12, fontWeight: '700', color: c.textSub, letterSpacing: 0.8 }, - body: { paddingLeft: 0 }, - }); -} - -function SectionBlock({ - section, sectionIdx, airlineId, editMode, onEdit, onAddItem, onEditItem, -}: { - section: Section; - sectionIdx: number; - airlineId: string; - editMode: boolean; - onEdit: () => void; - onAddItem: () => void; - onEditItem: (itemIdx: number) => void; -}) { - const { colors } = useAppTheme(); - const sectionStyles = useMemo(() => makeSectionStyles(colors), [colors]); - const [open, setOpen] = useState(true); - - const toggle = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setOpen(v => !v); - }; - - return ( - - - {section.title} - - {editMode && ( - - - - )} - - - - {open && ( - - {section.items.map((item, i) => ( - onEditItem(i)} - /> - ))} - {editMode && ( - - - Aggiungi voce - - )} - - )} - - ); -} - -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 }, - inputMulti: { minHeight: 100, paddingTop: 9 }, - colorRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, marginTop: 8 }, - 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 }, - btnCancel: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#ccc' }, - btnSave: {}, - btnDanger: { marginRight: 'auto', backgroundColor: '#FEE2E2' }, - btnText: { fontSize: 14, fontWeight: '600' }, - btnDangerText: { fontSize: 14, fontWeight: '600', color: '#DC2626' }, -}); - // ─── Main Screen ────────────────────────────────────────────────────────────── function makeStyles(c: ThemeColors) { return StyleSheet.create({ @@ -627,335 +59,6 @@ function makeStyles(c: ThemeColors) { }); } -function AirlineModal({ - modal, airlines, persist, closeModal, -}: { - modal: ModalState; - airlines: Airline[]; - persist: (a: Airline[]) => void; - closeModal: () => void; -}) { - const { colors } = useAppTheme(); - const isEdit = modal.kind === 'airline_edit'; - const existing = isEdit ? airlines.find(a => a.id === (modal as any).airlineId) : undefined; - - const [name, setName] = useState(existing?.name ?? ''); - const [code, setCode] = useState(existing?.code ?? ''); - const [colorIdx, setColorIdx] = useState(() => { - if (!existing) return 0; - const idx = AIRLINE_COLORS.findIndex(c => c.color === existing.color); - return idx >= 0 ? idx : 0; - }); - - const visible = modal.kind === 'airline_add' || modal.kind === 'airline_edit'; - - const save = () => { - if (!name.trim() || !code.trim()) return; - const chosen = AIRLINE_COLORS[colorIdx] ?? AIRLINE_COLORS[0]; - if (isEdit) { - const updated = airlines.map(a => - a.id === (modal as any).airlineId - ? { ...a, name: name.trim(), code: code.trim().toUpperCase(), color: chosen.color, textColor: chosen.textColor } - : a - ); - persist(updated); - } else { - const newAirline: Airline = { - id: Date.now().toString(), - name: name.trim(), - code: code.trim().toUpperCase(), - color: chosen.color, - textColor: chosen.textColor, - sections: [], - }; - persist([...airlines, newAirline]); - } - closeModal(); - }; - - const del = () => { - Alert.alert( - 'Elimina compagnia', - `Eliminare "${existing?.name}" e tutti i suoi contenuti?`, - [ - { text: 'Annulla', style: 'cancel' }, - { - text: 'Elimina', style: 'destructive', - onPress: () => { - persist(airlines.filter(a => a.id !== (modal as any).airlineId)); - closeModal(); - }, - }, - ] - ); - }; - - return ( - - - - - - {isEdit ? 'Modifica compagnia' : 'Nuova compagnia'} - - Nome - - Codice IATA - - Colore - - {AIRLINE_COLORS.map((c, i) => ( - setColorIdx(i)} - /> - ))} - - - {isEdit && ( - - Elimina - - )} - - Annulla - - - Salva - - - - - - - ); -} - -function SectionModal({ - modal, airlines, persist, closeModal, -}: { - modal: ModalState; - airlines: Airline[]; - persist: (a: Airline[]) => void; - closeModal: () => void; -}) { - const { colors } = useAppTheme(); - const isEdit = modal.kind === 'section_edit'; - const airlineId = (modal as any).airlineId as string | undefined; - const sectionIdx = (modal as any).sectionIdx as number | undefined; - const existingTitle = isEdit && airlineId !== undefined && sectionIdx !== undefined - ? airlines.find(a => a.id === airlineId)?.sections[sectionIdx]?.title ?? '' - : ''; - - const [title, setTitle] = useState(existingTitle); - const visible = modal.kind === 'section_add' || modal.kind === 'section_edit'; - - const save = () => { - if (!title.trim() || !airlineId) return; - const updated = airlines.map(a => { - if (a.id !== airlineId) return a; - if (isEdit && sectionIdx !== undefined) { - const sections = a.sections.map((s, i) => - i === sectionIdx ? { ...s, title: title.trim() } : s - ); - return { ...a, sections }; - } else { - return { ...a, sections: [...a.sections, { title: title.trim(), items: [] }] }; - } - }); - persist(updated); - closeModal(); - }; - - const del = () => { - const airline = airlines.find(a => a.id === airlineId); - const sectionTitle = sectionIdx !== undefined ? airline?.sections[sectionIdx]?.title : ''; - Alert.alert( - 'Elimina sezione', - `Eliminare la sezione "${sectionTitle}" e tutte le sue voci?`, - [ - { text: 'Annulla', style: 'cancel' }, - { - text: 'Elimina', style: 'destructive', - onPress: () => { - const updated = airlines.map(a => { - if (a.id !== airlineId) return a; - return { ...a, sections: a.sections.filter((_, i) => i !== sectionIdx) }; - }); - persist(updated); - closeModal(); - }, - }, - ] - ); - }; - - return ( - - - - - - {isEdit ? 'Modifica sezione' : 'Nuova sezione'} - - Titolo - - - {isEdit && ( - - Elimina - - )} - - Annulla - - - Salva - - - - - - - ); -} - -function ItemModal({ - modal, airlines, persist, closeModal, -}: { - modal: ModalState; - airlines: Airline[]; - persist: (a: Airline[]) => void; - closeModal: () => void; -}) { - const { colors } = useAppTheme(); - const isEdit = modal.kind === 'item_edit'; - const airlineId = (modal as any).airlineId as string | undefined; - const sectionIdx = (modal as any).sectionIdx as number | undefined; - const itemIdx = (modal as any).itemIdx as number | undefined; - - const existing = isEdit && airlineId && sectionIdx !== undefined && itemIdx !== undefined - ? airlines.find(a => a.id === airlineId)?.sections[sectionIdx]?.items[itemIdx] - : undefined; - - const [title, setTitle] = useState(existing?.title ?? ''); - const [body, setBody] = useState(existing?.body ?? ''); - const visible = modal.kind === 'item_add' || modal.kind === 'item_edit'; - - const save = () => { - if (!title.trim() || !airlineId || sectionIdx === undefined) return; - const updated = airlines.map(a => { - if (a.id !== airlineId) return a; - const sections = a.sections.map((s, si) => { - if (si !== sectionIdx) return s; - if (isEdit && itemIdx !== undefined) { - const items = s.items.map((it, ii) => - ii === itemIdx ? { title: title.trim(), body: body.trim() } : it - ); - return { ...s, items }; - } else { - return { ...s, items: [...s.items, { title: title.trim(), body: body.trim() }] }; - } - }); - return { ...a, sections }; - }); - persist(updated); - closeModal(); - }; - - const del = () => { - Alert.alert( - 'Elimina voce', - `Eliminare "${existing?.title}"?`, - [ - { text: 'Annulla', style: 'cancel' }, - { - text: 'Elimina', style: 'destructive', - onPress: () => { - const updated = airlines.map(a => { - if (a.id !== airlineId) return a; - const sections = a.sections.map((s, si) => { - if (si !== sectionIdx) return s; - return { ...s, items: s.items.filter((_, ii) => ii !== itemIdx) }; - }); - return { ...a, sections }; - }); - persist(updated); - closeModal(); - }, - }, - ] - ); - }; - - return ( - - - - - - {isEdit ? 'Modifica voce' : 'Nuova voce'} - - Titolo - - Contenuto - - - {isEdit && ( - - Elimina - - )} - - Annulla - - - Salva - - - - - - - ); -} - export default function ManualsScreen() { const { colors } = useAppTheme(); const s = useMemo(() => makeStyles(colors), [colors]); diff --git a/src/utils/manualsData.ts b/src/utils/manualsData.ts new file mode 100644 index 0000000..9f05c4e --- /dev/null +++ b/src/utils/manualsData.ts @@ -0,0 +1,281 @@ +export const STORAGE_KEY = 'manuals_data_v2'; + +export const AIRLINE_COLORS = [ + { color: '#FF6600', textColor: '#fff' }, + { color: '#C01380', textColor: '#fff' }, + { color: '#003580', textColor: '#fff' }, + { color: '#F7C800', textColor: '#1a1a1a' }, + { color: '#006DBF', textColor: '#fff' }, + { color: '#2E7D32', textColor: '#fff' }, + { color: '#B71C1C', textColor: '#fff' }, + { color: '#4A148C', textColor: '#fff' }, + { color: '#E65100', textColor: '#fff' }, + { color: '#37474F', textColor: '#fff' }, +]; + +export type ManualItem = { title: string; body: string }; +export type Section = { title: string; items: ManualItem[] }; +export type DCSCommand = { cmd: string; desc: string; category: string }; +export type Airline = { id: string; name: string; code: string; color: string; textColor: string; sections: Section[]; commands?: DCSCommand[] }; + +export type ModalState = + | { kind: 'none' } + | { kind: 'airline_add' } + | { kind: 'airline_edit'; airlineId: string } + | { kind: 'section_add'; airlineId: string } + | { kind: 'section_edit'; airlineId: string; sectionIdx: number } + | { kind: 'item_add'; airlineId: string; sectionIdx: number } + | { kind: 'item_edit'; airlineId: string; sectionIdx: number; itemIdx: number }; + +export const DEFAULT_AIRLINES: Airline[] = [ + { + id: 'easyjet', name: 'easyJet', code: 'EZY', + color: '#FF6600', textColor: '#fff', + commands: [ + { cmd: 'L', desc: 'Lista voli del giorno', category: 'Flight Ops' }, + { cmd: 'OF/[volo]', desc: 'Apri volo per check-in', category: 'Flight Ops' }, + { cmd: 'CF/[volo]', desc: 'Chiudi volo', category: 'Flight Ops' }, + { cmd: 'LOF', desc: 'Lista voli aperti', category: 'Flight Ops' }, + { cmd: 'GC/[gate]', desc: 'Cambia gate', category: 'Flight Ops' }, + { cmd: 'F[cognome]/[nome]', desc: 'Cerca passeggero per nome', category: 'Search' }, + { cmd: '.[booking ref]', desc: 'Cerca per codice prenotazione', category: 'Search' }, + { cmd: 'C[n]', desc: 'Check-in passeggero linea n', category: 'Check-in' }, + { cmd: 'C[n]-[m]', desc: 'Check-in gruppo (linee n-m)', category: 'Check-in' }, + { cmd: 'AB[n]', desc: 'Aggiungi bagaglio a passeggero', category: 'Baggage' }, + { cmd: '.DSP[n]', desc: 'Mostra/cambia seat plan', category: 'Seating' }, + { cmd: '.SS/[volo]/[s1]-[s2]', desc: 'Seat swap', category: 'Seating' }, + { cmd: '.SAG[volo]', desc: 'Lista Seat at Gate', category: 'Seating' }, + { cmd: 'DSP1', desc: 'Seat plan per SAG customer 1', category: 'Seating' }, + { cmd: '.COM[n]', desc: 'Visualizza commenti booking', category: 'Customer' }, + { cmd: 'COM[n]', desc: 'Aggiungi commento a booking', category: 'Customer' }, + { cmd: 'TM[n]', desc: 'Cambia titolo a Male', category: 'Customer' }, + { cmd: 'TF[n]', desc: 'Cambia titolo a Female', category: 'Customer' }, + { cmd: 'J[n]', desc: 'Cambia titolo a Child', category: 'Customer' }, + { cmd: 'EX', desc: 'Esci / logout', category: 'System' }, + ], + sections: [ + { + title: 'Check-in DCS', + items: [ + { + title: 'Apertura volo', + body: 'Accedere al DCS (opzione 16). Digitare `L` per lista voli.\n\nPer aprire il volo: `OF/[numero volo]` e confermare con Y.\n\nImpostare boarding card e bag tag su A (auto).\nInserire gate, orario boarding e capacità.\n\nVerificare:\n• Numero volo e data corretti\n• Rotta e gate assegnati\n• Segmenti collegati (connessioni)\n• Restrizioni eventuali sul volo', + }, + { + title: 'Check-in passeggeri', + body: 'Cercare passeggero: `F[cognome]/[nome]` oppure `.[booking ref]`.\n\nCheck-in: `C[n]` per singolo, `C[n]-[m]` per gruppo.\n\nRichiedere documento d\'identità valido.\n\nDocumenti accettati (UE):\n• Carta d\'identità\n• Passaporto\n\nVerificare: validità, nome uguale al biglietto, destinazione.\n\nAssegnare posto con `.DSP[n]`.', + }, + { + title: 'Bagaglio a mano', + body: 'Misure max bagaglio cabina:\n• Standard: 56×45×25 cm (inclusa impugnatura e ruote)\n• Borsa aggiuntiva: 45×36×20 cm\n\nSe eccede → addebitare hold baggage fee.\nSe il volo è pieno → imbarchare gratis in stiva.', + }, + { + title: 'Overbooking', + body: 'Procedura overbooking:\n1. Verificare numero di passeggeri in eccesso\n2. Cercare volontari (offrire voucher + volo alternativo)\n3. Se nessun volontario → denied boarding involontario\n\nAggiungere commento al booking con `COM[n]` selezionando il motivo appropriato.\n\nRegolamento EU 261/2004:\n• Voli ≤1.500 km → €250\n• Voli 1.500–3.500 km → €400\n• Voli >3.500 km → €600\n\nOltre a rimborso o volo alternativo.', + }, + ], + }, + { + title: 'Gate e Imbarco', + items: [ + { + title: 'Apertura gate', + body: 'Prima di aprire il gate:\n• Attivare gate nel sistema DCS\n• Per cambiare gate: `GC/[numero gate]`\n• Verificare lista SPEQ comunicata al capo cabina\n• Controllare info gate board\n• Comunicare ritardi se presenti\n\nPer lista SAG: `.SAG[numero volo]`', + }, + { + title: 'Sequenza boarding', + body: 'Ordine imbarco easyJet:\n1. Passeggeri speciali (WCHR, WCHC, famiglie con bambini <5 anni)\n2. easyJet Plus + posti Upfront & Extra Legroom\n3. Tutti gli altri passeggeri\n\nVerificare sempre boarding pass + documento.\n\nPer seat swap operativo: `.SS/[volo]/[posto1]-[posto2]`', + }, + { + title: 'Chiusura volo', + body: 'Procedura chiusura:\n1. Scansione ultimi passeggeri (T-15 min)\n2. Riconciliare no-show nel DCS\n3. Last-minute changes comunicati al capo volo\n4. Invio load sheet definitiva\n5. Chiusura volo: `CF/[numero volo]`\n6. Comunicare al gate supervisore\n\nPer verificare voli aperti: `LOF`', + }, + ], + }, + { + title: 'Passeggeri Speciali', + items: [ + { + title: 'Codici WCHR / WCHC / WCHS', + body: 'WCHR → può camminare, ha difficoltà sulle scale.\nWCHC → non può camminare, necessita assistenza totale.\nWCHS → non può fare scale (usa pontile o bus).\n\nSempre:\n• Pre-imbarcare\n• Notificare capo cabina e handling\n• Verificare posto (non uscita emergenza)', + }, + { + title: 'UM – Unaccompanied Minor', + body: 'Minori non accompagnati (5–14 anni):\n• Servizio UM obbligatorio\n• Modulo UM compilato da genitore/tutore\n• Accettare solo con form completo\n• Notificare capo cabina\n• Contatti ritirante a destinazione\n• Non lasciare mai il minore da solo', + }, + { + title: 'MEDA – Passeggero Medico', + body: 'Passeggero con condizione medica rilevante:\n• Richiede MEDA clearance pre-volo\n• MEDA form compilato dal medico curante\n• Eventuale accompagnatore medico (MEDA + 1)\n• Comunicare al Comandante\n• Equipaggiamento medico a bordo verificato', + }, + { + title: 'Documenti di viaggio', + body: 'Verificare sempre:\n• Validità documento (min. 3 mesi oltre la data di rientro per alcuni paesi)\n• Visto se necessario (TIMATIC nel DCS)\n• Per viaggi extra-Schengen: passaporto obbligatorio\n• APIS: inserire dati documento nel sistema se richiesto dalla rotta', + }, + ], + }, + { + title: 'Tariffe Aeroportuali', + items: [ + { + title: 'Bagaglio irregolare al gate', + body: 'Bagaglio cabina fuori misura o non conforme al gate: €58.\n\nViene imbarcato in stiva.\n\nMisure consentite:\n• Standard: 45×36×20 cm\n• Grande (Plus/Up Front/Extra Legroom): 56×45×25 cm', + }, + { + title: 'Bagaglio da aggiungere', + body: 'Bagaglio stiva da aggiungere in aeroporto: €65.\n\nBagaglio stiva 23kg prenotato online: da €11.99 (varia per rotta/periodo).', + }, + { + title: 'Sovrappeso bagaglio', + body: 'Bagaglio che supera il peso acquistato:\n• €15 per ogni kg in eccesso\n• Peso massimo consentito: 32kg\n\nPesare sempre il bagaglio e addebitare la differenza.', + }, + ], + }, + ], + }, + { + id: 'wizzair', name: 'Wizz Air', code: 'W6', + color: '#C01380', textColor: '#fff', + sections: [ + { + title: 'Check-in DCS', + items: [ + { + title: 'Apertura check-in', + body: 'Accedere al sistema DCS Wizz Air con credenziali personali.\n\nSelezionare il volo → verificare rotta, data e orario → avviare check-in.\n\nCheck-in online disponibile da 30 giorni a 3 ore prima del volo.', + }, + { + title: 'Documenti accettati', + body: 'Per TUTTI i voli:\n• Passaporto valido\n\nVoli intra-Schengen (paesi selezionati):\n• Carta d\'identità\n\nVerificare sempre validità e destinazione tramite TIMATIC.', + }, + { + title: 'WIZZ Priority', + body: 'Passeggeri WIZZ Priority:\n• Corsia dedicata al check-in\n• Imbarco prioritario\n• Bagaglio cabina grande (40×30×20 cm + borsa)\n• Verificare tariffa Priority sulla prenotazione o Wizz+ membership\n\nSenza Priority: solo borsa piccola 40×30×20 cm.', + }, + ], + }, + { + title: 'Bagaglio', + items: [ + { + title: 'Politica bagagli', + body: 'Bagaglio incluso in tutti i biglietti:\n• 1 borsa piccola 40×30×20 cm (sotto sedile) – GRATIS\n\nA pagamento (prenotato online o al banco):\n• Bagaglio cabina grande 55×40×23 cm\n\nBagaglio stiva disponibile: 10 kg / 20 kg / 26 kg / 32 kg\nVerificare la prenotazione nel DCS.', + }, + { + title: 'Eccesso bagaglio al banco', + body: 'Se il bagaglio non è prenotato o supera i limiti:\n1. Verificare nel DCS la prenotazione bagagli\n2. Addebitare la tariffa airport fee (superiore al prezzo online)\n3. Registrare il pagamento nel sistema\n4. Emettere ricevuta\n5. Attaccare tag bagaglio correttamente', + }, + ], + }, + { + title: 'Gate e Imbarco', + items: [ + { + title: 'Sequenza boarding', + body: 'Ordine imbarco Wizz Air:\n1. Passeggeri speciali + WCHR + famiglie con bambini piccoli\n2. WIZZ Priority\n3. Tutti gli altri\n\nVerificare boarding pass (app o cartaceo) + documento d\'identità ad ogni imbarco.', + }, + { + title: 'Chiusura volo', + body: 'Procedura chiusura volo:\n1. Scansione ultimi passeggeri\n2. Riconciliare no-show nel sistema\n3. Bags reconcile (bagagli senza passeggero → scaricare)\n4. Trasmettere dati finali al vettore\n5. Chiudere gate nel DCS\n6. Notificare supervisore', + }, + ], + }, + { + title: 'Tariffe Aeroportuali', + items: [ + { + title: 'Check-in aeroportuale', + body: 'Check-in al banco aeroporto: €40 a passeggero.\n\nIl passeggero deve aver fatto check-in online (da 30 giorni a 3h prima). Se non l\'ha fatto, addebitare la tariffa.', + }, + { + title: 'Bagaglio irregolare al gate', + body: 'Bagaglio cabina fuori misura o non conforme al gate: €58.\n\nViene imbarcato in stiva.\n\nMisure consentite senza Priority: 40×30×20 cm.\nCon WIZZ Priority: 55×40×23 cm inclusa.', + }, + { + title: 'Bagaglio da aggiungere', + body: 'Bagaglio stiva da aggiungere in aeroporto: €65.\n\nBagaglio stiva disponibile online: 10kg / 20kg / 26kg / 32kg.\nVerificare sempre la prenotazione nel DCS.', + }, + { + title: 'Sovrappeso bagaglio', + body: 'Bagaglio che supera il peso acquistato:\n• €15 per ogni kg in eccesso\n\nPesare sempre il bagaglio e addebitare la differenza.', + }, + ], + }, + ], + }, + { + id: 'ryanair', name: 'Ryanair', code: 'FR', + color: '#003580', textColor: '#fff', + sections: [ + { + title: 'Check-in DCS', + items: [ + { + title: 'Check-in online obbligatorio', + body: 'Ryanair richiede check-in online obbligatorio (2–30 giorni prima).\n\nAl banco: solo per casi speciali (SPEQ, documenti da verificare, bagagli).\n\nVerificare boarding pass su app Ryanair o PDF stampato.', + }, + { + title: 'Priority Boarding', + body: 'Passeggeri con Priority:\n• Boarding prioritario\n• Bagaglio cabina grande 55×40×20 cm incluso\n• Verificare tariffa (Regular Plus, Family Plus) o Ryanair+ membership\n\nSenza Priority:\n• Solo borsa 40×20×25 cm (sotto sedile) – GRATIS\n• Bagaglio cabina non incluso', + }, + { + title: 'Gestione bagagli non-Priority', + body: 'Se passeggero senza Priority porta bagaglio grande:\n• Al gate: imbarcare in stiva GRATIS (se volo non pieno)\n• Addebitare tariffa se supera i limiti\n\nAl banco check-in:\n• Addebitare bagaglio cabina se non incluso nel biglietto\n• Registrare nel DCS e emettere ricevuta', + }, + ], + }, + { + title: 'Gate Operations', + items: [ + { + title: 'Scan boarding pass', + body: 'Scansione boarding pass:\n• App Ryanair (QR code)\n• PDF stampato (con QR code leggibile)\n\nIn caso di errore lettura:\n1. Verificare volo e data\n2. Controllare documento d\'identità\n3. Verificare manualmente nel DCS\n4. Contattare supervisore se necessario', + }, + { + title: 'Eccesso bagaglio al gate', + body: 'Al gate – bagaglio fuori misura:\n1. Misurare con il gauge\n2. Addebitare gate fee (tariffa più alta)\n3. Imbarcare in stiva\n4. Emettere ricevuta\n\nNote: il gate fee è significativamente più alto della tariffa online.', + }, + { + title: 'Chiusura e DEP', + body: 'Chiusura volo Ryanair:\n• Chiudere scansione 15 min prima della partenza\n• Riconciliare no-show\n• Bags off per no-show (sicurezza)\n• Trasmettere DEP (Departure Message)\n• Comunicare via radio/telefono al supervisore operativo', + }, + ], + }, + ], + }, + { + id: 'vueling', name: 'Vueling', code: 'VY', + color: '#F7C800', textColor: '#1a1a1a', + sections: [ + { + title: 'Check-in Amadeus Altéa', + items: [ + { + title: 'Apertura volo', + body: 'DCS Vueling – Amadeus Altéa DCS:\n\nLogin → selezionare volo → verificare rotta e data → aprire check-in.\n\nVerificare eventuali restrizioni operative sul volo.', + }, + { + title: 'Tariffe e bagagli', + body: 'Tariffa Basic:\n• 1 borsa 40×20×30 cm (gratis, sotto sedile)\n\nTariffa Optima / TimeFlex:\n• Bagaglio cabina 55×35×25 cm incluso\n• Possibile bagaglio stiva incluso\n\nVerificare sempre la tariffa nella prenotazione.', + }, + { + title: 'Documenti e TIMATIC', + body: 'Consultare TIMATIC in Altéa per requisiti documenti.\n\nVoli intra-UE: carta d\'identità o passaporto.\nVoli extra-UE: passaporto + eventuale visto.\n\nVerificare validità documento rispetto alla data di rientro.', + }, + ], + }, + { + title: 'Imbarco', + items: [ + { + title: 'Sequenza imbarco', + body: 'Ordine imbarco Vueling:\n1. Passeggeri speciali\n2. Vueling Club Platinum / Gold\n3. Biglietti Excellence\n4. Tutti gli altri\n\nAnnuncio imbarco via interfono o display gate.', + }, + { + title: 'Chiusura e APIS', + body: 'Chiusura volo:\n1. Scansione passeggeri completata\n2. Riconciliare no-show in Altéa\n3. Trasmissione APIS se rotta lo richiede\n4. Load sheet definitiva approvata\n5. Chiusura volo nel sistema', + }, + ], + }, + ], + }, +];