Files
BrainWind/app/src/pages/SettingsModule.tsx
T

221 lines
9.6 KiB
TypeScript

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 { 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 { useI18n } from '@/store/i18nStore';
import { Badge } from '@/components/ui/badge';
import {
importProjectFromText,
readProjectFile,
type ImportResult,
} from '@/lib/import-project';
import AuditPanel from '@/components/AuditPanel';
const SettingsModule: React.FC = () => {
const { theme, setTheme, effectiveTheme } = useTheme();
const { projects, loading, error, remove } = useProjects();
const { t, locale } = useI18n();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [importResult, setImportResult] = useState<ImportResult | null>(null);
const [importing, setImporting] = useState(false);
const exportSnapshot = () => {
const state = useWindStore.getState();
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'ventoapp-state.json');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
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);
} catch (e) {
setImportResult({
ok: false,
error: e instanceof Error ? e.message : 'Falha ao ler arquivo',
});
} finally {
setImporting(false);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
return (
<div className="p-6 max-w-5xl mx-auto overflow-auto">
<h1 className="text-3xl font-bold mb-6">{t('nav_settings')}</h1>
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="config">Configurações</TabsTrigger>
<TabsTrigger value="testes">
<FlaskConical className="w-4 h-4 mr-1.5" />
Testes
</TabsTrigger>
</TabsList>
<TabsContent value="config" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('settings_appearance')}</CardTitle>
<CardDescription>{t('settings_appearance_desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-1 sm:gap-2 w-full sm:w-fit sm:flex">
<Button variant={theme === 'light' ? 'default' : 'outline'} className="text-xs sm:text-sm px-1 sm:px-4 h-8 sm:h-9" onClick={() => setTheme('light')}>
<Sun className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1 sm:mr-2 shrink-0" /> {t('settings_theme_light')}
</Button>
<Button variant={theme === 'dark' ? 'default' : 'outline'} className="text-xs sm:text-sm px-1 sm:px-4 h-8 sm:h-9" onClick={() => setTheme('dark')}>
<Moon className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1 sm:mr-2 shrink-0" /> {t('settings_theme_dark')}
</Button>
<Button variant={theme === 'system' ? 'default' : 'outline'} className="text-xs sm:text-sm px-1 sm:px-4 h-8 sm:h-9" onClick={() => setTheme('system')}>
<Laptop className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-1 sm:mr-2 shrink-0" /> {t('settings_theme_system')}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-3">
{t('settings_effective')}: <Badge variant="outline">{effectiveTheme}</Badge>
</p>
</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>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<p><strong>{t('app_title')}</strong> {t('settings_about_desc')}</p>
<p className="text-muted-foreground">
{t('settings_stack')}: React 19 + TypeScript + Vite + Tailwind v4 + shadcn/ui + Zustand + R3F + @react-pdf/renderer.
</p>
<p className="text-muted-foreground">
Cobertura: 11 seções normativas + 3 anexos. Todos os 8 marcos do plano de implementação concluídos.
</p>
<p className="text-[10px] text-muted-foreground/60 mt-3 pt-2 border-t border-border/50 italic">
Desenvolvido por BrainSteel do Brasil - resp. Eng. Marcos A. Reifonas
</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="testes" className="space-y-6">
<AuditPanel />
</TabsContent>
</Tabs>
</div>
);
};
export default SettingsModule;