385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
const { google } = require('googleapis');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
class GoogleSheetsService {
|
|
constructor() {
|
|
this.auth = null;
|
|
this.sheets = null;
|
|
this.drive = null;
|
|
}
|
|
|
|
// Inicializar autenticação OAuth 2.0
|
|
async initializeAuth(credentialsData = null) {
|
|
try {
|
|
let credentials;
|
|
|
|
if (credentialsData) {
|
|
// Usar credenciais fornecidas via parâmetro
|
|
credentials = credentialsData;
|
|
} else {
|
|
// Tentar carregar do arquivo (fallback)
|
|
const credentialsPath = path.join(__dirname, 'google-credentials.json');
|
|
|
|
if (!fs.existsSync(credentialsPath)) {
|
|
throw new Error('Credenciais do Google não configuradas. Configure na página de Configurações.');
|
|
}
|
|
|
|
credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
|
|
}
|
|
|
|
const { client_id, client_secret, redirect_uris } = credentials.web || credentials;
|
|
|
|
this.auth = new google.auth.OAuth2(
|
|
client_id,
|
|
client_secret,
|
|
redirect_uris[0]
|
|
);
|
|
|
|
this.sheets = google.sheets({ version: 'v4', auth: this.auth });
|
|
this.drive = google.drive({ version: 'v3', auth: this.auth });
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Erro ao inicializar autenticação Google:', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Gerar URL de autorização
|
|
getAuthUrl() {
|
|
const scopes = [
|
|
'https://www.googleapis.com/auth/spreadsheets',
|
|
'https://www.googleapis.com/auth/drive.file'
|
|
];
|
|
|
|
return this.auth.generateAuthUrl({
|
|
access_type: 'offline',
|
|
scope: scopes,
|
|
prompt: 'consent'
|
|
});
|
|
}
|
|
|
|
// Processar código de autorização
|
|
async handleAuthCallback(code) {
|
|
try {
|
|
const { tokens } = await this.auth.getToken(code);
|
|
this.auth.setCredentials(tokens);
|
|
|
|
// Salvar tokens para uso futuro
|
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
|
fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
|
|
|
|
return tokens;
|
|
} catch (error) {
|
|
console.error('Erro ao processar callback de autorização:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Carregar tokens salvos
|
|
async loadSavedTokens() {
|
|
try {
|
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
|
|
|
if (fs.existsSync(tokensPath)) {
|
|
const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
|
|
this.auth.setCredentials(tokens);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Erro ao carregar tokens salvos:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Criar ou atualizar planilha persistente
|
|
async createOrUpdatePersistentSpreadsheet(spreadsheetId = null, title = 'Liberi Kids - Sistema de Estoque') {
|
|
try {
|
|
if (spreadsheetId) {
|
|
// Tentar verificar se planilha existente ainda existe
|
|
try {
|
|
const existingSheet = await this.sheets.spreadsheets.get({
|
|
spreadsheetId: spreadsheetId
|
|
});
|
|
|
|
console.log('Planilha existente encontrada, usando a mesma...');
|
|
return {
|
|
spreadsheetId,
|
|
url: `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`,
|
|
exists: true
|
|
};
|
|
} catch (error) {
|
|
console.log('Planilha não encontrada ou inacessível, criando nova...');
|
|
}
|
|
}
|
|
|
|
// Criar nova planilha
|
|
const spreadsheet = await this.sheets.spreadsheets.create({
|
|
resource: {
|
|
properties: {
|
|
title: title
|
|
},
|
|
sheets: [
|
|
{
|
|
properties: {
|
|
sheetId: 0,
|
|
title: 'Produtos',
|
|
gridProperties: {
|
|
rowCount: 1000,
|
|
columnCount: 20
|
|
}
|
|
}
|
|
},
|
|
{
|
|
properties: {
|
|
sheetId: 1,
|
|
title: 'Vendas',
|
|
gridProperties: {
|
|
rowCount: 1000,
|
|
columnCount: 15
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
const newSpreadsheetId = spreadsheet.data.spreadsheetId;
|
|
|
|
// Aguardar um pouco para garantir que a planilha foi criada
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Configurar cabeçalhos
|
|
await this.setupHeaders(newSpreadsheetId);
|
|
|
|
return {
|
|
spreadsheetId: newSpreadsheetId,
|
|
url: `https://docs.google.com/spreadsheets/d/${newSpreadsheetId}/edit`
|
|
};
|
|
} catch (error) {
|
|
console.error('Erro ao criar planilha:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Configurar cabeçalhos das abas
|
|
async setupHeaders(spreadsheetId) {
|
|
try {
|
|
const produtosHeaders = [
|
|
'ID da Roupa',
|
|
'Nome do Produto',
|
|
'Fornecedor',
|
|
'Tamanho',
|
|
'Estação',
|
|
'Gênero',
|
|
'Valor da Compra',
|
|
'Valor da Venda',
|
|
'Data da Compra',
|
|
'Data da Venda',
|
|
'Estoque Atual',
|
|
'Marca'
|
|
];
|
|
|
|
const vendasHeaders = [
|
|
'ID da Venda',
|
|
'Cliente',
|
|
'Data da Venda',
|
|
'Tipo de Pagamento',
|
|
'Valor Total',
|
|
'Desconto',
|
|
'Valor Final',
|
|
'Status',
|
|
'Observações',
|
|
'Produtos Vendidos'
|
|
];
|
|
|
|
// Configurar cabeçalhos da aba Produtos
|
|
await this.sheets.spreadsheets.values.update({
|
|
spreadsheetId,
|
|
range: 'Produtos!A1:L1',
|
|
valueInputOption: 'USER_ENTERED',
|
|
resource: {
|
|
values: [produtosHeaders]
|
|
}
|
|
});
|
|
|
|
// Configurar cabeçalhos da aba Vendas
|
|
await this.sheets.spreadsheets.values.update({
|
|
spreadsheetId,
|
|
range: 'Vendas!A1:J1',
|
|
valueInputOption: 'USER_ENTERED',
|
|
resource: {
|
|
values: [vendasHeaders]
|
|
}
|
|
});
|
|
|
|
// Formatar cabeçalhos (opcional, pode falhar sem quebrar)
|
|
try {
|
|
const requests = [
|
|
{
|
|
repeatCell: {
|
|
range: {
|
|
sheetId: 0,
|
|
startRowIndex: 0,
|
|
endRowIndex: 1,
|
|
startColumnIndex: 0,
|
|
endColumnIndex: produtosHeaders.length
|
|
},
|
|
cell: {
|
|
userEnteredFormat: {
|
|
backgroundColor: { red: 0.2, green: 0.6, blue: 1.0 },
|
|
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
|
}
|
|
},
|
|
fields: 'userEnteredFormat'
|
|
}
|
|
},
|
|
{
|
|
repeatCell: {
|
|
range: {
|
|
sheetId: 1,
|
|
startRowIndex: 0,
|
|
endRowIndex: 1,
|
|
startColumnIndex: 0,
|
|
endColumnIndex: vendasHeaders.length
|
|
},
|
|
cell: {
|
|
userEnteredFormat: {
|
|
backgroundColor: { red: 0.2, green: 0.8, blue: 0.2 },
|
|
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }
|
|
}
|
|
},
|
|
fields: 'userEnteredFormat'
|
|
}
|
|
}
|
|
];
|
|
|
|
await this.sheets.spreadsheets.batchUpdate({
|
|
spreadsheetId,
|
|
resource: { requests }
|
|
});
|
|
} catch (formatError) {
|
|
console.log('Aviso: Não foi possível formatar cabeçalhos, mas planilha foi criada:', formatError.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao configurar cabeçalhos:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Exportar dados de produtos
|
|
async exportProdutos(spreadsheetId, produtos) {
|
|
try {
|
|
const values = produtos.map(produto => [
|
|
produto.id || '',
|
|
produto.nome || '',
|
|
produto.fornecedor_nome || '',
|
|
produto.tamanho || '',
|
|
produto.estacao || '',
|
|
produto.genero || '',
|
|
produto.valor_compra || 0,
|
|
produto.valor_revenda || 0,
|
|
produto.created_at ? new Date(produto.created_at).toLocaleDateString('pt-BR') : '',
|
|
'', // Data da venda será preenchida quando houver venda
|
|
produto.quantidade_total || 0,
|
|
produto.marca || ''
|
|
]);
|
|
|
|
await this.sheets.spreadsheets.values.update({
|
|
spreadsheetId,
|
|
range: 'Produtos!A2:L' + (values.length + 1),
|
|
valueInputOption: 'USER_ENTERED',
|
|
resource: { values }
|
|
});
|
|
|
|
return { success: true, rowsUpdated: values.length };
|
|
} catch (error) {
|
|
console.error('Erro ao exportar produtos:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Exportar dados de vendas
|
|
async exportVendas(spreadsheetId, vendas) {
|
|
try {
|
|
const values = vendas.map(venda => [
|
|
venda.id || '',
|
|
venda.cliente_nome || 'Cliente não informado',
|
|
venda.data_venda ? new Date(venda.data_venda).toLocaleDateString('pt-BR') : '',
|
|
venda.tipo_pagamento || '',
|
|
venda.valor_total || 0,
|
|
venda.desconto || 0,
|
|
venda.valor_final || 0,
|
|
venda.status || 'Concluída',
|
|
venda.observacoes || '',
|
|
venda.produtos ? venda.produtos.map(p => `${p.nome} (${p.quantidade}x)`).join(', ') : ''
|
|
]);
|
|
|
|
await this.sheets.spreadsheets.values.update({
|
|
spreadsheetId,
|
|
range: 'Vendas!A2:J' + (values.length + 1),
|
|
valueInputOption: 'USER_ENTERED',
|
|
resource: { values }
|
|
});
|
|
|
|
return { success: true, rowsUpdated: values.length };
|
|
} catch (error) {
|
|
console.error('Erro ao exportar vendas:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Verificar se está autenticado
|
|
isAuthenticated() {
|
|
return this.auth && this.auth.credentials && this.auth.credentials.access_token;
|
|
}
|
|
|
|
// Verificar se o token está próximo do vencimento
|
|
isTokenExpiringSoon() {
|
|
if (!this.auth || !this.auth.credentials || !this.auth.credentials.expiry_date) {
|
|
return false;
|
|
}
|
|
|
|
const now = new Date().getTime();
|
|
const expiry = this.auth.credentials.expiry_date;
|
|
const timeUntilExpiry = expiry - now;
|
|
|
|
// Considera que está expirando se faltam menos de 5 minutos
|
|
return timeUntilExpiry < 5 * 60 * 1000;
|
|
}
|
|
|
|
// Renovar token automaticamente se necessário
|
|
async refreshTokenIfNeeded() {
|
|
if (this.isTokenExpiringSoon()) {
|
|
try {
|
|
const { credentials } = await this.auth.refreshAccessToken();
|
|
this.auth.setCredentials(credentials);
|
|
|
|
// Salvar tokens atualizados
|
|
const tokensPath = path.join(__dirname, 'google-tokens.json');
|
|
fs.writeFileSync(tokensPath, JSON.stringify(credentials, null, 2));
|
|
|
|
console.log('Token Google renovado automaticamente');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Erro ao renovar token Google:', error);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Resetar o serviço (limpar autenticação)
|
|
reset() {
|
|
this.auth = null;
|
|
this.sheets = null;
|
|
this.drive = null;
|
|
console.log('Serviço Google Sheets resetado');
|
|
}
|
|
}
|
|
|
|
module.exports = new GoogleSheetsService();
|