diff --git a/src/components/areas/AreasView.tsx b/src/components/areas/AreasView.tsx index 7e33cf3..33ffae6 100644 --- a/src/components/areas/AreasView.tsx +++ b/src/components/areas/AreasView.tsx @@ -38,9 +38,21 @@ export function AreasView() { const [totalRegistros, setTotalRegistros] = useState(0); const [totalPaginas, setTotalPaginas] = useState(1); const [loadingList, setLoadingList] = useState(true); + const normalizedAreas = useMemo( + () => new Set(areas.map((area) => area.trim().toLocaleLowerCase())), + [areas] + ); const totalPages = Math.max(1, totalPaginas); 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(() => { let cancelled = false; @@ -90,7 +102,7 @@ export function AreasView() { const name = newName.trim(); const description = newDescription.trim(); if (!name) return; - if (areas.includes(name)) { + if (normalizedAreas.has(name.toLocaleLowerCase())) { toast.error("Já existe uma área com esse nome."); return; } @@ -146,7 +158,10 @@ export function AreasView() { } const name = editName.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."); return; } @@ -412,7 +427,14 @@ export function AreasView() { onChange={(e) => setNewName(e.target.value)} placeholder="Ex: Suporte" onKeyDown={(e) => e.key === "Enter" && handleCreate()} + aria-invalid={isCreateNameDuplicated} + className={isCreateNameDuplicated ? "border-destructive focus-visible:ring-destructive" : undefined} /> + {isCreateNameDuplicated && ( +

+ Já existe uma área com esse nome. Escolha um nome diferente. +

+ )}
@@ -430,7 +452,7 @@ export function AreasView() { - @@ -452,7 +474,14 @@ export function AreasView() { value={editName} onChange={(e) => setEditName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSaveEdit()} + aria-invalid={isEditNameDuplicated} + className={isEditNameDuplicated ? "border-destructive focus-visible:ring-destructive" : undefined} /> + {isEditNameDuplicated && ( +

+ Já existe uma área com esse nome. Escolha um nome diferente. +

+ )}
@@ -470,7 +499,7 @@ export function AreasView() { - diff --git a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx index c5cd0c6..0ecb75e 100644 --- a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx +++ b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx @@ -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 { titulo?: string; conteudoMarkdown?: string; @@ -83,7 +97,7 @@ export function ParecerJuridicoDetailView() { const [detailsOpen, setDetailsOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); - const { areaItems } = usePrompts(); + const { areaItems, refreshAreas } = usePrompts(); const [editOpen, setEditOpen] = useState(false); const [editTitulo, setEditTitulo] = useState(""); @@ -107,6 +121,25 @@ export function ParecerJuridicoDetailView() { const dragStartHeight = useRef(0); const chatEndRef = useRef(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 MAX_INPUT_HEIGHT = 400; @@ -135,6 +168,10 @@ export function ParecerJuridicoDetailView() { .finally(() => setLoading(false)); }, [id]); + useEffect(() => { + refreshAreas(); + }, [refreshAreas]); + const carregarChat = useCallback( async (silent = false) => { if (!id?.trim()) return; @@ -255,11 +292,29 @@ export function ParecerJuridicoDetailView() { } }; - const abrirModalEdicao = () => { - if (!parecer) return; - setEditTitulo(parecer.titulo ?? ""); - setEditAreaId(parecer.area_id ?? ""); - setEditOpen(true); + const handleDownloadAnexo = async () => { + if (!parecer?.anexo_url) return; + try { + const response = await fetch(parecer.anexo_url); + 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 () => { @@ -441,7 +496,7 @@ export function ParecerJuridicoDetailView() { size="sm" className="gap-2" disabled={!parecer || !id || editSaving} - onClick={abrirModalEdicao} + onClick={openEditModal} > Editar @@ -487,14 +542,14 @@ export function ParecerJuridicoDetailView() { - - + + Detalhes do parecer Resumo do parecer {parecer && ( - -
+
+
@@ -507,13 +562,21 @@ export function ParecerJuridicoDetailView() {

- {areaItems.find((a) => a.id === parecer.area_id)?.nome ?? "—"} + {parecer.area_nome?.trim() || + areaItems.find((a) => a.id === parecer.area_id)?.nome || + "—"}

-

{parecer.user_email || parecer.criado_por || "—"}

+

{textoUsuarioParecer(parecer)}

+ {textoAtualizadoPorValido(parecer.atualizado_por) && ( +
+ +

{textoAtualizadoPorValido(parecer.atualizado_por)}

+
+ )}

{formatarDataDetalhe(parecer.created_at)}

@@ -522,6 +585,18 @@ export function ParecerJuridicoDetailView() {

{formatarDataDetalhe(parecer.updated_at)}

+
+ +