feat(post-job): add preview and billing step to publish flow

This commit is contained in:
Tiago Yamamoto 2026-02-14 17:21:52 -03:00
parent a9f8e40d7e
commit 01e7a3b920
2 changed files with 121 additions and 16 deletions

View file

@ -171,12 +171,12 @@ Mapear o que já existe no GoHorseJobs e o que ainda falta para alcançar um flu
--- ---
### 2) Pré-visualização ### 2) Pré-visualização
- [ ] Exibir versão final da vaga com todos os metadados selecionados. - [x] Exibir versão final da vaga com todos os metadados selecionados.
- [ ] Indicar claramente campos ocultos (ex.: dados de empresa) antes de prosseguir. - [x] Indicar claramente campos ocultos (ex.: dados de empresa) antes de prosseguir.
- [ ] Permitir voltar para edição sem perda de dados. - [x] Permitir voltar para edição sem perda de dados.
### 3) Informações de faturamento ### 3) Informações de faturamento
- [ ] Capturar dados fiscais (pessoa/empresa, documento, endereço de cobrança). - [x] Capturar dados fiscais (pessoa/empresa, documento, endereço de cobrança).
- [ ] Exibir plano/preço por país e duração (ex.: **US$130/30 dias** para EUA, quando aplicável). - [ ] Exibir plano/preço por país e duração (ex.: **US$130/30 dias** para EUA, quando aplicável).
- [ ] Validar consistência entre país da vaga e país de faturamento conforme regra de negócio. - [ ] Validar consistência entre país da vaga e país de faturamento conforme regra de negócio.

View file

