First commit of the new app
This commit is contained in:
401
app/reserva/[referencia].tsx
Normal file
401
app/reserva/[referencia].tsx
Normal file
@@ -0,0 +1,401 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user