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();