feat: adicionar configuracoes de nomenclaturas de fabricas com preset Baldon Engemetal e labels dinamicos
This commit is contained in:
@@ -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<AppLabelsConfig>({ ...labels });
|
||||
const [selectedPreset, setSelectedPreset] = useState<string>('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 (
|
||||
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-blue-500/10 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400">
|
||||
<Building2 className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl text-foreground flex items-center gap-2">
|
||||
Nomenclaturas & Identificação da Fábrica
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700">
|
||||
Universal
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500 dark:text-slate-400">
|
||||
Personalize os termos de OF, Fases, Peças e regras de numeração de componentes para qualquer empresa.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="text-slate-600 border-slate-300 hover:bg-slate-100 dark:text-slate-300 dark:border-slate-600 dark:hover:bg-slate-700"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-1.5" />
|
||||
Restaurar Baldon
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium shadow-sm"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1.5" />
|
||||
Salvar Alterações
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6 pt-2">
|
||||
{/* Seletor de Perfis / Modelos de Empresas */}
|
||||
<div className="p-4 rounded-xl border border-blue-200/80 bg-blue-50/50 dark:border-blue-900/50 dark:bg-blue-950/20">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<Label className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
Perfil de Fábrica Selecionado
|
||||
</Label>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Modelos pré-configurados prontos para uso
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2.5">
|
||||
{PRESETS_EMPRESAS.map(preset => {
|
||||
const isSelected = selectedPreset === preset.id || formData.empresaNome === preset.config.empresaNome;
|
||||
return (
|
||||
<div
|
||||
key={preset.id}
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-semibold text-sm text-slate-900 dark:text-white flex items-center gap-1.5">
|
||||
{preset.nome}
|
||||
</span>
|
||||
{preset.isPadrao && (
|
||||
<Badge className="bg-emerald-100 text-emerald-800 hover:bg-emerald-100 text-[10px] px-1.5 py-0 border border-emerald-300 dark:bg-emerald-900/50 dark:text-emerald-300">
|
||||
Ativa
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 line-clamp-2">
|
||||
{preset.descricao}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulário de Campos e Nomenclaturas */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Nome da Empresa */}
|
||||
<div className="space-y-1.5 lg:col-span-2">
|
||||
<Label className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
||||
Nome da Empresa / Fábrica
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.empresaNome || ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Formato da Tag da Peça */}
|
||||
<div className="space-y-1.5 lg:col-span-2">
|
||||
<Label className="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
||||
Estrutura da Tag das Peças / Marcas
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.marcaFormato}
|
||||
onValueChange={(val: any) => setFormData(prev => ({ ...prev, marcaFormato: val }))}
|
||||
>
|
||||
<SelectTrigger className="bg-white border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-700 dark:text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-white border-slate-300 dark:bg-slate-800 dark:border-slate-700">
|
||||
<SelectItem value="of_fase_peca">OF-Fase-Peça (Padrão: B134-13-1)</SelectItem>
|
||||
<SelectItem value="fase_peca">Fase-Peça (Ex: 13-1)</SelectItem>
|
||||
<SelectItem value="peca_direto">Peça Direto / Marca Única (Ex: 1)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Ordem de Fabricação (OF) */}
|
||||
<div className="p-3.5 rounded-lg border border-slate-200 bg-slate-50/70 dark:border-slate-700 dark:bg-slate-800/40 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400">
|
||||
Identificador da Ordem
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Singular</Label>
|
||||
<Input
|
||||
value={formData.ofLabel}
|
||||
onChange={e => setFormData(prev => ({ ...prev, ofLabel: e.target.value }))}
|
||||
placeholder="OF, OC, Contrato..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Plural</Label>
|
||||
<Input
|
||||
value={formData.ofPlural}
|
||||
onChange={e => setFormData(prev => ({ ...prev, ofPlural: e.target.value }))}
|
||||
placeholder="OFs, OCs, Contratos..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fases / Etapas */}
|
||||
<div className="p-3.5 rounded-lg border border-slate-200 bg-slate-50/70 dark:border-slate-700 dark:bg-slate-800/40 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400">
|
||||
Divisão de Fases
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-slate-500">Usa Fases</span>
|
||||
<Switch
|
||||
checked={formData.temFases}
|
||||
onCheckedChange={checked => setFormData(prev => ({ ...prev, temFases: checked }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Singular</Label>
|
||||
<Input
|
||||
value={formData.faseLabel}
|
||||
disabled={!formData.temFases}
|
||||
onChange={e => setFormData(prev => ({ ...prev, faseLabel: e.target.value }))}
|
||||
placeholder="Fase, Etapa, Lote..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Plural</Label>
|
||||
<Input
|
||||
value={formData.fasePlural}
|
||||
disabled={!formData.temFases}
|
||||
onChange={e => setFormData(prev => ({ ...prev, fasePlural: e.target.value }))}
|
||||
placeholder="Fases, Etapas, Lotes..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Peças / Marcas */}
|
||||
<div className="p-3.5 rounded-lg border border-slate-200 bg-slate-50/70 dark:border-slate-700 dark:bg-slate-800/40 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400">
|
||||
Peça Principal / Conjunto
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Singular</Label>
|
||||
<Input
|
||||
value={formData.pecaLabel}
|
||||
onChange={e => setFormData(prev => ({ ...prev, pecaLabel: e.target.value }))}
|
||||
placeholder="Peça, Marca, Conjunto..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Plural</Label>
|
||||
<Input
|
||||
value={formData.pecaPlural}
|
||||
onChange={e => setFormData(prev => ({ ...prev, pecaPlural: e.target.value }))}
|
||||
placeholder="Peças, Marcas, Conjuntos..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Componentes / Acessórios */}
|
||||
<div className="p-3.5 rounded-lg border border-slate-200 bg-slate-50/70 dark:border-slate-700 dark:bg-slate-800/40 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hash className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400">
|
||||
Subitens / Componentes
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Nome Singular</Label>
|
||||
<Input
|
||||
value={formData.componenteLabel}
|
||||
onChange={e => setFormData(prev => ({ ...prev, componenteLabel: e.target.value }))}
|
||||
placeholder="Componente, Acessório..."
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] text-slate-500">Faixa Inicial (Numeração)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.faixaComponenteMin}
|
||||
onChange={e => setFormData(prev => ({ ...prev, faixaComponenteMin: parseInt(e.target.value, 10) || 1000 }))}
|
||||
placeholder="1000"
|
||||
className="h-8 text-sm bg-white dark:bg-slate-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pré-visualização Dinâmica */}
|
||||
<div className="p-4 rounded-xl border border-slate-200 bg-slate-50/60 dark:border-slate-700 dark:bg-slate-800/60">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||
<span className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
Simulação de Visualização no Sistema ({formData.empresaNome || 'Sua Empresa'})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="p-3 rounded-lg bg-white border border-slate-200 shadow-sm dark:bg-slate-800 dark:border-slate-700">
|
||||
<span className="text-[11px] text-slate-500 block mb-1">
|
||||
{formData.pecaLabel} Principal (Exemplo):
|
||||
</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-sm font-bold text-blue-600 dark:text-blue-400">
|
||||
{previewPecaTag}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
{formData.pecaLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-white border border-slate-200 shadow-sm dark:bg-slate-800 dark:border-slate-700">
|
||||
<span className="text-[11px] text-slate-500 block mb-1">
|
||||
{formData.componenteLabel} (Exemplo):
|
||||
</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-sm font-bold text-amber-600 dark:text-amber-400">
|
||||
{previewCompTag}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300">
|
||||
{formData.componenteLabel} (≥ {formData.faixaComponenteMin})
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-white border border-slate-200 shadow-sm dark:bg-slate-800 dark:border-slate-700 sm:col-span-2 md:col-span-1">
|
||||
<span className="text-[11px] text-slate-500 block mb-1">
|
||||
Tabela e Filtros:
|
||||
</span>
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300 font-medium">
|
||||
{formData.ofLabel} / {formData.faseLabel} / {formData.pecaLabel} / {formData.componenteLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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<PdfJsLib> => {
|
||||
};
|
||||
|
||||
const AdvanceSteelConverterContent: React.FC = () => {
|
||||
const { labels, isComponente } = useAppLabels();
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
@@ -809,7 +811,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
||||
{extractedData.length > 0 && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||
<div className="bg-slate-800/60 border border-slate-700/70 p-3 rounded-lg flex flex-col justify-between">
|
||||
<span className="text-[11px] font-medium text-slate-400">OF (Obra)</span>
|
||||
<span className="text-[11px] font-medium text-slate-400">{labels.ofLabel} (Obra)</span>
|
||||
<Input
|
||||
value={headerOf}
|
||||
onChange={(e) => handleOfChange(e.target.value)}
|
||||
@@ -818,7 +820,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800/60 border border-slate-700/70 p-3 rounded-lg flex flex-col justify-between">
|
||||
<span className="text-[11px] font-medium text-slate-400">Fase</span>
|
||||
<span className="text-[11px] font-medium text-slate-400">{labels.faseLabel}</span>
|
||||
<Input
|
||||
value={headerFase}
|
||||
onChange={(e) => handleFaseChange(e.target.value)}
|
||||
@@ -827,7 +829,7 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800/60 border border-slate-700/70 p-3 rounded-lg">
|
||||
<span className="text-[11px] font-medium text-slate-400">Total de Peças</span>
|
||||
<span className="text-[11px] font-medium text-slate-400">Total de {labels.pecaPlural}</span>
|
||||
<div className="text-lg font-bold text-white mt-1 flex items-baseline gap-1">
|
||||
{extractedData.length} <span className="text-xs font-normal text-slate-400">itens</span>
|
||||
</div>
|
||||
@@ -913,11 +915,11 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
||||
<table className="w-full text-xs text-left border-collapse">
|
||||
<thead className="bg-slate-900 text-slate-300 font-mono text-[11px] sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th className="p-2.5 border-b border-slate-800">OF</th>
|
||||
<th className="p-2.5 border-b border-slate-800">Fase</th>
|
||||
<th className="p-2.5 border-b border-slate-800">Marca</th>
|
||||
<th className="p-2.5 border-b border-slate-800">{labels.ofLabel}</th>
|
||||
<th className="p-2.5 border-b border-slate-800">{labels.faseLabel}</th>
|
||||
<th className="p-2.5 border-b border-slate-800">{labels.pecaLabel}</th>
|
||||
<th className="p-2.5 border-b border-slate-800">Descrição</th>
|
||||
<th className="p-2.5 border-b border-slate-800 text-center">Composto por Componentes</th>
|
||||
<th className="p-2.5 border-b border-slate-800 text-center">Com {labels.componentePlural}</th>
|
||||
<th className="p-2.5 border-b border-slate-800 text-center">Quantidade</th>
|
||||
<th className="p-2.5 border-b border-slate-800 text-right">Peso Unitário (kg)</th>
|
||||
<th className="p-2.5 border-b border-slate-800 text-right">Peso Total (kg)</th>
|
||||
|
||||
@@ -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<AppLabelsConfig>) => void;
|
||||
resetLabels: () => void;
|
||||
formatTag: (of: string, fase: string, peca: string) => string;
|
||||
isComponente: (marca: string | number) => boolean;
|
||||
}
|
||||
|
||||
const AppLabelsContext = createContext<AppLabelsContextData | undefined>(undefined);
|
||||
|
||||
export const AppLabelsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [labels, setLabels] = useState<AppLabelsConfig>(() => {
|
||||
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<AppLabelsConfig>) => {
|
||||
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 (
|
||||
<AppLabelsContext.Provider
|
||||
value={{
|
||||
labels,
|
||||
updateLabels,
|
||||
resetLabels,
|
||||
formatTag,
|
||||
isComponente
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppLabelsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useAppLabels, AppLabelsProvider, DEFAULT_APP_LABELS } from '@/contexts/AppLabelsContext';
|
||||
export type { AppLabelsConfig } from '@/contexts/AppLabelsContext';
|
||||
+9
-6
@@ -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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<IconStyleProvider>
|
||||
<ThemeProvider defaultTheme="system">
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
<AppLabelsProvider>
|
||||
<ThemeProvider defaultTheme="system">
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</AppLabelsProvider>
|
||||
</IconStyleProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -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 = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Configuração de Nomenclaturas e Identificação da Fábrica */}
|
||||
<NomenclaturasConfig />
|
||||
|
||||
{/* Configuração de Prioridades */}
|
||||
<PrioridadesConfig />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user