580 lines
28 KiB
TypeScript
580 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { apiPostFormData, apiPostJson, ApiError } from "@/lib/api/client";
|
|
|
|
type SolicitacaoDetalhe = {
|
|
id: string;
|
|
tipo: string;
|
|
tipo_display: string;
|
|
status: string;
|
|
status_display: string;
|
|
criado_em: string | null;
|
|
enviada_em: string | null;
|
|
finalizada_em: string | null;
|
|
solicitante: {
|
|
matricula: string | null;
|
|
nome: string | null;
|
|
} | null;
|
|
funcionario: {
|
|
id: string;
|
|
id_rm: string | null;
|
|
matricula: string | null;
|
|
nome: string | null;
|
|
cpf: string | null;
|
|
data_admissao: string | null;
|
|
situacao: string | null;
|
|
cargo: string | null;
|
|
setor: string | null;
|
|
centro_custo: string | null;
|
|
cod_funcao: string | null;
|
|
salario: string | null;
|
|
cod_sindicato: string | null;
|
|
saldo_banco_horas_minutos: number | null;
|
|
inicio_periodo_banco_horas: string | null;
|
|
fim_periodo_banco_horas: string | null;
|
|
sincronizado_em: string | null;
|
|
matricula_winthor: string | null;
|
|
} | null;
|
|
dados_winthor: {
|
|
basicos: { matricula: string | null; nome: string | null; cpf: string | null };
|
|
admissao: { admissao: string | null; situacao: string | null; dt_exclusao: string | null };
|
|
endereco: { endereco: string | null; bairro: string | null; cidade: string | null; estado: string | null };
|
|
} | null;
|
|
detalhes_tipo: {
|
|
tipo: string;
|
|
[key: string]: unknown;
|
|
} | null;
|
|
acoes: {
|
|
pode_aprovar: boolean;
|
|
pode_dar_parecer: boolean;
|
|
pode_enviar: boolean;
|
|
is_solicitante: boolean;
|
|
};
|
|
pareceres: Array<{
|
|
id: string;
|
|
etapa: string;
|
|
etapa_display: string;
|
|
texto: string;
|
|
criado_em: string | null;
|
|
usuario_nome: string | null;
|
|
anexo_url: string | null;
|
|
}>;
|
|
aprovacoes: Array<{
|
|
id: string;
|
|
etapa: string;
|
|
etapa_display: string;
|
|
decisao: string;
|
|
decisao_display: string;
|
|
justificativa: string;
|
|
decidido_em: string | null;
|
|
usuario_nome: string | null;
|
|
}>;
|
|
};
|
|
|
|
const DJANGO_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8888";
|
|
|
|
function toAbsoluteDjangoUrl(pathOrUrl: string) {
|
|
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
return pathOrUrl;
|
|
}
|
|
return `${DJANGO_BASE_URL}${pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`}`;
|
|
}
|
|
|
|
function formatarData(iso: string | null) {
|
|
if (!iso) return "—";
|
|
try {
|
|
return new Date(iso).toLocaleString("pt-BR");
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function formatarDinheiro(valor: string | null) {
|
|
if (!valor) return "—";
|
|
const numero = Number(valor);
|
|
if (Number.isNaN(numero)) return valor;
|
|
return numero.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
}
|
|
|
|
// #region agent log
|
|
function debugLog(hypothesisId: string, message: string, data: Record<string, unknown>) {
|
|
fetch("http://localhost:7687/ingest/ee56ea93-ad22-4673-ab77-595d83a9b3c5", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Debug-Session-Id": "6b638a",
|
|
},
|
|
body: JSON.stringify({
|
|
sessionId: "6b638a",
|
|
runId: "detail-permission-debug",
|
|
hypothesisId,
|
|
location: "frontend/app/solicitacoes/[id]/page.tsx",
|
|
message,
|
|
data,
|
|
timestamp: Date.now(),
|
|
}),
|
|
}).catch(() => {});
|
|
}
|
|
// #endregion
|
|
|
|
export default function SolicitacaoDetalhePage() {
|
|
const router = useRouter();
|
|
const params = useParams<{ id: string }>();
|
|
const solicitacaoId = useMemo(() => String(params?.id || ""), [params]);
|
|
|
|
const [data, setData] = useState<SolicitacaoDetalhe | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
const [actionFeedback, setActionFeedback] = useState("");
|
|
const [parecerTexto, setParecerTexto] = useState("");
|
|
const [parecerAnexo, setParecerAnexo] = useState<File | null>(null);
|
|
const [decisao, setDecisao] = useState<"APROVADO" | "REPROVADO">("APROVADO");
|
|
const [justificativaDecisao, setJustificativaDecisao] = useState("");
|
|
const [reloadTick, setReloadTick] = useState(0);
|
|
|
|
useEffect(() => {
|
|
async function carregarDetalhe() {
|
|
if (!solicitacaoId) {
|
|
setError("Solicitação inválida.");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setError("");
|
|
|
|
try {
|
|
// #region agent log
|
|
debugLog("H1_H2", "detail_fetch_start", {
|
|
solicitacaoId,
|
|
endpoint: `/api/solicitacoes/${solicitacaoId}/`,
|
|
});
|
|
// #endregion
|
|
const res = await fetch(`/api/solicitacoes/${solicitacaoId}/`, {
|
|
credentials: "include",
|
|
});
|
|
// #region agent log
|
|
let responsePreview = "";
|
|
try {
|
|
responsePreview = (await res.clone().text()).slice(0, 300);
|
|
} catch {
|
|
responsePreview = "unreadable_response";
|
|
}
|
|
debugLog("H1_H3_H4", "detail_fetch_response", {
|
|
solicitacaoId,
|
|
status: res.status,
|
|
ok: res.ok,
|
|
responsePreview,
|
|
});
|
|
// #endregion
|
|
|
|
if (res.status === 401) {
|
|
// #region agent log
|
|
debugLog("H5", "detail_fetch_unauthorized_redirect", {
|
|
solicitacaoId,
|
|
});
|
|
// #endregion
|
|
router.push(`/tela_login?next=${encodeURIComponent(`/solicitacoes/${solicitacaoId}`)}`);
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
if (res.status === 404) {
|
|
setError("Solicitação não encontrada ou sem permissão.");
|
|
} else {
|
|
setError("Erro ao carregar detalhes da solicitação.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const json = (await res.json()) as SolicitacaoDetalhe;
|
|
setData(json);
|
|
} catch {
|
|
setError("Erro de conexão ao carregar detalhes.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
carregarDetalhe();
|
|
}, [router, solicitacaoId, reloadTick]);
|
|
|
|
async function handleEnviar() {
|
|
if (!data) return;
|
|
setActionLoading(true);
|
|
setActionFeedback("");
|
|
try {
|
|
await apiPostJson(`/api/solicitacoes/${data.id}/enviar/`, {});
|
|
setActionFeedback("Solicitação enviada para aprovação.");
|
|
setReloadTick((v) => v + 1);
|
|
} catch (err) {
|
|
setActionFeedback(err instanceof ApiError ? err.message : "Erro ao enviar solicitação.");
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleParecer() {
|
|
if (!data) return;
|
|
if (!parecerTexto.trim()) {
|
|
setActionFeedback("Digite o texto do parecer.");
|
|
return;
|
|
}
|
|
setActionLoading(true);
|
|
setActionFeedback("");
|
|
try {
|
|
const fd = new FormData();
|
|
fd.append("texto", parecerTexto.trim());
|
|
if (parecerAnexo) fd.append("anexo", parecerAnexo);
|
|
await apiPostFormData(`/api/solicitacoes/${data.id}/parecer/`, fd);
|
|
setParecerTexto("");
|
|
setParecerAnexo(null);
|
|
setActionFeedback("Parecer registrado com sucesso.");
|
|
setReloadTick((v) => v + 1);
|
|
} catch (err) {
|
|
setActionFeedback(err instanceof ApiError ? err.message : "Erro ao registrar parecer.");
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleDecisao() {
|
|
if (!data) return;
|
|
if (decisao === "REPROVADO" && !justificativaDecisao.trim()) {
|
|
setActionFeedback("Justificativa é obrigatória para reprovação.");
|
|
return;
|
|
}
|
|
setActionLoading(true);
|
|
setActionFeedback("");
|
|
try {
|
|
await apiPostJson(`/api/solicitacoes/${data.id}/decidir/`, {
|
|
decisao,
|
|
justificativa: justificativaDecisao,
|
|
});
|
|
setActionFeedback("Decisão registrada com sucesso.");
|
|
setJustificativaDecisao("");
|
|
setReloadTick((v) => v + 1);
|
|
} catch (err) {
|
|
setActionFeedback(err instanceof ApiError ? err.message : "Erro ao registrar decisão.");
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-100">
|
|
<p className="text-slate-600">Carregando detalhes…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-100 p-6">
|
|
<div className="max-w-4xl mx-auto bg-white border border-slate-200 rounded-xl p-6">
|
|
<p className="text-red-600 font-medium">{error || "Falha ao carregar detalhes."}</p>
|
|
<div className="mt-4">
|
|
<Link href="/dashboard" className="text-blue-600 font-semibold hover:underline">
|
|
Voltar para dashboard
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-100 p-4 md:p-6">
|
|
<div className="max-w-5xl mx-auto space-y-4 text-slate-900">
|
|
<div className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
<h1 className="text-xl md:text-2xl font-bold text-slate-900">
|
|
{data.tipo_display}
|
|
</h1>
|
|
<span className="inline-flex py-1 px-2.5 rounded-full text-xs font-bold border bg-slate-100 text-slate-700 border-slate-200">
|
|
{data.status_display}
|
|
</span>
|
|
</div>
|
|
<p className="text-base text-slate-800 mt-2 font-medium">
|
|
Solicitação: <strong>{data.id}</strong>
|
|
</p>
|
|
<div className="mt-4 flex flex-wrap gap-4 text-base text-slate-800 font-medium">
|
|
<span>Criada em: {formatarData(data.criado_em)}</span>
|
|
<span>Enviada em: {formatarData(data.enviada_em)}</span>
|
|
<span>Finalizada em: {formatarData(data.finalizada_em)}</span>
|
|
</div>
|
|
<div className="mt-4 flex items-center gap-4 flex-wrap">
|
|
<Link href="/dashboard" className="text-blue-600 font-semibold hover:underline">
|
|
Voltar para dashboard
|
|
</Link>
|
|
<a
|
|
href={`${DJANGO_BASE_URL}/solicitacao/${data.id}/comprovante.pdf`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 font-semibold hover:underline"
|
|
>
|
|
Download PDF
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Dados gerais</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div>
|
|
<div className="text-slate-700 uppercase text-sm font-bold mb-1">Tipo de processo</div>
|
|
<div className="text-slate-900 font-semibold">{data.tipo_display}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-slate-700 uppercase text-sm font-bold mb-1">Solicitante</div>
|
|
<div className="text-slate-900 font-semibold">
|
|
{data.solicitante?.nome || "—"} ({data.solicitante?.matricula || "—"})
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-slate-700 uppercase text-sm font-bold mb-1">Status</div>
|
|
<div className="text-slate-900 font-semibold">{data.status_display}</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{data.funcionario && (
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5 border-l-4 border-l-blue-500">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Dados do colaborador (TOTVS RM)</h2>
|
|
<p className="text-sm text-slate-700 mb-4 font-medium">Snapshot no momento da criação da solicitação</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Matrícula</div><div className="font-semibold">{data.funcionario.matricula || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Nome completo</div><div className="font-bold">{data.funcionario.nome || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">CPF</div><div className="font-semibold">{data.funcionario.cpf || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data admissão</div><div className="font-semibold">{formatarData(data.funcionario.data_admissao)}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Cargo/Função</div><div className="font-semibold">{data.funcionario.cargo || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Cód. função</div><div className="font-semibold">{data.funcionario.cod_funcao || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Setor/Seção</div><div className="font-semibold">{data.funcionario.setor || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Centro de custo</div><div className="font-semibold">{data.funcionario.centro_custo || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Salário atual</div><div className="font-semibold text-emerald-700">{formatarDinheiro(data.funcionario.salario)}</div></div>
|
|
</div>
|
|
{data.funcionario.saldo_banco_horas_minutos !== null && (
|
|
<div className="mt-5 pt-4 border-t border-slate-100 grid grid-cols-1 md:grid-cols-2 gap-4 text-base">
|
|
<div>
|
|
<div className="text-slate-700 uppercase text-sm font-bold mb-1">Saldo banco de horas</div>
|
|
<div className="font-semibold">
|
|
{data.funcionario.saldo_banco_horas_minutos >= 0 ? "+" : ""}
|
|
{data.funcionario.saldo_banco_horas_minutos} min
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-slate-700 uppercase text-sm font-bold mb-1">Período referência</div>
|
|
<div className="font-semibold">
|
|
{formatarData(data.funcionario.inicio_periodo_banco_horas)} a {formatarData(data.funcionario.fim_periodo_banco_horas)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{data.funcionario.sincronizado_em && (
|
|
<p className="text-sm text-slate-600 mt-4 text-right font-medium">
|
|
Sincronizado com RM em {formatarData(data.funcionario.sincronizado_em)} | ID: {data.funcionario.id_rm || "—"}
|
|
</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{data.dados_winthor && (
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5 border-l-4 border-l-blue-500">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Dados do colaborador (Winthor)</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Matrícula</div><div className="font-semibold">{data.dados_winthor.basicos.matricula || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Nome</div><div className="font-semibold">{data.dados_winthor.basicos.nome || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">CPF</div><div className="font-semibold">{data.dados_winthor.basicos.cpf || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data admissão</div><div className="font-semibold">{formatarData(data.dados_winthor.admissao.admissao)}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Situação</div><div className="font-semibold">{data.dados_winthor.admissao.situacao || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data exclusão</div><div className="font-semibold">{formatarData(data.dados_winthor.admissao.dt_exclusao)}</div></div>
|
|
<div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Endereço</div><div className="font-semibold">{data.dados_winthor.endereco.endereco || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Bairro</div><div className="font-semibold">{data.dados_winthor.endereco.bairro || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Cidade</div><div className="font-semibold">{data.dados_winthor.endereco.cidade || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Estado</div><div className="font-semibold">{data.dados_winthor.endereco.estado || "—"}</div></div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{data.detalhes_tipo && (
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Detalhes da movimentação</h2>
|
|
{data.detalhes_tipo.tipo === "DESLIGAMENTO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Tipo desligamento</div><div className="font-semibold">{(data.detalhes_tipo.tipo_desligamento_display as string) || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Aviso prévio</div><div className="font-semibold">{(data.detalhes_tipo.aviso_previo_display as string) || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data prevista saída</div><div className="font-semibold">{formatarData((data.detalhes_tipo.data_prevista_desligamento as string) || null)}</div></div>
|
|
<div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Motivo</div><div className="font-semibold whitespace-pre-wrap">{(data.detalhes_tipo.motivo as string) || "—"}</div></div>
|
|
{(data.detalhes_tipo.observacoes as string) && <div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Observações</div><div className="font-semibold whitespace-pre-wrap">{data.detalhes_tipo.observacoes as string}</div></div>}
|
|
{(data.detalhes_tipo.arquivo_pedido_url as string) && (
|
|
<div className="md:col-span-3">
|
|
<a href={data.detalhes_tipo.arquivo_pedido_url as string} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-semibold hover:underline">
|
|
📎 Ver carta de pedido
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{data.detalhes_tipo.tipo === "MOVIMENTACAO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Nova função</div><div className="font-semibold">{(data.detalhes_tipo.novo_cod_funcao as string) || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Nova seção</div><div className="font-semibold">{(data.detalhes_tipo.novo_cod_secao as string) || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Novo salário</div><div className="font-semibold">{formatarDinheiro((data.detalhes_tipo.novo_salario as string) || null)}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data efetivação</div><div className="font-semibold">{formatarData((data.detalhes_tipo.data_efetivacao as string) || null)}</div></div>
|
|
<div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Justificativa</div><div className="font-semibold whitespace-pre-wrap">{(data.detalhes_tipo.justificativa as string) || "—"}</div></div>
|
|
</div>
|
|
)}
|
|
{data.detalhes_tipo.tipo === "ADM_SUBSTITUICAO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data prevista</div><div className="font-semibold">{formatarData((data.detalhes_tipo.data_previsao_contratacao as string) || null)}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Coligada/Filial</div><div className="font-semibold">{String(data.detalhes_tipo.cod_coligada_destino || "—")} / {String(data.detalhes_tipo.cod_filial_destino || "—")}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Seção destino</div><div className="font-semibold">{(data.detalhes_tipo.cod_secao_destino as string) || "—"}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Função destino</div><div className="font-semibold">{(data.detalhes_tipo.cod_funcao_destino as string) || "—"}</div></div>
|
|
<div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Justificativa</div><div className="font-semibold whitespace-pre-wrap">{(data.detalhes_tipo.justificativa as string) || "—"}</div></div>
|
|
</div>
|
|
)}
|
|
{data.detalhes_tipo.tipo === "ADM_AUMENTO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-base">
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Data prevista</div><div className="font-semibold">{formatarData((data.detalhes_tipo.data_previsao_contratacao as string) || null)}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Local destino</div><div className="font-semibold">Col: {String(data.detalhes_tipo.cod_coligada_destino || "—")} / Fil: {String(data.detalhes_tipo.cod_filial_destino || "—")}</div></div>
|
|
<div><div className="text-slate-700 uppercase text-sm font-bold mb-1">Suplementação orçamentária</div><div className="font-semibold">{data.detalhes_tipo.requer_suplementacao_orcamentaria ? "Sim" : "Não"}</div></div>
|
|
<div className="md:col-span-3"><div className="text-slate-700 uppercase text-sm font-bold mb-1">Justificativa estratégica</div><div className="font-semibold whitespace-pre-wrap">{(data.detalhes_tipo.justificativa_estrategica as string) || "—"}</div></div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Pareceres</h2>
|
|
{data.pareceres.length === 0 ? (
|
|
<p className="text-base text-slate-700">Nenhum parecer registrado.</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{data.pareceres.map((parecer) => (
|
|
<article key={parecer.id} className="border border-slate-200 rounded-lg p-3">
|
|
<p className="text-base font-bold text-slate-900">
|
|
{parecer.etapa_display} · {parecer.usuario_nome || "Usuário"}
|
|
</p>
|
|
<p className="text-sm text-slate-700 mb-2 font-medium">{formatarData(parecer.criado_em)}</p>
|
|
<p className="text-base text-slate-800 whitespace-pre-wrap font-medium">{parecer.texto}</p>
|
|
{parecer.anexo_url && (
|
|
<a
|
|
href={toAbsoluteDjangoUrl(parecer.anexo_url)}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-block mt-2 text-blue-600 text-sm font-semibold hover:underline"
|
|
>
|
|
📎 Ver anexo
|
|
</a>
|
|
)}
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-3">Aprovações</h2>
|
|
{data.aprovacoes.length === 0 ? (
|
|
<p className="text-base text-slate-700">Nenhuma aprovação registrada.</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{data.aprovacoes.map((aprovacao) => (
|
|
<article key={aprovacao.id} className="border border-slate-200 rounded-lg p-3">
|
|
<p className="text-base font-bold text-slate-900">
|
|
{aprovacao.etapa_display} · {aprovacao.decisao_display}
|
|
</p>
|
|
<p className="text-sm text-slate-700 font-medium">
|
|
{aprovacao.usuario_nome || "Usuário"} · {formatarData(aprovacao.decidido_em)}
|
|
</p>
|
|
<p className="text-base text-slate-800 mt-1 whitespace-pre-wrap font-medium">
|
|
{aprovacao.justificativa || "Sem justificativa."}
|
|
</p>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{data.acoes.pode_enviar && data.acoes.is_solicitante && (
|
|
<section className="bg-sky-50 border border-sky-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-sky-900 mb-1">Enviar para aprovação</h2>
|
|
<p className="text-sm text-sky-800 mb-3">
|
|
Sua solicitação está em rascunho. Revise os dados e envie para iniciar o fluxo.
|
|
</p>
|
|
<button
|
|
onClick={handleEnviar}
|
|
disabled={actionLoading}
|
|
className="inline-flex items-center gap-1.5 py-2 px-4 rounded-md text-sm font-semibold bg-blue-600 text-white no-underline hover:bg-blue-700 disabled:opacity-60"
|
|
>
|
|
Confirmar envio
|
|
</button>
|
|
</section>
|
|
)}
|
|
|
|
{data.acoes.pode_dar_parecer && (
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-2">Registrar parecer</h2>
|
|
<textarea
|
|
className="w-full border border-slate-300 rounded-md p-2 min-h-24"
|
|
value={parecerTexto}
|
|
onChange={(e) => setParecerTexto(e.target.value)}
|
|
placeholder="Digite seu parecer técnico..."
|
|
/>
|
|
<input
|
|
type="file"
|
|
className="mt-3 w-full border border-slate-300 rounded-md p-2"
|
|
onChange={(e) => setParecerAnexo(e.target.files?.[0] || null)}
|
|
/>
|
|
<button
|
|
onClick={handleParecer}
|
|
disabled={actionLoading}
|
|
className="mt-3 px-4 py-2 rounded-md bg-blue-600 text-white font-semibold hover:bg-blue-700 disabled:opacity-60"
|
|
>
|
|
Salvar parecer
|
|
</button>
|
|
</section>
|
|
)}
|
|
|
|
{data.acoes.pode_aprovar && (
|
|
<section className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h2 className="text-base font-semibold text-slate-900 mb-2">Registrar decisão</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<select
|
|
className="border border-slate-300 rounded-md p-2"
|
|
value={decisao}
|
|
onChange={(e) => setDecisao(e.target.value as "APROVADO" | "REPROVADO")}
|
|
>
|
|
<option value="APROVADO">Aprovar</option>
|
|
<option value="REPROVADO">Reprovar</option>
|
|
</select>
|
|
<input
|
|
className="border border-slate-300 rounded-md p-2"
|
|
placeholder="Justificativa (obrigatória para reprovação)"
|
|
value={justificativaDecisao}
|
|
onChange={(e) => setJustificativaDecisao(e.target.value)}
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleDecisao}
|
|
disabled={actionLoading}
|
|
className="mt-3 px-4 py-2 rounded-md bg-blue-600 text-white font-semibold hover:bg-blue-700 disabled:opacity-60"
|
|
>
|
|
Confirmar decisão
|
|
</button>
|
|
</section>
|
|
)}
|
|
|
|
{actionFeedback && (
|
|
<p className="text-sm font-semibold text-slate-800 bg-slate-200 px-3 py-2 rounded">
|
|
{actionFeedback}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|