221 lines
9.6 KiB
TypeScript
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; |