Fix Geradores
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user