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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/screens/FlightScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useAppTheme } from '../context/ThemeContext';
import { useAirport } from '../context/AirportContext';
import { getAirlineOps, getAirlineColor } from '../utils/airlineOps';
import { fetchAirportScheduleRaw } from '../utils/fr24api';
import { fetchStaffMonitorData, type StaffMonitorInfo } from '../utils/staffMonitor';
import { formatAirportHeader } from '../utils/airportSettings';
import { requestWidgetUpdate } from 'react-native-android-widget';
import { WIDGET_CACHE_KEY } from '../widgets/widgetTaskHandler';
Expand Down Expand Up @@ -245,6 +246,7 @@ export default function FlightScreen() {
const [scheduledCount, setScheduledCount] = useState(0);
const [pinnedFlightId, setPinnedFlightId] = useState<string | null>(null);
const [inboundArrivals, setInboundArrivals] = useState<Record<string, number>>({});
const [staffData, setStaffData] = useState<Map<string, StaffMonitorInfo>>(new Map());

// Carica preferenza notifiche salvata
useEffect(() => {
Expand Down Expand Up @@ -273,6 +275,9 @@ export default function FlightScreen() {
}
setInboundArrivals(inboundMap);

// Fetch dati operativi reali da staffMonitor (stand, banco CI, gate) — fail silenzioso
fetchStaffMonitorData().then(sm => setStaffData(sm));

setArrivals(fetchedArrivals);
setDepartures(fetchedDepartures);

Expand Down Expand Up @@ -544,6 +549,10 @@ export default function FlightScreen() {
const flightId = item.flight?.identification?.number?.default || null;
const isPinned = flightId !== null && flightId === pinnedFlightId;

// Dati operativi reali dallo StaffMonitor (stand fisico, banco CI, gate)
const smKey = (flightId || '').replace(/\s/g, '');
const smInfo = staffData.get(smKey);

return (
<SwipeableFlightCard
isPinned={isPinned}
Expand Down Expand Up @@ -654,6 +663,32 @@ export default function FlightScreen() {
)}
</View>
</View>
{/* StaffMonitor: Stand · Check-in banco · Gate */}
{!!smInfo && (!!smInfo.stand || !!smInfo.checkin || !!smInfo.gate) && (
<View style={s.staffRow}>
{!!smInfo.stand && (
<View style={s.staffBadge}>
<MaterialIcons name="local-parking" size={11} color={colors.textSub} />
<Text style={s.staffLabel}>Stand</Text>
<Text style={s.staffValue}>{smInfo.stand}</Text>
</View>
)}
{!!smInfo.checkin && (
<View style={s.staffBadge}>
<MaterialIcons name="desktop-windows" size={11} color={colors.textSub} />
<Text style={s.staffLabel}>CI</Text>
<Text style={s.staffValue}>{smInfo.checkin}</Text>
</View>
)}
{!!smInfo.gate && (
<View style={s.staffBadge}>
<MaterialIcons name="meeting-room" size={11} color={colors.textSub} />
<Text style={s.staffLabel}>Gate</Text>
<Text style={s.staffValue}>{smInfo.gate}</Text>
</View>
)}
</View>
)}
</SwipeableFlightCard>
);
}, [activeTab, userShift, s, pinnedFlightId, pinFlight, unpinFlight, inboundArrivals, colors]);
Expand Down Expand Up @@ -766,5 +801,26 @@ function makeStyles(c: any) {
opsTime: { fontSize: 13, fontWeight: '800', color: c.primaryDark },
pinBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: 'rgba(255,255,255,0.15)', justifyContent: 'center', alignItems: 'center' },
pinBtnActive: { backgroundColor: 'rgba(245,158,11,0.25)' },
staffRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
paddingHorizontal: 14,
paddingVertical: 8,
borderTopWidth: 1,
borderTopColor: c.border,
backgroundColor: c.cardSecondary ?? c.bg,
},
staffBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
backgroundColor: c.primaryLight,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 4,
},
staffLabel: { fontSize: 9, fontWeight: '600' as const, color: c.textSub, letterSpacing: 0.5 },
staffValue: { fontSize: 12, fontWeight: '800' as const, color: c.primaryDark },
});
}
134 changes: 134 additions & 0 deletions src/utils/staffMonitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// src/utils/staffMonitor.ts

export type StaffMonitorInfo = {
stand: string; // es. "27"
checkin: string; // es. "17" oppure "33 / 34" per più banchi
gate: string; // es. "11"
};

const BASE_URL = 'https://servizi.pisa-airport.com/staffMonitor/staffMonitor';
const TIMEOUT_MS = 8_000;

