🚀 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
+363
View File
@@ -0,0 +1,363 @@
/**
* Sistema de Cache Inteligente para Otimização de Performance
* Implementa cache em memória e localStorage com TTL e versionamento
*/
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
version: string;
}
interface PerformanceMetrics {
operation: string;
duration_ms: number;
cache_hit: boolean;
items_count: number;
query_count: number;
timestamp: Date;
}
export class OptimizedCache {
private static readonly CACHE_VERSIONS = {
HISTORICO: '1.0',
ITENS_DISPONIVEIS: '1.1',
COMPONENTES: '1.0',
PECAS: '1.0',
PROCESSOS: '1.0'
} as const;
public static readonly TTL_CONFIG = {
HISTORICO_OF: 5 * 60 * 1000, // 5 minutos
ITENS_DISPONIVEIS: 2 * 60 * 1000, // 2 minutos
COMPONENTES: 10 * 60 * 1000, // 10 minutos
PECAS: 10 * 60 * 1000, // 10 minutos
PROCESSOS: 15 * 60 * 1000 // 15 minutos
} as const;
private static memoryCache = new Map<string, { data: any; timestamp: number }>();
private static readonly MEMORY_TTL = 30 * 1000; // 30 segundos
/**
* Recupera dados do cache (memória primeiro, depois localStorage)
*/
static async get<T>(key: string, type: keyof typeof OptimizedCache.TTL_CONFIG): Promise<T | null> {
const startTime = performance.now();
// Verificar memória primeiro
const memoryKey = `mem_${key}`;
if (this.memoryCache.has(memoryKey)) {
const entry = this.memoryCache.get(memoryKey)!;
if (Date.now() - entry.timestamp < this.MEMORY_TTL) {
this.trackPerformance({
operation: `cache_get_${type}`,
duration_ms: Math.round(performance.now() - startTime),
cache_hit: true,
items_count: Array.isArray(entry.data) ? entry.data.length : 1,
query_count: 0,
timestamp: new Date()
});
return entry.data;
} else {
this.memoryCache.delete(memoryKey);
}
}
// Verificar localStorage
const storageKey = `cache_${key}`;
const stored = localStorage.getItem(storageKey);
if (stored) {
try {
const entry: CacheEntry = JSON.parse(stored);
const ttl = this.TTL_CONFIG[type];
const versionKey = type.split('_')[0] as keyof typeof this.CACHE_VERSIONS;
if (Date.now() - entry.timestamp < ttl &&
entry.version === this.CACHE_VERSIONS[versionKey]) {
// Armazenar em memória para próximas consultas
this.memoryCache.set(memoryKey, {
data: entry.data,
timestamp: Date.now()
});
this.trackPerformance({
operation: `cache_get_${type}`,
duration_ms: Math.round(performance.now() - startTime),
cache_hit: true,
items_count: Array.isArray(entry.data) ? entry.data.length : 1,
query_count: 0,
timestamp: new Date()
});
return entry.data;
} else {
localStorage.removeItem(storageKey);
}
} catch (error) {
console.warn('Erro ao recuperar cache:', error);
localStorage.removeItem(storageKey);
}
}
this.trackPerformance({
operation: `cache_get_${type}`,
duration_ms: Math.round(performance.now() - startTime),
cache_hit: false,
items_count: 0,
query_count: 0,
timestamp: new Date()
});
return null;
}
/**
* Armazena dados no cache (memória e localStorage)
*/
static set<T>(key: string, data: T, type: keyof typeof OptimizedCache.TTL_CONFIG): void {
const versionKey = type.split('_')[0] as keyof typeof this.CACHE_VERSIONS;
const entry: CacheEntry = {
data,
timestamp: Date.now(),
ttl: this.TTL_CONFIG[type],
version: this.CACHE_VERSIONS[versionKey] || '1.0'
};
// Armazenar em ambos os caches
this.memoryCache.set(`mem_${key}`, {
data,
timestamp: Date.now()
});
try {
localStorage.setItem(`cache_${key}`, JSON.stringify(entry));
} catch (error) {
console.warn('Erro ao armazenar cache no localStorage:', error);
// Se localStorage estiver cheio, limpar caches antigos
this.cleanupOldCache();
try {
localStorage.setItem(`cache_${key}`, JSON.stringify(entry));
} catch (retryError) {
console.error('Falha ao armazenar cache após limpeza:', retryError);
}
}
}
/**
* Invalida cache por padrão de chave
*/
static invalidatePattern(pattern: string): void {
// Limpar memória
for (const key of this.memoryCache.keys()) {
if (key.includes(pattern)) {
this.memoryCache.delete(key);
}
}
// Limpar localStorage
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.includes(pattern)) {
localStorage.removeItem(key);
}
}
}
/**
* Remove entrada específica do cache
*/
static remove(key: string): void {
this.memoryCache.delete(`mem_${key}`);
localStorage.removeItem(`cache_${key}`);
}
/**
* Limpa caches antigos para liberar espaço
*/
private static cleanupOldCache(): void {
const now = Date.now();
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('cache_')) {
try {
const entry: CacheEntry = JSON.parse(localStorage.getItem(key)!);
if (now - entry.timestamp > entry.ttl) {
keysToRemove.push(key);
}
} catch (error) {
keysToRemove.push(key);
}
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
console.log(`Cache cleanup: removidas ${keysToRemove.length} entradas antigas`);
}
/**
* Obtém estatísticas do cache
*/
static getStats(): {
memoryEntries: number;
localStorageEntries: number;
totalSize: number;
} {
let localStorageEntries = 0;
let totalSize = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('cache_')) {
localStorageEntries++;
totalSize += localStorage.getItem(key)!.length;
}
}
return {
memoryEntries: this.memoryCache.size,
localStorageEntries,
totalSize
};
}
/**
* Limpa todo o cache
*/
static clear(): void {
this.memoryCache.clear();
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('cache_')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}
/**
* Registra métricas de performance
*/
private static trackPerformance(metrics: PerformanceMetrics): void {
if (process.env.NODE_ENV === 'development') {
console.log('🚀 Cache Performance:', metrics);
}
// Armazenar métricas para análise posterior
const metricsKey = 'cache_performance_metrics';
const existingMetrics = JSON.parse(localStorage.getItem(metricsKey) || '[]');
existingMetrics.push(metrics);
// Manter apenas as últimas 100 métricas
if (existingMetrics.length > 100) {
existingMetrics.splice(0, existingMetrics.length - 100);
}
try {
localStorage.setItem(metricsKey, JSON.stringify(existingMetrics));
} catch (error) {
// Se não conseguir armazenar métricas, não é crítico
}
}
/**
* Obtém métricas de performance
*/
static getPerformanceMetrics(): PerformanceMetrics[] {
const metricsKey = 'cache_performance_metrics';
return JSON.parse(localStorage.getItem(metricsKey) || '[]');
}
}
/**
* Hook para cache inteligente com React Query/SWR style
*/
export const useSmartCache = <T>(
key: string,
fetcher: () => Promise<T>,
type: keyof typeof OptimizedCache.TTL_CONFIG,
options: {
enabled?: boolean;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
} = {}
) => {
const [data, setData] = React.useState<T | null>(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
if (options.enabled === false) return;
const loadData = async () => {
setLoading(true);
setError(null);
try {
// Tentar cache primeiro
const cached = await OptimizedCache.get<T>(key, type);
if (cached) {
setData(cached);
setLoading(false);
options.onSuccess?.(cached);
return;
}
// Buscar dados frescos
const freshData = await fetcher();
OptimizedCache.set(key, freshData, type);
setData(freshData);
options.onSuccess?.(freshData);
} catch (err) {
const error = err instanceof Error ? err : new Error('Erro desconhecido');
setError(error);
options.onError?.(error);
} finally {
setLoading(false);
}
};
loadData();
}, [key, type, options.enabled]);
const invalidate = () => {
OptimizedCache.remove(key);
};
const refetch = async () => {
OptimizedCache.remove(key);
setLoading(true);
setError(null);
try {
const freshData = await fetcher();
OptimizedCache.set(key, freshData, type);
setData(freshData);
options.onSuccess?.(freshData);
} catch (err) {
const error = err instanceof Error ? err : new Error('Erro desconhecido');
setError(error);
options.onError?.(error);
} finally {
setLoading(false);
}
};
return {
data,
loading,
error,
invalidate,
refetch
};
};
// Importar React para o hook
import React from 'react';
export default OptimizedCache;
+322
View File
@@ -0,0 +1,322 @@
/**
* Sistema de Monitoramento de Performance
* Rastreia métricas de performance e operações do sistema
*/
export interface PerformanceMetrics {
operation: string;
duration_ms: number;
cache_hit: boolean;
items_count: number;
query_count: number;
timestamp: Date;
metadata?: Record<string, any>;
}
export interface PerformanceTracker {
startTime: number;
operation: string;
metadata: Record<string, any>;
}
export interface SystemHealth {
cache_status: 'healthy' | 'degraded' | 'failed';
database_status: 'healthy' | 'slow' | 'failed';
average_response_time: number;
cache_hit_rate: number;
active_users: number;
timestamp: Date;
}
export class PerformanceMonitor {
private static trackers = new Map<string, PerformanceTracker>();
private static metrics: PerformanceMetrics[] = [];
private static readonly MAX_METRICS = 1000;
/**
* Inicia o rastreamento de uma operação
*/
static start(operation: string, metadata: Record<string, any> = {}): string {
const trackerId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.trackers.set(trackerId, {
startTime: performance.now(),
operation,
metadata
});
return trackerId;
}
/**
* Finaliza o rastreamento e retorna as métricas
*/
static end(trackerId: string): PerformanceMetrics | null {
const tracker = this.trackers.get(trackerId);
if (!tracker) {
console.warn(`Tracker não encontrado: ${trackerId}`);
return null;
}
const duration = performance.now() - tracker.startTime;
this.trackers.delete(trackerId);
const metrics: PerformanceMetrics = {
operation: tracker.operation,
duration_ms: Math.round(duration * 100) / 100, // 2 casas decimais
cache_hit: tracker.metadata.cache_hit || false,
items_count: tracker.metadata.items_count || 0,
query_count: tracker.metadata.query_count || 1,
timestamp: new Date(),
metadata: tracker.metadata
};
// Armazenar métricas
this.addMetrics(metrics);
// Log para desenvolvimento
if (process.env.NODE_ENV === 'development') {
this.logMetrics(metrics);
}
// Alertas para performance ruim
this.checkPerformanceAlerts(metrics);
return metrics;
}
/**
* Adiciona métricas ao histórico
*/
private static addMetrics(metrics: PerformanceMetrics): void {
this.metrics.push(metrics);
// Manter apenas as últimas métricas
if (this.metrics.length > this.MAX_METRICS) {
this.metrics.splice(0, this.metrics.length - this.MAX_METRICS);
}
// Persistir no localStorage para análise
try {
const persistedMetrics = this.metrics.slice(-100); // Últimas 100
localStorage.setItem('performance_metrics', JSON.stringify(persistedMetrics));
} catch (error) {
// Falha ao persistir não é crítica
}
}
/**
* Log formatado das métricas
*/
private static logMetrics(metrics: PerformanceMetrics): void {
const emoji = metrics.cache_hit ? '⚡' : '🔍';
const color = metrics.duration_ms > 1000 ? 'color: red' :
metrics.duration_ms > 500 ? 'color: orange' : 'color: green';
console.log(
`%c${emoji} ${metrics.operation}`,
color,
`${metrics.duration_ms}ms`,
metrics.cache_hit ? '(cache)' : '(fresh)',
metrics.items_count > 0 ? `${metrics.items_count} items` : ''
);
}
/**
* Verifica alertas de performance
*/
private static checkPerformanceAlerts(metrics: PerformanceMetrics): void {
// Alerta para operações muito lentas
if (metrics.duration_ms > 2000) {
console.warn(`⚠️ Operação lenta detectada: ${metrics.operation} (${metrics.duration_ms}ms)`);
}
// Alerta para baixa taxa de cache hit
const recentMetrics = this.metrics.slice(-20);
const cacheHitRate = recentMetrics.filter(m => m.cache_hit).length / recentMetrics.length;
if (recentMetrics.length >= 10 && cacheHitRate < 0.3) {
console.warn(`⚠️ Taxa de cache hit baixa: ${Math.round(cacheHitRate * 100)}%`);
}
}
/**
* Obtém estatísticas de performance
*/
static getStats(): {
totalOperations: number;
averageResponseTime: number;
cacheHitRate: number;
slowOperations: number;
operationsByType: Record<string, number>;
} {
if (this.metrics.length === 0) {
return {
totalOperations: 0,
averageResponseTime: 0,
cacheHitRate: 0,
slowOperations: 0,
operationsByType: {}
};
}
const totalOperations = this.metrics.length;
const averageResponseTime = this.metrics.reduce((sum, m) => sum + m.duration_ms, 0) / totalOperations;
const cacheHitRate = this.metrics.filter(m => m.cache_hit).length / totalOperations;
const slowOperations = this.metrics.filter(m => m.duration_ms > 1000).length;
const operationsByType = this.metrics.reduce((acc, m) => {
acc[m.operation] = (acc[m.operation] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return {
totalOperations,
averageResponseTime: Math.round(averageResponseTime * 100) / 100,
cacheHitRate: Math.round(cacheHitRate * 100) / 100,
slowOperations,
operationsByType
};
}
/**
* Obtém métricas recentes
*/
static getRecentMetrics(limit: number = 50): PerformanceMetrics[] {
return this.metrics.slice(-limit);
}
/**
* Limpa todas as métricas
*/
static clearMetrics(): void {
this.metrics = [];
this.trackers.clear();
localStorage.removeItem('performance_metrics');
}
/**
* Carrega métricas persistidas
*/
static loadPersistedMetrics(): void {
try {
const stored = localStorage.getItem('performance_metrics');
if (stored) {
const parsed = JSON.parse(stored);
this.metrics = parsed.map((m: any) => ({
...m,
timestamp: new Date(m.timestamp)
}));
}
} catch (error) {
console.warn('Erro ao carregar métricas persistidas:', error);
}
}
}
/**
* Hook para monitoramento automático de performance
*/
export const usePerformanceTracking = (
operation: string,
dependencies: any[] = [],
metadata: Record<string, any> = {}
) => {
const [metrics, setMetrics] = React.useState<PerformanceMetrics | null>(null);
const trackerRef = React.useRef<string | null>(null);
React.useEffect(() => {
// Iniciar rastreamento
trackerRef.current = PerformanceMonitor.start(operation, metadata);
return () => {
// Finalizar rastreamento
if (trackerRef.current) {
const result = PerformanceMonitor.end(trackerRef.current);
setMetrics(result);
trackerRef.current = null;
}
};
}, dependencies);
return metrics;
};
/**
* Hook para monitoramento de saúde do sistema
*/
export const useSystemHealth = () => {
const [health, setHealth] = React.useState<SystemHealth | null>(null);
const [loading, setLoading] = React.useState(false);
const checkHealth = React.useCallback(async (): Promise<SystemHealth> => {
const startTime = performance.now();
try {
// Simular verificação de conectividade (adaptar conforme necessário)
const response = await fetch('/api/health', {
method: 'HEAD',
cache: 'no-cache'
}).catch(() => null);
const dbResponseTime = performance.now() - startTime;
// Verificar status do cache
const cacheStats = OptimizedCache.getStats();
const cacheStatus = cacheStats.localStorageEntries > 0 ? 'healthy' : 'degraded';
// Calcular taxa de cache hit
const performanceStats = PerformanceMonitor.getStats();
const health: SystemHealth = {
cache_status: cacheStatus,
database_status: !response ? 'failed' :
dbResponseTime > 2000 ? 'slow' : 'healthy',
average_response_time: Math.round(dbResponseTime),
cache_hit_rate: performanceStats.cacheHitRate,
active_users: 1, // Implementar contagem real se necessário
timestamp: new Date()
};
return health;
} catch (error) {
return {
cache_status: 'failed',
database_status: 'failed',
average_response_time: -1,
cache_hit_rate: 0,
active_users: 0,
timestamp: new Date()
};
}
}, []);
React.useEffect(() => {
const performCheck = async () => {
setLoading(true);
try {
const healthData = await checkHealth();
setHealth(healthData);
} catch (error) {
console.error('Erro ao verificar saúde do sistema:', error);
} finally {
setLoading(false);
}
};
performCheck();
// Verificar a cada 30 segundos
const interval = setInterval(performCheck, 30000);
return () => clearInterval(interval);
}, [checkHealth]);
return { health, loading, refetch: checkHealth };
};
// Importações necessárias
import React from 'react';
import { OptimizedCache } from './OptimizedCache';
// Inicializar métricas persistidas ao carregar o módulo
PerformanceMonitor.loadPersistedMetrics();
export default PerformanceMonitor;
+147
View File
@@ -0,0 +1,147 @@
import { supabase } from '@/integrations/supabase/client';
interface ApiKey {
id: string;
name: string;
key: string;
is_primary: boolean;
}
class ApiKeyManager {
private static instance: ApiKeyManager;
private apiKeys: ApiKey[] = [];
private lastFetch: number = 0;
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutos
private constructor() {}
static getInstance(): ApiKeyManager {
if (!ApiKeyManager.instance) {
ApiKeyManager.instance = new ApiKeyManager();
}
return ApiKeyManager.instance;
}
private async fetchApiKeys(): Promise<void> {
const now = Date.now();
if (now - this.lastFetch < this.CACHE_DURATION && this.apiKeys.length > 0) {
return; // Usar cache
}
try {
const { data, error } = await (supabase as any)
.from('api_keys')
.select('*')
.order('is_primary', { ascending: false })
.order('created_at', { ascending: true });
if (error) throw error;
this.apiKeys = data || [];
this.lastFetch = now;
} catch (error) {
console.error('Erro ao buscar chaves API:', error);
throw error;
}
}
async getApiKeyWithFallback(): Promise<string | null> {
await this.fetchApiKeys();
if (this.apiKeys.length === 0) {
console.warn('Nenhuma chave API configurada');
return null;
}
// Retorna a chave principal primeiro, depois as secundárias
const primaryKey = this.apiKeys.find(key => key.is_primary);
if (primaryKey) {
return primaryKey.key;
}
// Se não há chave principal, retorna a primeira disponível
return this.apiKeys[0]?.key || null;
}
async getAllApiKeys(): Promise<string[]> {
await this.fetchApiKeys();
return this.apiKeys.map(key => key.key);
}
// Método para fazer requests com fallback automático
async makeRequestWithFallback<T>(
requestFn: (apiKey: string) => Promise<T>,
options: {
retries?: number;
timeout?: number;
} = {}
): Promise<T> {
const { retries = 1, timeout = 30000 } = options;
await this.fetchApiKeys();
if (this.apiKeys.length === 0) {
throw new Error('Nenhuma chave API configurada');
}
const keys = [...this.apiKeys].sort((a, b) => {
if (a.is_primary && !b.is_primary) return -1;
if (!a.is_primary && b.is_primary) return 1;
return 0;
});
let lastError: Error | null = null;
for (const apiKeyObj of keys) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
console.log(`Tentando chave: ${apiKeyObj.name} (tentativa ${attempt + 1})`);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout na requisição')), timeout)
);
const result = await Promise.race([
requestFn(apiKeyObj.key),
timeoutPromise
]);
console.log(`Sucesso com chave: ${apiKeyObj.name}`);
return result;
} catch (error: any) {
lastError = error;
const isAuthError = error?.status === 401 || error?.status === 403;
const isNetworkError = error?.message?.includes('Timeout') ||
error?.message?.includes('network') ||
error?.code === 'ECONNREFUSED';
console.warn(`Falha com chave ${apiKeyObj.name}:`, error?.message);
// Se é erro de autenticação, não tentar novamente com a mesma chave
if (isAuthError) {
console.log(`Chave ${apiKeyObj.name} inválida, tentando próxima...`);
break;
}
// Se é erro de rede e ainda há tentativas, aguarda um pouco
if (isNetworkError && attempt < retries) {
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
}
}
}
}
throw new Error(
`Todas as chaves API falharam. Último erro: ${lastError?.message || 'Erro desconhecido'}`
);
}
// Limpar cache (útil após atualizações nas chaves)
clearCache(): void {
this.apiKeys = [];
this.lastFetch = 0;
}
}
export const apiKeyManager = ApiKeyManager.getInstance();
+46
View File
@@ -0,0 +1,46 @@
export const parseCSV = (csvText: string): any[] => {
const lines = csvText.trim().split('\n');
if (lines.length < 2) return [];
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
if (values.length === headers.length) {
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index];
});
data.push(row);
}
}
return data;
};
export const generateCSV = (data: any[], headers: string[]): string => {
const csvHeaders = headers.join(',');
const csvRows = data.map(row =>
headers.map(header => {
const value = row[header];
if (value === null || value === undefined) return '';
return typeof value === 'string' && value.includes(',') ? `"${value}"` : value;
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
};
export const downloadCSV = (csvContent: string, filename: string) => {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
+97
View File
@@ -0,0 +1,97 @@
import { format, toZonedTime, fromZonedTime } from 'date-fns-tz';
import { ptBR } from 'date-fns/locale';
// Timezone de São Paulo
const SAO_PAULO_TIMEZONE = 'America/Sao_Paulo';
// Função para obter a data atual no timezone de São Paulo
export const getSystemDate = (): Date => {
return toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
};
// Função para obter a data atual como string no formato ISO (YYYY-MM-DD) no timezone de São Paulo
export const getSystemDateString = (): string => {
const saoPauloDate = toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
return format(saoPauloDate, 'yyyy-MM-dd', { timeZone: SAO_PAULO_TIMEZONE });
};
// Função para obter a data/hora atual como string no formato ISO completo no timezone de São Paulo
export const getSystemDateTime = (): string => {
const saoPauloDate = toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
return format(saoPauloDate, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", { timeZone: SAO_PAULO_TIMEZONE });
};
// Função para formatar data no padrão brasileiro (para objetos Date)
export const formatBrazilianDate = (date: Date | string): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
const saoPauloDate = toZonedTime(dateObj, SAO_PAULO_TIMEZONE);
return format(saoPauloDate, 'dd/MM/yyyy', { locale: ptBR, timeZone: SAO_PAULO_TIMEZONE });
};
// Função específica para formatar strings de data (YYYY-MM-DD) sem problemas de timezone
export const formatBrazilianDateFromString = (dateString: string): string => {
if (!dateString) return '';
// Dividir a string de data para evitar problemas de timezone
const [year, month, day] = dateString.split('-');
// Criar uma data local sem conversão de timezone
const localDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
const saoPauloDate = toZonedTime(localDate, SAO_PAULO_TIMEZONE);
return format(saoPauloDate, 'dd/MM/yyyy', { locale: ptBR, timeZone: SAO_PAULO_TIMEZONE });
};
// Função para formatar data/hora no padrão brasileiro
export const formatBrazilianDateTime = (date: Date | string): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
const saoPauloDate = toZonedTime(dateObj, SAO_PAULO_TIMEZONE);
return format(saoPauloDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR, timeZone: SAO_PAULO_TIMEZONE });
};
// Função para formatar apenas a hora
export const formatTime = (date: Date | string): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
const saoPauloDate = toZonedTime(dateObj, SAO_PAULO_TIMEZONE);
return format(saoPauloDate, 'HH:mm:ss', { locale: ptBR, timeZone: SAO_PAULO_TIMEZONE });
};
// Função para converter data local para UTC (para enviar ao banco)
export const toUTCDate = (date: Date): string => {
return fromZonedTime(date, SAO_PAULO_TIMEZONE).toISOString();
};
// Função para converter data UTC do banco para data local de São Paulo
export const fromUTCDate = (utcDate: string): Date => {
return toZonedTime(new Date(utcDate), SAO_PAULO_TIMEZONE);
};
// Função para obter ontem baseado na data do sistema em São Paulo
export const getYesterdayString = (): string => {
const saoPauloDate = toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
saoPauloDate.setDate(saoPauloDate.getDate() - 1);
return format(saoPauloDate, 'yyyy-MM-dd', { timeZone: SAO_PAULO_TIMEZONE });
};
// Função para obter uma data específica em relação ao sistema
export const getDateOffsetString = (days: number): string => {
const saoPauloDate = toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
saoPauloDate.setDate(saoPauloDate.getDate() + days);
return format(saoPauloDate, 'yyyy-MM-dd', { timeZone: SAO_PAULO_TIMEZONE });
};
// Função para verificar se uma data string é hoje
export const isToday = (dateString: string): boolean => {
return dateString === getSystemDateString();
};
// Função para verificar se uma data string é ontem
export const isYesterday = (dateString: string): boolean => {
return dateString === getYesterdayString();
};
// Função para obter a data/hora atual em São Paulo para exibição
export const getCurrentSaoPauloTime = (): Date => {
return toZonedTime(new Date(), SAO_PAULO_TIMEZONE);
};
+92
View File
@@ -0,0 +1,92 @@
import { supabase } from '@/integrations/supabase/client';
export const seedTiposMateriaPrima = async () => {
const tiposMateriaPrima = [
{
nome: 'Perfis Estruturais',
descricao: 'Perfis laminados e soldados para estruturas metálicas',
caracteristicas: {
resistencia: 'Alta',
soldabilidade: 'Excelente',
conformacao: 'Boa'
},
controles: {
certificado: true,
rastreabilidade: true,
teste_qualidade: true
}
},
{
nome: 'Chapas e Bobinas',
descricao: 'Chapas laminadas a quente e a frio',
caracteristicas: {
espessura: 'Variável',
acabamento: 'Laminado',
planicidade: 'Controlada'
},
controles: {
dimensoes: true,
planicidade: true,
acabamento_superficial: true
}
},
{
nome: 'Parafusos e Fixadores',
descricao: 'Elementos de fixação diversos',
caracteristicas: {
resistencia: 'Especificada',
acabamento: 'Galvanizado/Pintado',
rosca: 'Métrica/Whitworth'
},
controles: {
resistencia_mecanica: true,
dimensoes: true,
acabamento: true
}
},
{
nome: 'Eletrodos e Consumíveis',
descricao: 'Materiais para soldagem',
caracteristicas: {
classificacao: 'AWS/ASME',
diametro: 'Variável',
revestimento: 'Específico'
},
controles: {
umidade: true,
validade: true,
armazenamento: true
}
},
{
nome: 'Tintas e Revestimentos',
descricao: 'Materiais para proteção anticorrosiva',
caracteristicas: {
tipo: 'Primer/Acabamento',
base: 'Solvente/Água',
cor: 'Especificada'
},
controles: {
viscosidade: true,
validade: true,
armazenamento: true
}
}
];
try {
const { data, error } = await supabase
.from('tipos_materia_prima')
.insert(tiposMateriaPrima)
.select();
if (error) throw error;
console.log('Tipos de matéria-prima inseridos:', data);
return data;
} catch (error) {
console.error('Erro ao inserir tipos de matéria-prima:', error);
throw error;
}
};
+115
View File
@@ -0,0 +1,115 @@
import { supabase } from '@/integrations/supabase/client';
interface JsonCodeData {
id: string;
name: string;
description?: string;
json_code: any;
is_active: boolean;
}
class JsonCodeManager {
private static instance: JsonCodeManager;
private jsonCodes: JsonCodeData[] = [];
private lastFetch: number = 0;
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutos
private constructor() {}
static getInstance(): JsonCodeManager {
if (!JsonCodeManager.instance) {
JsonCodeManager.instance = new JsonCodeManager();
}
return JsonCodeManager.instance;
}
private async fetchJsonCodes(): Promise<void> {
const now = Date.now();
if (now - this.lastFetch < this.CACHE_DURATION && this.jsonCodes.length > 0) {
return; // Usar cache
}
try {
const { data, error } = await (supabase as any)
.from('json_codes')
.select('*')
.eq('is_active', true)
.order('created_at', { ascending: false });
if (error) throw error;
this.jsonCodes = data || [];
this.lastFetch = now;
} catch (error) {
console.error('Erro ao buscar códigos JSON:', error);
throw error;
}
}
async getJsonCodeByName(name: string): Promise<any | null> {
await this.fetchJsonCodes();
const code = this.jsonCodes.find(code => code.name === name);
if (!code) {
console.warn(`Código JSON com nome '${name}' não encontrado`);
return null;
}
return code.json_code;
}
async getAllJsonCodes(): Promise<JsonCodeData[]> {
await this.fetchJsonCodes();
return this.jsonCodes;
}
async getActiveJsonCodes(): Promise<JsonCodeData[]> {
await this.fetchJsonCodes();
return this.jsonCodes.filter(code => code.is_active);
}
async getJsonCodeById(id: string): Promise<any | null> {
await this.fetchJsonCodes();
const code = this.jsonCodes.find(code => code.id === id);
if (!code) {
console.warn(`Código JSON com ID '${id}' não encontrado`);
return null;
}
return code.json_code;
}
// Limpar cache (útil após atualizações nos códigos)
clearCache(): void {
this.jsonCodes = [];
this.lastFetch = 0;
}
}
export const jsonCodeManager = JsonCodeManager.getInstance();
// Exemplo de uso:
export async function exemploUsoJsonCode() {
try {
// Buscar código por nome
const meuConfig = await jsonCodeManager.getJsonCodeByName('configuracao_sistema');
if (meuConfig) {
console.log('Configuração encontrada:', meuConfig);
// Usar o código JSON conforme necessário
return meuConfig;
}
// Buscar todos os códigos ativos
const codigosAtivos = await jsonCodeManager.getActiveJsonCodes();
console.log('Códigos ativos:', codigosAtivos);
} catch (error) {
console.error('Erro ao buscar código JSON:', error);
throw error;
}
}
+30
View File
@@ -0,0 +1,30 @@
// Logger utilitário para produção
const isDevelopment = import.meta.env.DEV;
export const logger = {
info: (message: string, data?: any) => {
if (isDevelopment) {
console.log(`🔍 ${message}`, data || '');
}
},
error: (message: string, error?: any) => {
if (isDevelopment) {
console.error(`${message}`, error || '');
}
// Em produção, poderia enviar para serviço de logging
},
warn: (message: string, data?: any) => {
if (isDevelopment) {
console.warn(`⚠️ ${message}`, data || '');
}
},
success: (message: string, data?: any) => {
if (isDevelopment) {
console.log(`${message}`, data || '');
}
}
};
+32
View File
@@ -0,0 +1,32 @@
/**
* Função para ordenação numérica natural de strings
* Resolve o problema de ordenação alfanumérica onde "10" vem antes de "2"
*/
export const naturalSort = (a: string, b: string): number => {
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base'
});
return collator.compare(a, b);
};
/**
* Função para ordenar array de objetos por uma propriedade com ordenação natural
*/
export const sortByProperty = <T>(array: T[], property: keyof T): T[] => {
return [...array].sort((a, b) => naturalSort(String(a[property]), String(b[property])));
};
/**
* Função para ordenar array de objetos por múltiplas propriedades com ordenação natural
*/
export const sortByMultipleProperties = <T>(array: T[], properties: (keyof T)[]): T[] => {
return [...array].sort((a, b) => {
for (const property of properties) {
const comparison = naturalSort(String(a[property]), String(b[property]));
if (comparison !== 0) return comparison;
}
return 0;
});
};
+437
View File
@@ -0,0 +1,437 @@
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export const generateProfessionalPDF = async (elementId: string, filename: string) => {
try {
const element = document.getElementById(elementId);
if (!element) {
throw new Error('Elemento não encontrado para gerar PDF');
}
// Aguardar um pouco para garantir que o elemento esteja completamente renderizado
await new Promise(resolve => setTimeout(resolve, 200));
// Garantir que o elemento esteja visível e com dimensões corretas
const originalDisplay = element.style.display;
const originalVisibility = element.style.visibility;
const originalPosition = element.style.position;
element.style.display = 'block';
element.style.visibility = 'visible';
element.style.position = 'relative';
// Forçar um reflow
element.offsetHeight;
// Aguardar mais um pouco após forçar o reflow
await new Promise(resolve => setTimeout(resolve, 300));
console.log('Gerando PDF para elemento:', elementId);
console.log('Dimensões do elemento:', {
width: element.offsetWidth,
height: element.offsetHeight,
scrollWidth: element.scrollWidth,
scrollHeight: element.scrollHeight
});
// Se o elemento não tem dimensões, isso pode causar PDF em branco
if (element.offsetWidth === 0 || element.offsetHeight === 0) {
throw new Error('Elemento tem dimensões zero - não é possível gerar PDF');
}
// Configurações otimizadas para html2canvas
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: false,
backgroundColor: '#ffffff',
width: element.scrollWidth,
height: element.scrollHeight,
scrollX: 0,
scrollY: 0,
windowWidth: Math.max(element.scrollWidth, 1200),
windowHeight: Math.max(element.scrollHeight, 800),
foreignObjectRendering: false,
removeContainer: false,
imageTimeout: 10000,
logging: false
});
console.log('Canvas criado com sucesso:', {
width: canvas.width,
height: canvas.height
});
// Verificar se o canvas foi criado corretamente
if (canvas.width === 0 || canvas.height === 0) {
throw new Error('Canvas criado com dimensões zero');
}
// Restaurar estilos originais
element.style.display = originalDisplay;
element.style.visibility = originalVisibility;
element.style.position = originalPosition;
// Criar PDF com configurações otimizadas
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
compress: true
});
// Dimensões A4 em mm
const pageWidth = 210;
const pageHeight = 297;
// Margens adequadas
const marginTop = 15;
const marginBottom = 15;
const marginLeft = 15;
const marginRight = 15;
// Área útil para conteúdo
const contentWidth = pageWidth - marginLeft - marginRight;
const contentHeight = pageHeight - marginTop - marginBottom;
// Calcular proporções mantendo aspect ratio
const imgWidth = contentWidth;
const imgHeight = (canvas.height * contentWidth) / canvas.width;
// Converter canvas para imagem
const imgData = canvas.toDataURL('image/png', 1.0);
console.log('Adicionando imagem ao PDF:', {
imgWidth,
imgHeight,
contentHeight,
totalPages: Math.ceil(imgHeight / contentHeight)
});
// Verificar se os dados da imagem foram gerados
if (!imgData || imgData === 'data:,') {
throw new Error('Falha ao gerar dados da imagem do canvas');
}
// Sistema de paginação
let currentY = 0;
let pageNumber = 1;
const totalPages = Math.ceil(imgHeight / contentHeight);
// Função para adicionar rodapé com numeração
const addFooter = (pageNum: number, totalPages: number) => {
pdf.setFontSize(8);
pdf.setTextColor(100, 100, 100);
const footerText = `Página ${pageNum} de ${totalPages}`;
const textWidth = pdf.getTextWidth(footerText);
const footerX = (pageWidth - textWidth) / 2;
const footerY = pageHeight - 8;
pdf.text(footerText, footerX, footerY);
};
// Primeira página
if (imgHeight <= contentHeight) {
// Conteúdo cabe em uma página
pdf.addImage(imgData, 'PNG', marginLeft, marginTop, imgWidth, imgHeight);
addFooter(1, 1);
} else {
// Conteúdo precisa de múltiplas páginas
while (currentY < imgHeight) {
if (pageNumber > 1) {
pdf.addPage();
}
// Calcular a altura restante do conteúdo
const remainingHeight = imgHeight - currentY;
const currentPageHeight = Math.min(contentHeight, remainingHeight);
// Criar um canvas temporário para a seção atual
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
tempCanvas.width = canvas.width;
tempCanvas.height = (currentPageHeight * canvas.width) / imgWidth;
// Desenhar a seção atual do canvas original
tempCtx.drawImage(
canvas,
0,
(currentY * canvas.width) / imgWidth,
canvas.width,
tempCanvas.height,
0,
0,
canvas.width,
tempCanvas.height
);
// Converter para dados de imagem
const tempImgData = tempCanvas.toDataURL('image/png', 1.0);
// Adicionar a seção ao PDF
pdf.addImage(tempImgData, 'PNG', marginLeft, marginTop, imgWidth, currentPageHeight);
}
// Adicionar rodapé
addFooter(pageNumber, totalPages);
// Preparar para próxima página
currentY += contentHeight;
pageNumber++;
}
}
// Forçar o download do PDF
console.log('Iniciando download do PDF:', filename);
pdf.save(filename);
// Aguardar um momento para garantir que o download seja iniciado
await new Promise(resolve => setTimeout(resolve, 500));
console.log('PDF salvo com sucesso:', filename);
return true;
} catch (error) {
console.error('Erro detalhado ao gerar PDF:', error);
throw new Error(`Erro ao gerar PDF: ${error.message}`);
}
};
export const printProfessionalPDF = async (elementId: string) => {
try {
const element = document.getElementById(elementId);
if (!element) {
throw new Error('Elemento não encontrado');
}
// Criar nova janela para impressão
const printWindow = window.open('', '_blank');
if (!printWindow) {
throw new Error('Não foi possível abrir janela de impressão');
}
// Obter estilos do documento atual
const styleSheets = Array.from(document.styleSheets);
let allStyles = '';
try {
styleSheets.forEach(sheet => {
try {
if (sheet.cssRules) {
Array.from(sheet.cssRules).forEach(rule => {
allStyles += rule.cssText + '\n';
});
}
} catch (e) {
// Ignorar erros de CORS
}
});
} catch (e) {
console.warn('Não foi possível obter alguns estilos', e);
}
// HTML otimizado para impressão sem áreas vazias
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Relatório de Produção</title>
<meta charset="UTF-8">
<style>
${allStyles}
/* Reset e configuração básica */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@page {
size: A4;
margin: 15mm 20mm 15mm 20mm;
padding: 0;
}
@media print {
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
margin: 0 !important;
padding: 0 !important;
font-size: 10px !important;
line-height: 1.2 !important;
}
/* Remover elementos desnecessários */
.no-print {
display: none !important;
}
/* Otimizar espaçamento */
#${elementId} {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
transform: none !important;
}
/* Compactar seções */
.statistics-grid {
margin-bottom: 8px !important;
gap: 4px !important;
}
.process-section {
margin-bottom: 10px !important;
page-break-inside: avoid;
}
.priority-section {
margin-bottom: 10px !important;
page-break-inside: avoid;
}
/* Otimizar tabelas */
table {
width: 100% !important;
border-collapse: collapse !important;
margin: 0 !important;
page-break-inside: avoid;
}
th, td {
padding: 2px 4px !important;
font-size: 9px !important;
line-height: 1.1 !important;
}
/* Compactar cabeçalhos */
h1 {
font-size: 16px !important;
margin-bottom: 4px !important;
line-height: 1.1 !important;
}
h2 {
font-size: 14px !important;
margin-bottom: 6px !important;
margin-top: 8px !important;
line-height: 1.1 !important;
}
h3 {
font-size: 12px !important;
margin-bottom: 4px !important;
line-height: 1.1 !important;
}
/* Reduzir espaçamentos desnecessários */
header {
margin-bottom: 8px !important;
padding-bottom: 8px !important;
}
section {
margin-bottom: 8px !important;
}
/* Otimizar cards de estatísticas */
.statistics-grid > div {
padding: 4px !important;
margin: 0 !important;
}
/* Evitar quebras desnecessárias */
.avoid-break {
page-break-inside: avoid !important;
}
.process-header {
page-break-after: avoid !important;
}
.priority-header {
page-break-after: avoid !important;
}
/* Compactar processos menores */
.process-section h3 {
padding: 3px 6px !important;
margin: 0 !important;
}
.process-section .grid {
gap: 3px !important;
padding: 3px !important;
margin-bottom: 3px !important;
}
/* Melhorar aproveitamento do espaço */
.overflow-x-auto {
overflow: visible !important;
}
/* Remover espaços extras */
p {
margin: 0 !important;
padding: 0 !important;
}
/* Compactar divs com display grid */
div[style*="display: grid"] {
gap: 3px !important;
}
/* Reduzir espaçamentos entre elementos */
div[style*="margin-bottom"] {
margin-bottom: 6px !important;
}
/* Otimizar áreas de processo */
.process-section > div:last-child {
margin-bottom: 0 !important;
}
/* Compactar mais os elementos */
div[style*="padding: 10px"] {
padding: 4px !important;
}
div[style*="padding: 8px"] {
padding: 3px !important;
}
div[style*="padding: 6px"] {
padding: 2px !important;
}
}
</style>
</head>
<body>
${element.outerHTML}
<script>
window.onload = function() {
// Aguardar renderização e depois imprimir
setTimeout(() => {
window.print();
window.close();
}, 800);
}
</script>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
return true;
} catch (error) {
console.error('Erro ao imprimir PDF:', error);
throw error;
}
};
+90
View File
@@ -0,0 +1,90 @@
// Dados dos perfis W/HP extraídos da imagem fornecida
export const perfilsData = [
{ descricao: "W 130 x 13,0", peso: 13.0 },
{ descricao: "W 150 x 18,0", peso: 18.0 },
{ descricao: "W 150 x 22,5 (H)", peso: 22.5 },
{ descricao: "W 150 x 24,0", peso: 24.0 },
{ descricao: "W 150 x 29,8 (H)", peso: 29.8 },
{ descricao: "W 150 x 37,1 (H)", peso: 37.1 },
{ descricao: "W 200 x 15,0", peso: 15.0 },
{ descricao: "W 200 x 19,3", peso: 19.3 },
{ descricao: "W 200 x 22,5", peso: 22.5 },
{ descricao: "W 200 x 26,6", peso: 26.6 },
{ descricao: "W 200 x 31,3", peso: 31.3 },
{ descricao: "W 200 x 35,9 (H)", peso: 35.9 },
{ descricao: "W 200 x 41,7 (H)", peso: 41.7 },
{ descricao: "W 200 x 46,1 (H)", peso: 46.1 },
{ descricao: "W 200 x 52,0 (H)", peso: 52.0 },
{ descricao: "HP 200 x 53,0 (H)", peso: 53.0 },
{ descricao: "W 200 x 59,0 (H)", peso: 59.0 },
{ descricao: "W 200 x 71,0 (H)", peso: 71.0 },
{ descricao: "W 200 x 86,0 (H)", peso: 86.0 },
{ descricao: "W 250 x 17,9", peso: 17.9 },
{ descricao: "W 250 x 22,3", peso: 22.3 },
{ descricao: "W 250 x 25,3", peso: 25.3 },
{ descricao: "W 250 x 28,4", peso: 28.4 },
{ descricao: "W 250 x 32,7", peso: 32.7 },
{ descricao: "W 250 x 38,5", peso: 38.5 },
{ descricao: "W 250 x 44,8", peso: 44.8 },
{ descricao: "W 250 x 60,2 (H)", peso: 60.2 },
{ descricao: "W 250 x 73,0 (H)", peso: 73.0 },
{ descricao: "W 250 x 80,0 (H)", peso: 80.0 },
{ descricao: "HP 250 x 85,0 (H)", peso: 85.0 },
{ descricao: "W 250 x 89,0 (H)", peso: 89.0 },
{ descricao: "W 250 x 101,0 (H)", peso: 101.0 },
{ descricao: "W 250 x 115,0 (H)", peso: 115.0 },
{ descricao: "W 310 x 21,0", peso: 21.0 },
{ descricao: "W 310 x 23,8", peso: 23.8 },
{ descricao: "W 310 x 28,3", peso: 28.3 },
{ descricao: "W 310 x 32,7", peso: 32.7 },
{ descricao: "W 310 x 38,7", peso: 38.7 },
{ descricao: "W 310 x 44,5", peso: 44.5 },
{ descricao: "W 310 x 52,0", peso: 52.0 },
{ descricao: "HP 310 x 79,0 (H)", peso: 79.0 },
{ descricao: "HP 310 x 93,0 (H)", peso: 93.0 },
{ descricao: "W 310 x 97,0 (H)", peso: 97.0 },
{ descricao: "W 310 x 107,0 (H)", peso: 107.0 },
{ descricao: "HP 310 x 110,0 (H)", peso: 110.0 },
{ descricao: "W 360 x 32,9", peso: 32.9 },
{ descricao: "HP 310 x 125,0 (H)", peso: 125.0 },
{ descricao: "W 360 x 39,0", peso: 39.0 },
{ descricao: "W 360 x 44,0", peso: 44.0 },
{ descricao: "W 360 x 51,0", peso: 51.0 },
{ descricao: "W 360 x 57,8", peso: 57.8 },
{ descricao: "W 360 x 64,0", peso: 64.0 },
{ descricao: "W 360 x 72,0", peso: 72.0 },
{ descricao: "W 360 x 79,0", peso: 79.0 },
{ descricao: "W 360 x 91,0 (H)", peso: 91.0 },
{ descricao: "W 360 x 101,0 (H)", peso: 101.0 },
{ descricao: "W 360 x 110,0 (H)", peso: 110.0 },
{ descricao: "W 360 x 122,0 (H)", peso: 122.0 },
{ descricao: "W 410 x 38,8", peso: 38.8 },
{ descricao: "W 410 x 46,1", peso: 46.1 },
{ descricao: "W 410 x 53,0", peso: 53.0 },
{ descricao: "W 410 x 60,0", peso: 60.0 },
{ descricao: "W 410 x 67,0", peso: 67.0 },
{ descricao: "W 410 x 75,0", peso: 75.0 },
{ descricao: "W 410 x 85,0", peso: 85.0 },
{ descricao: "W 460 x 52,0", peso: 52.0 },
{ descricao: "W 460 x 60,0", peso: 60.0 },
{ descricao: "W 460 x 68,0", peso: 68.0 },
{ descricao: "W 460 x 74,0", peso: 74.0 },
{ descricao: "W 460 x 82,0", peso: 82.0 },
{ descricao: "W 460 x 89,0", peso: 89.0 },
{ descricao: "W 460 x 97,0", peso: 97.0 },
{ descricao: "W 460 x 106,0", peso: 106.0 },
{ descricao: "W 530 x 66,0", peso: 66.0 },
{ descricao: "W 530 x 72,0", peso: 72.0 },
{ descricao: "W 530 x 74,0", peso: 74.0 },
{ descricao: "W 530 x 82,0", peso: 82.0 },
{ descricao: "W 530 x 85,0", peso: 85.0 },
{ descricao: "W 530 x 92,0", peso: 92.0 },
{ descricao: "W 530 x 109,0", peso: 109.0 },
{ descricao: "W 610 x 101,0", peso: 101.0 },
{ descricao: "W 610 x 113,0", peso: 113.0 },
{ descricao: "W 610 x 125,0", peso: 125.0 },
{ descricao: "W 610 x 140,0", peso: 140.0 },
{ descricao: "W 610 x 155,0", peso: 155.0 },
{ descricao: "W 610 x 174,0", peso: 174.0 }
];
+75
View File
@@ -0,0 +1,75 @@
/**
* Utilitário para filtros de range usando o símbolo @
* Exemplo: "32@47" filtra peças de 32 a 47
*/
/**
* Verifica se um valor de filtro contém um range (simbolo @)
*/
export const isRangeFilter = (filterValue: string): boolean => {
return filterValue.includes('@');
};
/**
* Extrai os valores inicial e final de um filtro de range
*/
export const parseRangeFilter = (filterValue: string): { start: number; end: number } | null => {
if (!isRangeFilter(filterValue)) return null;
const parts = filterValue.split('@');
if (parts.length !== 2) return null;
const start = parseInt(parts[0].trim());
const end = parseInt(parts[1].trim());
if (isNaN(start) || isNaN(end)) return null;
return { start: Math.min(start, end), end: Math.max(start, end) };
};
/**
* Extrai o número da marca de uma peça
* Assume que a marca pode ter formato como "123", "P123", "PECA-123", etc.
*/
export const extractNumberFromMarca = (marca: string): number | null => {
// Remove espaços e converte para maiúsculo
const cleanMarca = marca.trim().toUpperCase();
// Tenta extrair números da marca usando regex
const numberMatch = cleanMarca.match(/(\d+)/);
if (!numberMatch) return null;
return parseInt(numberMatch[1]);
};
/**
* Verifica se uma marca está dentro do range especificado
*/
export const isInRange = (marca: string, range: { start: number; end: number }): boolean => {
const marcaNumber = extractNumberFromMarca(marca);
if (marcaNumber === null) return false;
return marcaNumber >= range.start && marcaNumber <= range.end;
};
/**
* Aplica filtro de marca com suporte a range
* Retorna true se o item deve ser incluído no resultado
*/
export const applyMarcaFilter = (marca: string, filterValue: string): boolean => {
if (!filterValue.trim()) return true;
// Se for um filtro de range
if (isRangeFilter(filterValue)) {
const range = parseRangeFilter(filterValue);
if (!range) return false;
return isInRange(marca, range);
}
// Filtro normal por substring (case-insensitive)
return marca.toLowerCase().includes(filterValue.toLowerCase());
};
+92
View File
@@ -0,0 +1,92 @@
import jsPDF from 'jspdf';
import 'jspdf-autotable';
import { SolicitacaoCompra } from '@/hooks/useSolicitacoesCompra';
declare module 'jspdf' {
interface jsPDF {
autoTable: (options: any) => jsPDF;
}
}
export const generateSolicitacaoComprasPDF = (solicitacao: SolicitacaoCompra) => {
const doc = new jsPDF();
// Cabeçalho
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('SOLICITAÇÃO DE COMPRA', 105, 20, { align: 'center' });
// Informações básicas
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
const startY = 40;
doc.text(`Número SC: ${solicitacao.numero_sc}`, 20, startY);
doc.text(`Data: ${new Date(solicitacao.data_solicitacao).toLocaleDateString('pt-BR')}`, 120, startY);
doc.text(`Status: ${solicitacao.status}`, 20, startY + 10);
doc.text(`Revisão: ${solicitacao.revisao}`, 120, startY + 10);
if (solicitacao.of_number) {
doc.text(`OF: ${solicitacao.of_number}`, 20, startY + 20);
}
if (solicitacao.objetivo) {
doc.text(`Objetivo: ${solicitacao.objetivo}`, 20, startY + 30);
}
if (solicitacao.justificativa) {
doc.text('Justificativa:', 20, startY + 40);
const splitJustificativa = doc.splitTextToSize(solicitacao.justificativa, 170);
doc.text(splitJustificativa, 20, startY + 50);
}
// Tabela de itens
if (solicitacao.itens && solicitacao.itens.length > 0) {
const tableStartY = startY + 80;
const tableColumns = [
{ header: 'Código', dataKey: 'codigo' },
{ header: 'Descrição', dataKey: 'descricao' },
{ header: 'Unidade', dataKey: 'unidade' },
{ header: 'Quantidade', dataKey: 'quantidade' },
{ header: 'Prazo', dataKey: 'prazo' }
];
const tableRows = solicitacao.itens.map(item => ({
codigo: item.material?.id?.substring(0, 8) || 'N/A',
descricao: item.material?.descricao || 'Material não encontrado',
unidade: item.material?.unidade || 'UN',
quantidade: item.quantidade.toString(),
prazo: new Date(item.prazo_recebimento).toLocaleDateString('pt-BR')
}));
doc.autoTable({
head: [tableColumns.map(col => col.header)],
body: tableRows.map(row => tableColumns.map(col => row[col.dataKey as keyof typeof row])),
startY: tableStartY,
styles: {
fontSize: 10,
cellPadding: 3
},
headStyles: {
fillColor: [66, 139, 202],
textColor: 255,
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: [245, 245, 245]
}
});
}
// Rodapé
const pageHeight = doc.internal.pageSize.height;
doc.setFontSize(8);
doc.text(`Gerado em: ${new Date().toLocaleString('pt-BR')}`, 20, pageHeight - 20);
doc.text('TrackSteel - Sistema de Gestão Industrial', 105, pageHeight - 10, { align: 'center' });
// Download do arquivo
doc.save(`SC-${solicitacao.numero_sc}.pdf`);
};
+115
View File
@@ -0,0 +1,115 @@
import { supabase } from '@/integrations/supabase/client';
export const syncInterfaceResources = async (menuItems: any[], adminMenuItems: any[]) => {
try {
// Definir todos os recursos disponíveis no sistema (lista única e limpa)
const allResources = [
// Menu Principal
{ resource_key: 'dashboard', resource_name: 'Dashboard', icon_name: 'BarChart3', route_path: '/dashboard', is_submenu: false, order_index: 1 },
// Cadastro - Menu pai
{ resource_key: 'cadastro', resource_name: 'Cadastro', icon_name: 'FileText', route_path: null, is_submenu: false, order_index: 2 },
{ resource_key: 'cadastro-of', resource_name: 'Ficha Técnica da OF', icon_name: null, route_path: '/cadastro-of', is_submenu: true, parent_key: 'cadastro', order_index: 1 },
{ resource_key: 'cadastro-pecas', resource_name: 'Cadastro de Peças', icon_name: null, route_path: '/seletor-of', is_submenu: true, parent_key: 'cadastro', order_index: 2 },
{ resource_key: 'equipamentos', resource_name: 'Equipamentos', icon_name: null, route_path: '/equipamentos', is_submenu: true, parent_key: 'cadastro', order_index: 3 },
// Ferramentas - Menu pai
{ resource_key: 'ferramentas', resource_name: 'Ferramentas', icon_name: 'Wrench', route_path: null, is_submenu: false, order_index: 3 },
{ resource_key: 'ferramentas-conversores', resource_name: 'Conversores de dados', icon_name: null, route_path: '/ferramentas/conversores', is_submenu: true, parent_key: 'ferramentas', order_index: 1 },
{ resource_key: 'ferramentas-inconsistencias', resource_name: 'Ver Inconsistências', icon_name: null, route_path: '/ferramentas/inconsistencias', is_submenu: true, parent_key: 'ferramentas', order_index: 2 },
// Estoque
{ resource_key: 'estoque', resource_name: 'Estoque', icon_name: 'Warehouse', route_path: '/estoque', is_submenu: false, order_index: 4 },
{ resource_key: 'estoque-solicitacao-compras', resource_name: 'Solicitação de Compras', icon_name: null, route_path: '/estoque/solicitacao-compras', is_submenu: true, parent_key: 'estoque', order_index: 1 },
// OFs - Menu pai
{ resource_key: 'ofs', resource_name: 'OFs', icon_name: 'FolderOpen', route_path: null, is_submenu: false, order_index: 5 },
{ resource_key: 'ofs-lista', resource_name: 'Ordens de Fabricação', icon_name: null, route_path: '/ofs', is_submenu: true, parent_key: 'ofs', order_index: 1 },
{ resource_key: 'ofs-cronograma', resource_name: 'Cronograma', icon_name: null, route_path: '/ofs/cronograma', is_submenu: true, parent_key: 'ofs', order_index: 2 },
{ resource_key: 'ofs-concluidas', resource_name: 'OFs Concluídas', icon_name: null, route_path: '/cadastro/ofs-concluidas', is_submenu: true, parent_key: 'ofs', order_index: 3 },
// Produção - Menu pai
{ resource_key: 'producao', resource_name: 'Produção', icon_name: 'Building2', route_path: null, is_submenu: false, order_index: 6 },
{ resource_key: 'producao-visao', resource_name: 'Visão Geral', icon_name: null, route_path: '/producao', is_submenu: true, parent_key: 'producao', order_index: 1 },
{ resource_key: 'diario-producao', resource_name: 'Diário de Produção', icon_name: null, route_path: '/diario-producao', is_submenu: true, parent_key: 'producao', order_index: 2 },
{ resource_key: 'producao-apontamento', resource_name: 'Apontamento de Produção', icon_name: null, route_path: '/apontamento-producao', is_submenu: true, parent_key: 'producao', order_index: 3 },
{ resource_key: 'producao-dashboard', resource_name: 'Dashboard de Produção', icon_name: null, route_path: '/dashboard-producao', is_submenu: true, parent_key: 'producao', order_index: 4 },
{ resource_key: 'prioridades-fabricacao', resource_name: 'Prioridades de Fabricação', icon_name: null, route_path: '/prioridades-fabricacao', is_submenu: true, parent_key: 'producao', order_index: 5 },
// Painel Industrial
{ resource_key: 'painel-industrial', resource_name: 'Painel Industrial', icon_name: 'Monitor', route_path: '/painel-industrial', is_submenu: false, order_index: 7 },
// Expedição
{ resource_key: 'expedicao', resource_name: 'Expedição', icon_name: 'Truck', route_path: '/expedicao', is_submenu: false, order_index: 8 },
// Obra - Menu pai
{ resource_key: 'obra', resource_name: 'Obra', icon_name: 'HardHat', route_path: null, is_submenu: false, order_index: 9 },
{ resource_key: 'obra-dashboard', resource_name: 'Dashboard de Obras', icon_name: null, route_path: '/obra', is_submenu: true, parent_key: 'obra', order_index: 1 },
{ resource_key: 'obra-configuracoes', resource_name: 'Configurações da Obra', icon_name: null, route_path: '/obra/configuracoes', is_submenu: true, parent_key: 'obra', order_index: 2 },
// Tarefas - Menu pai
{ resource_key: 'tarefas', resource_name: 'Tarefas', icon_name: 'CheckSquare', route_path: null, is_submenu: false, order_index: 10 },
{ resource_key: 'tarefas-lista', resource_name: 'Lista de Tarefas', icon_name: null, route_path: '/tarefas', is_submenu: true, parent_key: 'tarefas', order_index: 1 },
{ resource_key: 'tarefas-historico', resource_name: 'Histórico de Tarefas', icon_name: null, route_path: '/tarefas/historico', is_submenu: true, parent_key: 'tarefas', order_index: 2 },
// Biblioteca - Menu pai
{ resource_key: 'biblioteca', resource_name: 'Biblioteca', icon_name: 'Book', route_path: null, is_submenu: false, order_index: 11 },
{ resource_key: 'biblioteca-catalogos', resource_name: 'Catálogos', icon_name: null, route_path: '/biblioteca/catalogos', is_submenu: true, parent_key: 'biblioteca', order_index: 1 },
{ resource_key: 'biblioteca-normas', resource_name: 'Normas', icon_name: null, route_path: '/biblioteca/normas', is_submenu: true, parent_key: 'biblioteca', order_index: 2 },
{ resource_key: 'biblioteca-referencias', resource_name: 'Referências', icon_name: null, route_path: '/biblioteca/referencias', is_submenu: true, parent_key: 'biblioteca', order_index: 3 },
// Sistema
{ resource_key: 'sistema', resource_name: 'Sistema', icon_name: 'Clipboard', route_path: '/sistema', is_submenu: false, order_index: 12 },
// Sugestões
{ resource_key: 'sugestoes', resource_name: 'Sugestões', icon_name: 'MessageSquare', route_path: '/sugestoes', is_submenu: false, order_index: 13 },
// Atribuições
{ resource_key: 'atribuicoes', resource_name: 'Atribuições', icon_name: 'Users', route_path: '/atribuicoes', is_submenu: false, order_index: 14 },
// Mapa Interativo
{ resource_key: 'mapa-interativo', resource_name: 'Mapa Interativo', icon_name: 'Network', route_path: '/mapa-interativo', is_submenu: false, order_index: 15 },
// Configurações - Menu pai
{ resource_key: 'configuracoes', resource_name: 'Configurações', icon_name: 'Settings', route_path: null, is_submenu: false, order_index: 16 },
{ resource_key: 'configuracoes-gerais', resource_name: 'Configurações Gerais', icon_name: null, route_path: '/configuracoes', is_submenu: true, parent_key: 'configuracoes', order_index: 1 },
{ resource_key: 'theme-customization', resource_name: 'Personalização de Tema', icon_name: null, route_path: '/admin/theme-customization', is_submenu: true, parent_key: 'configuracoes', order_index: 2 },
// Admin
{ resource_key: 'admin', resource_name: 'Admin', icon_name: 'Shield', route_path: '/admin', is_submenu: false, order_index: 17 },
{ resource_key: 'user-management', resource_name: 'Gerenciar Usuários', icon_name: 'UserCog', route_path: '/user-management', is_submenu: false, order_index: 18 }
];
console.log('Iniciando limpeza completa de recursos duplicados...');
// ETAPA 1: Limpar completamente a tabela e reconstruir
const { error: deleteError } = await supabase
.from('interface_resources')
.delete()
.neq('id', '00000000-0000-0000-0000-000000000000'); // Delete all records
if (deleteError) {
console.error('Erro ao limpar tabela:', deleteError);
} else {
console.log('Tabela limpa com sucesso');
}
// ETAPA 2: Inserir todos os recursos limpos
const { error: insertError } = await supabase
.from('interface_resources')
.insert(allResources);
if (insertError) {
console.error('Erro ao inserir recursos:', insertError);
} else {
console.log(`${allResources.length} recursos inseridos com sucesso`);
console.log('✅ Recurso "equipamentos" sincronizado:', allResources.find(r => r.resource_key === 'equipamentos'));
}
console.log('Sincronização concluída - duplicatas removidas');
} catch (error) {
console.error('Erro na sincronização de recursos:', error);
}
};
+60
View File
@@ -0,0 +1,60 @@
export const getTaskStatusColor = (status: string) => {
const statusColors = {
'a_fazer': 'bg-yellow-100 text-yellow-800',
'em_andamento': 'bg-blue-100 text-blue-800',
'revisao': 'bg-purple-100 text-purple-800',
'pendente': 'bg-orange-100 text-orange-800',
'bloqueado': 'bg-red-100 text-red-800',
'concluido': 'bg-green-100 text-green-800',
'comprado': 'bg-blue-100 text-blue-800'
};
return statusColors[status as keyof typeof statusColors] || 'bg-gray-100 text-gray-800';
};
export const getTaskStatusBadgeColor = (status: string) => {
const statusColors = {
'a_fazer': 'bg-yellow-500',
'em_andamento': 'bg-blue-500',
'revisao': 'bg-purple-500',
'pendente': 'bg-orange-500',
'bloqueado': 'bg-red-500',
'concluido': 'bg-green-500',
'comprado': 'bg-blue-600'
};
return statusColors[status as keyof typeof statusColors] || 'bg-gray-500';
};
// Alias para compatibilidade
export const getStatusBadgeColor = getTaskStatusBadgeColor;
// Função para cores de borda das tarefas
export const getTaskBorderColor = (task: any): string => {
if (task.is_completed || task.status === 'concluido') {
return 'border-l-green-500';
}
if (task.status === 'comprado') {
return 'border-l-blue-600';
}
if (task.status === 'bloqueado') {
return 'border-l-red-500';
}
if (task.status === 'em_andamento') {
return 'border-l-blue-500';
}
if (task.status === 'revisao') {
return 'border-l-purple-500';
}
if (task.status === 'pendente') {
return 'border-l-orange-500';
}
return 'border-l-yellow-500';
};
+134
View File
@@ -0,0 +1,134 @@
import { createClient } from '@supabase/supabase-js';
// Configuração do Supabase
const supabaseUrl = 'https://lwjppiicofojfcdfjsto.supabase.co';
const supabaseServiceKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx3anBwaWljb2ZvamZjZGZqc3RvIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1MDQ2MzA1MywiZXhwIjoyMDY2MDM5MDUzfQ.t9vlXHQH4ou2S-CKSeDYSnAeMDYpmkklqlwyGDvpocI';
const supabase = createClient(supabaseUrl, supabaseServiceKey);
interface ApontamentoRecord {
id: string;
of_number: string;
data_apontamento: string;
quantidade_produzida: number;
processo_nome: string;
processo_ordem: number;
created_at: string;
}
interface UpdateResult {
success: boolean;
message: string;
affectedRecords?: number;
records?: ApontamentoRecord[];
error?: string;
}
/**
* Script para atualizar registros na tabela apontamentos_producao
* Altera a data dos apontamentos da OF B101 na fase 9 de 08/09/2025 para 10/09/2025
*/
export class ApontamentosUpdateScript {
private readonly OF_TARGET = 'B101';
private readonly FASE_TARGET = 9;
private readonly DATA_ORIGEM = '2025-09-08';
private readonly DATA_DESTINO = '2025-09-10';
/**
* Busca os registros que serão afetados pela atualização
*/
async previewRecords(): Promise<UpdateResult> {
try {
console.log('🔍 Buscando registros para preview...');
const { data, error } = await supabase
.from('apontamentos_producao')
.select(`
id,
of_number,
data_apontamento,
quantidade_produzida,
created_at,
processos_fabricacao!inner(
nome,
ordem
)
`)
.eq('of_number', this.OF_TARGET)
.eq('data_apontamento', this.DATA_ORIGEM)
.eq('processos_fabricacao.ordem', this.FASE_TARGET);
if (error) {
console.error('❌ Erro ao buscar registros:', error);
return {
success: false,
message: 'Erro ao buscar registros para preview',
error: error.message
};
}
const records: ApontamentoRecord[] = data?.map((record: any) => ({
id: record.id,
of_number: record.of_number,
data_apontamento: record.data_apontamento,
quantidade_produzida: record.quantidade_produzida,
processo_nome: record.processos_fabricacao?.nome || 'N/A',
processo_ordem: record.processos_fabricacao?.ordem || 0,
created_at: record.created_at
})) || [];
return {
success: true,
message: `Encontrados ${records.length} registros para atualização`,
records
};
} catch (error: any) {
console.error('❌ Erro inesperado no preview:', error);
return {
success: false,
message: 'Erro inesperado ao buscar registros',
error: error.message
};
}
}
/**
* Executa a atualização dos registros
*/
async executeUpdate(): Promise<UpdateResult> {
try {
console.log('🚀 Executando atualização...');
const { data, error } = await supabase
.from('apontamentos_producao')
.update({ data_apontamento: this.DATA_DESTINO })
.eq('of_number', this.OF_TARGET)
.eq('data_apontamento', this.DATA_ORIGEM)
.select();
if (error) {
console.error('❌ Erro na atualização:', error);
return {
success: false,
message: 'Erro ao executar atualização',
error: error.message
};
}
console.log(`✅ Atualização concluída! ${data?.length || 0} registros afetados.`);
return {
success: true,
message: `Atualização concluída com sucesso! ${data?.length || 0} registros atualizados.`,
affectedRecords: data?.length || 0
};
} catch (error: any) {
console.error('❌ Erro inesperado na atualização:', error);
return {
success: false,
message: 'Erro inesperado na atualização',
error: error.message
};
}
}
}