🚀 Auto-deploy: BrainWind atualizado em 14/07/2026 15:18:53
This commit is contained in:
+41
-9
@@ -14,7 +14,7 @@ import PiperackModule from './pages/PiperackModule';
|
||||
import {
|
||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||
Square, Layers, BarChart3, Activity, Warehouse,
|
||||
Settings2, Sun, Moon, Frame,
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -141,13 +141,14 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
</nav>
|
||||
<div className="border-t p-2 flex flex-col gap-1 items-center justify-center">
|
||||
<GlobalWindSettingsModal
|
||||
trigger={
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors text-sm w-full text-left cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0',
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full text-left cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Parâmetros do Vento' : undefined}
|
||||
>
|
||||
@@ -156,18 +157,49 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</nav>
|
||||
<div className="border-t p-2 flex flex-col gap-2 items-center justify-center">
|
||||
<Link
|
||||
to="/settings?tab=gerenciador"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'gerenciador'
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Arquivos' : undefined}
|
||||
>
|
||||
<FolderOpen className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Arquivos</span>}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0',
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && (!location.search || location.search.includes('tab=config') || location.search.includes('tab=testes'))
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? t('nav_settings') : undefined}
|
||||
>
|
||||
<Settings className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav_settings')}</span>}
|
||||
{!isCollapsed && <span>Configurações</span>}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings?tab=glossary"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'glossary'
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Glossário' : undefined}
|
||||
>
|
||||
<BookOpen className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Glossário</span>}
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search, BookOpen } from 'lucide-react';
|
||||
import { nbr6123Glossary } from '@/data/glossary';
|
||||
|
||||
const Glossary: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredTerms = useMemo(() => {
|
||||
const termLower = searchTerm.toLowerCase();
|
||||
return nbr6123Glossary.filter(item => {
|
||||
if (item.term.toLowerCase().includes(termLower)) return true;
|
||||
if (item.definition.toLowerCase().includes(termLower)) return true;
|
||||
if (item.tags.some(t => t.toLowerCase().includes(termLower))) return true;
|
||||
if (item.reference.toLowerCase().includes(termLower)) return true;
|
||||
return false;
|
||||
});
|
||||
}, [searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="border-border shadow-sm bg-card/50">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Glossário NBR 6123</CardTitle>
|
||||
<CardDescription>
|
||||
Dicionário técnico com termos aerodinâmicos e variáveis normativas explicadas de forma didática.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar termo, sigla (ex: Cpe, S1) ou conceito..."
|
||||
className="pl-10 max-w-full"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{filteredTerms.length > 0 ? (
|
||||
filteredTerms.map((item) => (
|
||||
<Card key={item.id} className="overflow-hidden hover:border-primary/50 transition-colors">
|
||||
<CardHeader className="p-4 bg-muted/20 border-b pb-3">
|
||||
<CardTitle className="text-base text-primary flex items-center justify-between">
|
||||
{item.term}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground font-mono mt-1">
|
||||
{item.reference}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-sm leading-relaxed text-foreground/90">
|
||||
{item.definition}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 mt-4">
|
||||
{item.tags.map((tag, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-[10px] font-normal px-2 py-0 h-5">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-1 md:col-span-2 py-12 text-center text-muted-foreground border border-dashed rounded-lg">
|
||||
Nenhum termo encontrado para "{searchTerm}".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Glossary;
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Save, FileJson, FileBox } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog';
|
||||
import { useProjects } from '../lib/hooks/useProjects';
|
||||
import { useWindStore } from '../store/appStore';
|
||||
|
||||
interface SaveModuleDialogProps {
|
||||
moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
inputs: Record<string, unknown>;
|
||||
triggerButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SaveModuleDialog: React.FC<SaveModuleDialogProps> = ({ moduleType, inputs, triggerButton }) => {
|
||||
const { save } = useProjects();
|
||||
const windStore = useWindStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState<'projeto' | 'template'>('projeto');
|
||||
|
||||
// Gera um nome sugerido ao abrir
|
||||
React.useEffect(() => {
|
||||
if (open && !name) {
|
||||
const date = new Date().toLocaleDateString('pt-BR').replace(/\//g, '');
|
||||
const prefix = type === 'projeto' ? 'PRJ' : 'TPL';
|
||||
const modName = moduleType.charAt(0).toUpperCase() + moduleType.slice(1);
|
||||
setName(`${prefix}-${modName}-${date}`);
|
||||
}
|
||||
}, [open, type, moduleType, name]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
const windData = type === 'projeto' ? {
|
||||
v0: windStore.v0,
|
||||
s1: windStore.s1,
|
||||
s3: windStore.s3,
|
||||
s3Group: windStore.s3Group,
|
||||
terrainCategory: windStore.terrainCategory,
|
||||
largestDimension: windStore.largestDimension,
|
||||
heightZ: windStore.heightZ,
|
||||
} : undefined;
|
||||
|
||||
await save({
|
||||
name,
|
||||
type,
|
||||
module: moduleType,
|
||||
inputs,
|
||||
windData,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{triggerButton || (
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Save className="h-4 w-4" />
|
||||
Salvar
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Salvar Módulo</DialogTitle>
|
||||
<DialogDescription>
|
||||
Escolha como deseja salvar este módulo.
|
||||
Projetos incluem os dados de vento atuais. Templates salvam apenas a geometria da estrutura.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant={type === 'projeto' ? 'default' : 'outline'}
|
||||
className="h-20 flex flex-col gap-2"
|
||||
onClick={() => setType('projeto')}
|
||||
>
|
||||
<FileBox className="h-6 w-6" />
|
||||
Projeto Completo
|
||||
</Button>
|
||||
<Button
|
||||
variant={type === 'template' ? 'default' : 'outline'}
|
||||
className="h-20 flex flex-col gap-2"
|
||||
onClick={() => setType('template')}
|
||||
>
|
||||
<FileJson className="h-6 w-6" />
|
||||
Template (Geometria)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Nome do Arquivo</label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Digite um nome para identificar..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={handleSave} disabled={!name.trim()}>Salvar {type === 'projeto' ? 'Projeto' : 'Template'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -55,48 +55,52 @@ function PiperackModel({
|
||||
);
|
||||
}
|
||||
|
||||
// Viga Longitudinal da fileira
|
||||
// Viga Longitudinal da fileira (representando a face reticulada do pórtico)
|
||||
frames.push(
|
||||
<mesh key={`beam-${i}`} position={[0, elevation, z]} receiveShadow castShadow>
|
||||
<boxGeometry args={[width, height, 0.3]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
{/* Opacidade ajustada para atingir 90% quando a solidez for 1.0 */}
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={Math.max(0.1, Math.min(0.9, phiTotal * 0.9))} depthWrite={false} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
// Vigas transversais conectando as fileiras nas pontas
|
||||
if (numFrames > 1) {
|
||||
frames.push(
|
||||
<mesh key={`cross-1`} position={[-halfW, elevation, 0]} receiveShadow castShadow>
|
||||
<boxGeometry args={[0.3, height, totalSpacing]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
</mesh>
|
||||
);
|
||||
frames.push(
|
||||
<mesh key={`cross-2`} position={[halfW, elevation, 0]} receiveShadow castShadow>
|
||||
<boxGeometry args={[0.3, height, totalSpacing]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
// Gera as tubulações correndo longitudinalmente (ao longo de X)
|
||||
const pipeMeshes = pipes.map((pipe, idx) => {
|
||||
// Distribui os tubos ao longo do espaçamento transversal (Z)
|
||||
const zPos = numFrames > 1
|
||||
? -zOffset + 0.5 + (idx % Math.max(pipes.length, 1)) * (totalSpacing - 1) / Math.max(pipes.length - 1, 1) || 0
|
||||
: 0;
|
||||
// elevationOffset varia de 0 (base da estrutura reticulada) a height (topo da estrutura)
|
||||
// A base da estrutura fica em: rackTop - height, que é elevation - height/2
|
||||
const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2;
|
||||
const color = pipeColors[idx % pipeColors.length];
|
||||
// Agrupa tubos pela mesma elevação para evitar colisões e distribuí-los lado a lado (ao longo de Z)
|
||||
const pipeMeshes: any[] = [];
|
||||
const groups: Record<number, {pipe: PipeDef, idx: number}[]> = {};
|
||||
|
||||
pipes.forEach((pipe, idx) => {
|
||||
// Arredondamos a elevação para agrupar valores muito próximos
|
||||
const key = Math.round(pipe.elevationOffset * 100) / 100;
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push({pipe, idx});
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh key={`pipe-${pipe.id}`} position={[0, yPos, zPos]} rotation={[0, 0, Math.PI / 2]} receiveShadow castShadow>
|
||||
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, width + 1, 16]} />
|
||||
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
||||
</mesh>
|
||||
);
|
||||
Object.values(groups).forEach(group => {
|
||||
const gap = 0.2; // 20cm de espaçamento entre tubos na mesma elevação
|
||||
const totalDiam = group.reduce((sum, item) => sum + item.pipe.diameter, 0);
|
||||
const totalGroupWidth = totalDiam + gap * (group.length - 1);
|
||||
|
||||
// Inicia a distribuição centralizada em Z=0
|
||||
let currentZ = -totalGroupWidth / 2;
|
||||
|
||||
group.forEach((item) => {
|
||||
const { pipe, idx } = item;
|
||||
const zPos = currentZ + pipe.diameter / 2;
|
||||
currentZ += pipe.diameter + gap;
|
||||
|
||||
const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2;
|
||||
const color = pipeColors[idx % pipeColors.length];
|
||||
|
||||
pipeMeshes.push(
|
||||
<mesh key={`pipe-${pipe.id}`} position={[0, yPos, zPos]} rotation={[0, 0, Math.PI / 2]} receiveShadow castShadow>
|
||||
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, width + 1, 16]} />
|
||||
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
export interface GlossaryTerm {
|
||||
id: string;
|
||||
term: string;
|
||||
definition: string;
|
||||
reference: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const nbr6123Glossary: GlossaryTerm[] = [
|
||||
{
|
||||
id: 'v0',
|
||||
term: 'Velocidade Básica do Vento (V₀)',
|
||||
definition: 'Velocidade do vento obtida a partir de dados meteorológicos. É definida como a velocidade média de 3 segundos, excedida em média uma vez em 50 anos, a 10 metros de altura, sobre terreno plano e aberto. Usada como base para todos os cálculos aerodinâmicos na NBR 6123.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.2',
|
||||
tags: ['velocidade', 'isopletas', 'parâmetro básico']
|
||||
},
|
||||
{
|
||||
id: 's1',
|
||||
term: 'Fator Topográfico (S₁)',
|
||||
definition: 'Coeficiente que considera as variações do relevo do terreno. Leva em conta se o terreno é plano, fracamente acidentado, talude ou morro, aumentando a velocidade do vento em encostas e cumes.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.3',
|
||||
tags: ['relevo', 'topografia', 'talude', 'morro']
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
term: 'Fator de Rugosidade (S₂)',
|
||||
definition: 'Coeficiente que leva em consideração a rugosidade do terreno (obstáculos como árvores e prédios) e as dimensões da edificação. Depende da categoria do terreno (I a V) e aumenta de acordo com a altura (z) da estrutura.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.4 / Tabela 3',
|
||||
tags: ['rugosidade', 'altura', 'categoria', 'dimensões']
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
term: 'Fator Estatístico (S₃)',
|
||||
definition: 'Fator de probabilidade que considera o nível de segurança e a vida útil da edificação. Valores comuns são 1,0 para edificações normais (50 anos), mas pode ser maior (ex: hospitais) ou menor (ex: instalações temporárias).',
|
||||
reference: 'NBR 6123:2023 - Seção 5.5',
|
||||
tags: ['segurança', 'probabilidade', 'uso']
|
||||
},
|
||||
{
|
||||
id: 'vk',
|
||||
term: 'Velocidade Característica (Vₖ)',
|
||||
definition: 'A velocidade do vento de projeto em uma determinada altura e situação, calculada multiplicando a velocidade básica pelos três fatores: Vₖ = V₀ × S₁ × S₂ × S₃.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.1',
|
||||
tags: ['velocidade de projeto', 'fórmula']
|
||||
},
|
||||
{
|
||||
id: 'q',
|
||||
term: 'Pressão Dinâmica (q)',
|
||||
definition: 'A energia cinética do vento transformada em pressão. É calculada a partir da velocidade característica: q = 0,613 × Vₖ² (em N/m²). Esta pressão é aplicada nas superfícies junto com os coeficientes aerodinâmicos.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.6',
|
||||
tags: ['pressão', 'energia cinética']
|
||||
},
|
||||
{
|
||||
id: 'cpe',
|
||||
term: 'Coeficiente de Pressão Externa (Cpe)',
|
||||
definition: 'Valor adimensional que indica como o vento atua nas faces externas de uma edificação (paredes e telhados). Valores positivos indicam pressão (empurrando a face), valores negativos indicam sucção (puxando a face).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.2 / Tabelas 6 a 12',
|
||||
tags: ['pressão', 'sucção', 'externo', 'fachada', 'cobertura']
|
||||
},
|
||||
{
|
||||
id: 'cpi',
|
||||
term: 'Coeficiente de Pressão Interna (Cpi)',
|
||||
definition: 'Valor que determina a pressão exercida pelo ar no interior da edificação, gerada devido a aberturas e frestas (portas, janelas, ventilação). Depende do índice de permeabilidade de cada face do prédio.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.3',
|
||||
tags: ['pressão', 'interno', 'permeabilidade', 'aberturas']
|
||||
},
|
||||
{
|
||||
id: 'barlavento',
|
||||
term: 'Barlavento',
|
||||
definition: 'Região ou face da edificação por onde o vento "chega" ou incide primeiro. Normalmente as superfícies a barlavento sofrem pressões positivas (empuxo contra a parede).',
|
||||
reference: 'NBR 6123:2023 - Conceito Geral',
|
||||
tags: ['incidência', 'pressão positiva', 'vento']
|
||||
},
|
||||
{
|
||||
id: 'sotavento',
|
||||
term: 'Sotavento',
|
||||
definition: 'Região ou face da edificação que fica "escondida" ou protegida do vento incidente (a face traseira). Normalmente sofre pressões negativas (sucção), pois o vento passa e "puxa" o ar daquela região.',
|
||||
reference: 'NBR 6123:2023 - Conceito Geral',
|
||||
tags: ['traseira', 'sucção', 'esteira']
|
||||
},
|
||||
{
|
||||
id: 'ca',
|
||||
term: 'Coeficiente de Arrasto (Ca)',
|
||||
definition: 'Coeficiente de força global. Diferente do Cpe, que atua pontualmente em painéis, o Ca define a força total de arrasto gerada no volume inteiro da estrutura (ex: chaminés, torres, pórticos).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.2',
|
||||
tags: ['arrasto', 'força global', 'torres', 'cilindros']
|
||||
},
|
||||
{
|
||||
id: 'esbeltez',
|
||||
term: 'Índice de Esbeltez (λ)',
|
||||
definition: 'Relação entre a altura da estrutura e a sua largura base (ou diâmetro). Estruturas muito esbeltas (finas e altas) são mais sensíveis aos efeitos do vento e exigem coeficientes de arrasto específicos e análises dinâmicas.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1 / Diversas Tabelas',
|
||||
tags: ['altura', 'largura', 'aerodinâmica']
|
||||
},
|
||||
{
|
||||
id: 'vortex',
|
||||
term: 'Desprendimento de Vórtices (Vortex Shedding)',
|
||||
definition: 'Fenômeno dinâmico onde o vento, ao passar por uma estrutura (geralmente cilíndrica ou prismática), cria vórtices alternados nas laterais, causando oscilações transversais à direção do vento. Pode gerar ressonância estrutural perigosa.',
|
||||
reference: 'NBR 6123:2023 - Seção 10.2',
|
||||
tags: ['dinâmica', 'ressonância', 'cilindros', 'chaminés', 'vibração transversal']
|
||||
},
|
||||
{
|
||||
id: 'flutter',
|
||||
term: 'Esvoaçamento (Flutter)',
|
||||
definition: 'Instabilidade aeroelástica dinâmica onde a estrutura (ex: tabuleiro de ponte estaiada) começa a girar e vibrar de forma autossustentada e crescente devido à interação complexa entre o vento e o formato da seção.',
|
||||
reference: 'NBR 6123:2023 - Seção 11',
|
||||
tags: ['pontes', 'aeroelasticidade', 'torção', 'instabilidade']
|
||||
},
|
||||
{
|
||||
id: 'galloping',
|
||||
term: 'Galope (Galloping)',
|
||||
definition: 'Fenômeno dinâmico de instabilidade em flexão pura transversal à direção do vento. Comum em perfis não circulares ou cabos com formação de gelo/água assimétrica.',
|
||||
reference: 'NBR 6123:2023 - Seção 11.4',
|
||||
tags: ['dinâmica', 'pontes', 'cabos', 'vibração lateral']
|
||||
},
|
||||
{
|
||||
id: 'phi',
|
||||
term: 'Índice de Solidez (φ)',
|
||||
definition: 'Fator que define a "porosidade" de uma estrutura vazada (como uma torre treliçada, letreiro vazado ou pipe-rack). É a área efetiva (cheia) dos perfis dividida pela área de contorno total do painel. Aumenta o coeficiente de arrasto quanto mais "sólida" for a estrutura.',
|
||||
reference: 'NBR 6123:2023 - Seção 7',
|
||||
tags: ['treliças', 'torres', 'permeabilidade', 'bloqueio']
|
||||
},
|
||||
{
|
||||
id: 'eta',
|
||||
term: 'Fator de Proteção (η)',
|
||||
definition: 'Quando há pórticos ou treliças posicionados um atrás do outro (ex: pipe-racks ou torres retangulares), a face traseira sofre menor incidência do vento por estar "protegida" pela da frente. O fator η pondera essa redução.',
|
||||
reference: 'NBR 6123:2023 - Seção 7 / Tabela 28',
|
||||
tags: ['treliças', 'sombreamento', 'pipe-racks', 'arrasto conjunto']
|
||||
},
|
||||
{
|
||||
id: 'rugosidade',
|
||||
term: 'Categoria de Terreno',
|
||||
definition: 'A norma define 5 categorias de rugosidade: I (superfícies lisas, mar calmo), II (campos abertos), III (terrenos planos com pequenos obstáculos, fazendas), IV (áreas urbanas, subúrbios densos) e V (centros de grandes cidades).',
|
||||
reference: 'NBR 6123:2023 - Seção 5.4.1',
|
||||
tags: ['terreno', 's2', 'urbanização', 'relevo']
|
||||
},
|
||||
{
|
||||
id: 'forca_atrito',
|
||||
term: 'Força de Atrito (Ff)',
|
||||
definition: 'Força que o vento exerce tangencialmente às superfícies paralelas ao seu escoamento, devido à fricção superficial (ex: tetos muito compridos ou paredes extensas).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.5',
|
||||
tags: ['fricção', 'tangencial', 'galpões compridos']
|
||||
},
|
||||
{
|
||||
id: 'pressao_efetiva',
|
||||
term: 'Pressão Efetiva (Δp)',
|
||||
definition: 'A diferença real de pressão que atua em uma superfície (como parede ou telhado), combinando a ação externa (Cpe) e interna (Cpi). Fórmula: Δp = q × (Cpe - Cpi). Resulta na carga real em N/m² que a estrutura vai suportar.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.3',
|
||||
tags: ['pressão resultante', 'cálculo final']
|
||||
}
|
||||
];
|
||||
@@ -8,8 +8,10 @@
|
||||
export interface SavedProject {
|
||||
id?: number;
|
||||
name: string;
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics';
|
||||
type: 'projeto' | 'template';
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
inputs: Record<string, unknown>;
|
||||
windData?: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -12,11 +15,32 @@ import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
|
||||
|
||||
const BridgeModule: React.FC = () => {
|
||||
const { v0, s1, terrainCategory, q } = useWindStore();
|
||||
const [lp, setLp] = React.useState(120);
|
||||
const [width, setWidth] = React.useState(14);
|
||||
const [deckHeight, setDeckHeight] = React.useState(15);
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'bridge') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.lp !== undefined) setLp(p.lp);
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.deckHeight !== undefined) setDeckHeight(p.deckHeight);
|
||||
if (p.heg !== undefined) setHeg(p.heg);
|
||||
if (p.mass !== undefined) setMass(p.mass);
|
||||
if (p.fv !== undefined) setFv(p.fv);
|
||||
if (p.vk !== undefined) setVk(p.vk);
|
||||
if (p.alpha !== undefined) setAlpha(p.alpha);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad]);
|
||||
|
||||
// Constantes normativas de flutter da seção
|
||||
const [heg, setHeg] = React.useState(2.5);
|
||||
const [mass, setMass] = React.useState(18000);
|
||||
const [fv, setFv] = React.useState(0.6);
|
||||
@@ -246,7 +270,13 @@ const BridgeModule: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="bridge"
|
||||
inputs={{ lp, width, deckHeight, heg, mass, fv, vk, alpha }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
|
||||
const GalpaoModule: React.FC = () => {
|
||||
const {
|
||||
@@ -33,6 +35,21 @@ const GalpaoModule: React.FC = () => {
|
||||
windAngle,
|
||||
setWindAngle,
|
||||
} = useWindStore();
|
||||
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'galpao') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.length !== undefined) setLength(p.length);
|
||||
if (p.height !== undefined) setHeight(p.height);
|
||||
if (p.roofPitch !== undefined) setRoofPitch(p.roofPitch);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad, setWidth, setLength, setHeight, setRoofPitch]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
@@ -146,7 +163,6 @@ const GalpaoModule: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
||||
<ExportMenu />
|
||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,6 +177,17 @@ const GalpaoModule: React.FC = () => {
|
||||
{/* Coluna Direita-Inferior: Cargas Lineares (M9.2) */}
|
||||
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<LinearLoadsTable />
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="galpao"
|
||||
inputs={{ width, length, height, roofPitch }}
|
||||
/>
|
||||
<ExportMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
<FtoolExportCard />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -24,6 +27,23 @@ const PiperackModule: React.FC = () => {
|
||||
const [phiStruct, setPhiStruct] = useState(0.5);
|
||||
const [pipes, setPipes] = useState<PipeDef[]>([]);
|
||||
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'piperack') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.elevation !== undefined) setElevation(p.elevation);
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.height !== undefined) setHeight(p.height);
|
||||
if (p.spacing !== undefined) setSpacing(p.spacing);
|
||||
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
|
||||
if (p.pipes !== undefined) setPipes(p.pipes);
|
||||
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad]);
|
||||
|
||||
const addPipe = () => {
|
||||
setPipes([...pipes, { id: Math.random().toString(36).substr(2, 9), diameter: 0.5, elevationOffset: height / 2 }]);
|
||||
};
|
||||
@@ -91,7 +111,7 @@ const PiperackModule: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-3.5rem)] md:h-screen bg-background">
|
||||
<header className="shrink-0 flex items-center justify-between px-6 py-4 border-b bg-card">
|
||||
<header className="shrink-0 flex items-center px-6 py-4 border-b bg-card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Layers className="w-5 h-5 text-primary" />
|
||||
@@ -101,9 +121,6 @@ const PiperackModule: React.FC = () => {
|
||||
<p className="text-sm text-muted-foreground">Vento transversal (Sec. 7 - NBR 6123)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col lg:flex-row p-4 gap-4 overflow-hidden bg-muted/30">
|
||||
@@ -403,6 +420,16 @@ const PiperackModule: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="piperack"
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+140
-109
@@ -1,27 +1,31 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, CheckCircle2, AlertCircle, FlaskConical } from 'lucide-react';
|
||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, FlaskConical, Download, FolderOpen, FileBox, FileJson } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { useProjects } from '@/lib/hooks/useProjects';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { useI18n } from '@/store/i18nStore';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
importProjectFromText,
|
||||
readProjectFile,
|
||||
type ImportResult,
|
||||
} from '@/lib/import-project';
|
||||
import AuditPanel from '@/components/AuditPanel';
|
||||
import Glossary from '@/components/Glossary';
|
||||
|
||||
const SettingsModule: React.FC = () => {
|
||||
const { theme, setTheme, effectiveTheme } = useTheme();
|
||||
const { projects, loading, error, remove } = useProjects();
|
||||
const { projects, loading, error, remove, save } = useProjects();
|
||||
const { t, locale } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setPendingLoad = useHydrationStore((s) => s.setPendingLoad);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const exportSnapshot = () => {
|
||||
@@ -30,12 +34,36 @@ const SettingsModule: React.FC = () => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'ventoapp-state.json');
|
||||
link.setAttribute('download', 'ventoapp-snapshot.json');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleDownloadJSON = (project: any) => {
|
||||
const blob = new Blob([JSON.stringify(project, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${project.name}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleLoadProject = (project: any) => {
|
||||
if (project.type === 'projeto' && project.windData) {
|
||||
// Restore wind data
|
||||
useWindStore.setState(project.windData);
|
||||
}
|
||||
|
||||
// Set pending load
|
||||
setPendingLoad(project.module, project.inputs);
|
||||
|
||||
// Navigate to module
|
||||
navigate(`/${project.module === 'isolated-roof' ? 'cobertura-isolada' : project.module === 'sign' ? 'muros' : project.module === 'bar' ? 'barras' : project.module === 'bridge' ? 'pontes' : project.module === 'dynamics' ? 'dinamica' : project.module === 'vault' ? 'abobada' : project.module === 'dome' ? 'cupula' : project.module}`);
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
@@ -44,29 +72,51 @@ const SettingsModule: React.FC = () => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
try {
|
||||
const text = await readProjectFile(file);
|
||||
const result = importProjectFromText(text);
|
||||
setImportResult(result);
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed.module && parsed.inputs) {
|
||||
// Formato SavedProject (Projeto ou Template)
|
||||
const newProject = {
|
||||
...parsed,
|
||||
name: parsed.name || 'Importado - ' + file.name,
|
||||
type: parsed.type || 'projeto',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
// Ensure type is correct
|
||||
if (!['projeto', 'template'].includes(newProject.type)) newProject.type = 'projeto';
|
||||
|
||||
await save(newProject as any);
|
||||
} else {
|
||||
// Old snapshot import fallback
|
||||
importProjectFromText(text);
|
||||
}
|
||||
} catch (e) {
|
||||
setImportResult({
|
||||
ok: false,
|
||||
error: e instanceof Error ? e.message : 'Falha ao ler arquivo',
|
||||
});
|
||||
console.error(e);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const currentTab = new URLSearchParams(location.search).get('tab') || 'config';
|
||||
const isSettingsGroup = currentTab === 'config' || currentTab === 'testes';
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto overflow-auto">
|
||||
<h1 className="text-3xl font-bold mb-6">{t('nav_settings')}</h1>
|
||||
<h1 className="text-3xl font-bold mb-6 md:hidden">Configurações</h1>
|
||||
<h1 className="text-3xl font-bold mb-6 hidden md:block">
|
||||
{currentTab === 'gerenciador' ? 'Arquivos' :
|
||||
currentTab === 'glossary' ? 'Glossário' :
|
||||
'Configurações'}
|
||||
</h1>
|
||||
|
||||
<Tabs defaultValue="config" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="config">Configurações</TabsTrigger>
|
||||
<Tabs value={currentTab} onValueChange={(val) => navigate(`/settings?tab=${val}`)} className="w-full">
|
||||
<TabsList className={`grid w-full mb-6 ${isSettingsGroup ? 'grid-cols-4 md:grid-cols-2' : 'grid-cols-4 md:hidden'}`}>
|
||||
<TabsTrigger value="config">Preferências</TabsTrigger>
|
||||
<TabsTrigger value="gerenciador" className="md:hidden">Arquivos</TabsTrigger>
|
||||
<TabsTrigger value="glossary" className="text-red-500 uppercase font-bold tracking-wider md:hidden">GLOSSÁRIO</TabsTrigger>
|
||||
<TabsTrigger value="testes">
|
||||
<FlaskConical className="w-4 h-4 mr-1.5" />
|
||||
Testes
|
||||
@@ -98,99 +148,6 @@ const SettingsModule: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_projects')}</CardTitle>
|
||||
<CardDescription>{t('settings_projects_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-3 gap-2 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings_projects_count', { count: projects.length })}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-1.5 w-full sm:w-auto sm:flex">
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={handleImportClick} disabled={importing}>
|
||||
<Upload className="w-3.5 h-3.5 mr-1.5 shrink-0" /> {importing ? t('settings_importing') : t('export_import')}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileSelected}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={exportSnapshot}>
|
||||
<Save className="w-3.5 h-3.5 mr-1.5 shrink-0" /> {t('export_snapshot')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importResult && (
|
||||
<div
|
||||
className={`rounded-md border p-3 mb-3 text-sm flex items-start gap-2 ${
|
||||
importResult.ok
|
||||
? 'bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950 dark:border-emerald-800 dark:text-emerald-300'
|
||||
: 'bg-red-50 border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300'
|
||||
}`}
|
||||
>
|
||||
{importResult.ok ? (
|
||||
<CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium">
|
||||
{importResult.ok ? t('settings_import_success') : t('settings_import_error')}
|
||||
</div>
|
||||
{importResult.module && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_module')}: <Badge variant="outline">{importResult.module}</Badge>
|
||||
{importResult.projectName && (
|
||||
<span className="ml-2">{t('settings_import_project')}: {importResult.projectName}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{importResult.appliedFields && importResult.appliedFields.length > 0 && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_fields', { count: importResult.appliedFields.length })}: {importResult.appliedFields.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.warnings && importResult.warnings.length > 0 && (
|
||||
<div className="text-xs mt-1 opacity-80">
|
||||
{t('settings_import_warnings')}: {importResult.warnings.join('; ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.error && (
|
||||
<div className="text-xs mt-1">{importResult.error}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">{t('common_loading')}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{projects.length === 0 && !loading && (
|
||||
<p className="text-sm text-muted-foreground">{t('settings_no_projects')}</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{projects.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-2 border rounded-md hover:bg-muted/40">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{p.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{p.module} · {new Date(p.updatedAt).toLocaleString(locale === 'pt-BR' ? 'pt-BR' : 'en-US')}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={() => p.id && remove(p.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_about')}</CardTitle>
|
||||
@@ -210,10 +167,84 @@ const SettingsModule: React.FC = () => {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="gerenciador" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Arquivos (Gerenciador)</CardTitle>
|
||||
<CardDescription>Gerencie seus Projetos Completos e Templates de geometria salvos localmente.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-4 gap-2 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{projects.length} arquivo(s) armazenado(s).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-1.5 w-full sm:w-auto sm:flex">
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={handleImportClick} disabled={importing}>
|
||||
<Upload className="w-3.5 h-3.5 mr-1.5 shrink-0" /> Importar Arquivo
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={exportSnapshot}>
|
||||
<Save className="w-3.5 h-3.5 mr-1.5 shrink-0" /> Exportar Wind Snapshot
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">{t('common_loading')}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{projects.length === 0 && !loading && (
|
||||
<p className="text-sm text-muted-foreground">Nenhum projeto ou template salvo ainda.</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{projects.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-3 border rounded-md hover:bg-muted/40 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{p.type === 'projeto' ? (
|
||||
<div className="p-2 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md">
|
||||
<FileBox className="w-5 h-5" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 rounded-md">
|
||||
<FileJson className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium text-sm flex items-center gap-2">
|
||||
{p.name}
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4">
|
||||
{p.module}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
{p.type === 'projeto' ? 'Projeto (Vento + Geometria)' : 'Template (Apenas Geometria)'} · {new Date(p.updatedAt).toLocaleString(locale === 'pt-BR' ? 'pt-BR' : 'en-US')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost" title="Carregar" className="h-8 w-8 p-0 text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-950/50" onClick={() => handleLoadProject(p)}>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" title="Baixar JSON" className="h-8 w-8 p-0" onClick={() => handleDownloadJSON(p)}>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" title="Excluir" className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/50" onClick={() => p.id && remove(p.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="glossary" className="space-y-6">
|
||||
<Glossary />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="testes" className="space-y-6">
|
||||
<AuditPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileSelected} className="hidden" accept=".json" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface HydrationState {
|
||||
pendingLoad: {
|
||||
module: string;
|
||||
inputs: Record<string, unknown>;
|
||||
} | null;
|
||||
setPendingLoad: (module: string, inputs: Record<string, unknown>) => void;
|
||||
clearPendingLoad: () => void;
|
||||
}
|
||||
|
||||
export const useHydrationStore = create<HydrationState>((set) => ({
|
||||
pendingLoad: null,
|
||||
setPendingLoad: (module, inputs) => set({ pendingLoad: { module, inputs } }),
|
||||
clearPendingLoad: () => set({ pendingLoad: null }),
|
||||
}));
|
||||
Reference in New Issue
Block a user