🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { JSZip } from 'https://deno.land/x/jszip@0.11.0/mod.ts'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const { data: { user } } = await supabaseClient.auth.getUser(token)
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Verificar se o usuário é admin
const { data: userRole } = await supabaseClient
.from('user_roles')
.select('role')
.eq('user_id', user.id)
.eq('role', 'admin')
.single()
if (!userRole) {
return new Response(
JSON.stringify({ error: 'Forbidden' }),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Criar timestamp para o nome do arquivo
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const backupFileName = `backup_${timestamp}.zip`
// Criar registro de log inicial
const { data: logEntry } = await supabaseClient
.from('backup_logs')
.insert({
operation_type: 'backup',
file_name: backupFileName,
created_by: user.id
})
.select()
.single()
if (!logEntry) {
return new Response(
JSON.stringify({ error: 'Failed to create backup log' }),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
try {
// Lista das principais tabelas para backup
const tablesToBackup = [
'profiles',
'functions',
'privileges',
'user_roles',
'pecas',
'ordens_fabricacao',
'processos_fabricacao',
'apontamentos_producao',
'componentes_peca',
'estoque_materiais',
'movimentacoes_estoque',
'romaneios_expedicao',
'itens_romaneio_pecas',
'itens_romaneio_insumos',
'cronogramas_of',
'tasks',
'contratos_obra',
'catalogos',
'api_keys',
'json_codes',
'webhook_configs',
'prompts',
'session_logs',
'password_reset_requests',
'sugestoes',
'backup_logs',
'ficha_tecnica_contratos',
'diarios_producao',
'empenhos_material'
]
// Criar ZIP
const zip = new JSZip()
let totalRecords = 0
let tablesCount = 0
const backupStartTime = new Date()
const tablesSchema: any = {}
const operationLogs: any[] = []
// Fazer backup de cada tabela
for (const tableName of tablesToBackup) {
try {
operationLogs.push({
timestamp: new Date().toISOString(),
operation: 'backup_table_start',
table: tableName
})
const { data: tableData, error } = await supabaseClient
.from(tableName)
.select('*')
if (!error && tableData) {
// Adicionar dados da tabela na pasta data/
zip.addFile(`data/${tableName}.json`, JSON.stringify(tableData, null, 2))
// Coletar informações do schema (simplificado)
if (tableData.length > 0) {
const sampleRecord = tableData[0]
tablesSchema[tableName] = {
columns: Object.keys(sampleRecord).map(key => ({
name: key,
type: typeof sampleRecord[key]
})),
record_count: tableData.length
}
} else {
tablesSchema[tableName] = {
columns: [],
record_count: 0
}
}
totalRecords += tableData.length
tablesCount++
operationLogs.push({
timestamp: new Date().toISOString(),
operation: 'backup_table_success',
table: tableName,
records: tableData.length
})
console.log(`Backup da tabela ${tableName}: ${tableData.length} registros`)
} else {
operationLogs.push({
timestamp: new Date().toISOString(),
operation: 'backup_table_error',
table: tableName,
error: error?.message || 'Unknown error'
})
console.warn(`Erro ao fazer backup da tabela ${tableName}:`, error)
}
} catch (error) {
operationLogs.push({
timestamp: new Date().toISOString(),
operation: 'backup_table_error',
table: tableName,
error: error.message
})
console.error(`Erro ao fazer backup da tabela ${tableName}:`, error)
}
}
// Criar metadata.json
const metadata = {
backup_info: {
created_at: backupStartTime.toISOString(),
completed_at: new Date().toISOString(),
version: '1.0',
created_by: user.email,
database_name: 'TrackSteel Production Database'
},
statistics: {
total_tables: tablesCount,
total_records: totalRecords,
backup_size_estimate: 'calculated_after_compression'
}
}
// Criar schema.json
const schema = {
version: '1.0',
created_at: new Date().toISOString(),
tables: tablesSchema
}
// Criar logs.json
const logs = {
backup_start: backupStartTime.toISOString(),
backup_end: new Date().toISOString(),
operations: operationLogs,
summary: {
total_operations: operationLogs.length,
successful_tables: operationLogs.filter(log => log.operation === 'backup_table_success').length,
failed_tables: operationLogs.filter(log => log.operation === 'backup_table_error').length
}
}
// Adicionar arquivos de controle ao ZIP
zip.addFile('metadata.json', JSON.stringify(metadata, null, 2))
zip.addFile('schema.json', JSON.stringify(schema, null, 2))
zip.addFile('logs.json', JSON.stringify(logs, null, 2))
// Gerar o arquivo ZIP
const zipData = await zip.generateAsync({ type: 'uint8array' })
// Atualizar log como concluído
await supabaseClient
.from('backup_logs')
.update({
status: 'completed',
file_size: zipData.length,
tables_count: tablesCount,
records_count: totalRecords
})
.eq('id', logEntry.id)
console.log(`Backup concluído: ${tablesCount} tabelas, ${totalRecords} registros, ${zipData.length} bytes`)
return new Response(zipData, {
headers: {
...corsHeaders,
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${backupFileName}"`,
}
})
} catch (error) {
console.error('Erro durante o backup:', error)
// Atualizar log com erro
await supabaseClient
.from('backup_logs')
.update({
status: 'failed',
error_message: error.message
})
.eq('id', logEntry.id)
return new Response(
JSON.stringify({ error: `Erro durante o backup: ${error.message}` }),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
} catch (error) {
console.error('Erro geral no backup:', error)
return new Response(
JSON.stringify({ error: `Erro no backup: ${error.message}` }),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
})

View File

@@ -0,0 +1,304 @@
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
interface DuplicateGroup {
chave_agrupamento: string;
of_number: string;
marca: string;
etapa_fase: string;
processo_nome: string;
quantidade_total_peca: number;
apontamentos: Array<{
id: string;
data_apontamento: string;
created_at: string;
quantidade_produzida: number;
tipo_apontamento: string;
}>;
total_apontado: number;
excesso: number;
}
Deno.serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const { of_number, action = 'execute' } = await req.json();
if (!of_number) {
return new Response(
JSON.stringify({
success: false,
error: 'Número da OF é obrigatório'
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400
}
);
}
console.log(`🔍 Iniciando ${action === 'analyze' ? 'análise' : 'limpeza'} de duplicatas para OF ${of_number}...`);
// Buscar todos os apontamentos da OF com dados completos
const { data: apontamentos, error } = await supabase
.from('apontamentos_producao')
.select(`
id,
of_number,
processo_id,
quantidade_produzida,
data_apontamento,
created_at,
tipo_apontamento,
peca_id,
componente_id,
peca:pecas(id, marca, etapa_fase, quantidade),
componente:componentes_peca(id, marca_componente),
processo:processos_fabricacao(nome)
`)
.eq('of_number', of_number)
.order('data_apontamento', { ascending: true })
.order('created_at', { ascending: true });
if (error) {
console.error('❌ Erro ao buscar apontamentos:', error);
throw error;
}
console.log(`📋 Encontrados ${apontamentos?.length || 0} apontamentos para OF ${of_number}`);
if (!apontamentos || apontamentos.length === 0) {
return new Response(
JSON.stringify({
success: true,
message: `Nenhum apontamento encontrado para OF ${of_number}`,
duplicatesRemoved: 0,
totalGroups: 0,
groupsWithDuplicates: 0,
details: []
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200
}
);
}
// Agrupar apontamentos por peça+fase+processo
const grupos = new Map<string, DuplicateGroup>();
apontamentos.forEach(apt => {
let marca = '';
let etapa_fase = '';
let quantidade_total_peca = 0;
if (apt.tipo_apontamento === 'peca' && apt.peca) {
marca = apt.peca.marca;
etapa_fase = apt.peca.etapa_fase || '0';
quantidade_total_peca = apt.peca.quantidade || 0;
} else if (apt.tipo_apontamento === 'componente' && apt.componente) {
marca = apt.componente.marca_componente;
etapa_fase = '0';
quantidade_total_peca = 0;
return; // Pular componentes nesta análise
} else {
console.warn('⚠️ Apontamento sem peça válida:', apt.id);
return;
}
// Chave única: OF + Marca + Fase + Processo
const chave = `${apt.of_number}|${marca}|${etapa_fase}|${apt.processo_id}`;
const processo_nome = apt.processo?.nome || 'Desconhecido';
console.log(`📝 Processando: OF=${apt.of_number}, Marca=${marca}, Fase=${etapa_fase}, Processo=${processo_nome}, Data=${apt.data_apontamento}, Qtd=${apt.quantidade_produzida}`);
if (!grupos.has(chave)) {
grupos.set(chave, {
chave_agrupamento: chave,
of_number: apt.of_number,
marca,
etapa_fase,
processo_nome,
quantidade_total_peca,
apontamentos: [],
total_apontado: 0,
excesso: 0
});
}
const grupo = grupos.get(chave)!;
grupo.apontamentos.push({
id: apt.id,
data_apontamento: apt.data_apontamento,
created_at: apt.created_at,
quantidade_produzida: apt.quantidade_produzida,
tipo_apontamento: apt.tipo_apontamento
});
});
console.log(`🔍 Identificados ${grupos.size} grupos únicos de OF+Marca+Fase+Processo`);
// Identificar duplicatas REAIS
const idsParaExcluir: string[] = [];
const detalhesLimpeza: DuplicateGroup[] = [];
grupos.forEach((grupo) => {
// Calcular total apontado para este grupo
grupo.total_apontado = grupo.apontamentos.reduce((sum, apt) => sum + apt.quantidade_produzida, 0);
grupo.excesso = Math.max(0, grupo.total_apontado - grupo.quantidade_total_peca);
console.log(`\n🔄 Analisando grupo: ${grupo.marca} | Fase: ${grupo.etapa_fase} | Processo: ${grupo.processo_nome}`);
console.log(` 📊 Quantidade total da peça: ${grupo.quantidade_total_peca}`);
console.log(` 📈 Total apontado: ${grupo.total_apontado}`);
console.log(` 📝 Número de apontamentos: ${grupo.apontamentos.length}`);
// Se só tem 1 apontamento, não há duplicata
if (grupo.apontamentos.length <= 1) {
console.log(` ✅ Apenas 1 apontamento - OK`);
return;
}
// Ordenar apontamentos por data (mais antigo primeiro)
grupo.apontamentos.sort((a, b) => {
const dataA = new Date(a.data_apontamento);
const dataB = new Date(b.data_apontamento);
if (dataA.getTime() !== dataB.getTime()) {
return dataA.getTime() - dataB.getTime();
}
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
});
console.log(` 📅 Apontamentos ordenados por data:`);
grupo.apontamentos.forEach((apt, idx) => {
console.log(` ${idx + 1}. Data: ${apt.data_apontamento}, Qtd: ${apt.quantidade_produzida}, ID: ${apt.id}`);
});
// Verificar se há excesso real de apontamentos ou múltiplas datas
const datasUnicas = [...new Set(grupo.apontamentos.map(a => a.data_apontamento))];
const temExcesso = grupo.total_apontado > grupo.quantidade_total_peca;
const temMultiplasDatas = datasUnicas.length > 1;
if (temExcesso || temMultiplasDatas) {
console.log(` 🚨 DUPLICATA DETECTADA!`);
if (temExcesso) {
console.log(` ⚠️ EXCESSO: Total apontado (${grupo.total_apontado}) > Quantidade da peça (${grupo.quantidade_total_peca})`);
}
if (temMultiplasDatas) {
console.log(` 📅 MÚLTIPLAS DATAS: Mesma peça apontada em ${datasUnicas.length} datas diferentes: ${datasUnicas.join(', ')}`);
}
let quantidadeAcumulada = 0;
// Processar apontamentos em ordem cronológica (manter os mais antigos)
for (let i = 0; i < grupo.apontamentos.length; i++) {
const apt = grupo.apontamentos[i];
if (quantidadeAcumulada + apt.quantidade_produzida <= grupo.quantidade_total_peca) {
// Este apontamento ainda cabe na quantidade da peça
quantidadeAcumulada += apt.quantidade_produzida;
console.log(` ✅ MANTIDO: ${apt.data_apontamento} - Qtd: ${apt.quantidade_produzida} (Acum: ${quantidadeAcumulada})`);
} else {
// Este apontamento é excesso - REMOVER
idsParaExcluir.push(apt.id);
console.log(` 🗑️ REMOVIDO: ${apt.data_apontamento} - Qtd: ${apt.quantidade_produzida} - MOTIVO: Excesso`);
}
}
detalhesLimpeza.push(grupo);
}
});
const resultado = {
success: true,
message: action === 'analyze'
? `Análise concluída para OF ${of_number}. ${detalhesLimpeza.length} duplicatas encontradas.`
: `Limpeza concluída para OF ${of_number}. ${idsParaExcluir.length} duplicatas/excessos removidos.`,
duplicatesRemoved: action === 'execute' ? idsParaExcluir.length : 0,
totalGroups: grupos.size,
groupsWithDuplicates: detalhesLimpeza.length,
details: detalhesLimpeza
};
// Se for apenas análise, não executar exclusões
if (action === 'analyze') {
console.log('\n📊 Análise concluída - não executando exclusões');
return new Response(
JSON.stringify(resultado),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200
}
);
}
// Executar exclusões em lotes (apenas se action === 'execute')
let duplicatasRemovidas = 0;
if (idsParaExcluir.length > 0) {
console.log(`\n🗑 Iniciando exclusão de ${idsParaExcluir.length} apontamentos duplicados/excessivos`);
const loteSize = 50;
for (let i = 0; i < idsParaExcluir.length; i += loteSize) {
const lote = idsParaExcluir.slice(i, i + loteSize);
console.log(`🔄 Processando lote ${Math.floor(i/loteSize) + 1}/${Math.ceil(idsParaExcluir.length/loteSize)} com ${lote.length} registros...`);
const { error: deleteError } = await supabase
.from('apontamentos_producao')
.delete()
.in('id', lote);
if (deleteError) {
console.error(`❌ Erro ao excluir lote:`, deleteError);
throw deleteError;
}
duplicatasRemovidas += lote.length;
console.log(`✅ Lote processado: ${lote.length} registros excluídos`);
}
}
resultado.duplicatesRemoved = duplicatasRemovidas;
console.log('\n✅ Operação concluída:', resultado);
return new Response(
JSON.stringify(resultado),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200
}
);
} catch (error) {
console.error('❌ Erro durante operação:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message || 'Erro interno do servidor',
duplicatesRemoved: 0
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500
}
);
}
});

View File

@@ -0,0 +1,139 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const { data: { user } } = await supabaseClient.auth.getUser(token)
if (!user) {
return new Response('Unauthorized', { status: 401, headers: corsHeaders })
}
// Verificar se o usuário é admin
const { data: userRole } = await supabaseClient
.from('user_roles')
.select('role')
.eq('user_id', user.id)
.eq('role', 'admin')
.single()
if (!userRole) {
return new Response('Forbidden', { status: 403, headers: corsHeaders })
}
// Ler arquivo do request
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return new Response('No file provided', { status: 400, headers: corsHeaders })
}
// Criar registro de log inicial
const { data: logEntry } = await supabaseClient
.from('backup_logs')
.insert({
operation_type: 'restore',
file_name: file.name,
file_size: file.size,
created_by: user.id
})
.select()
.single()
try {
// Ler conteúdo do arquivo
const fileContent = await file.text()
const backupData = JSON.parse(fileContent)
// Validar estrutura do backup
if (!backupData.version || !backupData.tables) {
throw new Error('Formato de backup inválido')
}
let totalRecords = 0
// Restaurar cada tabela
for (const [tableName, tableData] of Object.entries(backupData.tables)) {
if (Array.isArray(tableData) && tableData.length > 0) {
try {
// Limpar tabela antes de restaurar (cuidado!)
await supabaseClient
.from(tableName)
.delete()
.not('id', 'is', null)
// Inserir dados em lotes
const batchSize = 100
for (let i = 0; i < tableData.length; i += batchSize) {
const batch = tableData.slice(i, i + batchSize)
await supabaseClient
.from(tableName)
.insert(batch)
}
totalRecords += tableData.length
} catch (error) {
console.error(`Erro ao restaurar tabela ${tableName}:`, error)
// Continuar com outras tabelas mesmo se uma falhar
}
}
}
// Atualizar log como concluído
await supabaseClient
.from('backup_logs')
.update({
status: 'completed',
tables_count: Object.keys(backupData.tables).length,
records_count: totalRecords
})
.eq('id', logEntry.id)
return new Response(JSON.stringify({
success: true,
message: 'Restauração concluída com sucesso',
tablesRestored: Object.keys(backupData.tables).length,
recordsRestored: totalRecords
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} catch (error) {
// Atualizar log com erro
await supabaseClient
.from('backup_logs')
.update({
status: 'failed',
error_message: error.message
})
.eq('id', logEntry.id)
throw error
}
} catch (error) {
console.error('Erro na restauração:', error)
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})