🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 || '');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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 }
|
||||
];
|
||||
@@ -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());
|
||||
};
|
||||
@@ -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`);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user