Ajustes e melhorias no Codex
This commit is contained in:
@@ -38,9 +38,21 @@ export function AreasView() {
|
|||||||
const [totalRegistros, setTotalRegistros] = useState(0);
|
const [totalRegistros, setTotalRegistros] = useState(0);
|
||||||
const [totalPaginas, setTotalPaginas] = useState(1);
|
const [totalPaginas, setTotalPaginas] = useState(1);
|
||||||
const [loadingList, setLoadingList] = useState(true);
|
const [loadingList, setLoadingList] = useState(true);
|
||||||
|
const normalizedAreas = useMemo(
|
||||||
|
() => new Set(areas.map((area) => area.trim().toLocaleLowerCase())),
|
||||||
|
[areas]
|
||||||
|
);
|
||||||
|
|
||||||
const totalPages = Math.max(1, totalPaginas);
|
const totalPages = Math.max(1, totalPaginas);
|
||||||
const isFiltering = filterNome.trim().length > 0;
|
const isFiltering = filterNome.trim().length > 0;
|
||||||
|
const createName = newName.trim();
|
||||||
|
const isCreateNameDuplicated = createName.length > 0 && normalizedAreas.has(createName.toLocaleLowerCase());
|
||||||
|
const editNameNormalized = editName.trim().toLocaleLowerCase();
|
||||||
|
const selectedAreaNormalized = selectedAreaItem?.nome.trim().toLocaleLowerCase();
|
||||||
|
const isEditNameDuplicated =
|
||||||
|
editName.trim().length > 0 &&
|
||||||
|
editNameNormalized !== selectedAreaNormalized &&
|
||||||
|
normalizedAreas.has(editNameNormalized);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -90,7 +102,7 @@ export function AreasView() {
|
|||||||
const name = newName.trim();
|
const name = newName.trim();
|
||||||
const description = newDescription.trim();
|
const description = newDescription.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
if (areas.includes(name)) {
|
if (normalizedAreas.has(name.toLocaleLowerCase())) {
|
||||||
toast.error("Já existe uma área com esse nome.");
|
toast.error("Já existe uma área com esse nome.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -146,7 +158,10 @@ export function AreasView() {
|
|||||||
}
|
}
|
||||||
const name = editName.trim();
|
const name = editName.trim();
|
||||||
const description = editDescription.trim();
|
const description = editDescription.trim();
|
||||||
if (name !== selectedAreaItem.nome && areas.includes(name)) {
|
if (
|
||||||
|
name.toLocaleLowerCase() !== selectedAreaItem.nome.trim().toLocaleLowerCase() &&
|
||||||
|
normalizedAreas.has(name.toLocaleLowerCase())
|
||||||
|
) {
|
||||||
toast.error("Já existe uma área com esse nome.");
|
toast.error("Já existe uma área com esse nome.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -412,7 +427,14 @@ export function AreasView() {
|
|||||||
onChange={(e) => setNewName(e.target.value)}
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
placeholder="Ex: Suporte"
|
placeholder="Ex: Suporte"
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
||||||
|
aria-invalid={isCreateNameDuplicated}
|
||||||
|
className={isCreateNameDuplicated ? "border-destructive focus-visible:ring-destructive" : undefined}
|
||||||
/>
|
/>
|
||||||
|
{isCreateNameDuplicated && (
|
||||||
|
<p className="mt-1 text-sm text-destructive">
|
||||||
|
Já existe uma área com esse nome. Escolha um nome diferente.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="new-area-desc">Descrição</Label>
|
<Label htmlFor="new-area-desc">Descrição</Label>
|
||||||
@@ -430,7 +452,7 @@ export function AreasView() {
|
|||||||
<Button variant="outline" onClick={() => setIsCreateOpen(false)} disabled={isCreatingArea}>
|
<Button variant="outline" onClick={() => setIsCreateOpen(false)} disabled={isCreatingArea}>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleCreate} disabled={!newName.trim() || areas.includes(newName.trim()) || isCreatingArea}>
|
<Button onClick={handleCreate} disabled={!newName.trim() || isCreateNameDuplicated || isCreatingArea}>
|
||||||
{isCreatingArea ? "Criando..." : "Criar"}
|
{isCreatingArea ? "Criando..." : "Criar"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -452,7 +474,14 @@ export function AreasView() {
|
|||||||
value={editName}
|
value={editName}
|
||||||
onChange={(e) => setEditName(e.target.value)}
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSaveEdit()}
|
onKeyDown={(e) => e.key === "Enter" && handleSaveEdit()}
|
||||||
|
aria-invalid={isEditNameDuplicated}
|
||||||
|
className={isEditNameDuplicated ? "border-destructive focus-visible:ring-destructive" : undefined}
|
||||||
/>
|
/>
|
||||||
|
{isEditNameDuplicated && (
|
||||||
|
<p className="mt-1 text-sm text-destructive">
|
||||||
|
Já existe uma área com esse nome. Escolha um nome diferente.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="edit-area-desc">Descrição</Label>
|
<Label htmlFor="edit-area-desc">Descrição</Label>
|
||||||
@@ -470,7 +499,7 @@ export function AreasView() {
|
|||||||
<Button variant="outline" onClick={() => setIsEditOpen(false)} disabled={isSavingEdit}>
|
<Button variant="outline" onClick={() => setIsEditOpen(false)} disabled={isSavingEdit}>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSaveEdit} disabled={!editName.trim() || isSavingEdit}>
|
<Button onClick={handleSaveEdit} disabled={!editName.trim() || isEditNameDuplicated || isSavingEdit}>
|
||||||
{isSavingEdit ? "Salvando..." : "Salvar"}
|
{isSavingEdit ? "Salvando..." : "Salvar"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ function formatarDataDetalhe(iso: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Evita exibir expressões n8n ($('...')) como nome de usuário. */
|
||||||
|
function textoAtualizadoPorValido(val: string | null | undefined): string | null {
|
||||||
|
const v = val?.trim();
|
||||||
|
if (!v) return null;
|
||||||
|
if (v.includes("$(") || v.includes("{{") || v.startsWith("$")) return null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textoUsuarioParecer(p: ParecerDetalhe): string {
|
||||||
|
const email = p.user_email?.trim();
|
||||||
|
const criado = p.criado_por?.trim();
|
||||||
|
return email || criado || "—";
|
||||||
|
}
|
||||||
|
|
||||||
interface ParecerPreviewState {
|
interface ParecerPreviewState {
|
||||||
titulo?: string;
|
titulo?: string;
|
||||||
conteudoMarkdown?: string;
|
conteudoMarkdown?: string;
|
||||||
@@ -83,7 +97,7 @@ export function ParecerJuridicoDetailView() {
|
|||||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const { areaItems } = usePrompts();
|
const { areaItems, refreshAreas } = usePrompts();
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [editTitulo, setEditTitulo] = useState("");
|
const [editTitulo, setEditTitulo] = useState("");
|
||||||
@@ -107,6 +121,25 @@ export function ParecerJuridicoDetailView() {
|
|||||||
const dragStartHeight = useRef(0);
|
const dragStartHeight = useRef(0);
|
||||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
/** Preenche título e área com os dados atuais do parecer (sempre que abrir o modal de edição). */
|
||||||
|
const aplicarDadosAtuaisNoFormularioEdicao = useCallback(() => {
|
||||||
|
if (!parecer) return;
|
||||||
|
setEditTitulo(parecer.titulo?.trim() ?? "");
|
||||||
|
setEditAreaId(parecer.area_id?.trim() ?? "");
|
||||||
|
}, [parecer]);
|
||||||
|
|
||||||
|
const openEditModal = useCallback(() => {
|
||||||
|
aplicarDadosAtuaisNoFormularioEdicao();
|
||||||
|
setEditOpen(true);
|
||||||
|
}, [aplicarDadosAtuaisNoFormularioEdicao]);
|
||||||
|
|
||||||
|
/** Fecha detalhes e abre edição já com título e área atuais. */
|
||||||
|
const openEditFromDetailsModal = useCallback(() => {
|
||||||
|
aplicarDadosAtuaisNoFormularioEdicao();
|
||||||
|
setDetailsOpen(false);
|
||||||
|
setEditOpen(true);
|
||||||
|
}, [aplicarDadosAtuaisNoFormularioEdicao]);
|
||||||
|
|
||||||
const MIN_INPUT_HEIGHT = 60;
|
const MIN_INPUT_HEIGHT = 60;
|
||||||
const MAX_INPUT_HEIGHT = 400;
|
const MAX_INPUT_HEIGHT = 400;
|
||||||
|
|
||||||
@@ -135,6 +168,10 @@ export function ParecerJuridicoDetailView() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshAreas();
|
||||||
|
}, [refreshAreas]);
|
||||||
|
|
||||||
const carregarChat = useCallback(
|
const carregarChat = useCallback(
|
||||||
async (silent = false) => {
|
async (silent = false) => {
|
||||||
if (!id?.trim()) return;
|
if (!id?.trim()) return;
|
||||||
@@ -255,11 +292,29 @@ export function ParecerJuridicoDetailView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const abrirModalEdicao = () => {
|
const handleDownloadAnexo = async () => {
|
||||||
if (!parecer) return;
|
if (!parecer?.anexo_url) return;
|
||||||
setEditTitulo(parecer.titulo ?? "");
|
try {
|
||||||
setEditAreaId(parecer.area_id ?? "");
|
const response = await fetch(parecer.anexo_url);
|
||||||
setEditOpen(true);
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erro ${response.status} ao baixar anexo.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const fallbackName = parecer.anexo_nome?.trim() || "anexo";
|
||||||
|
const disposition = response.headers.get("Content-Disposition");
|
||||||
|
const match = disposition?.match(/filename\*?=(?:UTF-8'')?"?([^";\n]+)"?/i) || disposition?.match(/filename="?([^";\n]+)"?/i);
|
||||||
|
const fileName = match ? decodeURIComponent(match[1].trim()) : fallbackName;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : "Erro ao baixar anexo.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSalvarEdicao = async () => {
|
const handleSalvarEdicao = async () => {
|
||||||
@@ -441,7 +496,7 @@ export function ParecerJuridicoDetailView() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
disabled={!parecer || !id || editSaving}
|
disabled={!parecer || !id || editSaving}
|
||||||
onClick={abrirModalEdicao}
|
onClick={openEditModal}
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4" />
|
<Pencil className="w-4 h-4" />
|
||||||
Editar
|
Editar
|
||||||
@@ -487,14 +542,14 @@ export function ParecerJuridicoDetailView() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||||
<DialogContent className="max-w-4xl w-[calc(100vw-2rem)] max-h-[90vh] flex flex-col gap-0 p-0 sm:max-w-4xl">
|
<DialogContent className="max-w-4xl w-[calc(100vw-2rem)] max-h-[90vh] flex flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl">
|
||||||
<DialogHeader className="px-6 pt-6 pb-2">
|
<DialogHeader className="shrink-0 px-6 pt-6 pb-2 pr-12">
|
||||||
<DialogTitle>Detalhes do parecer</DialogTitle>
|
<DialogTitle>Detalhes do parecer</DialogTitle>
|
||||||
<DialogDescription className="sr-only">Resumo do parecer</DialogDescription>
|
<DialogDescription className="sr-only">Resumo do parecer</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{parecer && (
|
{parecer && (
|
||||||
<ScrollArea className="max-h-[min(70vh,640px)] px-6 pb-6">
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6">
|
||||||
<div className="space-y-5 text-sm pr-4">
|
<div className="space-y-5 text-sm pr-1">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Label className="text-muted-foreground text-xs">Título</Label>
|
<Label className="text-muted-foreground text-xs">Título</Label>
|
||||||
@@ -507,13 +562,21 @@ export function ParecerJuridicoDetailView() {
|
|||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground text-xs">Área</Label>
|
<Label className="text-muted-foreground text-xs">Área</Label>
|
||||||
<p className="mt-1">
|
<p className="mt-1">
|
||||||
{areaItems.find((a) => a.id === parecer.area_id)?.nome ?? "—"}
|
{parecer.area_nome?.trim() ||
|
||||||
|
areaItems.find((a) => a.id === parecer.area_id)?.nome ||
|
||||||
|
"—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Label className="text-muted-foreground text-xs">Usuário</Label>
|
<Label className="text-muted-foreground text-xs">Usuário</Label>
|
||||||
<p className="mt-1 break-all">{parecer.user_email || parecer.criado_por || "—"}</p>
|
<p className="mt-1 break-all">{textoUsuarioParecer(parecer)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{textoAtualizadoPorValido(parecer.atualizado_por) && (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Label className="text-muted-foreground text-xs">Atualizado por</Label>
|
||||||
|
<p className="mt-1 break-all">{textoAtualizadoPorValido(parecer.atualizado_por)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground text-xs">Criado em</Label>
|
<Label className="text-muted-foreground text-xs">Criado em</Label>
|
||||||
<p className="mt-1">{formatarDataDetalhe(parecer.created_at)}</p>
|
<p className="mt-1">{formatarDataDetalhe(parecer.created_at)}</p>
|
||||||
@@ -522,6 +585,18 @@ export function ParecerJuridicoDetailView() {
|
|||||||
<Label className="text-muted-foreground text-xs">Atualizado em</Label>
|
<Label className="text-muted-foreground text-xs">Atualizado em</Label>
|
||||||
<p className="mt-1">{formatarDataDetalhe(parecer.updated_at)}</p>
|
<p className="mt-1">{formatarDataDetalhe(parecer.updated_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Label htmlFor="detalhes-instrucao" className="text-muted-foreground text-xs">
|
||||||
|
Instrução enviada
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="detalhes-instrucao"
|
||||||
|
readOnly
|
||||||
|
value={parecer.instrucao?.trim() ? parecer.instrucao : "—"}
|
||||||
|
className="mt-2 min-h-[120px] resize-y whitespace-pre-wrap text-sm leading-relaxed bg-muted/40 cursor-text"
|
||||||
|
aria-readonly="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Label className="text-muted-foreground text-xs">Anexo</Label>
|
<Label className="text-muted-foreground text-xs">Anexo</Label>
|
||||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
<div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
||||||
@@ -529,30 +604,59 @@ export function ParecerJuridicoDetailView() {
|
|||||||
{parecer.anexo_nome ?? (parecer.anexo_url ? "Arquivo anexado" : "—")}
|
{parecer.anexo_nome ?? (parecer.anexo_url ? "Arquivo anexado" : "—")}
|
||||||
</p>
|
</p>
|
||||||
{parecer.anexo_url ? (
|
{parecer.anexo_url ? (
|
||||||
<Button type="button" variant="outline" size="sm" className="w-fit shrink-0 gap-2" asChild>
|
<Button
|
||||||
<a href={parecer.anexo_url} target="_blank" rel="noreferrer" download>
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-fit shrink-0 gap-2"
|
||||||
|
onClick={() => void handleDownloadAnexo()}
|
||||||
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Download
|
Download
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-muted-foreground text-xs">Prompt utilizado</Label>
|
<Label htmlFor="detalhes-prompt" className="text-muted-foreground text-xs">
|
||||||
<div className="mt-2 rounded-md border bg-muted/40 p-4 max-h-56 overflow-y-auto whitespace-pre-wrap text-sm leading-relaxed">
|
Prompt utilizado
|
||||||
{parecer.prompt_conteudo || "—"}
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="detalhes-prompt"
|
||||||
|
readOnly
|
||||||
|
value={parecer.prompt_conteudo?.trim() ? parecer.prompt_conteudo : "—"}
|
||||||
|
className="mt-2 min-h-[160px] resize-y whitespace-pre-wrap text-sm leading-relaxed bg-muted/40 cursor-text"
|
||||||
|
aria-readonly="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
)}
|
||||||
|
{parecer && (
|
||||||
|
<DialogFooter className="shrink-0 border-t px-6 py-4 sm:justify-between gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setDetailsOpen(false)}>
|
||||||
|
Fechar
|
||||||
|
</Button>
|
||||||
|
<Button type="button" className="gap-2" onClick={openEditFromDetailsModal}>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
Editar parecer
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
<Dialog
|
||||||
<DialogContent className="max-w-md">
|
open={editOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setEditOpen(open);
|
||||||
|
if (open) {
|
||||||
|
aplicarDadosAtuaisNoFormularioEdicao();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-md" key={editOpen && parecer ? `edit-${parecer.id}` : "edit-closed"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Editar título e área</DialogTitle>
|
<DialogTitle>Editar título e área</DialogTitle>
|
||||||
<DialogDescription>Atualize os campos obrigatórios do parecer.</DialogDescription>
|
<DialogDescription>Atualize os campos obrigatórios do parecer.</DialogDescription>
|
||||||
@@ -585,12 +689,23 @@ export function ParecerJuridicoDetailView() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="sr-only">obrigatório</span>
|
<span className="sr-only">obrigatório</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={editAreaId || "none"} onValueChange={(v) => setEditAreaId(v === "none" ? "" : v)}>
|
<Select
|
||||||
|
value={editAreaId || "none"}
|
||||||
|
onValueChange={(v) => setEditAreaId(v === "none" ? "" : v)}
|
||||||
|
key={`area-select-${parecer?.id}-${editAreaId}`}
|
||||||
|
>
|
||||||
<SelectTrigger id="edit-area">
|
<SelectTrigger id="edit-area">
|
||||||
<SelectValue placeholder="Selecione a área" />
|
<SelectValue placeholder="Selecione a área" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">Selecione a área</SelectItem>
|
<SelectItem value="none">Selecione a área</SelectItem>
|
||||||
|
{parecer?.area_id &&
|
||||||
|
!areaItems.some((a) => a.id === parecer.area_id) &&
|
||||||
|
parecer.area_id.trim() !== "" && (
|
||||||
|
<SelectItem value={parecer.area_id}>
|
||||||
|
{parecer.area_nome?.trim() || `Área (${parecer.area_id.slice(0, 8)}…)`}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
{areaItems.map((a) => (
|
{areaItems.map((a) => (
|
||||||
<SelectItem key={a.id} value={a.id}>
|
<SelectItem key={a.id} value={a.id}>
|
||||||
{a.nome}
|
{a.nome}
|
||||||
@@ -716,10 +831,10 @@ export function ParecerJuridicoDetailView() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col md:flex-row min-h-0">
|
<div className="flex-1 flex flex-col md:flex-row min-h-0">
|
||||||
<section className="flex-1 min-w-0 flex flex-col border-b md:border-b-0 md:border-r border-border bg-white">
|
<section className="flex-1 min-w-0 flex flex-col border-b md:border-b-0 md:border-r border-border bg-background">
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="p-4 md:p-6 bg-white min-h-full">
|
<div className="p-4 md:p-6 bg-background min-h-full">
|
||||||
<article className="prose prose-sm prose-neutral dark:prose-invert max-w-none prose-headings:font-semibold prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-p:leading-relaxed prose-ul:my-3 prose-ol:my-3 prose-li:my-0.5">
|
<article className="prose prose-sm prose-neutral dark:prose-invert max-w-none text-foreground prose-headings:font-semibold prose-h1:text-xl prose-h2:text-lg prose-h3:text-base prose-p:leading-relaxed prose-ul:my-3 prose-ol:my-3 prose-li:my-0.5">
|
||||||
{conteudoMarkdown.trim() ? (
|
{conteudoMarkdown.trim() ? (
|
||||||
<ReactMarkdown>{conteudoMarkdown}</ReactMarkdown>
|
<ReactMarkdown>{conteudoMarkdown}</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function statusEstilo(status: string): { dot: string; text: string; label: strin
|
|||||||
|
|
||||||
export const ParecerJuridicoView = () => {
|
export const ParecerJuridicoView = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { areaItems } = usePrompts();
|
const { areaItems, refreshAreas } = usePrompts();
|
||||||
|
|
||||||
const [data, setData] = useState<ParecerListItem[]>([]);
|
const [data, setData] = useState<ParecerListItem[]>([]);
|
||||||
const [totalRegistros, setTotalRegistros] = useState(0);
|
const [totalRegistros, setTotalRegistros] = useState(0);
|
||||||
@@ -95,6 +95,10 @@ export const ParecerJuridicoView = () => {
|
|||||||
loadList();
|
loadList();
|
||||||
}, [loadList]);
|
}, [loadList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshAreas();
|
||||||
|
}, [refreshAreas]);
|
||||||
|
|
||||||
const handleItemsPerPageChange = (value: string) => {
|
const handleItemsPerPageChange = (value: string) => {
|
||||||
setPerPage(Number(value));
|
setPerPage(Number(value));
|
||||||
setPaginaAtual(1);
|
setPaginaAtual(1);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useMemo, useEffect } from "react";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FileText, Plus, Eye, Edit, Copy } from "lucide-react";
|
import { FileText, Plus, Eye, Edit, Copy, Trash2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { usePrompts } from "@/contexts/PromptsContext";
|
import { usePrompts } from "@/contexts/PromptsContext";
|
||||||
import type { Prompt } from "@/contexts/PromptsContext";
|
import type { Prompt } from "@/contexts/PromptsContext";
|
||||||
@@ -26,10 +27,12 @@ function apiItemToPrompt(item: { id: string; titulo: string; area_nome: string;
|
|||||||
|
|
||||||
export const PromptsView = () => {
|
export const PromptsView = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { prompts, setPrompts, areaItems } = usePrompts();
|
const { prompts, setPrompts, areaItems, refreshAreas } = usePrompts();
|
||||||
|
|
||||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||||
|
const [promptToDelete, setPromptToDelete] = useState<Prompt | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ export const PromptsView = () => {
|
|||||||
const [totalRegistros, setTotalRegistros] = useState(0);
|
const [totalRegistros, setTotalRegistros] = useState(0);
|
||||||
const [totalPaginas, setTotalPaginas] = useState(1);
|
const [totalPaginas, setTotalPaginas] = useState(1);
|
||||||
const [loadingList, setLoadingList] = useState(true);
|
const [loadingList, setLoadingList] = useState(true);
|
||||||
|
const [isDeletingPrompt, setIsDeletingPrompt] = useState(false);
|
||||||
|
|
||||||
const totalPages = Math.max(1, totalPaginas);
|
const totalPages = Math.max(1, totalPaginas);
|
||||||
|
|
||||||
@@ -88,6 +92,10 @@ export const PromptsView = () => {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [filterNome, filterAreaId]);
|
}, [filterNome, filterAreaId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshAreas();
|
||||||
|
}, [refreshAreas]);
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setFilterNome("");
|
setFilterNome("");
|
||||||
setFilterAreaId("");
|
setFilterAreaId("");
|
||||||
@@ -112,6 +120,35 @@ export const PromptsView = () => {
|
|||||||
navigate(`/codex/prompts/${selectedPrompt.id}`);
|
navigate(`/codex/prompts/${selectedPrompt.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (prompt: Prompt) => {
|
||||||
|
setPromptToDelete(prompt);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePrompt = async () => {
|
||||||
|
if (!promptToDelete) return;
|
||||||
|
setIsDeletingPrompt(true);
|
||||||
|
try {
|
||||||
|
await promptsService.deletar(promptToDelete.id);
|
||||||
|
setPromptsList((prev) => prev.filter((p) => p.id !== promptToDelete.id));
|
||||||
|
setPrompts((prev) => prev.filter((p) => p.id !== promptToDelete.id));
|
||||||
|
setTotalRegistros((prev) => Math.max(0, prev - 1));
|
||||||
|
if (selectedPrompt?.id === promptToDelete.id) {
|
||||||
|
setIsDetailsDialogOpen(false);
|
||||||
|
setSelectedPrompt(null);
|
||||||
|
}
|
||||||
|
toast.success("Prompt excluído com sucesso!");
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
setPromptToDelete(null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e && typeof e === "object" && "message" in e ? (e as { message: string }).message : "Erro ao excluir prompt.";
|
||||||
|
toast.error(msg);
|
||||||
|
} finally {
|
||||||
|
setIsDeletingPrompt(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
|
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
|
||||||
<div className="p-3 md:p-6 border-b border-border">
|
<div className="p-3 md:p-6 border-b border-border">
|
||||||
@@ -240,6 +277,15 @@ export const PromptsView = () => {
|
|||||||
>
|
>
|
||||||
<Edit className="w-3 h-3 md:w-4 md:h-4" />
|
<Edit className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => openDeleteDialog(prompt)}
|
||||||
|
title="Excluir"
|
||||||
|
className="h-7 w-7 md:h-9 md:w-9 text-destructive hover:text-destructive hover:bg-slate-300 dark:hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -365,6 +411,27 @@ export const PromptsView = () => {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Excluir prompt</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Deseja excluir o prompt "{promptToDelete?.titulo ?? ""}"? Esta ação não pode ser desfeita.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={isDeletingPrompt}>Cancelar</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDeletePrompt}
|
||||||
|
disabled={isDeletingPrompt || !promptToDelete}
|
||||||
|
className={(isDeletingPrompt || !promptToDelete) ? "opacity-50 cursor-not-allowed" : "bg-destructive text-destructive-foreground hover:bg-destructive/90"}
|
||||||
|
>
|
||||||
|
{isDeletingPrompt ? "Excluindo..." : "Excluir"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
|
||||||
import { areasService, type AreaItem } from "@/services/areas";
|
import { areasService, type AreaItem } from "@/services/areas";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ interface PromptsContextValue {
|
|||||||
areaIdByNome: AreaIdByNome;
|
areaIdByNome: AreaIdByNome;
|
||||||
areasLoading: boolean;
|
areasLoading: boolean;
|
||||||
areasError: string | null;
|
areasError: string | null;
|
||||||
|
refreshAreas: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromptsContext = createContext<PromptsContextValue | null>(null);
|
const PromptsContext = createContext<PromptsContextValue | null>(null);
|
||||||
@@ -48,11 +49,11 @@ export function PromptsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [areasLoading, setAreasLoading] = useState(true);
|
const [areasLoading, setAreasLoading] = useState(true);
|
||||||
const [areasError, setAreasError] = useState<string | null>(null);
|
const [areasError, setAreasError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshAreas = useCallback(async () => {
|
||||||
setAreasLoading(true);
|
setAreasLoading(true);
|
||||||
setAreasError(null);
|
setAreasError(null);
|
||||||
areasService.listarTotal()
|
try {
|
||||||
.then((data) => {
|
const data = await areasService.listarTotal();
|
||||||
const nomes = data.map((a) => a.nome).sort((a, b) => a.localeCompare(b));
|
const nomes = data.map((a) => a.nome).sort((a, b) => a.localeCompare(b));
|
||||||
const descricoes = Object.fromEntries(data.map((a) => [a.nome, a.descricao ?? ""]));
|
const descricoes = Object.fromEntries(data.map((a) => [a.nome, a.descricao ?? ""]));
|
||||||
const byNome = Object.fromEntries(data.map((a) => [a.nome, a.id]));
|
const byNome = Object.fromEntries(data.map((a) => [a.nome, a.id]));
|
||||||
@@ -60,18 +61,22 @@ export function PromptsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setAreaDescriptions(descricoes);
|
setAreaDescriptions(descricoes);
|
||||||
setAreaItems(data);
|
setAreaItems(data);
|
||||||
setAreaIdByNome(byNome);
|
setAreaIdByNome(byNome);
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
const message = (err as { message?: string })?.message ?? "Erro ao carregar áreas";
|
||||||
const message = err?.message ?? "Erro ao carregar áreas";
|
|
||||||
console.error("[PromptsContext] areasService.listarTotal falhou:", err);
|
console.error("[PromptsContext] areasService.listarTotal falhou:", err);
|
||||||
setAreasError(message);
|
setAreasError(message);
|
||||||
toast.error("Não foi possível carregar as áreas. Verifique a conexão e a chave da API.");
|
toast.error("Não foi possível carregar as áreas. Verifique a conexão e a chave da API.");
|
||||||
})
|
} finally {
|
||||||
.finally(() => setAreasLoading(false));
|
setAreasLoading(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshAreas();
|
||||||
|
}, [refreshAreas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptsContext.Provider value={{ prompts, setPrompts, areas, setAreas, areaDescriptions, setAreaDescriptions, areaItems, areaIdByNome, areasLoading, areasError }}>
|
<PromptsContext.Provider value={{ prompts, setPrompts, areas, setAreas, areaDescriptions, setAreaDescriptions, areaItems, areaIdByNome, areasLoading, areasError, refreshAreas }}>
|
||||||
{children}
|
{children}
|
||||||
</PromptsContext.Provider>
|
</PromptsContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ export interface ParecerDetalhe {
|
|||||||
user_email: string;
|
user_email: string;
|
||||||
titulo: string;
|
titulo: string;
|
||||||
area_id: string;
|
area_id: string;
|
||||||
prompt_id: string;
|
/** Nome da área vindo da API (evita depender só do cache de áreas no front). */
|
||||||
|
area_nome?: string | null;
|
||||||
|
prompt_id?: string | null;
|
||||||
prompt_conteudo: string;
|
prompt_conteudo: string;
|
||||||
instrucao: string;
|
instrucao: string;
|
||||||
conteudo_gerado: string;
|
conteudo_gerado: string;
|
||||||
@@ -84,6 +86,61 @@ export interface ParecerDetalhe {
|
|||||||
atualizado_por: string | null;
|
atualizado_por: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extrai o objeto de detalhe quando a API devolve array, objeto com `data`, ou mistura. */
|
||||||
|
function extrairParecerDetalheRaw(raw: unknown): Record<string, unknown> | null {
|
||||||
|
if (raw == null) return null;
|
||||||
|
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
const first = raw[0];
|
||||||
|
if (first == null) return null;
|
||||||
|
if (typeof first === "object" && !Array.isArray(first)) {
|
||||||
|
const o = first as Record<string, unknown>;
|
||||||
|
if (Array.isArray(o.data)) {
|
||||||
|
const row = o.data[0];
|
||||||
|
return row && typeof row === "object" && !Array.isArray(row) ? (row as Record<string, unknown>) : null;
|
||||||
|
}
|
||||||
|
if ("id" in o) return o;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof raw === "object") {
|
||||||
|
const o = raw as Record<string, unknown>;
|
||||||
|
if (Array.isArray(o.data)) {
|
||||||
|
const row = o.data[0];
|
||||||
|
return row && typeof row === "object" && !Array.isArray(row) ? (row as Record<string, unknown>) : null;
|
||||||
|
}
|
||||||
|
if (o.data != null && typeof o.data === "object" && !Array.isArray(o.data) && "id" in (o.data as object)) {
|
||||||
|
return o.data as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
if ("id" in o) return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizarParecerDetalhe(row: Record<string, unknown>): ParecerDetalhe {
|
||||||
|
return {
|
||||||
|
id: String(row.id ?? ""),
|
||||||
|
estabelecimento_id: Number(row.estabelecimento_id ?? 0),
|
||||||
|
user_email: String(row.user_email ?? ""),
|
||||||
|
titulo: String(row.titulo ?? ""),
|
||||||
|
area_id: String(row.area_id ?? ""),
|
||||||
|
area_nome: row.area_nome != null ? String(row.area_nome) : null,
|
||||||
|
prompt_id: row.prompt_id != null ? String(row.prompt_id) : null,
|
||||||
|
prompt_conteudo: String(row.prompt_conteudo ?? ""),
|
||||||
|
instrucao: String(row.instrucao ?? ""),
|
||||||
|
conteudo_gerado: String(row.conteudo_gerado ?? ""),
|
||||||
|
anexo_url: row.anexo_url != null ? String(row.anexo_url) : null,
|
||||||
|
anexo_nome: row.anexo_nome != null ? String(row.anexo_nome) : null,
|
||||||
|
status: String(row.status ?? ""),
|
||||||
|
created_at: String(row.created_at ?? ""),
|
||||||
|
updated_at: String(row.updated_at ?? ""),
|
||||||
|
criado_por: row.criado_por != null ? String(row.criado_por) : null,
|
||||||
|
atualizado_por: row.atualizado_por != null ? String(row.atualizado_por) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditarParecerTituloAreaBody {
|
export interface EditarParecerTituloAreaBody {
|
||||||
titulo: string;
|
titulo: string;
|
||||||
area_id: string;
|
area_id: string;
|
||||||
@@ -231,13 +288,12 @@ class ParecerService {
|
|||||||
if (!id?.trim()) {
|
if (!id?.trim()) {
|
||||||
throw new Error("ID do parecer é obrigatório");
|
throw new Error("ID do parecer é obrigatório");
|
||||||
}
|
}
|
||||||
const response = await apiService.get<ParecerDetalhe[] | ParecerDetalhe>(PARECER_DETALHE_URL(id));
|
const response = await apiService.get<unknown>(PARECER_DETALHE_URL(id));
|
||||||
const raw = response.data;
|
const row = extrairParecerDetalheRaw(response.data);
|
||||||
const item = Array.isArray(raw) ? raw[0] : raw;
|
if (!row?.id) {
|
||||||
if (!item || typeof item !== "object" || !("id" in item)) {
|
|
||||||
throw new Error("Parecer não encontrado");
|
throw new Error("Parecer não encontrado");
|
||||||
}
|
}
|
||||||
return item as ParecerDetalhe;
|
return normalizarParecerDetalhe(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enviarMensagemChat(
|
async enviarMensagemChat(
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ export interface EditarPromptErrorResponse {
|
|||||||
missing_fields?: string[];
|
missing_fields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeletarPromptSuccessResponse {
|
||||||
|
success: true;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeletarPromptErrorResponse {
|
||||||
|
success: false;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
class PromptsService {
|
class PromptsService {
|
||||||
async listar(params: ListarPromptsParams): Promise<ListarPromptsResponseBody> {
|
async listar(params: ListarPromptsParams): Promise<ListarPromptsResponseBody> {
|
||||||
const { page, per_page, titulo, area_id } = params;
|
const { page, per_page, titulo, area_id } = params;
|
||||||
@@ -181,6 +191,33 @@ class PromptsService {
|
|||||||
|
|
||||||
return data as EditarPromptSuccessResponse;
|
return data as EditarPromptSuccessResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deletar(id: string): Promise<DeletarPromptSuccessResponse> {
|
||||||
|
if (!id?.trim()) {
|
||||||
|
throw new Error("ID do prompt é obrigatório");
|
||||||
|
}
|
||||||
|
|
||||||
|
const idEnc = encodeURIComponent(id.trim());
|
||||||
|
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/4e6c2374-9c22-4c81-b558-45f0cfefa5c3/codex/parecer/prompt/deletar/${idEnc}`;
|
||||||
|
|
||||||
|
const response = await apiService.delete<
|
||||||
|
DeletarPromptSuccessResponse | DeletarPromptErrorResponse | (DeletarPromptSuccessResponse | DeletarPromptErrorResponse)[]
|
||||||
|
>(url);
|
||||||
|
|
||||||
|
const raw = response.data;
|
||||||
|
const data = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
|
||||||
|
if (!data || typeof data !== "object" || !("success" in data)) {
|
||||||
|
throw new Error("Resposta inválida ao excluir prompt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success === false) {
|
||||||
|
const err = data as DeletarPromptErrorResponse;
|
||||||
|
throw new Error(err.message ?? "Erro ao excluir prompt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as DeletarPromptSuccessResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const promptsService = new PromptsService();
|
export const promptsService = new PromptsService();
|
||||||
|
|||||||
Reference in New Issue
Block a user