402 lines
15 KiB
TypeScript
402 lines
15 KiB
TypeScript
import { LoadingSpinner } from '@/assets/components/LoadingSpinner';
|
|
import { DashedDivider } from '@/assets/components/reserva/DashedDivider';
|
|
import { DocumentoRow } from '@/assets/components/reserva/DocumentoRow';
|
|
import {
|
|
formatCurrency,
|
|
formatDateLong,
|
|
formatDateShort,
|
|
getImageUrl,
|
|
} from '@/assets/components/reserva/formatters';
|
|
import { HotelCard } from '@/assets/components/reserva/HotelCard';
|
|
import { PassageiroCard } from '@/assets/components/reserva/PassageiroCard';
|
|
import { Section } from '@/assets/components/reserva/Section';
|
|
import { StatusBadge } from '@/assets/components/reserva/StatusBadge';
|
|
import { ValorReservaCard } from '@/assets/components/reserva/ValorReservaCard';
|
|
import { VooCard } from '@/assets/components/reserva/VooCard';
|
|
import { API_ENDPOINTS, buildApiUrl } from '@/assets/config/api';
|
|
import { useAuth } from '@/assets/contexts/useAuth';
|
|
import { cacheReservaFull, getCachedReservaFull } from '@/assets/services/offlineStorage';
|
|
import {
|
|
Documento,
|
|
Escala,
|
|
JsonData,
|
|
Pagamento,
|
|
ReservaData,
|
|
ReservaResponse,
|
|
VooDirection,
|
|
VooSegment,
|
|
} from '@/assets/types';
|
|
import styles from '@/styles/screens/reserva/detail.styles';
|
|
import { FontAwesome } from '@expo/vector-icons';
|
|
import { Stack, useLocalSearchParams } from 'expo-router';
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
Image,
|
|
RefreshControl,
|
|
ScrollView,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
|
|
export default function ReservaDetalheScreen() {
|
|
const { referencia } = useLocalSearchParams<{ referencia: string }>();
|
|
const { token } = useAuth();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [reservaData, setReservaData] = useState<ReservaData | null>(null);
|
|
const [pagamentos, setPagamentos] = useState<Pagamento[]>([]);
|
|
const [documentos, setDocumentos] = useState<Documento[]>([]);
|
|
const [expandedPassageiros, setExpandedPassageiros] = useState<Set<number>>(new Set());
|
|
const [cachedAt, setCachedAt] = useState<string | null>(null);
|
|
const [isFromCache, setIsFromCache] = useState(false);
|
|
|
|
const togglePassageiro = (idx: number) => {
|
|
setExpandedPassageiros((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(idx)) {
|
|
next.delete(idx);
|
|
} else {
|
|
next.add(idx);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const fetchReserva = async () => {
|
|
try {
|
|
setError(null);
|
|
if (!token || !referencia) {
|
|
throw new Error('Dados de autenticacao em falta');
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('token', token);
|
|
formData.append('referenciaViagem', referencia);
|
|
|
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.GET_RESERVA), {
|
|
method: 'POST',
|
|
headers: { Accept: 'application/json' },
|
|
body: formData,
|
|
});
|
|
|
|
const data: ReservaResponse = await response.json();
|
|
if (!response.ok || data.status !== 200 || !data.reserva) {
|
|
throw new Error(data.message || 'Nao foi possivel carregar a reserva');
|
|
}
|
|
|
|
const pagamentosData = data.pagamentos || [];
|
|
const documentosData = data.documentos || [];
|
|
const now = new Date().toISOString();
|
|
|
|
setReservaData(data.reserva);
|
|
setPagamentos(pagamentosData);
|
|
setDocumentos(documentosData);
|
|
setCachedAt(now);
|
|
setIsFromCache(false);
|
|
|
|
await cacheReservaFull(referencia, {
|
|
reservaData: data.reserva,
|
|
pagamentos: pagamentosData,
|
|
documentos: documentosData,
|
|
cachedAt: now,
|
|
});
|
|
} catch {
|
|
const cached = await getCachedReservaFull(referencia);
|
|
if (cached) {
|
|
setReservaData(cached.reservaData);
|
|
setPagamentos(cached.pagamentos);
|
|
setDocumentos(cached.documentos);
|
|
setCachedAt(cached.cachedAt);
|
|
setIsFromCache(true);
|
|
setError(null);
|
|
} else {
|
|
setError('Sem ligação à internet e sem dados guardados para esta reserva.');
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchReserva();
|
|
}, [referencia, token]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await fetchReserva();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const parsedJsonData = useMemo((): JsonData | null => {
|
|
if (!reservaData?.jsonData) return null;
|
|
try {
|
|
return typeof reservaData.jsonData === 'string'
|
|
? (JSON.parse(reservaData.jsonData) as JsonData)
|
|
: reservaData.jsonData;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, [reservaData?.jsonData]);
|
|
|
|
const hotelInfo = parsedJsonData?.hotel || [];
|
|
const extrasData = parsedJsonData?.extras ?? parsedJsonData?.extra ?? [];
|
|
const voosData = parsedJsonData?.voo;
|
|
|
|
const getVoosFromLeg = (leg?: VooDirection): (Escala | VooSegment)[] => {
|
|
if (!leg) return [];
|
|
if (Array.isArray(leg)) return leg;
|
|
return leg.infoEscalas ?? [];
|
|
};
|
|
|
|
const voosIda = getVoosFromLeg(voosData?.departure);
|
|
const voosVolta = getVoosFromLeg(voosData?.arrival);
|
|
|
|
const buildQuartosPassageiros = (data: ReservaData) => {
|
|
const partes: string[] = [];
|
|
if (data.quartos) partes.push(`${data.quartos} Quartos`);
|
|
if (data.adultos) partes.push(`${data.adultos} Adultos`);
|
|
if (data.criancas && data.criancas !== '0') partes.push(`${data.criancas} Crianças`);
|
|
return partes.length ? partes.join(' · ') : `${data.pessoas} Passageiros`;
|
|
};
|
|
|
|
const statusCode = String(reservaData?.status ?? '');
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Stack.Screen
|
|
options={{
|
|
headerShown: true,
|
|
headerTitleAlign: 'center',
|
|
headerBackButtonDisplayMode: 'minimal',
|
|
headerShadowVisible: false,
|
|
headerStyle: { backgroundColor: '#F4F7FE' },
|
|
headerTitle: () => (
|
|
<Image
|
|
source={require('@/assets/icons/logotipo-azul.png')}
|
|
style={styles.logo}
|
|
resizeMode="contain"
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
{isLoading ? (
|
|
<View style={styles.centered}>
|
|
<LoadingSpinner size="large" />
|
|
</View>
|
|
) : (
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={styles.content}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
|
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
|
|
|
{reservaData && (
|
|
<>
|
|
{isFromCache && (
|
|
<View style={styles.offlineBanner}>
|
|
<FontAwesome name="wifi" size={13} color="#fff" />
|
|
<Text style={styles.offlineBannerText}>Sem ligação — a mostrar dados guardados</Text>
|
|
</View>
|
|
)}
|
|
|
|
<Text style={styles.destination}>{reservaData.destino}</Text>
|
|
|
|
<Section>
|
|
{!!reservaData.imagemCidade && (
|
|
<Image
|
|
source={{ uri: getImageUrl(reservaData.imagemCidade) }}
|
|
style={styles.summaryImage}
|
|
/>
|
|
)}
|
|
<View style={styles.rowBetween}>
|
|
<View style={styles.idBlock}>
|
|
<Text style={styles.idTop}>ID #{reservaData.referenciaViagem}</Text>
|
|
<Text style={styles.fieldLabel}>Ref. da Reserva</Text>
|
|
</View>
|
|
<StatusBadge statusCode={statusCode} />
|
|
</View>
|
|
<DashedDivider width={0.5} borderStyle="solid" />
|
|
<View>
|
|
<Text style={styles.idTop}>{reservaData.localizador || '---'}</Text>
|
|
<Text style={styles.fieldLabel}>Ref. do Operador</Text>
|
|
</View>
|
|
</Section>
|
|
|
|
<Section title="Resumo da Reserva">
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
<View style={styles.rowLineNoBorder}>
|
|
<Text style={styles.rowLabel}>Destino</Text>
|
|
<Text style={styles.rowValue}>{reservaData.destino}</Text>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
<View style={styles.rowLineNoBorder}>
|
|
<Text style={styles.rowLabel}>Data Viagem</Text>
|
|
<Text style={styles.rowValue}>
|
|
{formatDateLong(reservaData.startDate)} -{' '}
|
|
{formatDateLong(reservaData.endDate)}
|
|
</Text>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
<View style={styles.rowLineNoBorder}>
|
|
<Text style={styles.rowLabel}>Nr. de Noites</Text>
|
|
<Text style={styles.rowValue}>{reservaData.noites} noites</Text>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
<View style={styles.rowLineNoBorder}>
|
|
<Text style={styles.rowLabel}>Quartos e Passageiros</Text>
|
|
<Text style={styles.rowValue}>{buildQuartosPassageiros(reservaData)}</Text>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
<View style={styles.rowLineNoBorder}>
|
|
<Text style={styles.rowLabel}>Operador</Text>
|
|
<Text style={styles.rowValue}>{reservaData.operador || '---'}</Text>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
</Section>
|
|
|
|
<Section title="Alojamento">
|
|
{hotelInfo.length > 0 ? (
|
|
hotelInfo.map((hotel, idx) => (
|
|
<HotelCard
|
|
key={`hotel-${hotel.id_hotel}-${idx}`}
|
|
hotel={hotel}
|
|
isFirst={idx === 0}
|
|
/>
|
|
))
|
|
) : (
|
|
<Text style={styles.empty}>Sem dados de alojamento.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Voos">
|
|
{voosIda.length > 0 || voosVolta.length > 0 ? (
|
|
<View style={styles.vooList}>
|
|
{voosIda.length > 0 && (
|
|
<VooCard segmentos={voosIda} tipo="ida" />
|
|
)}
|
|
{voosVolta.length > 0 && (
|
|
<VooCard segmentos={voosVolta} tipo="volta" />
|
|
)}
|
|
</View>
|
|
) : (
|
|
<Text style={styles.empty}>Sem dados de voos.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Passageiros">
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
{reservaData.passageiros && reservaData.passageiros.length > 0 ? (
|
|
reservaData.passageiros.map((passageiro, idx) => (
|
|
<PassageiroCard
|
|
key={`passageiro-${idx}`}
|
|
passageiro={passageiro}
|
|
index={idx}
|
|
isFirst={idx === 0}
|
|
isOpen={expandedPassageiros.has(idx)}
|
|
onToggle={() => togglePassageiro(idx)}
|
|
/>
|
|
))
|
|
) : (
|
|
<Text style={styles.empty}>Sem passageiros associados.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Extras">
|
|
{extrasData.length > 0 ? (
|
|
<View style={styles.extrasList}>
|
|
{extrasData.map((extra) => (
|
|
<View style={styles.extrasItem} key={extra.name}>
|
|
<Text style={styles.extrasItemText}>{extra.name}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
) : (
|
|
<Text style={styles.empty}>Sem extras associados.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Histórico de Pagamentos">
|
|
{pagamentos.length > 0 ? (
|
|
<>
|
|
<View style={styles.tableHeader}>
|
|
<View style={styles.tableCellLeft}>
|
|
<Text style={styles.tableHeaderText}>Data</Text>
|
|
</View>
|
|
<View style={styles.tableCellMid}>
|
|
<Text style={styles.tableHeaderText}>Valor</Text>
|
|
</View>
|
|
<View style={styles.tableCellRight}>
|
|
<Text style={styles.tableHeaderText}>Meio Pag.</Text>
|
|
</View>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
{pagamentos.map((pagamento, idx) => (
|
|
<React.Fragment key={`pagamento-${idx}`}>
|
|
<View
|
|
style={[
|
|
styles.tableRow,
|
|
idx === pagamentos.length - 1 && styles.tableRowLast,
|
|
]}>
|
|
<View style={styles.tableCellLeft}>
|
|
<Text style={styles.tableValueText}>
|
|
{formatDateShort(pagamento.data_pagamento || pagamento.data_hora)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.tableCellMid}>
|
|
<Text style={styles.tableValueText}>
|
|
{formatCurrency(pagamento.valor)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.tableCellRight}>
|
|
<Text style={styles.tableValueText}>{pagamento.metodoPagamento}</Text>
|
|
</View>
|
|
</View>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
</React.Fragment>
|
|
))}
|
|
</>
|
|
) : (
|
|
<Text style={styles.empty}>Sem pagamentos registados.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
<ValorReservaCard reserva={reservaData} />
|
|
|
|
<Section title="Documentos">
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
{documentos.length > 0 ? (
|
|
documentos.map((documento, idx) => (
|
|
<React.Fragment key={`documento-${idx}`}>
|
|
<DocumentoRow
|
|
documento={documento}
|
|
isLast={idx === documentos.length - 1}
|
|
/>
|
|
<DashedDivider width={1} borderStyle="dashed" />
|
|
</React.Fragment>
|
|
))
|
|
) : (
|
|
<Text style={styles.empty}>Sem documentos associados.</Text>
|
|
)}
|
|
</Section>
|
|
|
|
{!!cachedAt && (
|
|
<Text style={styles.syncLabel}>
|
|
Atualizado em: {new Date(cachedAt).toLocaleDateString('pt-PT', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</Text>
|
|
)}
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|