Fix Geradores

This commit is contained in:
2025-11-03 21:28:23 +00:00
parent 32315e179e
commit 0df0baf822
3 changed files with 123 additions and 65 deletions

View File

@@ -1,16 +1,17 @@
import { useState } from "react";
// src/components/Geradores/GeradorCard.tsx
import { useCallback, useState } from "react";
import { RefreshCwIcon, CopyIcon } from "lucide-react";
import { Button } from "../ui/Button";
import { Input } from "../ui/Input";
import { Select } from "../ui/Select";
import { ResultDisplay } from "../ui/ResultDisplay";
import { toast } from 'react-hot-toast';
import { toast } from "react-hot-toast";
type Parametro =
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string }
| { nome: string; tipo: "select"; options: { label: string; value: string }[] };
export type Parametro =
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string; default?: string }
| { nome: string; tipo: "select"; options: { label: string; value: string }[]; default?: string };
type GeradorCardProps = {
export type GeradorCardProps = {
readonly titulo: string;
readonly descricao?: string;
readonly parametros?: Parametro[];
@@ -25,50 +26,92 @@ export default function GeradorCard({
onGerar,
onValidar,
}: GeradorCardProps) {
const [resultado, setResultado] = useState("");
const [paramInputs, setParamInputs] = useState<Record<string, string>>({});
const [validado, setValidado] = useState<boolean | null>(null);
const handleInputChange = (nome: string, valor: string) => {
setParamInputs((prev) => ({ ...prev, [nome]: valor }));
};
const handleGerar = async () => {
const gerado = await onGerar(paramInputs);
setResultado(gerado);
setValidado(null);
};
const handleValidar = async () => {
if (onValidar) {
const isValid = await onValidar(resultado);
setValidado(isValid);
// Inicializa paramInputs com defaults vindos de `parametros`
const initialParamInputs = parametros.reduce<Record<string, string>>((acc, p) => {
debugger;
if (p.default) {
acc[p.nome] = p.default;
} else {
acc[p.nome] = "";
}
};
return acc;
}, {});
const handleCopiar = () => {
const [resultado, setResultado] = useState("");
const [paramInputs, setParamInputs] = useState<Record<string, string>>(initialParamInputs);
const [validado, setValidado] = useState<boolean | null>(null);
const [loading, setLoading] = useState(false);
const handleInputChange = useCallback((nome: string, valor: string) => {
setParamInputs((prev) => ({ ...prev, [nome]: valor }));
}, []);
const handleGerar = useCallback(async () => {
setLoading(true);
setValidado(null);
try {
const gerado = await onGerar(paramInputs);
setResultado(typeof gerado === "string" ? gerado : String(gerado ?? ""));
toast.success(`${titulo} gerado com sucesso.`, { duration: 2000 });
} catch (err) {
console.error("Erro ao gerar:", err);
toast.error("Falha ao gerar. Tente novamente.");
} finally {
setLoading(false);
}
}, [onGerar, paramInputs, titulo]);
const handleValidar = useCallback(async () => {
if (!onValidar) return;
if (!resultado) {
toast.error("Nenhum valor para validar.");
return;
}
setLoading(true);
try {
const isValid = await onValidar(resultado);
setValidado(Boolean(isValid));
if (isValid) {
toast.success("Válido", { duration: 1500 });
} else {
toast.error("Inválido", { duration: 1500 });
}
} catch (err) {
console.error("Erro ao validar:", err);
toast.error("Falha na validação. Tente novamente.");
setValidado(null);
} finally {
setLoading(false);
}
}, [onValidar, resultado]);
const handleCopiar = useCallback(async () => {
if (!resultado) return;
navigator.clipboard.writeText(resultado);
toast.success(`${titulo} copiado!`,
{
duration: 3000
});
};
try {
await navigator.clipboard.writeText(resultado);
toast.success(`${titulo} copiado!`, { duration: 2000 });
} catch (err) {
console.error("Erro ao copiar:", err);
toast.error("Não foi possível copiar para a área de transferência.");
}
}, [resultado, titulo]);
const handleResultadoChange = (valor: string) => {
const handleResultadoChange = useCallback((valor: string) => {
setResultado(valor);
setValidado(null); // resetar estado de validação ao editar
};
setValidado(null); // reset ao editar
}, []);
return (
<div className="w-full max-w-md rounded-2xl border border-gray-200 bg-white dark:bg-zinc-900 p-6 shadow-lg hover:shadow-xl transition-shadow duration-300">
{/* Título e descrição */}
<div
className="w-full max-w-md rounded-2xl border border-gray-200 bg-white dark:bg-zinc-900 p-6 shadow-lg hover:shadow-xl transition-shadow duration-300"
aria-busy={loading}
>
<div className="mb-6">
<h3 className="text-xl font-bold text-blue-700 dark:text-blue-400">{titulo}</h3>
{descricao && <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{descricao}</p>}
</div>
{/* Parâmetros dinâmicos */}
{parametros.length > 0 && (
<div className="space-y-4 mb-6">
{parametros.map((param) => {
@@ -78,9 +121,10 @@ export default function GeradorCard({
key={param.nome}
label={param.nome}
options={param.options}
value={paramInputs[param.nome] || ""}
value={paramInputs[param.nome] ?? ""}
onChange={(val) => handleInputChange(param.nome, val)}
placeholder={`Selecionar ${param.nome}`}
aria-label={param.nome}
/>
);
}
@@ -90,21 +134,23 @@ export default function GeradorCard({
key={param.nome}
type={param.tipo}
placeholder={param.placeholder || param.nome}
value={paramInputs[param.nome] || ""}
value={paramInputs[param.nome] ?? ""}
onChange={(e) => handleInputChange(param.nome, e.target.value)}
aria-label={param.nome}
/>
);
})}
</div>
)}
{/* Resultado + botão de copiar como ícone */}
<div className="mb-6 flex items-center gap-2 relative">
<ResultDisplay
value={resultado}
validated={validado}
onChange={handleResultadoChange}
aria-label={`${titulo} resultado`}
/>
{resultado && (
<Button
onClick={handleCopiar}
@@ -112,25 +158,31 @@ export default function GeradorCard({
variant="secondary"
className="flex items-center gap-1 px-2 py-1 text-xs h-auto"
title="Copiar"
>
</Button>
/>
)}
</div>
{/* Botões distribuídos numa linha com preenchimento igual */}
<div className="flex w-full gap-x-2">
<Button
onClick={handleGerar}
icon={<RefreshCwIcon className="w-4 h-4" />}
variant="primary"
className="flex-1"
title={`Gerar ${titulo}`}
disabled={loading}
>
Gerar
{loading ? "A gerar..." : "Gerar"}
</Button>
{onValidar && (
<Button onClick={handleValidar} variant="danger" className="flex-1">
Validar
<Button
onClick={handleValidar}
variant={validado === true ? "primary" : "danger"}
className="flex-1"
title={`Validar ${titulo}`}
disabled={loading || !resultado}
>
{loading ? "Verificando..." : "Validar"}
</Button>
)}
</div>

View File

@@ -1,5 +1,5 @@
import GeradorCard from "../GeradorCard";
import { EnumToOptions } from "../../../library/utils";
import GeradorCard, { type Parametro } from "../GeradorCard";
import { EnumToOptions, getEnumKeyByValue } from "../../../library/utils";
import { NIFType } from "../../../service/api";
import GeradorService from "../../../Api/GeradorApi";
@@ -9,31 +9,31 @@ const NifTypes = EnumToOptions(NIFType).map((opt) => ({
}));
const GenerateNIF = () => {
const handleGenerateNIF = async (params: Record<string, string>) => {
const tipoSelecionado = params["Tipo de NIF"];
const nif = await GeradorService.GenerateNIF(tipoSelecionado);
const handleGenerateNIF = async (params?: Record<string, string>) => {
const tipoSelecionado = params?.["Tipo de NIF"] ?? String(NIFType.PessoaSingular2);
const nif = String(await GeradorService.GenerateNIF(tipoSelecionado));
return nif;
};
const handleValidateNIF = async (valor: string) => {
// Lógica real de validação
const bol = await GeradorService.ValidateNIF(valor);
const bol = Boolean(await GeradorService.ValidateNIF(valor));
return bol;
};
const parametros: Parametro[] = [
{
nome: "Tipo de NIF",
tipo: "select",
options: NifTypes,
default: getEnumKeyByValue(NIFType, NIFType.PessoaSingular2)
},
];
return (
<div className="max-w-md mx-auto p-6">
<GeradorCard
titulo="NIF"
parametros={[
{
nome: "Tipo de NIF",
tipo: "select",
options: NifTypes,
},
]}
parametros={parametros}
onGerar={handleGenerateNIF}
onValidar={handleValidateNIF}
/>

View File

@@ -1,18 +1,24 @@
const Utils = {
EnumToOptions,
// Outras fun<75><6E>es podem ser adicionadas aqui
// Outras fun<75><6E>es podem ser adicionadas aqui
};
export function EnumToOptions<T extends Record<string, string>>(enumObj: T): any[] {
return Object.entries(enumObj)
.filter(([key]) => isNaN(Number(key))) // Ignora entradas num<75>ricas, caso existam
.filter(([key]) => isNaN(Number(key))) // Ignora entradas num<75>ricas, caso existam
.map(([key, value]) => ({
value: key, // A chave do enum como `value`
label: value, // O valor do enum como `label`
}));
}
export function getEnumKeyByValue<T extends Record<string, string>>(
enumObj: T,
value: string
): string | undefined {
return Object.keys(enumObj).find((key) => enumObj[key as keyof T] === value);
}