/**
* Rimuove gli zeri iniziali dalla parte numerica del numero volo.
* Il codice IATA è sempre i primi 2 caratteri (lettere o cifre).
* FR08973 → FR8973
* U202490 → U22490
* HV05424 → HV5424
*/
function normalizeFlightNumber(raw: string): string {
const s = raw.trim().toUpperCase(); // normalizza a maiuscolo per match con dati FR24
if (s.length < 3) return s;
const iata = s.slice(0, 2);
const num = parseInt(s.slice(2), 10);
if (isNaN(num)) return s;
return `${iata}${num}`;
}

/** Rimuove tutti i tag HTML e decodifica le entity comuni. */
function extractText(html: string): string {
return html
.replace(/<br\s*\/?>/gi, '\n') // converti <br> in newline prima di strippare i tag
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.trim();
}

/**
* Estrae il contenuto di ogni TD da un blocco TR.
* Normalizza i TD self-closing (<TD ... />) prima del parsing.
*/
function extractCells(rowHtml: string): string[] {
const normalised = rowHtml.replace(/<TD(\s[^>]*)?\s*\/>/gi, '<TD$1></TD>');
const cells: string[] = [];
const tdRe = /<TD(?:\s[^>]*)?>([\s\S]*?)<\/TD>/gi;
let m: RegExpExecArray | null;
while ((m = tdRe.exec(normalised)) !== null) {
cells.push(m[1]);
}
return cells;
}

/**
* Parsa una riga della tabella (array di celle) estraendo:
* - colonna 1: numero volo (class="clsFlight")
* - colonna 12: stand
* - colonna 13: banco/i check-in
* - colonna 14: gate
*/
function parseRow(cells: string[]): { flightNumber: string; info: StaffMonitorInfo } | null {
if (cells.length < 15) return null;

const rawFlight = extractText(cells[1]);
if (!rawFlight || rawFlight.length < 3) return null;
// Solo righe con codice IATA valido (2 alfanumerici + cifre)
if (!/^[A-Z0-9]{2}\d+$/i.test(rawFlight)) return null;

const flightNumber = normalizeFlightNumber(rawFlight);

const stand = extractText(cells[12]).replace(/°/g, '').trim();

// Il banco CI può avere più valori su righe separate, es. "33°\n34°"
const checkin = extractText(cells[13])
.split(/[\n\r]+/)
.map(d => d.replace(/°/g, '').trim())
.filter(d => d.length > 0)
.join(' / ');

const gate = extractText(cells[14]).replace(/°/g, '').trim();

return { flightNumber, info: { stand, checkin, gate } };
}

/** Parsa una pagina HTML del monitor in una Map flightNumber → StaffMonitorInfo. */
function parseHtml(html: string): Map<string, StaffMonitorInfo> {
const result = new Map<string, StaffMonitorInfo>();
const trRe = /<TR(?:\s[^>]*)?>([\s\S]*?)<\/TR>/gi;
let trMatch: RegExpExecArray | null;
while ((trMatch = trRe.exec(html)) !== null) {
const parsed = parseRow(extractCells(trMatch[1]));
if (parsed) result.set(parsed.flightNumber, parsed.info);
}
return result;
}

async function fetchPage(nature: 'D' | 'A', signal: AbortSignal): Promise<string> {
const res = await fetch(`${BASE_URL}?trans=true&nature=${nature}`, { signal });
if (!res.ok) throw new Error(`staffMonitor ${nature}: HTTP ${res.status}`);
return res.text();
}

/**
* Fetcha e parsa le pagine partenze (D) e arrivi (A) dello StaffMonitor di Pisa in parallelo.
*
* Restituisce una Map keyed per numero volo normalizzato (es. "FR8973") con
* i dati operativi reali: stand, banco check-in, gate.
*
* Non rigetta mai — restituisce Map vuota in caso di qualsiasi errore.
*/
export async function fetchStaffMonitorData(): Promise<Map<string, StaffMonitorInfo>> {
const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const [depHtml, arrHtml] = await Promise.all([
fetchPage('D', controller.signal),
fetchPage('A', controller.signal),
]);
// Arrival data ha precedenza sulle departure per voli in transito/turnaround (intenzionale)
return new Map<string, StaffMonitorInfo>([
...parseHtml(depHtml),
...parseHtml(arrHtml),
]);
} catch (err) {
controller.abort(); // cancella l'eventuale fetch gemella ancora in volo
if (__DEV__) console.warn('[staffMonitor] fetch failed:', err);
return new Map();
} finally {
clearTimeout(tid);
}
}
Loading