[MVPTN-90] refactor(Correção): corrigindo observações passadas relacionadas a utilizar Enum de situações, retirar trycatch, arrumando coluna de situação com badges

This commit is contained in:
= 2025-10-03 13:48:49 -03:00
parent 727e665db4
commit 66f285ba74
11 changed files with 331 additions and 34 deletions

11
package-lock.json generated
View file

@ -27,6 +27,7 @@
"clsx": "^2.1.1",
"cookies-next": "^6.1.0",
"faker-js": "^1.0.0",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
@ -2238,6 +2239,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",

View file

@ -28,6 +28,7 @@
"clsx": "^2.1.1",
"cookies-next": "^6.1.0",
"faker-js": "^1.0.0",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",

View file

@ -12,8 +12,8 @@ import { useTTBReconhecimentoTipoReadHook } from "../../_hooks/t_tb_reconhecimen
import { useTTBReconhecimentoTipoSaveHook } from "../../_hooks/t_tb_reconhecimentotipo/useTTBReconhecimentoTipoSaveHook";
import { useTTBReconhecimentoTipoDeleteHook } from "../../_hooks/t_tb_reconhecimentotipo/useTTBReconhecimentoTipoDeleteHook";
import ConfirmDialog from "@/app/_components/confirm_dialog/ConfirmDialog";
import { useConfirmDialog } from "@/app/_components/confirm_dialog/useConfirmDialog";
import ConfirmExclusion from "@/app/_components/confirm_exclusion/ConfirmExclusion";
import { useConfirmExclusion } from "@/app/_components/confirm_exclusion/useConfirmExclusion";
import TTBReconhecimentoTipoInterface from "../../_interfaces/TTBReconhecimentoTipoInterface";
@ -32,14 +32,26 @@ export default function TTBAndamentoServico() {
const [itemToDelete, setItemToDelete] = useState<TTBReconhecimentoTipoInterface | null>(null);
/**
* Hook do modal de confirmação
* Hook do modal de exclusão com OTP
*/
const {
isOpen: isConfirmOpen,
openDialog: openConfirmDialog,
handleConfirm,
handleCancel,
} = useConfirmDialog();
code,
isValid,
handleChange,
} = useConfirmExclusion({
expectedCode: "123456", // 🔑 aqui você define o código esperado
onConfirm: () => {
console.log("Confirmação aceita com código OTP válido");
},
onCancel: () => {
console.log("Ação cancelada");
},
});
/**
* Abre o formulário no modo de edição ou criação
@ -78,7 +90,7 @@ export default function TTBAndamentoServico() {
// Define o item atual para remoção
setItemToDelete(item);
// Abre o modal de confirmação
// Abre o modal de exclusão
openConfirmDialog();
}, [openConfirmDialog]);
@ -105,6 +117,10 @@ export default function TTBAndamentoServico() {
}, [itemToDelete, fetchTTBReconhecimentosTipos, handleCancel]);
const handleResendCode = (async () => {
return alert("Reenviando código de exclusão")
})
/**
* Busca inicial dos dados
*/
@ -140,18 +156,20 @@ export default function TTBAndamentoServico() {
</CardContent>
</Card>
{/* Modal de confirmação */}
<ConfirmDialog
{/* Modal de confirmação de exclusão com OTP */}
<ConfirmExclusion
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o andamento "${itemToDelete?.descricao}"?`}
confirmText="Sim, excluir"
description="Digite o código de exclusão para confirmar:"
confirmText="Continuar"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
expectedCode={"123456"}
onResendCode={handleResendCode}
/>
{/* Formulário de criação/edição */}
<TTBReconhecimentoTipoForm
isOpen={isFormOpen}

View file

@ -29,6 +29,7 @@ import { Label } from "@/components/ui/label";
import { TCensecTipoAtoSchema } from "../../_schemas/TCensecTipoAtoSchema";
import { TCensecTipoAtoInterface } from "../../_interfaces/TCensecTipoAtoInterface";
import TCensecInterface from "../../_interfaces/TCensecInterface";
import { SituacoesEnum } from "@/enums/SituacoesEnum";
type FormValues = z.infer<typeof TCensecTipoAtoSchema>;
@ -55,7 +56,7 @@ export default function TCensecTipoAtoForm({
censec_id: 0,
codigo: null,
descricao: "",
situacao: "A",
situacao: SituacoesEnum.A,
tipo_separacao: null,
tipo_revogacao: null,
},
@ -105,17 +106,17 @@ export default function TCensecTipoAtoForm({
name="censec_id"
render={({ field }) => (
<FormItem>
<FormLabel>Censec</FormLabel>
<FormLabel>Central do Censec</FormLabel>
<Select
value={field.value?.toString()}
onValueChange={(val) => field.onChange(Number(val))}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecione um Censec" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectContent className="w-full">
{tCensec.map((c) => (
<SelectItem key={c.censec_id} value={String(c.censec_id)}>
{c.descricao}
@ -128,6 +129,7 @@ export default function TCensecTipoAtoForm({
)}
/>
{/* Situação */}
<Controller
name="situacao"
@ -155,11 +157,11 @@ export default function TCensecTipoAtoForm({
value={field.value ?? ""}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectContent className="w-full">
<SelectItem value="S">Sim</SelectItem>
<SelectItem value="N">Não</SelectItem>
</SelectContent>
@ -181,11 +183,11 @@ export default function TCensecTipoAtoForm({
value={field.value ?? ""}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectContent className="w-full">
<SelectItem value="S">Sim</SelectItem>
<SelectItem value="N">Não</SelectItem>
</SelectContent>

View file

@ -28,6 +28,25 @@ interface TCensecTipoAtoTableProps {
onDelete: (item: TCensecTipoAtoInterface, isEditingFormStatus: boolean) => void;
}
function StatusBadge({ situacao }: { situacao: string }) {
const isActive = situacao === "A";
const baseClasses =
"text-xs font-medium px-2.5 py-0.5 rounded-sm me-2";
const activeClasses =
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
const inactiveClasses =
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
return (
<span className={`${baseClasses} ${isActive ? activeClasses : inactiveClasses}`}>
{isActive ? "Ativo" : "Inativo"}
</span>
);
}
export default function TCensecTipoAtoTable({
data,
tCensec,
@ -39,10 +58,10 @@ export default function TCensecTipoAtoTable({
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>CENSEC</TableHead>
<TableHead>Situação</TableHead>
<TableHead>Central do CENSEC</TableHead>
{/*<TableHead>Código</TableHead>*/}
<TableHead>Descrição</TableHead>
<TableHead>Situação</TableHead>
<TableHead>Separação</TableHead>
<TableHead>Revogação</TableHead>
<TableHead className="text-right">Ações</TableHead>
@ -57,12 +76,12 @@ export default function TCensecTipoAtoTable({
<TableCell className="font-medium">
{item.censec_tipoato_id}
</TableCell>
<TableCell>
<StatusBadge situacao={item.situacao} />
</TableCell>
<TableCell>{censecDesc}</TableCell>
{/*<TableCell>{item.codigo ?? "-"}</TableCell>*/}
<TableCell>{item.descricao}</TableCell>
<TableCell>
{item.situacao === "A" ? "Ativo" : "Inativo"}
</TableCell>
<TableCell>{item.tipo_separacao ?? "-"}</TableCell>
<TableCell>{item.tipo_revogacao ?? "-"}</TableCell>
<TableCell className="text-right">

View file

@ -1,11 +1,12 @@
import { z } from "zod";
import { SituacoesEnum } from "@/enums/SituacoesEnum";
export const TCensecTipoAtoSchema = z.object({
censec_tipoato_id: z.number().int(),
censec_id: z.number().int(),
censec_id: z.number("Precisa referir a qual Central do Censec pertence").int(),
codigo: z.number().int().nullable(),
descricao: z.string(),
situacao: z.enum(["A", "I"]),
descricao: z.string().min(1, "Campo descrição deve ser preenchido"),
situacao: z.enum(SituacoesEnum, { message: "Tipo inválido" }),
tipo_separacao: z.enum(["S", "N"]).nullable(),
tipo_revogacao: z.enum(["S", "N"]).nullable(),
});

View file

@ -3,13 +3,8 @@ import { TCensecTipoAtoIndexData } from "../../_data/TCensecTipoAto/GMedidaTipoI
async function executeTCensecTipoAtoIndexService() {
try {
const response = await TCensecTipoAtoIndexData();
return response;
} catch (error) {
console.log(error)
return error
}
}
export const TCensecTipoAtoIndexService = withClientErrorHandler(executeTCensecTipoAtoIndexService)

View file

@ -6,8 +6,6 @@ async function executeTCensecTipoAtoSaveService(data: TCensecTipoAtoInterface) {
const response = await TCensecTipoAtoSaveData(data);
console.log('GTBRegimeComunhaoSaveData', response)
return response;
}

View file

@ -0,0 +1,124 @@
'use client';
import React, { useState } from 'react';
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogAction,
} from '@/components/ui/alert-dialog';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
interface ConfirmExclusionProps {
isOpen: boolean;
title: string;
description?: string;
expectedCode: string;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
onCancel: () => void;
onResendCode: () => void; // Função para reenviar o código
}
export default function ConfirmExclusion({
isOpen,
title,
description,
expectedCode,
confirmText = 'Continuar',
cancelText = 'Cancelar',
onConfirm,
onCancel,
onResendCode, // A função para reenvio do código
}: ConfirmExclusionProps) {
const [code, setCode] = useState('');
const [isValid, setIsValid] = useState(false);
const [isResending, setIsResending] = useState(false); // Novo estado para controle de envio do código
const handleChange = (value: string) => {
setCode(value);
setIsValid(value === expectedCode);
};
const handleResendCode = async () => {
setIsResending(true);
try {
await onResendCode(); // Chamando a função de reenvio
} catch (error) {
console.error("Erro ao reenviar código", error);
} finally {
setIsResending(false); // Resetando o estado de envio
}
};
return (
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
<AlertDialogContent className="max-w-lg mx-auto p-6">
<AlertDialogHeader className="text-center">
<AlertDialogTitle className="text-xl font-semibold text-center">{title}</AlertDialogTitle>
{description && (
<AlertDialogDescription className="mt-2 text-sm text-muted-foreground text-center">
{description}
</AlertDialogDescription>
)}
</AlertDialogHeader>
<div className="py-4 text-sm text-muted-foreground space-y-4">
<div className="flex justify-center">
<InputOTP
maxLength={expectedCode.length}
value={code}
onChange={handleChange}
>
<InputOTPGroup className="flex gap-4">
{expectedCode.split('').map((_, index) => (
<InputOTPSlot
key={index}
index={index}
className="w-12 h-12 text-center text-lg border-2 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
))}
</InputOTPGroup>
</InputOTP>
</div>
{/* Botão "Reenviar Código" */}
<div className="flex justify-center items-center text-center gap-2 mt-4">
<AlertDialogDescription>
Não recebeu o código?
</AlertDialogDescription>
<button
onClick={handleResendCode}
className={`text-blue-500 font-semibold cursor-pointer ${isResending ? 'cursor-not-allowed text-gray-400' : 'hover:text-blue-600'}`}
disabled={isResending} // Desabilita o botão enquanto o código está sendo reenviado
>
{isResending ? 'Enviando...' : 'Reenviar Código'}
</button>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel} className="text-gray-600 hover:text-gray-800">
{cancelText}
</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
disabled={!isValid}
>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View file

@ -0,0 +1,51 @@
import { useCallback, useState } from "react";
interface UseConfirmExclusionOptions {
onConfirm?: () => void;
onCancel?: () => void;
expectedCode: string; // código que o usuário precisa digitar
}
export function useConfirmExclusion({ onConfirm, onCancel, expectedCode }: UseConfirmExclusionOptions) {
const [isOpen, setIsOpen] = useState(false);
const [code, setCode] = useState("");
const [isValid, setIsValid] = useState(false);
const openDialog = useCallback(() => setIsOpen(true), []);
const closeDialog = useCallback(() => {
setIsOpen(false);
setCode(""); // limpa o código quando fecha
setIsValid(false);
}, []);
const handleConfirm = useCallback(() => {
if (isValid) {
onConfirm?.();
closeDialog();
}
}, [isValid, onConfirm, closeDialog]);
const handleCancel = useCallback(() => {
onCancel?.();
closeDialog();
}, [onCancel, closeDialog]);
const handleChange = useCallback(
(value: string) => {
setCode(value);
setIsValid(value === expectedCode);
},
[expectedCode]
);
return {
isOpen,
code,
isValid,
openDialog,
closeDialog,
handleConfirm,
handleCancel,
handleChange,
};
}

View file

@ -0,0 +1,77 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
)
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
)
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
)
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }