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',
+ },
+ ],
+ },
+ ],
+ },
+];