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 { RefreshCwIcon, CopyIcon } from "lucide-react";
import { Button } from "../ui/Button"; import { Button } from "../ui/Button";
import { Input } from "../ui/Input"; import { Input } from "../ui/Input";
import { Select } from "../ui/Select"; import { Select } from "../ui/Select";
import { ResultDisplay } from "../ui/ResultDisplay"; import { ResultDisplay } from "../ui/ResultDisplay";
import { toast } from 'react-hot-toast'; import { toast } from "react-hot-toast";
type Parametro = export type Parametro =
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string } | { nome: string; tipo: "text" | "number" | "password"; placeholder?: string; default?: string }
| { nome: string; tipo: "select"; options: { label: string; value: string }[] }; | { nome: string; tipo: "select"; options: { label: string; value: string }[]; default?: string };
type GeradorCardProps = { export type GeradorCardProps = {
readonly titulo: string; readonly titulo: string;
readonly descricao?: string; readonly descricao?: string;
readonly parametros?: Parametro[]; readonly parametros?: Parametro[];
@@ -25,50 +26,92 @@ export default function GeradorCard({
onGerar, onGerar,
onValidar, onValidar,
}: GeradorCardProps) { }: GeradorCardProps) {
const [resultado, setResultado] = useState(""); // Inicializa paramInputs com defaults vindos de `parametros`
const [paramInputs, setParamInputs] = useState<Record<string, string>>({}); const initialParamInputs = parametros.reduce<Record<string, string>>((acc, p) => {
const [validado, setValidado] = useState<boolean | null>(null); debugger;
if (p.default) {
const handleInputChange = (nome: string, valor: string) => { acc[p.nome] = p.default;
setParamInputs((prev) => ({ ...prev, [nome]: valor })); } else {
}; acc[p.nome] = "";
const handleGerar = async () => {
const gerado = await onGerar(paramInputs);
setResultado(gerado);
setValidado(null);
};
const handleValidar = async () => {
if (onValidar) {
const isValid = await onValidar(resultado);
setValidado(isValid);
} }
}; 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; if (!resultado) return;
navigator.clipboard.writeText(resultado); try {
toast.success(`${titulo} copiado!`, await navigator.clipboard.writeText(resultado);
{ toast.success(`${titulo} copiado!`, { duration: 2000 });
duration: 3000 } 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); setResultado(valor);
setValidado(null); // resetar estado de validação ao editar setValidado(null); // reset ao editar
}; }, []);
return ( 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"> <div
{/* Título e descrição */} 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"> <div className="mb-6">
<h3 className="text-xl font-bold text-blue-700 dark:text-blue-400">{titulo}</h3> <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>} {descricao && <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{descricao}</p>}
</div> </div>
{/* Parâmetros dinâmicos */}
{parametros.length > 0 && ( {parametros.length > 0 && (
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">
{parametros.map((param) => { {parametros.map((param) => {
@@ -78,9 +121,10 @@ export default function GeradorCard({
key={param.nome} key={param.nome}
label={param.nome} label={param.nome}
options={param.options} options={param.options}
value={paramInputs[param.nome] || ""} value={paramInputs[param.nome] ?? ""}
onChange={(val) => handleInputChange(param.nome, val)} onChange={(val) => handleInputChange(param.nome, val)}
placeholder={`Selecionar ${param.nome}`} placeholder={`Selecionar ${param.nome}`}
aria-label={param.nome}
/> />
); );
} }
@@ -90,21 +134,23 @@ export default function GeradorCard({
key={param.nome} key={param.nome}
type={param.tipo} type={param.tipo}
placeholder={param.placeholder || param.nome} placeholder={param.placeholder || param.nome}
value={paramInputs[param.nome] || ""} value={paramInputs[param.nome] ?? ""}
onChange={(e) => handleInputChange(param.nome, e.target.value)} onChange={(e) => handleInputChange(param.nome, e.target.value)}
aria-label={param.nome}
/> />
); );
})} })}
</div> </div>
)} )}
{/* Resultado + botão de copiar como ícone */}
<div className="mb-6 flex items-center gap-2 relative"> <div className="mb-6 flex items-center gap-2 relative">
<ResultDisplay <ResultDisplay
value={resultado} value={resultado}
validated={validado} validated={validado}
onChange={handleResultadoChange} onChange={handleResultadoChange}
aria-label={`${titulo} resultado`}
/> />
{resultado && ( {resultado && (
<Button <Button
onClick={handleCopiar} onClick={handleCopiar}
@@ -112,25 +158,31 @@ export default function GeradorCard({
variant="secondary" variant="secondary"
className="flex items-center gap-1 px-2 py-1 text-xs h-auto" className="flex items-center gap-1 px-2 py-1 text-xs h-auto"
title="Copiar" title="Copiar"
> />
</Button>
)} )}
</div> </div>
{/* Botões distribuídos numa linha com preenchimento igual */}
<div className="flex w-full gap-x-2"> <div className="flex w-full gap-x-2">
<Button <Button
onClick={handleGerar} onClick={handleGerar}
icon={<RefreshCwIcon className="w-4 h-4" />} icon={<RefreshCwIcon className="w-4 h-4" />}
variant="primary" variant="primary"
className="flex-1" className="flex-1"
title={`Gerar ${titulo}`}
disabled={loading}
> >
Gerar {loading ? "A gerar..." : "Gerar"}
</Button> </Button>
{onValidar && ( {onValidar && (
<Button onClick={handleValidar} variant="danger" className="flex-1"> <Button
Validar onClick={handleValidar}
variant={validado === true ? "primary" : "danger"}
className="flex-1"
title={`Validar ${titulo}`}
disabled={loading || !resultado}
>
{loading ? "Verificando..." : "Validar"}
</Button> </Button>
)} )}
</div> </div>

View File

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

View File

@@ -1,18 +1,24 @@
const Utils = { const Utils = {
EnumToOptions, 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[] { export function EnumToOptions<T extends Record<string, string>>(enumObj: T): any[] {
return Object.entries(enumObj) 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]) => ({ .map(([key, value]) => ({
value: key, // A chave do enum como `value` value: key, // A chave do enum como `value`
label: value, // O valor do enum como `label` 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);
}