First commit of the new app
This commit is contained in:
146
assets/services/documentSync.ts
Normal file
146
assets/services/documentSync.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { buildApiUrl, API_BASE_URL, API_ENDPOINTS } from "../config/api";
|
||||
import { DocumentoChecksum, DocumentosChecksumsResponse, ReservaDocumentos } from "../types";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as FileSystem from "expo-file-system/legacy";
|
||||
|
||||
const DOCUMENTOS_DIR = `${FileSystem.documentDirectory}documentos/`;
|
||||
|
||||
// Devolve o caminho local de um documento
|
||||
const getLocalPath = (idDocumento: string, caminhoFicheiro: string): string => {
|
||||
const fileName = caminhoFicheiro.split("/").pop() || `documento_${idDocumento}.pdf`;
|
||||
return `${DOCUMENTOS_DIR}${idDocumento}_${fileName}`;
|
||||
};
|
||||
|
||||
// Garante que o diretório de documentos existe
|
||||
const ensureDir = async (): Promise<void> => {
|
||||
const info = await FileSystem.getInfoAsync(DOCUMENTOS_DIR);
|
||||
if (!info.exists) {
|
||||
await FileSystem.makeDirectoryAsync(DOCUMENTOS_DIR, { intermediates: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Constrói URL completa a partir de um caminho relativo
|
||||
const buildUrl = (path: string | null | undefined): string => {
|
||||
if (!path) return "";
|
||||
if (path.startsWith("http")) return path;
|
||||
const base = API_BASE_URL.replace(/\/pt\/app$/, "");
|
||||
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
};
|
||||
|
||||
// Checksum guardado localmente para um documento
|
||||
export const getStoredChecksum = async (idDocumento: string): Promise<string | null> => {
|
||||
try {
|
||||
return await AsyncStorage.getItem(`@cruiseLovers:documento:${idDocumento}:checksum`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Guarda o checksum localmente
|
||||
export const saveStoredChecksum = async (idDocumento: string, checksum: string): Promise<void> => {
|
||||
try {
|
||||
await AsyncStorage.setItem(`@cruiseLovers:documento:${idDocumento}:checksum`, checksum);
|
||||
} catch {
|
||||
// silencioso
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Devolve o URI local de um documento se este já estiver descarregado, ou null caso contrário.
|
||||
*/
|
||||
export const getLocalDocumentUri = async (idDocumento: string, caminhoFicheiro: string): Promise<string | null> => {
|
||||
try {
|
||||
const localPath = getLocalPath(idDocumento, caminhoFicheiro);
|
||||
const info = await FileSystem.getInfoAsync(localPath);
|
||||
return info.exists ? localPath : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Faz download de um único documento
|
||||
export const downloadDocumento = async (documento: DocumentoChecksum): Promise<string | null> => {
|
||||
try {
|
||||
const url = buildUrl(documento.caminhoFicheiro);
|
||||
if (!url) return null;
|
||||
|
||||
await ensureDir();
|
||||
|
||||
const localPath = getLocalPath(documento.idDocumento, documento.caminhoFicheiro);
|
||||
|
||||
const result = await FileSystem.downloadAsync(url, localPath);
|
||||
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
console.error(`Download falhou com status ${result.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (documento.checksum) {
|
||||
await saveStoredChecksum(documento.idDocumento, documento.checksum);
|
||||
}
|
||||
|
||||
return localPath;
|
||||
} catch (error) {
|
||||
console.error("Erro ao fazer download:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Busca checksums do servidor
|
||||
export const getDocumentosChecksums = async (token: string): Promise<DocumentosChecksumsResponse | null> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("token", token);
|
||||
|
||||
const response = await fetch(buildApiUrl(API_ENDPOINTS.GET_DOCUMENTOS_CHECKSUMS), {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json" },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return (await response.json()) as DocumentosChecksumsResponse;
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar checksums de documentos:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Verifica quais documentos precisam ser (re)descarregados
|
||||
export const verificarDocumentosParaDownload = async (
|
||||
reservaDocumentos: ReservaDocumentos[]
|
||||
): Promise<DocumentoChecksum[]> => {
|
||||
const para: DocumentoChecksum[] = [];
|
||||
|
||||
for (const reserva of reservaDocumentos) {
|
||||
for (const doc of reserva.documentos) {
|
||||
if (!doc.caminhoFicheiro || !doc.checksum) continue;
|
||||
|
||||
const storedChecksum = await getStoredChecksum(doc.idDocumento);
|
||||
const localPath = getLocalPath(doc.idDocumento, doc.caminhoFicheiro);
|
||||
const info = await FileSystem.getInfoAsync(localPath);
|
||||
|
||||
if (doc.checksum !== storedChecksum || !info.exists) {
|
||||
para.push(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return para;
|
||||
};
|
||||
|
||||
// Descarrega múltiplos documentos
|
||||
export const downloadDocumentos = async (
|
||||
documentos: DocumentoChecksum[],
|
||||
onProgress?: (current: number, total: number) => void
|
||||
): Promise<{ sucesso: number; falhas: number }> => {
|
||||
let sucesso = 0;
|
||||
let falhas = 0;
|
||||
|
||||
for (let i = 0; i < documentos.length; i++) {
|
||||
if (onProgress) onProgress(i + 1, documentos.length);
|
||||
const resultado = await downloadDocumento(documentos[i]);
|
||||
resultado ? sucesso++ : falhas++;
|
||||
}
|
||||
|
||||
return { sucesso, falhas };
|
||||
};
|
||||
229
assets/services/offlineStorage.ts
Normal file
229
assets/services/offlineStorage.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { ContactData, Documento, Pagamento, Reserva, ReservaData, SocialMedia } from "../types";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
RESERVAS: "@cruiseLovers:reservas",
|
||||
RESERVA_DETAIL: "@cruiseLovers:reserva:",
|
||||
RESERVA_FULL: "@cruiseLovers:reservaFull:",
|
||||
CONTACTS: "@cruiseLovers:contacts",
|
||||
SOCIALS: "@cruiseLovers:socials",
|
||||
USER_INFO: "@cruiseLovers:userInfo",
|
||||
LAST_SYNC: "@cruiseLovers:lastSync",
|
||||
CACHE_VERSION: "@cruiseLovers:cacheVersion",
|
||||
} as const;
|
||||
|
||||
export interface CachedReservaFull {
|
||||
reservaData: ReservaData;
|
||||
pagamentos: Pagamento[];
|
||||
documentos: Documento[];
|
||||
cachedAt: string;
|
||||
}
|
||||
|
||||
const CACHE_VERSION = "1.0.0";
|
||||
|
||||
/**
|
||||
* Armazena a lista de reservas no cache
|
||||
*/
|
||||
export const cacheReservas = async (reservas: Reserva[]): Promise<void> => {
|
||||
try {
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.RESERVAS, JSON.stringify(reservas));
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.LAST_SYNC, new Date().toISOString());
|
||||
} catch (error) {
|
||||
console.error("Erro ao armazenar reservas no cache:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém a lista de reservas do cache
|
||||
*/
|
||||
export const getCachedReservas = async (): Promise<Reserva[] | null> => {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(STORAGE_KEYS.RESERVAS);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter reservas do cache:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Armazena os detalhes de uma reserva no cache
|
||||
*/
|
||||
export const cacheReservaDetail = async (id: string, reservaData: ReservaData): Promise<void> => {
|
||||
try {
|
||||
const key = `${STORAGE_KEYS.RESERVA_DETAIL}${id}`;
|
||||
await AsyncStorage.setItem(key, JSON.stringify(reservaData));
|
||||
} catch (error) {
|
||||
console.error(`Erro ao armazenar detalhes da reserva ${id} no cache:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém os detalhes de uma reserva do cache
|
||||
*/
|
||||
export const getCachedReservaDetail = async (id: string): Promise<ReservaData | null> => {
|
||||
try {
|
||||
const key = `${STORAGE_KEYS.RESERVA_DETAIL}${id}`;
|
||||
const cached = await AsyncStorage.getItem(key);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Erro ao obter detalhes da reserva ${id} do cache:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Armazena os dados completos de uma reserva (reserva + pagamentos + documentos) no cache
|
||||
*/
|
||||
export const cacheReservaFull = async (referencia: string, data: CachedReservaFull): Promise<void> => {
|
||||
try {
|
||||
const key = `${STORAGE_KEYS.RESERVA_FULL}${referencia}`;
|
||||
await AsyncStorage.setItem(key, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error(`Erro ao armazenar dados completos da reserva ${referencia} no cache:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém os dados completos de uma reserva do cache
|
||||
*/
|
||||
export const getCachedReservaFull = async (referencia: string): Promise<CachedReservaFull | null> => {
|
||||
try {
|
||||
const key = `${STORAGE_KEYS.RESERVA_FULL}${referencia}`;
|
||||
const cached = await AsyncStorage.getItem(key);
|
||||
return cached ? (JSON.parse(cached) as CachedReservaFull) : null;
|
||||
} catch (error) {
|
||||
console.error(`Erro ao obter dados completos da reserva ${referencia} do cache:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Armazena os contactos no cache
|
||||
*/
|
||||
export const cacheContacts = async (contactData: ContactData, socials: SocialMedia[]): Promise<void> => {
|
||||
try {
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.CONTACTS, JSON.stringify(contactData));
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.SOCIALS, JSON.stringify(socials));
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.LAST_SYNC, new Date().toISOString());
|
||||
} catch (error) {
|
||||
console.error("Erro ao armazenar contactos no cache:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém os contactos do cache
|
||||
*/
|
||||
export const getCachedContacts = async (): Promise<{ contactData: ContactData | null; socials: SocialMedia[] }> => {
|
||||
try {
|
||||
const cachedContact = await AsyncStorage.getItem(STORAGE_KEYS.CONTACTS);
|
||||
const cachedSocials = await AsyncStorage.getItem(STORAGE_KEYS.SOCIALS);
|
||||
|
||||
return {
|
||||
contactData: cachedContact ? JSON.parse(cachedContact) : null,
|
||||
socials: cachedSocials ? JSON.parse(cachedSocials) : [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter contactos do cache:", error);
|
||||
return { contactData: null, socials: [] };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém a data da última sincronização
|
||||
*/
|
||||
export const getLastSyncDate = async (): Promise<Date | null> => {
|
||||
try {
|
||||
const lastSync = await AsyncStorage.getItem(STORAGE_KEYS.LAST_SYNC);
|
||||
return lastSync ? new Date(lastSync) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Limpa todo o cache
|
||||
*/
|
||||
export const clearCache = async (): Promise<void> => {
|
||||
try {
|
||||
const keys = await AsyncStorage.getAllKeys();
|
||||
const appKeys = keys.filter(key => key.startsWith("@cruiseLovers:"));
|
||||
await AsyncStorage.multiRemove(appKeys);
|
||||
} catch (error) {
|
||||
console.error("Erro ao limpar cache:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Limpa apenas o cache de reservas
|
||||
*/
|
||||
export const clearReservasCache = async (): Promise<void> => {
|
||||
try {
|
||||
const keys = await AsyncStorage.getAllKeys();
|
||||
const reservaKeys = keys.filter(key =>
|
||||
key === STORAGE_KEYS.RESERVAS ||
|
||||
key.startsWith(STORAGE_KEYS.RESERVA_DETAIL)
|
||||
);
|
||||
await AsyncStorage.multiRemove(reservaKeys);
|
||||
} catch (error) {
|
||||
console.error("Erro ao limpar cache de reservas:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Armazena informações do perfil no cache
|
||||
*/
|
||||
export const cacheUserInfo = async (userInfo: any): Promise<void> => {
|
||||
try {
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo));
|
||||
} catch (error) {
|
||||
console.error("Erro ao armazenar perfil no cache:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém informações do perfil do cache
|
||||
*/
|
||||
export const getCachedUserInfo = async (): Promise<any | null> => {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(STORAGE_KEYS.USER_INFO);
|
||||
return cached ? JSON.parse(cached) : null;
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter perfil do cache:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtém informações sobre o tamanho do cache
|
||||
*/
|
||||
export const getCacheInfo = async (): Promise<{ size: number; lastSync: Date | null }> => {
|
||||
try {
|
||||
const keys = await AsyncStorage.getAllKeys();
|
||||
const appKeys = keys.filter(key => key.startsWith("@cruiseLovers:"));
|
||||
const items = await AsyncStorage.multiGet(appKeys);
|
||||
|
||||
let totalSize = 0;
|
||||
items.forEach(([_, value]) => {
|
||||
if (value) {
|
||||
totalSize += value.length;
|
||||
}
|
||||
});
|
||||
|
||||
const lastSync = await getLastSyncDate();
|
||||
|
||||
return {
|
||||
size: totalSize,
|
||||
lastSync,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao obter informações do cache:", error);
|
||||
return { size: 0, lastSync: null };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user