321 lines
15 KiB
TypeScript
321 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { apiGet, apiPostFormData, apiPostJson, ApiError } from "@/lib/api/client";
|
|
import { validateSolicitacaoPayload } from "@/lib/validation/solicitacao";
|
|
import { Field } from "@/components/forms/Field";
|
|
|
|
type Meta = {
|
|
cargos: Array<{ codigo: string; nome: string }>;
|
|
secoes: Array<{ codigo: string; descricao: string }>;
|
|
coligadas: Array<{ codigo: number; nome: string }>;
|
|
tipos: Array<{ value: string; label: string; precisa_colaborador: boolean }>;
|
|
};
|
|
|
|
type Colaborador = {
|
|
id: string | null;
|
|
matricula: string | null;
|
|
nome: string | null;
|
|
cargo: string | null;
|
|
setor: string | null;
|
|
};
|
|
|
|
type FormState = Record<string, string | boolean>;
|
|
|
|
export default function NovaSolicitacaoPage() {
|
|
const router = useRouter();
|
|
const [meta, setMeta] = useState<Meta | null>(null);
|
|
const [tipo, setTipo] = useState("");
|
|
const [busca, setBusca] = useState("");
|
|
const [colaboradores, setColaboradores] = useState<Colaborador[]>([]);
|
|
const [funcionarioId, setFuncionarioId] = useState("");
|
|
const [arquivoPedido, setArquivoPedido] = useState<File | null>(null);
|
|
const [form, setForm] = useState<FormState>({});
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
const [loading, setLoading] = useState(false);
|
|
const [feedback, setFeedback] = useState("");
|
|
|
|
const selectedTipo = useMemo(
|
|
() => meta?.tipos.find((t) => t.value === tipo) || null,
|
|
[meta, tipo]
|
|
);
|
|
|
|
async function ensureMeta() {
|
|
if (meta) return meta;
|
|
const loaded = await apiGet<Meta>("/api/nova-solicitacao/metadata/");
|
|
setMeta(loaded);
|
|
return loaded;
|
|
}
|
|
|
|
async function buscarColaboradores() {
|
|
if (!tipo) return;
|
|
setLoading(true);
|
|
setFeedback("");
|
|
try {
|
|
await ensureMeta();
|
|
const query = new URLSearchParams();
|
|
if (busca.trim()) query.set("q", busca.trim());
|
|
if (tipo === "ADM_SUBSTITUICAO") query.set("tipo", "substituicao");
|
|
const res = await apiGet<{ colaboradores: Colaborador[] }>(
|
|
`/api/colaboradores/?${query.toString()}`
|
|
);
|
|
setColaboradores(res.colaboradores);
|
|
} catch (error) {
|
|
setFeedback(error instanceof Error ? error.message : "Erro ao buscar colaboradores.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const payload: Record<string, unknown> = { ...form, tipo };
|
|
if (selectedTipo?.precisa_colaborador) payload.funcionario_id = funcionarioId;
|
|
if (tipo === "DESLIGAMENTO") {
|
|
payload.data_prevista_desligamento = form.data_prevista_desligamento || "";
|
|
}
|
|
const validation = validateSolicitacaoPayload(payload);
|
|
setErrors(validation);
|
|
if (Object.keys(validation).length > 0) return;
|
|
|
|
setLoading(true);
|
|
setFeedback("");
|
|
try {
|
|
if (tipo === "DESLIGAMENTO" && arquivoPedido) {
|
|
const fd = new FormData();
|
|
Object.entries(payload).forEach(([k, v]) => {
|
|
if (typeof v === "boolean") fd.append(k, v ? "true" : "false");
|
|
else fd.append(k, String(v ?? ""));
|
|
});
|
|
fd.append("arquivo_pedido", arquivoPedido);
|
|
const result = await apiPostFormData<{ redirect: string }>(
|
|
"/api/solicitacoes/",
|
|
fd
|
|
);
|
|
router.push(result.redirect);
|
|
return;
|
|
}
|
|
const result = await apiPostJson<{ redirect: string }>(
|
|
"/api/solicitacoes/",
|
|
payload
|
|
);
|
|
router.push(result.redirect);
|
|
} catch (error) {
|
|
if (error instanceof ApiError) {
|
|
setFeedback(error.message);
|
|
} else {
|
|
setFeedback("Erro ao criar solicitação.");
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-100 p-4 md:p-6">
|
|
<div className="max-w-5xl mx-auto space-y-4">
|
|
<div className="bg-white rounded-xl border border-slate-200 p-5">
|
|
<h1 className="text-2xl font-bold text-slate-900">Nova Solicitação</h1>
|
|
<p className="text-slate-700 mt-1">Crie e envie solicitações sem sair do Next.</p>
|
|
<div className="mt-3">
|
|
<Link href="/dashboard" className="text-blue-600 font-semibold hover:underline">
|
|
Voltar ao dashboard
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="bg-white rounded-xl border border-slate-200 p-5 space-y-4">
|
|
<Field label="Tipo de solicitação" required error={errors.tipo}>
|
|
<select
|
|
className="w-full border border-slate-300 rounded-md p-2 text-slate-900"
|
|
value={tipo}
|
|
onChange={(e) => {
|
|
setTipo(e.target.value);
|
|
setFuncionarioId("");
|
|
setColaboradores([]);
|
|
setForm({});
|
|
}}
|
|
onFocus={ensureMeta}
|
|
>
|
|
<option value="">Selecione…</option>
|
|
{(meta?.tipos || []).map((t) => (
|
|
<option key={t.value} value={t.value}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
|
|
{selectedTipo?.precisa_colaborador && (
|
|
<div className="border border-slate-200 rounded-lg p-4 space-y-3">
|
|
<h2 className="text-base font-bold text-slate-900">Selecionar colaborador</h2>
|
|
<div className="flex gap-2">
|
|
<input
|
|
className="flex-1 border border-slate-300 rounded-md p-2 text-slate-900"
|
|
placeholder="Buscar por nome ou matrícula"
|
|
value={busca}
|
|
onChange={(e) => setBusca(e.target.value)}
|
|
/>
|
|
<button
|
|
onClick={buscarColaboradores}
|
|
disabled={loading}
|
|
className="px-4 py-2 rounded-md bg-blue-600 text-white font-semibold disabled:opacity-60"
|
|
>
|
|
Buscar
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-slate-600">
|
|
Para substituição, serão listados colaboradores desligados.
|
|
</p>
|
|
<Field label="Colaborador" required error={errors.funcionario_id}>
|
|
<select
|
|
className="w-full border border-slate-300 rounded-md p-2 text-slate-900"
|
|
value={funcionarioId}
|
|
onChange={(e) => setFuncionarioId(e.target.value)}
|
|
>
|
|
<option value="">Selecione…</option>
|
|
{colaboradores.map((c) => (
|
|
<option key={`${c.id || "rm"}-${c.matricula}`} value={c.id || ""}>
|
|
{c.nome} ({c.matricula || "sem matrícula"})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
</div>
|
|
)}
|
|
|
|
{tipo === "DESLIGAMENTO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Field label="Tipo de desligamento" required error={errors.tipo_desligamento}>
|
|
<input className="w-full border border-slate-300 rounded-md p-2" value={String(form.tipo_desligamento || "")} onChange={(e) => setForm((f) => ({ ...f, tipo_desligamento: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Aviso prévio" required error={errors.aviso_previo}>
|
|
<input className="w-full border border-slate-300 rounded-md p-2" value={String(form.aviso_previo || "")} onChange={(e) => setForm((f) => ({ ...f, aviso_previo: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Data prevista de saída" required error={errors.data_prevista_desligamento}>
|
|
<input type="date" className="w-full border border-slate-300 rounded-md p-2" value={String(form.data_prevista_desligamento || "")} onChange={(e) => setForm((f) => ({ ...f, data_prevista_desligamento: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Carta de pedido (opcional)">
|
|
<input type="file" className="w-full border border-slate-300 rounded-md p-2" onChange={(e) => setArquivoPedido(e.target.files?.[0] || null)} />
|
|
</Field>
|
|
<div className="md:col-span-2">
|
|
<Field label="Justificativa" required error={errors.motivo}>
|
|
<textarea className="w-full border border-slate-300 rounded-md p-2 min-h-28" value={String(form.motivo || "")} onChange={(e) => setForm((f) => ({ ...f, motivo: e.target.value }))} />
|
|
</Field>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<Field label="Observações adicionais">
|
|
<textarea className="w-full border border-slate-300 rounded-md p-2 min-h-20" value={String(form.observacoes || "")} onChange={(e) => setForm((f) => ({ ...f, observacoes: e.target.value }))} />
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{tipo === "MOVIMENTACAO" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Field label="Data de efetivação" required error={errors.data_efetivacao}>
|
|
<input type="date" className="w-full border border-slate-300 rounded-md p-2" value={String(form.data_efetivacao || "")} onChange={(e) => setForm((f) => ({ ...f, data_efetivacao: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Novo salário">
|
|
<input type="number" className="w-full border border-slate-300 rounded-md p-2" value={String(form.novo_salario || "")} onChange={(e) => setForm((f) => ({ ...f, novo_salario: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Nova função (código)">
|
|
<input className="w-full border border-slate-300 rounded-md p-2" value={String(form.novo_cod_funcao || "")} onChange={(e) => setForm((f) => ({ ...f, novo_cod_funcao: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Nova seção (código)">
|
|
<input className="w-full border border-slate-300 rounded-md p-2" value={String(form.novo_cod_secao || "")} onChange={(e) => setForm((f) => ({ ...f, novo_cod_secao: e.target.value }))} />
|
|
</Field>
|
|
<div className="md:col-span-2 flex gap-6">
|
|
<label className="text-sm font-semibold text-slate-800">
|
|
<input type="checkbox" checked={Boolean(form.altera_funcao)} onChange={(e) => setForm((f) => ({ ...f, altera_funcao: e.target.checked }))} className="mr-2" />
|
|
Altera função
|
|
</label>
|
|
<label className="text-sm font-semibold text-slate-800">
|
|
<input type="checkbox" checked={Boolean(form.altera_centro_custo)} onChange={(e) => setForm((f) => ({ ...f, altera_centro_custo: e.target.checked }))} className="mr-2" />
|
|
Altera centro de custo
|
|
</label>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<Field label="Justificativa" required error={errors.justificativa}>
|
|
<textarea className="w-full border border-slate-300 rounded-md p-2 min-h-28" value={String(form.justificativa || "")} onChange={(e) => setForm((f) => ({ ...f, justificativa: e.target.value }))} />
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(tipo === "ADM_SUBSTITUICAO" || tipo === "ADM_AUMENTO") && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Field label="Data prevista contratação" required error={errors.data_previsao_contratacao}>
|
|
<input type="date" className="w-full border border-slate-300 rounded-md p-2" value={String(form.data_previsao_contratacao || "")} onChange={(e) => setForm((f) => ({ ...f, data_previsao_contratacao: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Coligada" required error={errors.cod_coligada_destino}>
|
|
<select className="w-full border border-slate-300 rounded-md p-2" value={String(form.cod_coligada_destino || "")} onFocus={ensureMeta} onChange={(e) => setForm((f) => ({ ...f, cod_coligada_destino: e.target.value }))}>
|
|
<option value="">Selecione…</option>
|
|
{(meta?.coligadas || []).map((c) => (
|
|
<option key={c.codigo} value={String(c.codigo)}>{c.codigo} - {c.nome}</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field label="Filial" required error={errors.cod_filial_destino}>
|
|
<input className="w-full border border-slate-300 rounded-md p-2" value={String(form.cod_filial_destino || "")} onChange={(e) => setForm((f) => ({ ...f, cod_filial_destino: e.target.value }))} />
|
|
</Field>
|
|
<Field label="Seção destino" required error={errors.cod_secao_destino}>
|
|
<select className="w-full border border-slate-300 rounded-md p-2" value={String(form.cod_secao_destino || "")} onFocus={ensureMeta} onChange={(e) => setForm((f) => ({ ...f, cod_secao_destino: e.target.value }))}>
|
|
<option value="">Selecione…</option>
|
|
{(meta?.secoes || []).map((s) => (
|
|
<option key={s.codigo} value={s.codigo}>{s.codigo} - {s.descricao}</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field label="Função destino" required error={errors.cod_funcao_destino}>
|
|
<select className="w-full border border-slate-300 rounded-md p-2" value={String(form.cod_funcao_destino || "")} onFocus={ensureMeta} onChange={(e) => setForm((f) => ({ ...f, cod_funcao_destino: e.target.value }))}>
|
|
<option value="">Selecione…</option>
|
|
{(meta?.cargos || []).map((c) => (
|
|
<option key={c.codigo} value={c.codigo}>{c.codigo} - {c.nome}</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<div className="md:col-span-2">
|
|
<Field
|
|
label={tipo === "ADM_AUMENTO" ? "Justificativa estratégica" : "Justificativa"}
|
|
required
|
|
error={tipo === "ADM_AUMENTO" ? errors.justificativa_estrategica : errors.justificativa}
|
|
>
|
|
<textarea
|
|
className="w-full border border-slate-300 rounded-md p-2 min-h-28"
|
|
value={String(
|
|
tipo === "ADM_AUMENTO"
|
|
? form.justificativa_estrategica || ""
|
|
: form.justificativa || ""
|
|
)}
|
|
onChange={(e) =>
|
|
setForm((f) => ({
|
|
...f,
|
|
[tipo === "ADM_AUMENTO" ? "justificativa_estrategica" : "justificativa"]:
|
|
e.target.value,
|
|
}))
|
|
}
|
|
/>
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{feedback && <p className="text-sm font-semibold text-red-600">{feedback}</p>}
|
|
|
|
<div className="pt-2">
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={loading || !tipo}
|
|
className="px-5 py-2.5 rounded-md bg-blue-600 text-white font-semibold hover:bg-blue-700 disabled:opacity-60"
|
|
>
|
|
{loading ? "Salvando..." : "Criar solicitação"}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|