From 0fc5db37463ee2cfc1979276126d6d52adefd377 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Wed, 16 Sep 2026 14:01:36 +0000 Subject: [PATCH] feat: adicionar configuracoes de nomenclaturas de fabricas com preset Baldon Engemetal e labels dinamicos --- .../configuracoes/NomenclaturasConfig.tsx | 381 ++++++++++++++++++ .../conversores/AdvanceSteelConverter.tsx | 16 +- src/contexts/AppLabelsContext.tsx | 206 ++++++++++ src/hooks/useAppLabels.ts | 2 + src/main.tsx | 15 +- src/pages/Configuracoes.tsx | 4 + 6 files changed, 611 insertions(+), 13 deletions(-) create mode 100644 src/components/configuracoes/NomenclaturasConfig.tsx create mode 100644 src/contexts/AppLabelsContext.tsx create mode 100644 src/hooks/useAppLabels.ts diff --git a/src/components/configuracoes/NomenclaturasConfig.tsx b/src/components/configuracoes/NomenclaturasConfig.tsx new file mode 100644 index 0000000..011e89d --- /dev/null +++ b/src/components/configuracoes/NomenclaturasConfig.tsx @@ -0,0 +1,381 @@ +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { toast } from 'sonner'; +import { + Building2, + Settings2, + Layers, + Hash, + Sparkles, + RotateCcw, + Save, + CheckCircle2, + FileSpreadsheet, + Tag +} from 'lucide-react'; +import { useAppLabels } from '@/hooks/useAppLabels'; +import { AppLabelsConfig, PRESETS_EMPRESAS } from '@/contexts/AppLabelsContext'; + +export const NomenclaturasConfig: React.FC = () => { + const { labels, updateLabels, resetLabels, formatTag, isComponente } = useAppLabels(); + const [formData, setFormData] = useState({ ...labels }); + const [selectedPreset, setSelectedPreset] = useState('baldon_engemetal'); + + const handlePresetSelect = (presetId: string) => { + setSelectedPreset(presetId); + const preset = PRESETS_EMPRESAS.find(p => p.id === presetId); + if (preset) { + setFormData({ ...preset.config }); + toast.info(`Preset carregado: ${preset.nome}`); + } + }; + + const handleSave = () => { + updateLabels(formData); + toast.success('Configurações de identificação e nomenclaturas salvas com sucesso!'); + }; + + const handleReset = () => { + resetLabels(); + const defaultPreset = PRESETS_EMPRESAS[0]; + setFormData({ ...defaultPreset.config }); + setSelectedPreset('baldon_engemetal'); + toast.info('Restaurado para o padrão de fábrica da Baldon Engemetal.'); + }; + + // Live preview examples + const sampleOF = 'B134'; + const sampleFase = '13'; + const samplePeca = '1'; + const sampleComp = '1000'; + + const previewPecaTag = formData.marcaFormato === 'peca_direto' + ? samplePeca + : formData.marcaFormato === 'fase_peca' + ? `${sampleFase}-${samplePeca}` + : `${sampleOF}-${sampleFase}-${samplePeca}`; + + const previewCompTag = formData.marcaFormato === 'peca_direto' + ? sampleComp + : formData.marcaFormato === 'fase_peca' + ? `${sampleFase}-${sampleComp}` + : `${sampleOF}-${sampleFase}-${sampleComp}`; + + return ( + + +
+
+
+ +
+
+ + Nomenclaturas & Identificação da Fábrica + + Universal + + + + Personalize os termos de OF, Fases, Peças e regras de numeração de componentes para qualquer empresa. + +
+
+ +
+ + +
+
+
+ + + {/* Seletor de Perfis / Modelos de Empresas */} +
+
+
+ + +
+ + Modelos pré-configurados prontos para uso + +
+ +
+ {PRESETS_EMPRESAS.map(preset => { + const isSelected = selectedPreset === preset.id || formData.empresaNome === preset.config.empresaNome; + return ( +
handlePresetSelect(preset.id)} + className={`cursor-pointer p-3 rounded-lg border transition-all text-left relative ${ + isSelected + ? 'bg-white border-blue-500 shadow-sm ring-2 ring-blue-500/20 dark:bg-slate-800 dark:border-blue-400' + : 'bg-white/60 border-slate-200 hover:bg-white hover:border-slate-300 dark:bg-slate-800/40 dark:border-slate-700 dark:hover:bg-slate-800' + }`} + > +
+ + {preset.nome} + + {preset.isPadrao && ( + + Ativa + + )} +
+

+ {preset.descricao} +

+
+ ); + })} +
+
+ + {/* Formulário de Campos e Nomenclaturas */} +
+ {/* Nome da Empresa */} +
+ + setFormData(prev => ({ ...prev, empresaNome: e.target.value }))} + placeholder="Ex: Baldon Engemetal" + className="bg-white border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-700 dark:text-white" + /> +
+ + {/* Formato da Tag da Peça */} +
+ + +
+ + {/* Ordem de Fabricação (OF) */} +
+
+ + + Identificador da Ordem + +
+
+
+ + setFormData(prev => ({ ...prev, ofLabel: e.target.value }))} + placeholder="OF, OC, Contrato..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+ + setFormData(prev => ({ ...prev, ofPlural: e.target.value }))} + placeholder="OFs, OCs, Contratos..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+
+ + {/* Fases / Etapas */} +
+
+
+ + + Divisão de Fases + +
+
+ Usa Fases + setFormData(prev => ({ ...prev, temFases: checked }))} + /> +
+
+
+
+ + setFormData(prev => ({ ...prev, faseLabel: e.target.value }))} + placeholder="Fase, Etapa, Lote..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+ + setFormData(prev => ({ ...prev, fasePlural: e.target.value }))} + placeholder="Fases, Etapas, Lotes..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+
+ + {/* Peças / Marcas */} +
+
+ + + Peça Principal / Conjunto + +
+
+
+ + setFormData(prev => ({ ...prev, pecaLabel: e.target.value }))} + placeholder="Peça, Marca, Conjunto..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+ + setFormData(prev => ({ ...prev, pecaPlural: e.target.value }))} + placeholder="Peças, Marcas, Conjuntos..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+
+ + {/* Componentes / Acessórios */} +
+
+ + + Subitens / Componentes + +
+
+
+ + setFormData(prev => ({ ...prev, componenteLabel: e.target.value }))} + placeholder="Componente, Acessório..." + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+ + setFormData(prev => ({ ...prev, faixaComponenteMin: parseInt(e.target.value, 10) || 1000 }))} + placeholder="1000" + className="h-8 text-sm bg-white dark:bg-slate-800" + /> +
+
+
+
+ + {/* Pré-visualização Dinâmica */} +
+
+ + + Simulação de Visualização no Sistema ({formData.empresaNome || 'Sua Empresa'}) + +
+ +
+
+ + {formData.pecaLabel} Principal (Exemplo): + +
+ + {previewPecaTag} + + + {formData.pecaLabel} + +
+
+ +
+ + {formData.componenteLabel} (Exemplo): + +
+ + {previewCompTag} + + + {formData.componenteLabel} (≥ {formData.faixaComponenteMin}) + +
+
+ +
+ + Tabela e Filtros: + + + {formData.ofLabel} / {formData.faseLabel} / {formData.pecaLabel} / {formData.componenteLabel} + +
+
+
+
+
+ ); +}; diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 63bad3f..477d9db 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -23,6 +23,7 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import * as XLSX from 'xlsx'; +import { useAppLabels } from '@/hooks/useAppLabels'; export interface ExtractedPiece { of: string; @@ -125,6 +126,7 @@ const ensurePdfJs = async (): Promise => { }; const AdvanceSteelConverterContent: React.FC = () => { + const { labels, isComponente } = useAppLabels(); const [selectedFile, setSelectedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isProcessing, setIsProcessing] = useState(false); @@ -809,7 +811,7 @@ const AdvanceSteelConverterContent: React.FC = () => { {extractedData.length > 0 && (
- OF (Obra) + {labels.ofLabel} (Obra) handleOfChange(e.target.value)} @@ -818,7 +820,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
- Fase + {labels.faseLabel} handleFaseChange(e.target.value)} @@ -827,7 +829,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
- Total de Peças + Total de {labels.pecaPlural}
{extractedData.length} itens
@@ -913,11 +915,11 @@ const AdvanceSteelConverterContent: React.FC = () => { - - - + + + - + diff --git a/src/contexts/AppLabelsContext.tsx b/src/contexts/AppLabelsContext.tsx new file mode 100644 index 0000000..b73bd82 --- /dev/null +++ b/src/contexts/AppLabelsContext.tsx @@ -0,0 +1,206 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +export interface AppLabelsConfig { + empresaNome: string; + ofLabel: string; + ofPlural: string; + faseLabel: string; + fasePlural: string; + temFases: boolean; + pecaLabel: string; + pecaPlural: string; + componenteLabel: string; + componentePlural: string; + marcaFormato: 'of_fase_peca' | 'fase_peca' | 'peca_direto' | 'personalizado'; + faixaComponenteMin: number; + formatoPrefixoComponente?: string; +} + +export interface EmpresaPreset { + id: string; + nome: string; + descricao: string; + isPadrao?: boolean; + config: AppLabelsConfig; +} + +export const PRESETS_EMPRESAS: EmpresaPreset[] = [ + { + id: 'baldon_engemetal', + nome: 'Baldon Engemetal', + descricao: 'Padrão industrial da Baldon: OF + Fase + Peça (ex: B134-13-1) e Componentes 1000-9999', + isPadrao: true, + config: { + empresaNome: 'Baldon Engemetal', + ofLabel: 'OF', + ofPlural: 'OFs', + faseLabel: 'Fase', + fasePlural: 'Fases', + temFases: true, + pecaLabel: 'Peça', + pecaPlural: 'Peças', + componenteLabel: 'Componente', + componentePlural: 'Componentes', + marcaFormato: 'of_fase_peca', + faixaComponenteMin: 1000 + } + }, + { + id: 'padrao_oc_etapa', + nome: 'Padrão OC & Etapas', + descricao: 'Modelo com Ordem de Compra/Corte (OC) e Etapas de Fabricação', + config: { + empresaNome: 'Indústria Metálica (Modelo OC)', + ofLabel: 'OC', + ofPlural: 'OCs', + faseLabel: 'Etapa', + fasePlural: 'Etapas', + temFases: true, + pecaLabel: 'Marca', + pecaPlural: 'Marcas', + componenteLabel: 'Acessório', + componentePlural: 'Acessórios', + marcaFormato: 'of_fase_peca', + faixaComponenteMin: 1000 + } + }, + { + id: 'padrao_contrato_lote', + nome: 'Padrão Contrato & Lotes', + descricao: 'Modelo por Número de Contrato e Lote de Montagem', + config: { + empresaNome: 'Fabricante de Galpões (Contrato)', + ofLabel: 'Contrato', + ofPlural: 'Contratos', + faseLabel: 'Lote', + fasePlural: 'Lotes', + temFases: true, + pecaLabel: 'Conjunto', + pecaPlural: 'Conjuntos', + componenteLabel: 'Subcomponente', + componentePlural: 'Subcomponentes', + marcaFormato: 'of_fase_peca', + faixaComponenteMin: 1000 + } + }, + { + id: 'padrao_direto_peca', + nome: 'Padrão Direto por Marca', + descricao: 'Modelo simplificado sem fases ou prefixos na tag da peça', + config: { + empresaNome: 'Serralheria / Estruturas Leves', + ofLabel: 'Pedido', + ofPlural: 'Pedidos', + faseLabel: 'Fase', + fasePlural: 'Fases', + temFases: false, + pecaLabel: 'Peça', + pecaPlural: 'Peças', + componenteLabel: 'Componente', + componentePlural: 'Componentes', + marcaFormato: 'peca_direto', + faixaComponenteMin: 1000 + } + } +]; + +export const DEFAULT_APP_LABELS: AppLabelsConfig = PRESETS_EMPRESAS[0].config; + +const STORAGE_KEY = 'tracksteel_app_labels_config'; + +interface AppLabelsContextData { + labels: AppLabelsConfig; + updateLabels: (newConfig: Partial) => void; + resetLabels: () => void; + formatTag: (of: string, fase: string, peca: string) => string; + isComponente: (marca: string | number) => boolean; +} + +const AppLabelsContext = createContext(undefined); + +export const AppLabelsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [labels, setLabels] = useState(() => { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + return { ...DEFAULT_APP_LABELS, ...JSON.parse(saved) }; + } + } catch (err) { + console.warn('Erro ao carregar configurações de labels:', err); + } + return DEFAULT_APP_LABELS; + }); + + const updateLabels = (newConfig: Partial) => { + setLabels((prev) => { + const updated = { ...prev, ...newConfig }; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + } catch (err) { + console.warn('Erro ao salvar labels no localStorage:', err); + } + return updated; + }); + }; + + const resetLabels = () => { + setLabels(DEFAULT_APP_LABELS); + try { + localStorage.removeItem(STORAGE_KEY); + } catch (err) { + console.warn('Erro ao resetar labels:', err); + } + }; + + const formatTag = (of: string, fase: string, peca: string): string => { + switch (labels.marcaFormato) { + case 'fase_peca': + return fase ? `${fase}-${peca}` : peca; + case 'peca_direto': + return peca; + case 'of_fase_peca': + default: + return of ? (fase ? `${of}-${fase}-${peca}` : `${of}-${peca}`) : peca; + } + }; + + const isComponente = (marca: string | number): boolean => { + const num = typeof marca === 'number' ? marca : parseInt(String(marca).replace(/\D/g, ''), 10); + if (!isNaN(num) && num >= labels.faixaComponenteMin) { + return true; + } + const str = String(marca).toUpperCase(); + return str.startsWith('PL') || str.startsWith('CH') || str.startsWith('G-') || str.startsWith('COMP-'); + }; + + return ( + + {children} + + ); +}; + +export const useAppLabels = () => { + const context = useContext(AppLabelsContext); + if (!context) { + return { + labels: DEFAULT_APP_LABELS, + updateLabels: () => {}, + resetLabels: () => {}, + formatTag: (of: string, fase: string, peca: string) => (of ? `${of}-${fase}-${peca}` : peca), + isComponente: (marca: string | number) => { + const num = typeof marca === 'number' ? marca : parseInt(String(marca).replace(/\D/g, ''), 10); + return !isNaN(num) && num >= 1000; + } + }; + } + return context; +}; diff --git a/src/hooks/useAppLabels.ts b/src/hooks/useAppLabels.ts new file mode 100644 index 0000000..9f5db09 --- /dev/null +++ b/src/hooks/useAppLabels.ts @@ -0,0 +1,2 @@ +export { useAppLabels, AppLabelsProvider, DEFAULT_APP_LABELS } from '@/contexts/AppLabelsContext'; +export type { AppLabelsConfig } from '@/contexts/AppLabelsContext'; diff --git a/src/main.tsx b/src/main.tsx index e548786..00b5d47 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,7 @@ import { Toaster } from '@/components/ui/sonner' import { AuthProvider } from './hooks/useAuth' import { IconStyleProvider } from './hooks/useIconStyle' +import { AppLabelsProvider } from './contexts/AppLabelsContext' import { ErrorBoundary } from './components/ErrorBoundary' import { AppRoutes } from './components/AppRoutes' @@ -38,12 +39,14 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - + + + + + + + + diff --git a/src/pages/Configuracoes.tsx b/src/pages/Configuracoes.tsx index a4512d4..1acd04f 100644 --- a/src/pages/Configuracoes.tsx +++ b/src/pages/Configuracoes.tsx @@ -12,6 +12,7 @@ import { useBrandSettings } from '@/hooks/useBrandSettings'; import { useIconStyle } from '@/hooks/useIconStyle'; import { MenuBuilder } from '@/components/menu-builder/MenuBuilder'; import { PrioridadesConfig } from '@/components/configuracoes/PrioridadesConfig'; +import { NomenclaturasConfig } from '@/components/configuracoes/NomenclaturasConfig'; const FONT_OPTIONS = [{ value: 'Arial', @@ -291,6 +292,9 @@ const Configuracoes = () => { + {/* Configuração de Nomenclaturas e Identificação da Fábrica */} + + {/* Configuração de Prioridades */}
OFFaseMarca{labels.ofLabel}{labels.faseLabel}{labels.pecaLabel} DescriçãoComposto por ComponentesCom {labels.componentePlural} Quantidade Peso Unitário (kg) Peso Total (kg)