23 KiB
Banco de Dados SQLite - Estrutura e Funcionalidades
Visão Geral
O aplicativo utiliza SQLite como banco de dados local principal, com fallback para AsyncStorage quando SQLite não está disponível (ex: plataforma web). O banco é usado para armazenamento offline, cache de dados e sincronização.
Configuração do Banco
Inicialização
Arquivo: src/services/database.ts
import SQLite from 'expo-sqlite';
// Verificar disponibilidade do SQLite
let SQLite: any;
let db: any;
let usingSQLite = false;
try {
if (Platform.OS !== "web") {
SQLite = require("expo-sqlite");
if (SQLite && typeof SQLite.openDatabase === "function") {
db = SQLite.openDatabase("truckdelivery.db");
usingSQLite = true;
}
}
} catch (error) {
console.warn("SQLite não disponível, usando AsyncStorage");
}
Fallback para AsyncStorage
// Prefixos para chaves do AsyncStorage
const USERS_KEY = "@TruckDelivery:users:"
const DELIVERIES_KEY = "@TruckDelivery:deliveries:"
const ROUTES_KEY = "@TruckDelivery:routes:"
const SETTINGS_KEY = "@TruckDelivery:settings:"
Estrutura das Tabelas
1. Tabela users
Propósito: Armazenar dados dos usuários
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT,
email TEXT,
role TEXT,
last_login INTEGER
);
Campos:
id: Identificador único do usuárioname: Nome completo do usuárioemail: Email do usuáriorole: Função/cargo do usuáriolast_login: Timestamp do último login
Operações:
// Inserir usuário
await executeQuery(
"INSERT INTO users (id, name, email, role, last_login) VALUES (?, ?, ?, ?, ?)",
[id, name, email, role, lastLogin]
);
// Buscar usuário
const result = await executeQuery("SELECT * FROM users WHERE id = ?", [id]);
2. Tabela deliveries
Propósito: Armazenar dados das entregas
CREATE TABLE IF NOT EXISTS deliveries (
id TEXT PRIMARY KEY,
outId TEXT,
customerId TEXT,
customerName TEXT,
street TEXT,
streetNumber TEXT,
neighborhood TEXT,
city TEXT,
state TEXT,
zipCode TEXT,
customerPhone TEXT,
lat REAL,
lng REAL,
latFrom REAL,
lngFrom REAL,
deliverySeq INTEGER,
routing INTEGER,
sellerId TEXT,
storeId TEXT,
status TEXT,
outDate TEXT,
notes TEXT,
signature TEXT,
photos TEXT,
completedTime INTEGER,
completedBy TEXT,
version INTEGER DEFAULT 1,
lastModified INTEGER DEFAULT (strftime('%s', 'now')),
syncTimestamp INTEGER,
syncStatus TEXT DEFAULT 'pending'
);
Campos:
id: Identificador único da entregaoutId: ID da entrega no sistema externocustomerId: ID do clientecustomerName: Nome do clientestreet: Rua do endereçostreetNumber: Número do endereçoneighborhood: Bairrocity: Cidadestate: EstadozipCode: CEPcustomerPhone: Telefone do clientelat/lng: Coordenadas de destinolatFrom/lngFrom: Coordenadas de origemdeliverySeq: Sequência na rotarouting: ID da rotasellerId: ID do vendedorstoreId: ID da lojastatus: Status da entrega (pending, in_progress, delivered, failed)outDate: Data de saídanotes: Observações da entregasignature: Assinatura em base64photos: URLs das fotos em JSONcompletedTime: Timestamp de conclusãocompletedBy: ID do usuário que completouversion: Versão do registrolastModified: Última modificaçãosyncTimestamp: Timestamp da sincronizaçãosyncStatus: Status de sincronização (pending, synced)
Operações:
// Salvar entrega
await executeQuery(
`INSERT INTO deliveries (
id, client, address, coordinates, status,
scheduled_time, completed_time, signature, photos, notes, sync_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, client, address, JSON.stringify(coordinates), status,
scheduledTime, completedTime, signature, JSON.stringify(photos),
notes, syncStatus]
);
// Buscar entregas por status
const result = await executeQuery(
"SELECT * FROM deliveries WHERE status = ? ORDER BY scheduled_time ASC",
[status]
);
3. Tabela routes
Propósito: Armazenar dados das rotas
CREATE TABLE IF NOT EXISTS routes (
id TEXT PRIMARY KEY,
name TEXT,
date TEXT,
deliveries TEXT,
status TEXT,
sync_status TEXT
);
Campos:
id: Identificador único da rotaname: Nome da rotadate: Data da rotadeliveries: IDs das entregas em JSONstatus: Status da rotasync_status: Status de sincronização
4. Tabela settings
Propósito: Armazenar configurações do aplicativo
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE,
value TEXT
);
Campos:
id: Chave primária auto-incrementokey: Chave da configuraçãovalue: Valor da configuração
Configurações Padrão:
INSERT OR IGNORE INTO settings (key, value) VALUES
('last_sync', '0'),
('offline_mode', 'false'),
('auto_sync', 'true');
5. Tabela deliveries_offline
Propósito: Armazenar entregas offline para sincronização
CREATE TABLE IF NOT EXISTS deliveries_offline (
id TEXT PRIMARY KEY,
outId INTEGER,
transactionId INTEGER,
deliveryDate TEXT,
receiverDoc TEXT,
receiverName TEXT,
lat REAL,
lng REAL,
broken INTEGER,
devolution INTEGER,
reasonDevolution TEXT,
deliveryImages TEXT,
userId INTEGER,
sync_status TEXT
);
Campos:
id: Identificador únicooutId: ID da entrega no sistematransactionId: ID da transaçãodeliveryDate: Data da entregareceiverDoc: Documento do receptorreceiverName: Nome do receptorlat: Latitudelng: Longitudebroken: Produto quebrado (0/1)devolution: Devolução (0/1)reasonDevolution: Motivo da devoluçãodeliveryImages: Imagens em JSONuserId: ID do usuáriosync_status: Status de sincronização
6. Tabela customer_invoices
Propósito: Armazenar notas fiscais dos clientes
CREATE TABLE IF NOT EXISTS customer_invoices (
id TEXT PRIMARY KEY,
invoiceId TEXT,
transactionId INTEGER,
customerId TEXT,
customerName TEXT,
invoiceValue REAL,
status TEXT,
items TEXT,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
sync_status TEXT DEFAULT 'pending'
);
Campos:
id: Identificador únicoinvoiceId: ID da nota fiscaltransactionId: ID da transaçãocustomerId: ID do clientecustomerName: Nome do clienteinvoiceValue: Valor da nota fiscalstatus: Status da nota fiscalitems: Itens da nota fiscal em JSONcreated_at: Data de criaçãosync_status: Status de sincronização
7. Tabela delivery_images
Propósito: Gerenciar imagens das entregas
CREATE TABLE IF NOT EXISTS delivery_images (
id TEXT PRIMARY KEY,
deliveryId TEXT,
transactionId INTEGER,
imagePath TEXT,
imageUrl TEXT,
uploadStatus TEXT DEFAULT 'pending',
uploadAttempts INTEGER DEFAULT 0,
lastUploadAttempt INTEGER,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (deliveryId) REFERENCES deliveries(id)
);
Campos:
id: Identificador únicodeliveryId: ID da entregatransactionId: ID da transaçãoimagePath: Caminho local da imagemimageUrl: URL da imagem no servidoruploadStatus: Status do upload (pending, uploaded, failed)uploadAttempts: Número de tentativas de uploadlastUploadAttempt: Timestamp da última tentativacreated_at: Data de criação
8. Tabela sync_queue
Propósito: Fila de sincronização para operações pendentes
CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
record_id TEXT NOT NULL,
action TEXT NOT NULL,
data TEXT,
priority INTEGER DEFAULT 1,
attempts INTEGER DEFAULT 0,
max_attempts INTEGER DEFAULT 3,
last_attempt INTEGER,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
status TEXT DEFAULT 'pending'
);
Campos:
id: Identificador únicotable_name: Nome da tabelarecord_id: ID do registroaction: Ação a ser executada (INSERT, UPDATE, DELETE)data: Dados em JSONpriority: Prioridade da operaçãoattempts: Número de tentativasmax_attempts: Máximo de tentativaslast_attempt: Timestamp da última tentativacreated_at: Data de criaçãostatus: Status da operação (pending, processing, completed, failed)
9. Tabela photo_uploads
Propósito: Controle de upload de fotos
CREATE TABLE IF NOT EXISTS photo_uploads (
id TEXT PRIMARY KEY,
deliveryId TEXT,
transactionId INTEGER,
localPath TEXT,
serverUrl TEXT,
uploadStatus TEXT DEFAULT 'pending',
uploadProgress REAL DEFAULT 0,
uploadAttempts INTEGER DEFAULT 0,
lastUploadAttempt INTEGER,
errorMessage TEXT,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (deliveryId) REFERENCES deliveries(id)
);
Campos:
id: Identificador únicodeliveryId: ID da entregatransactionId: ID da transaçãolocalPath: Caminho local da fotoserverUrl: URL da foto no servidoruploadStatus: Status do upload (pending, uploading, completed, failed)uploadProgress: Progresso do upload (0-1)uploadAttempts: Número de tentativaslastUploadAttempt: Timestamp da última tentativaerrorMessage: Mensagem de errocreated_at: Data de criação
Funções de Acesso aos Dados
1. Função Genérica de Query
export const executeQuery = async (query: string, params: any[] = []): Promise<any> => {
if (usingSQLite) {
return new Promise((resolve, reject) => {
db.transaction((tx: any) => {
tx.executeSql(
query,
params,
(_: any, result: any) => resolve(result),
(_: any, error: any) => {
reject(error);
return false;
}
);
});
});
} else {
// Implementação para AsyncStorage
return executeAsyncStorageQuery(query, params);
}
};
2. Operações CRUD para Entregas
Buscar Entregas
export const getDeliveries = async (status?: string): Promise<any[]> => {
try {
if (usingSQLite) {
let query = "SELECT * FROM deliveries";
const params: any[] = [];
if (status) {
query += " WHERE status = ?";
params.push(status);
}
query += " ORDER BY scheduled_time ASC";
const result = await executeQuery(query, params);
return result.rows._array;
} else {
// Fallback para AsyncStorage
const allDeliveries = await getAllDeliveriesFromAsyncStorage();
return status ? allDeliveries.filter(d => d.status === status) : allDeliveries;
}
} catch (error) {
console.error("Erro ao obter entregas:", error);
return [];
}
};
Salvar Entrega
export const saveDelivery = async (delivery: any): Promise<boolean> => {
try {
const {
id, client, address, coordinates, status,
scheduled_time, completed_time, signature, photos, notes, sync_status
} = delivery;
if (usingSQLite) {
const existingDelivery = await getDeliveryById(id);
if (existingDelivery) {
// Atualizar entrega existente
await executeQuery(
`UPDATE deliveries SET
client = ?, address = ?, coordinates = ?, status = ?,
scheduled_time = ?, completed_time = ?, signature = ?,
photos = ?, notes = ?, sync_status = ?
WHERE id = ?`,
[client, address, JSON.stringify(coordinates), status,
scheduled_time, completed_time, signature,
JSON.stringify(photos), notes, sync_status, id]
);
} else {
// Inserir nova entrega
await executeQuery(
`INSERT INTO deliveries (
id, client, address, coordinates, status,
scheduled_time, completed_time, signature, photos, notes, sync_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, client, address, JSON.stringify(coordinates), status,
scheduled_time, completed_time, signature,
JSON.stringify(photos), notes, sync_status]
);
}
} else {
// AsyncStorage fallback
await AsyncStorage.setItem(`${DELIVERIES_KEY}${id}`, JSON.stringify(delivery));
}
return true;
} catch (error) {
console.error("Erro ao salvar entrega:", error);
return false;
}
};
Atualizar Status
export const updateDeliveryStatus = async (
id: string,
status: string,
completedTime?: number
): Promise<boolean> => {
try {
if (usingSQLite) {
let query = "UPDATE deliveries SET status = ?, sync_status = ?";
const params: any[] = [status, "pending"];
if (completedTime) {
query += ", completed_time = ?";
params.push(completedTime);
}
query += " WHERE id = ?";
params.push(id);
await executeQuery(query, params);
} else {
// AsyncStorage fallback
const deliveryJson = await AsyncStorage.getItem(`${DELIVERIES_KEY}${id}`);
if (deliveryJson) {
const delivery = JSON.parse(deliveryJson);
delivery.status = status;
delivery.sync_status = "pending";
if (completedTime) {
delivery.completed_time = completedTime;
}
await AsyncStorage.setItem(`${DELIVERIES_KEY}${id}`, JSON.stringify(delivery));
}
}
return true;
} catch (error) {
console.error("Erro ao atualizar status da entrega:", error);
return false;
}
};
3. Operações de Sincronização
Buscar Entregas Não Sincronizadas
export const getUnsyncedDeliveries = async (): Promise<any[]> => {
try {
if (usingSQLite) {
const result = await executeQuery(
"SELECT * FROM deliveries WHERE sync_status = ?",
["pending"]
);
return result.rows._array;
} else {
const allDeliveries = await getAllDeliveriesFromAsyncStorage();
return allDeliveries.filter(d => d.sync_status === "pending");
}
} catch (error) {
console.error("Erro ao obter entregas não sincronizadas:", error);
return [];
}
};
Marcar como Sincronizada
export const markDeliveryAsSynced = async (id: string): Promise<boolean> => {
try {
if (usingSQLite) {
await executeQuery(
"UPDATE deliveries SET sync_status = ? WHERE id = ?",
["synced", id]
);
} else {
const deliveryJson = await AsyncStorage.getItem(`${DELIVERIES_KEY}${id}`);
if (deliveryJson) {
const delivery = JSON.parse(deliveryJson);
delivery.sync_status = "synced";
await AsyncStorage.setItem(`${DELIVERIES_KEY}${id}`, JSON.stringify(delivery));
}
}
return true;
} catch (error) {
console.error("Erro ao marcar entrega como sincronizada:", error);
return false;
}
};
4. Operações de Configurações
Obter Configuração
export const getSetting = async (key: string): Promise<string | null> => {
try {
if (usingSQLite) {
const result = await executeQuery(
"SELECT value FROM settings WHERE key = ?",
[key]
);
if (result.rows.length > 0) {
return result.rows._array[0].value;
}
return null;
} else {
return await AsyncStorage.getItem(`${SETTINGS_KEY}${key}`);
}
} catch (error) {
console.error("Erro ao obter configuração:", error);
return null;
}
};
Salvar Configuração
export const saveSetting = async (key: string, value: string): Promise<boolean> => {
try {
if (usingSQLite) {
await executeQuery(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
[key, value]
);
} else {
await AsyncStorage.setItem(`${SETTINGS_KEY}${key}`, value);
}
return true;
} catch (error) {
console.error("Erro ao salvar configuração:", error);
return false;
}
};
Implementação AsyncStorage (Fallback)
Funções Auxiliares
async function getAllUsersFromAsyncStorage() {
try {
const keys = await AsyncStorage.getAllKeys();
const userKeys = keys.filter(key => key.startsWith(USERS_KEY));
const userItems = await AsyncStorage.multiGet(userKeys);
return userItems.map(([_, value]) => JSON.parse(value));
} catch (error) {
console.error("Erro ao obter usuários do AsyncStorage:", error);
return [];
}
}
async function getAllDeliveriesFromAsyncStorage() {
try {
const keys = await AsyncStorage.getAllKeys();
const deliveryKeys = keys.filter(key => key.startsWith(DELIVERIES_KEY));
const deliveryItems = await AsyncStorage.multiGet(deliveryKeys);
return deliveryItems.map(([_, value]) => JSON.parse(value));
} catch (error) {
console.error("Erro ao obter entregas do AsyncStorage:", error);
return [];
}
}
Simulação de Queries SQL
async function executeAsyncStorageQuery(query: string, params: any[]): Promise<any> {
try {
if (query.toUpperCase().startsWith("SELECT")) {
if (query.includes("FROM users")) {
const allUsers = await getAllUsersFromAsyncStorage();
return { rows: { _array: allUsers } };
} else if (query.includes("FROM deliveries")) {
const allDeliveries = await getAllDeliveriesFromAsyncStorage();
return { rows: { _array: allDeliveries } };
} else if (query.includes("FROM settings")) {
const key = params[0];
const value = await AsyncStorage.getItem(`${SETTINGS_KEY}${key}`);
return { rows: { _array: value ? [{ value }] : [] } };
}
} else if (query.toUpperCase().startsWith("INSERT") || query.toUpperCase().startsWith("UPDATE")) {
// Implementação simplificada para INSERT/UPDATE
return { rowsAffected: 1 };
}
return { rowsAffected: 1 };
} catch (error) {
console.error("Erro na operação do AsyncStorage:", error);
throw error;
}
}
Operações de Entregas Offline
Salvar Entrega Offline
export const saveOfflineDelivery = async (delivery: any): Promise<boolean> => {
try {
if (usingSQLite) {
await executeQuery(
`INSERT OR REPLACE INTO deliveries_offline (
id, outId, transactionId, deliveryDate, receiverDoc, receiverName,
lat, lng, broken, devolution, reasonDevolution, deliveryImages, userId, sync_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
delivery.id, delivery.outId, delivery.transactionId, delivery.deliveryDate,
delivery.receiverDoc, delivery.receiverName, delivery.lat, delivery.lng,
delivery.broken ? 1 : 0, delivery.devolution ? 1 : 0,
delivery.reasonDevolution, JSON.stringify(delivery.deliveryImages),
delivery.userId, delivery.sync_status || 'pending'
]
);
} else {
await AsyncStorage.setItem(
`@TruckDelivery:deliveries_offline:${delivery.id}`,
JSON.stringify(delivery)
);
}
return true;
} catch (error) {
console.error('Erro ao salvar entrega offline:', error);
return false;
}
};
Buscar Entregas Offline
export const getOfflineDeliveries = async (): Promise<any[]> => {
try {
if (usingSQLite) {
const result = await executeQuery(
'SELECT * FROM deliveries_offline WHERE sync_status = ?',
['pending']
);
return result.rows._array.map((row: any) => ({
...row,
deliveryImages: row.deliveryImages ? JSON.parse(row.deliveryImages) : [],
broken: !!row.broken,
devolution: !!row.devolution,
}));
} else {
const keys = await AsyncStorage.getAllKeys();
const offlineKeys = keys.filter(key =>
key.startsWith('@TruckDelivery:deliveries_offline:')
);
const items = await AsyncStorage.multiGet(offlineKeys);
return items.map(([_, value]) => JSON.parse(value))
.filter(d => d.sync_status === 'pending');
}
} catch (error) {
console.error('Erro ao buscar entregas offline:', error);
return [];
}
};
Otimizações e Índices
Índices Recomendados
-- Índices para melhorar performance
CREATE INDEX IF NOT EXISTS idx_deliveries_status ON deliveries(status);
CREATE INDEX IF NOT EXISTS idx_deliveries_sync_status ON deliveries(sync_status);
CREATE INDEX IF NOT EXISTS idx_deliveries_scheduled_time ON deliveries(scheduled_time);
CREATE INDEX IF NOT EXISTS idx_deliveries_offline_sync_status ON deliveries_offline(sync_status);
CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key);
Limpeza de Dados Antigos
export const cleanupOldData = async (daysOld: number = 30): Promise<void> => {
try {
const cutoffDate = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
if (usingSQLite) {
// Remover entregas antigas já sincronizadas
await executeQuery(
"DELETE FROM deliveries WHERE completed_time < ? AND sync_status = ?",
[cutoffDate, "synced"]
);
// Remover entregas offline antigas
await executeQuery(
"DELETE FROM deliveries_offline WHERE deliveryDate < ? AND sync_status = ?",
[cutoffDate, "synced"]
);
}
} catch (error) {
console.error("Erro na limpeza de dados:", error);
}
};
Monitoramento e Logs
Informações de Armazenamento
export const storageInfo = {
type: usingSQLite ? "SQLite" : "AsyncStorage",
isUsingSQLite: usingSQLite,
};
// Função para obter estatísticas do banco
export const getDatabaseStats = async (): Promise<any> => {
try {
if (usingSQLite) {
const deliveriesResult = await executeQuery("SELECT COUNT(*) as count FROM deliveries");
const offlineResult = await executeQuery("SELECT COUNT(*) as count FROM deliveries_offline");
const unsyncedResult = await executeQuery(
"SELECT COUNT(*) as count FROM deliveries WHERE sync_status = ?",
["pending"]
);
return {
totalDeliveries: deliveriesResult.rows._array[0].count,
offlineDeliveries: offlineResult.rows._array[0].count,
unsyncedDeliveries: unsyncedResult.rows._array[0].count,
storageType: "SQLite"
};
} else {
const keys = await AsyncStorage.getAllKeys();
return {
totalKeys: keys.length,
storageType: "AsyncStorage"
};
}
} catch (error) {
console.error("Erro ao obter estatísticas:", error);
return null;
}
};
Considerações para Sincronização Offline
1. Estrutura para Sincronização Incremental
-- Adicionar campos de controle de versão
ALTER TABLE deliveries ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE deliveries ADD COLUMN last_modified INTEGER;
ALTER TABLE deliveries ADD COLUMN sync_timestamp INTEGER;
2. Tabela de Log de Sincronização
CREATE TABLE IF NOT EXISTS sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
record_id TEXT,
action TEXT,
timestamp INTEGER,
success INTEGER,
error_message TEXT
);
3. Controle de Conflitos
CREATE TABLE IF NOT EXISTS sync_conflicts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
record_id TEXT,
local_data TEXT,
server_data TEXT,
resolution TEXT,
timestamp INTEGER
);
Esta documentação fornece uma visão completa da estrutura do banco de dados SQLite utilizado pelo aplicativo, incluindo todas as tabelas, operações e considerações para implementação de sincronização offline eficiente.