@ -54,7 +54,7 @@ const getCurrencySymbol = (code: string): string => {
export default function PostJobPage() { export default function PostJobPage() {
const router = useRouter(); const router = useRouter();
const [step, setStep] = useState<1 | 2 | 3>(1); const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
const { locale, setLocale } = useTranslation(); const { locale, setLocale } = useTranslation();
const lang = useMemo<Language>(() => (locale === "pt-BR" ? "pt" : locale), [locale]); const lang = useMemo<Language>(() => (locale === "pt-BR" ? "pt" : locale), [locale]);
@ -87,6 +87,14 @@ export default function PostJobPage() {
employeeCount: "", employeeCount: "",
foundedYear: "", foundedYear: "",
description: "", description: "",
hidePublicProfile: false,
});
const [billing, setBilling] = useState({
legalType: "company",
document: "",
billingCountry: "",
address: "",
}); });
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
@ -213,6 +221,12 @@ export default function PostJobPage() {
return false; return false;
} }
if (!billing.document || !billing.billingCountry || !billing.address) {
toast.error("Preencha os dados obrigatórios de faturamento.");
setStep(4);
return false;
}
return true; return true;
}; };
@ -242,6 +256,8 @@ export default function PostJobPage() {
setStep(2); setStep(2);
} else if (step === 2) { } else if (step === 2) {
setStep(3); setStep(3);
} else if (step === 3) {
setStep(4);
} }
}; };
@ -307,6 +323,10 @@ export default function PostJobPage() {
category: job.jobCategory || null, category: job.jobCategory || null,
resumeRequirement: job.resumeRequirement, resumeRequirement: job.resumeRequirement,
applicationChannel: job.applicationChannel, applicationChannel: job.applicationChannel,
applicationEmail: job.applicationEmail || null,
applicationUrl: job.applicationUrl || null,
applicationPhone: job.applicationPhone || null,
hideCompanyData: company.hidePublicProfile,
}, },
benefits: { benefits: {
selected: job.benefits, selected: job.benefits,
@ -368,7 +388,7 @@ export default function PostJobPage() {
{/* Progress Steps */} {/* Progress Steps */}
<div className="flex justify-center gap-4 mb-8"> <div className="flex justify-center gap-4 mb-8">
{[1, 2, 3].map((s) => ( {[1, 2, 3, 4].map((s) => (
<div <div
key={s} key={s}
className={`flex items-center gap-2 ${step >= s ? "text-primary" : "text-muted-foreground"}`} className={`flex items-center gap-2 ${step >= s ? "text-primary" : "text-muted-foreground"}`}
@ -377,7 +397,10 @@ export default function PostJobPage() {
{s} {s}
</div> </div>
<span className="hidden sm:inline text-sm"> <span className="hidden sm:inline text-sm">
{s === 1 ? t.steps.data : s === 2 ? "Formulário" : t.steps.confirm} {s === 1 && "Dados"}
{s === 2 && "Formulário"}
{s === 3 && "Pré-visualização"}
{s === 4 && "Faturamento"}
</span> </span>
</div> </div>
))} ))}
@ -388,12 +411,14 @@ export default function PostJobPage() {
<CardTitle> <CardTitle>
{step === 1 && t.cardTitle.step1} {step === 1 && t.cardTitle.step1}
{step === 2 && "Configure o Formulário"} {step === 2 && "Configure o Formulário"}
{step === 3 && t.cardTitle.step2} {step === 3 && "Pré-visualização"}
{step === 4 && t.cardTitle.step2}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{step === 1 && t.cardDesc.step1} {step === 1 && t.cardDesc.step1}
{step === 2 && "Defina as perguntas que os candidatos deverão responder."} {step === 2 && "Defina as perguntas que os candidatos deverão responder."}
{step === 3 && t.cardDesc.step2} {step === 3 && "Confira como o anúncio será exibido antes de prosseguir."}
{step === 4 && "Informe os dados fiscais para finalizar a publicação."}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -574,6 +599,20 @@ export default function PostJobPage() {
/> />
</div> </div>
<div className="flex items-start gap-3 rounded-md border p-3">
<input
id="hide-company-data"
type="checkbox"
checked={company.hidePublicProfile}
onChange={(e) => setCompany({ ...company, hidePublicProfile: e.target.checked })}
className="mt-1"
/>
<div>
<Label htmlFor="hide-company-data" className="cursor-pointer">Ocultar dados da empresa</Label>
<p className="text-xs text-muted-foreground">Quando ativo, nome, site e descrição da empresa não aparecem na vaga pública.</p>
</div>
</div>
{/* Separator */} {/* Separator */}
<div className="border-t pt-6 mt-6"> <div className="border-t pt-6 mt-6">
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2"> <h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
@ -897,13 +936,13 @@ export default function PostJobPage() {
{t.buttons.back} {t.buttons.back}
</Button> </Button>
<Button onClick={handleNext} className="flex-1"> <Button onClick={handleNext} className="flex-1">
Revisar e Publicar Ir para pré-visualização
</Button> </Button>
</div> </div>
</div> </div>
)} )}
{/* Step 3: Confirm */} {/* Step 3: Preview */}
{step === 3 && ( {step === 3 && (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-muted/50 rounded-lg p-4"> <div className="bg-muted/50 rounded-lg p-4">
@ -934,6 +973,10 @@ export default function PostJobPage() {
<p><strong>Currículo:</strong> {job.resumeRequirement}</p> <p><strong>Currículo:</strong> {job.resumeRequirement}</p>
<p><strong>Área:</strong> {job.jobCategory || "Não informado"}</p> <p><strong>Área:</strong> {job.jobCategory || "Não informado"}</p>
<p><strong>Benefícios:</strong> {job.benefits.length > 0 ? job.benefits.join(", ") : "Não informado"}</p> <p><strong>Benefícios:</strong> {job.benefits.length > 0 ? job.benefits.join(", ") : "Não informado"}</p>
<p><strong>Visibilidade da empresa:</strong> {company.hidePublicProfile ? "Oculta na vaga pública" : "Visível"}</p>
{company.hidePublicProfile && (
<p className="text-xs text-muted-foreground mt-1">Campos ocultos: nome da empresa, site e descrição.</p>
)}
<p><strong>{t.common.type}:</strong> { <p><strong>{t.common.type}:</strong> {
(job.employmentType ? (t.options.contract[job.employmentType as keyof typeof t.options.contract] || job.employmentType) : t.options.any) (job.employmentType ? (t.options.contract[job.employmentType as keyof typeof t.options.contract] || job.employmentType) : t.options.any)
} / { } / {
@ -946,6 +989,68 @@ export default function PostJobPage() {
<Button variant="outline" onClick={() => setStep(2)} className="flex-1"> <Button variant="outline" onClick={() => setStep(2)} className="flex-1">
{t.buttons.back} {t.buttons.back}
</Button> </Button>
<Button onClick={handleNext} className="flex-1">
Prosseguir para faturamento
</Button>
</div>
</div>
)}
{/* Step 4: Billing + Publish */}
{step === 4 && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Tipo fiscal</Label>
<select
value={billing.legalType}
onChange={(e) => setBilling({ ...billing, legalType: e.target.value })}
className="w-full px-3 py-2 border rounded-lg bg-background"
>
<option value="company">Pessoa jurídica</option>
<option value="individual">Pessoa física</option>
</select>
</div>
<div>
<Label>Documento fiscal *</Label>
<Input
value={billing.document}
onChange={(e) => setBilling({ ...billing, document: e.target.value })}
placeholder={billing.legalType === "company" ? "CNPJ" : "CPF/NIF"}
/>
</div>
</div>
<div>
<Label>País de faturamento *</Label>
<select
value={billing.billingCountry}
onChange={(e) => setBilling({ ...billing, billingCountry: e.target.value })}
className="w-full px-3 py-2 border rounded-lg bg-background"
>
<option value="">Selecione</option>
{JOB_COUNTRIES.map((country) => (
<option key={country} value={country}>{country}</option>
))}
</select>
</div>
<div>
<Label>Endereço de cobrança *</Label>
<Textarea
value={billing.address}
onChange={(e) => setBilling({ ...billing, address: e.target.value })}
placeholder="Rua, número, cidade, estado e CEP"
/>
</div>
<div className="bg-muted/50 rounded-lg p-4 text-sm">
<p><strong>Resumo:</strong> {job.title} · {job.country || "País não informado"}</p>
<p><strong>Status após envio:</strong> pending_review</p>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => setStep(3)} className="flex-1">
{t.buttons.back}
</Button>
<Button onClick={handleSubmit} disabled={loading} className="flex-1"> <Button onClick={handleSubmit} disabled={loading} className="flex-1">
{loading ? t.buttons.publishing : t.buttons.publish} {loading ? t.buttons.publishing : t.buttons.publish}
</Button> </Button>