1068 lines
63 KiB
TypeScript
1068 lines
63 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { translations, Language } from "./translations";
|
|
import { toast } from "sonner";
|
|
import { Navbar } from "@/components/navbar";
|
|
import { Footer } from "@/components/footer";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import {
|
|
Building2, Briefcase, Mail, Lock, Phone, MapPin,
|
|
Eye, EyeOff, Globe
|
|
} from "lucide-react";
|
|
import { LocationPicker } from "@/components/location-picker";
|
|
import { RichTextEditor } from "@/components/rich-text-editor";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
import { JobFormBuilder, Question } from "@/components/job-form-builder";
|
|
|
|
// Common Country Codes
|
|
const COUNTRY_CODES = [
|
|
{ code: "+55", country: "Brasil (BR)" },
|
|
{ code: "+1", country: "Estados Unidos (US)" },
|
|
{ code: "+351", country: "Portugal (PT)" },
|
|
{ code: "+44", country: "Reino Unido (UK)" },
|
|
{ code: "+33", country: "França (FR)" },
|
|
{ code: "+49", country: "Alemanha (DE)" },
|
|
{ code: "+34", country: "Espanha (ES)" },
|
|
{ code: "+39", country: "Itália (IT)" },
|
|
{ code: "+81", country: "Japão (JP)" },
|
|
{ code: "+86", country: "China (CN)" },
|
|
{ code: "+91", country: "Índia (IN)" },
|
|
{ code: "+52", country: "México (MX)" },
|
|
{ code: "+54", country: "Argentina (AR)" },
|
|
{ code: "+57", country: "Colômbia (CO)" },
|
|
{ code: "+56", country: "Chile (CL)" },
|
|
{ code: "+51", country: "Peru (PE)" },
|
|
].sort((a, b) => a.country.localeCompare(b.country));
|
|
|
|
// Currency symbol helper
|
|
const getCurrencySymbol = (code: string): string => {
|
|
const symbols: Record<string, string> = {
|
|
'BRL': 'R$', 'USD': '$', 'EUR': '€', 'JPY': '¥', 'GBP': '£',
|
|
'CNY': '¥', 'AED': 'د.إ', 'CAD': 'C$', 'AUD': 'A$', 'CHF': 'Fr'
|
|
};
|
|
return symbols[code] || code;
|
|
};
|
|
|
|
|
|
|
|
export default function PostJobPage() {
|
|
const router = useRouter();
|
|
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
|
|
const { locale, setLocale } = useTranslation();
|
|
|
|
const lang = useMemo<Language>(() => (locale === "pt-BR" ? "pt" : locale), [locale]);
|
|
const t = translations[lang];
|
|
|
|
// Helper inside to use t
|
|
const getSalaryPeriodLabel = (type: string): string => {
|
|
const labels: Record<string, string> = {
|
|
'hourly': t.options.period.hourly,
|
|
'daily': t.options.period.daily,
|
|
'weekly': t.options.period.weekly,
|
|
'monthly': t.options.period.monthly,
|
|
'yearly': t.options.period.yearly
|
|
};
|
|
return labels[type] || '';
|
|
};
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Company/User data
|
|
const [company, setCompany] = useState({
|
|
name: "",
|
|
email: "",
|
|
document: "",
|
|
password: "",
|
|
confirmPassword: "",
|
|
ddi: "+55",
|
|
phone: "",
|
|
website: "",
|
|
employeeCount: "",
|
|
foundedYear: "",
|
|
description: "",
|
|
hidePublicProfile: false,
|
|
});
|
|
|
|
const [billing, setBilling] = useState({
|
|
legalType: "company",
|
|
document: "",
|
|
billingCountry: "",
|
|
address: "",
|
|
});
|
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
// Job data
|
|
const [job, setJob] = useState({
|
|
title: "",
|
|
description: "",
|
|
location: "",
|
|
country: "",
|
|
salaryMin: "",
|
|
salaryMax: "",
|
|
salaryFixed: "", // For fixed salary mode
|
|
currency: "BRL", // Default currency
|
|
salaryType: "monthly", // Default salary period
|
|
employmentType: "",
|
|
workMode: "remote",
|
|
workingHours: "",
|
|
salaryNegotiable: false, // Candidate proposes salary
|
|
descriptionLanguage: "",
|
|
applicationChannel: "email",
|
|
applicationEmail: "",
|
|
applicationUrl: "",
|
|
applicationPhone: "",
|
|
resumeRequirement: "optional",
|
|
jobCategory: "",
|
|
benefits: [] as string[],
|
|
});
|
|
|
|
const [questions, setQuestions] = useState<Question[]>([]);
|
|
|
|
// Salary mode toggle: 'fixed' | 'range'
|
|
const [salaryMode, setSalaryMode] = useState<'fixed' | 'range'>('fixed');
|
|
|
|
const BENEFIT_OPTIONS = ["Plano de saúde", "Vale refeição", "Vale transporte", "Bônus", "Home office", "Gym pass"];
|
|
|
|
const JOB_CATEGORIES = ["Tecnologia", "Produto", "Dados", "Marketing", "Vendas", "Operações", "Financeiro", "RH"];
|
|
|
|
const JOB_COUNTRIES = ["BR", "PT", "US", "ES", "UK", "DE", "FR", "JP"];
|
|
|
|
const cleanCNPJ = (value: string) => value.replace(/\D/g, "");
|
|
|
|
const isValidCNPJ = (value: string) => {
|
|
const cnpj = cleanCNPJ(value);
|
|
if (cnpj.length !== 14) return false;
|
|
if (/^(\d)\1+$/.test(cnpj)) return false;
|
|
|
|
const calcCheckDigit = (base: string, weights: number[]) => {
|
|
const sum = base
|
|
.split("")
|
|
.reduce((acc, current, index) => acc + Number(current) * weights[index], 0);
|
|
const mod = sum % 11;
|
|
return mod < 2 ? 0 : 11 - mod;
|
|
};
|
|
|
|
const base12 = cnpj.slice(0, 12);
|
|
const digit1 = calcCheckDigit(base12, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
|
const digit2 = calcCheckDigit(`${base12}${digit1}`, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
|
|
return cnpj === `${base12}${digit1}${digit2}`;
|
|
};
|
|
|
|
const isHttpsUrl = (value: string) => /^https:\/\/.+/i.test(value);
|
|
const isPhoneWithDDI = (value: string) => /^\+\d{1,3}\s?\d{8,14}$/.test(value.trim());
|
|
|
|
const formatPhoneForDisplay = (value: string) => {
|
|
// Simple formatting to just allow numbers and basic separators if needed
|
|
// For now, just pass through but maybe restrict chars?
|
|
return value.replace(/[^\d\s-]/g, "");
|
|
};
|
|
|
|
const validateForm = () => {
|
|
if (!company.name || !company.email || !company.password) {
|
|
toast.error(t.errors.company_required);
|
|
setStep(1); // Ensure we are on step 1 for company data errors
|
|
return false;
|
|
}
|
|
|
|
if (company.document && !isValidCNPJ(company.document)) {
|
|
toast.error("CNPJ inválido.");
|
|
setStep(1);
|
|
return false;
|
|
}
|
|
|
|
if (company.password !== company.confirmPassword) {
|
|
toast.error(t.errors.password_mismatch);
|
|
setStep(1); // Ensure we are on step 1 for password mismatch
|
|
return false;
|
|
}
|
|
|
|
if (company.password.length < 8) {
|
|
toast.error(t.errors.password_length);
|
|
setStep(1); // Ensure we are on step 1 for password length
|
|
return false;
|
|
}
|
|
|
|
if (!job.title || !job.description || !job.location || !job.country || !job.descriptionLanguage) {
|
|
toast.error(t.errors.job_required);
|
|
setStep(1); // Stay on step 1 for job data errors
|
|
return false;
|
|
}
|
|
|
|
if (job.title.length > 65) {
|
|
toast.error("O título da vaga deve ter no máximo 65 caracteres.");
|
|
setStep(1);
|
|
return false;
|
|
}
|
|
|
|
if (job.applicationChannel === "email" && !job.applicationEmail) {
|
|
toast.error("Informe um e-mail para candidatura.");
|
|
setStep(1);
|
|
return false;
|
|
}
|
|
|
|
if (job.applicationChannel === "url" && !isHttpsUrl(job.applicationUrl)) {
|
|
toast.error("Informe uma URL HTTPS válida para candidatura.");
|
|
setStep(1);
|
|
return false;
|
|
}
|
|
|
|
if (job.applicationChannel === "phone" && !isPhoneWithDDI(job.applicationPhone)) {
|
|
toast.error("Informe um telefone com DDI válido (ex: +55 11999998888).");
|
|
setStep(1);
|
|
return false;
|
|
}
|
|
|
|
if (!billing.document || !billing.billingCountry || !billing.address) {
|
|
toast.error("Preencha os dados obrigatórios de faturamento.");
|
|
setStep(4);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const handleNext = () => {
|
|
// Only validate step 1 fields to move to step 2
|
|
if (step === 1) {
|
|
if (!company.name || !company.email || !company.password) {
|
|
toast.error(t.errors.company_required);
|
|
return;
|
|
}
|
|
if (company.password !== company.confirmPassword) {
|
|
toast.error(t.errors.password_mismatch);
|
|
return;
|
|
}
|
|
if (company.password.length < 8) {
|
|
toast.error(t.errors.password_length);
|
|
return;
|
|
}
|
|
if (!job.title || !job.description || !job.location || !job.country || !job.descriptionLanguage) {
|
|
toast.error("Preencha título, localidade, país, idioma e descrição da vaga.");
|
|
return;
|
|
}
|
|
if (job.title.length > 65) {
|
|
toast.error("O título da vaga deve ter no máximo 65 caracteres.");
|
|
return;
|
|
}
|
|
setStep(2);
|
|
} else if (step === 2) {
|
|
setStep(3);
|
|
} else if (step === 3) {
|
|
setStep(4);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!validateForm()) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const apiBase = process.env.NEXT_PUBLIC_API_URL || "";
|
|
|
|
// Format phone: DDI + Phone (digits only)
|
|
const cleanPhone = company.phone.replace(/\D/g, "");
|
|
const finalPhone = cleanPhone ? `${company.ddi}${cleanPhone}` : "";
|
|
|
|
// 1. Register Company (creates user + company)
|
|
const registerRes = await fetch(`${apiBase}/api/v1/auth/register/company`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
companyName: company.name,
|
|
email: company.email,
|
|
document: cleanCNPJ(company.document) || null,
|
|
password: company.password,
|
|
phone: finalPhone,
|
|
website: company.website || null,
|
|
employeeCount: company.employeeCount || null,
|
|
foundedYear: company.foundedYear ? parseInt(company.foundedYear) : null,
|
|
description: company.description || null,
|
|
}),
|
|
});
|
|
|
|
if (!registerRes.ok) {
|
|
const err = await registerRes.json();
|
|
throw new Error(err.message || "Erro ao registrar empresa");
|
|
}
|
|
|
|
const { token } = await registerRes.json();
|
|
|
|
// 2. Create Job with token
|
|
const jobRes = await fetch(`${apiBase}/api/v1/jobs`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
title: job.title,
|
|
description: job.description,
|
|
location: `${job.location}, ${job.country}`,
|
|
// Salary logic: if negotiable, send null values
|
|
salaryMin: job.salaryNegotiable ? null : (salaryMode === 'fixed' ? (job.salaryFixed ? parseInt(job.salaryFixed) : null) : (job.salaryMin ? parseInt(job.salaryMin) : null)),
|
|
salaryMax: job.salaryNegotiable ? null : (salaryMode === 'fixed' ? (job.salaryFixed ? parseInt(job.salaryFixed) : null) : (job.salaryMax ? parseInt(job.salaryMax) : null)),
|
|
salaryType: job.salaryNegotiable ? null : job.salaryType,
|
|
currency: job.salaryNegotiable ? null : job.currency,
|
|
salaryNegotiable: job.salaryNegotiable,
|
|
employmentType: job.employmentType || null,
|
|
workingHours: job.workingHours || null,
|
|
workMode: job.workMode,
|
|
status: "pending",
|
|
questions: questions.length > 0 ? questions : null,
|
|
languageLevel: job.descriptionLanguage || null,
|
|
requirements: {
|
|
category: job.jobCategory || null,
|
|
resumeRequirement: job.resumeRequirement,
|
|
applicationChannel: job.applicationChannel,
|
|
applicationEmail: job.applicationEmail || null,
|
|
applicationUrl: job.applicationUrl || null,
|
|
applicationPhone: job.applicationPhone || null,
|
|
hideCompanyData: company.hidePublicProfile,
|
|
},
|
|
benefits: {
|
|
selected: job.benefits,
|
|
},
|
|
}),
|
|
});
|
|
|
|
if (!jobRes.ok) {
|
|
const err = await jobRes.json();
|
|
throw new Error(err.message || "Erro ao criar vaga");
|
|
}
|
|
|
|
// Save token for future use
|
|
localStorage.setItem("token", token);
|
|
localStorage.setItem("auth_token", token);
|
|
|
|
toast.success(t.success.job_created);
|
|
router.push("/dashboard/jobs");
|
|
|
|
} catch (err: any) {
|
|
toast.error(err.message || t.errors.submit_failed);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col">
|
|
<Navbar />
|
|
|
|
<main className="flex-1 py-12">
|
|
<div className="container max-w-2xl mx-auto px-4">
|
|
<div className="relative mb-8">
|
|
<div className="absolute right-0 top-0">
|
|
<Select
|
|
value={lang}
|
|
onValueChange={(value) => {
|
|
const nextLang = value as Language;
|
|
setLocale(nextLang === "pt" ? "pt-BR" : nextLang);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="pt">🇧🇷 Português</SelectItem>
|
|
<SelectItem value="en">🇺🇸 English</SelectItem>
|
|
<SelectItem value="es">🇪🇸 Español</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="text-center pt-2">
|
|
<h1 className="text-3xl font-bold mb-2">{t.title}</h1>
|
|
<p className="text-muted-foreground">
|
|
{t.subtitle}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="flex justify-center gap-4 mb-8">
|
|
{[1, 2, 3, 4].map((s) => (
|
|
<div
|
|
key={s}
|
|
className={`flex items-center gap-2 ${step >= s ? "text-primary" : "text-muted-foreground"}`}
|
|
>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${step >= s ? "bg-primary text-white" : "bg-muted"}`}>
|
|
{s}
|
|
</div>
|
|
<span className="hidden sm:inline text-sm">
|
|
{s === 1 && "Dados"}
|
|
{s === 2 && "Formulário"}
|
|
{s === 3 && "Pré-visualização"}
|
|
{s === 4 && "Faturamento"}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{step === 1 && t.cardTitle.step1}
|
|
{step === 2 && "Configure o Formulário"}
|
|
{step === 3 && "Pré-visualização"}
|
|
{step === 4 && t.cardTitle.step2}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{step === 1 && t.cardDesc.step1}
|
|
{step === 2 && "Defina as perguntas que os candidatos deverão responder."}
|
|
{step === 3 && "Confira como o anúncio será exibido antes de prosseguir."}
|
|
{step === 4 && "Informe os dados fiscais para finalizar a publicação."}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Step 1: Company */}
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>{t.company.name}</Label>
|
|
<div className="relative">
|
|
<Building2 className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
value={company.name}
|
|
onChange={(e) => setCompany({ ...company, name: e.target.value })}
|
|
placeholder={t.company.namePlaceholder}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label>{t.company.email}</Label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="email"
|
|
value={company.email}
|
|
onChange={(e) => setCompany({ ...company, email: e.target.value })}
|
|
placeholder="contato@empresa.com"
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>CNPJ da empresa (opcional)</Label>
|
|
<Input
|
|
value={company.document}
|
|
onChange={(e) => setCompany({ ...company, document: e.target.value })}
|
|
placeholder="00.000.000/0000-00"
|
|
/>
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div>
|
|
<Label>{t.company.password}</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type={showPassword ? "text" : "password"}
|
|
value={company.password}
|
|
onChange={(e) => setCompany({ ...company, password: e.target.value })}
|
|
placeholder="••••••••"
|
|
className="pl-10 pr-10" // Extra padding for eye icon
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground focus:outline-none"
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confirm Password Field */}
|
|
<div>
|
|
<Label>{t.company.confirmPassword}</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
value={company.confirmPassword}
|
|
onChange={(e) => setCompany({ ...company, confirmPassword: e.target.value })}
|
|
placeholder="••••••••"
|
|
className="pl-10 pr-10"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground focus:outline-none"
|
|
>
|
|
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Phone Field with DDI */}
|
|
<div>
|
|
<Label>{t.company.phone}</Label>
|
|
<div className="flex border rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
<div className="w-[140px] border-r">
|
|
<Select value={company.ddi} onValueChange={(val) => setCompany({ ...company, ddi: val })}>
|
|
<SelectTrigger className="pl-9 relative border-0 shadow-none focus:ring-0 rounded-r-none h-10">
|
|
<Globe className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<SelectValue placeholder="DDI" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{COUNTRY_CODES.map((item) => (
|
|
<SelectItem key={item.country} value={item.code}>
|
|
<span className="flex items-center justify-between w-full min-w-[80px]">
|
|
<span>{item.code}</span>
|
|
<span className="text-muted-foreground ml-2 text-xs truncate max-w-[100px]">{item.country}</span>
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="relative flex-1">
|
|
<Phone className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="tel"
|
|
value={formatPhoneForDisplay(company.phone)}
|
|
onChange={(e) => setCompany({ ...company, phone: e.target.value })}
|
|
placeholder="11 99999-9999"
|
|
className="pl-10 border-0 shadow-none focus-visible:ring-0 rounded-l-none h-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1 ml-1">
|
|
{t.company.phoneHelp}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Website */}
|
|
<div>
|
|
<Label>{t.company.website}</Label>
|
|
<div className="relative">
|
|
<Globe className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="url"
|
|
value={company.website}
|
|
onChange={(e) => setCompany({ ...company, website: e.target.value })}
|
|
placeholder="https://www.suaempresa.com.br"
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Employee Count & Founded Year */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>{t.company.size}</Label>
|
|
<select
|
|
value={company.employeeCount}
|
|
onChange={(e) => setCompany({ ...company, employeeCount: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="">{t.company.sizePlaceholder}</option>
|
|
<option value="1-10">1-10</option>
|
|
<option value="11-50">11-50</option>
|
|
<option value="51-200">51-200</option>
|
|
<option value="201-500">201-500</option>
|
|
<option value="501-1000">501-1000</option>
|
|
<option value="1001+">1001+</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>{t.company.founded}</Label>
|
|
<Input
|
|
type="number"
|
|
value={company.foundedYear}
|
|
onChange={(e) => setCompany({ ...company, foundedYear: e.target.value })}
|
|
placeholder="ex: 2010"
|
|
min="1800"
|
|
max={new Date().getFullYear()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* About Company */}
|
|
<div>
|
|
<Label>{t.company.description}</Label>
|
|
<RichTextEditor
|
|
value={company.description}
|
|
onChange={(val) => setCompany({ ...company, description: val })}
|
|
placeholder="Descreva sua empresa, cultura, missão e valores..."
|
|
minHeight="120px"
|
|
/>
|
|
</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 */}
|
|
<div className="border-t pt-6 mt-6">
|
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
<Briefcase className="h-5 w-5" /> {t.job.title}
|
|
</h3>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>{t.job.jobTitle}</Label>
|
|
<div className="relative">
|
|
<Briefcase className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
value={job.title}
|
|
onChange={(e) => setJob({ ...job, title: e.target.value })}
|
|
placeholder={t.job.jobTitlePlaceholder}
|
|
className="pl-10"
|
|
maxLength={65}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">{job.title.length}/65 caracteres</p>
|
|
</div>
|
|
<div>
|
|
<Label>{t.job.description}</Label>
|
|
<RichTextEditor
|
|
value={job.description}
|
|
onChange={(val) => setJob({ ...job, description: val })}
|
|
placeholder="Descreva as responsabilidades, requisitos e benefícios..."
|
|
minHeight="150px"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div>
|
|
{/* Includes Label and Layout internally. */}
|
|
<LocationPicker
|
|
value={job.location}
|
|
onChange={(val) => setJob({ ...job, location: val })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>País da vaga *</Label>
|
|
<select
|
|
value={job.country}
|
|
onChange={(e) => setJob({ ...job, country: 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>Idioma da descrição *</Label>
|
|
<select
|
|
value={job.descriptionLanguage}
|
|
onChange={(e) => setJob({ ...job, descriptionLanguage: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="">Selecione</option>
|
|
<option value="pt">Português</option>
|
|
<option value="en">English</option>
|
|
<option value="es">Español</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
{/* Salary Section */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label>{t.job.salary}</Label>
|
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={job.salaryNegotiable}
|
|
onChange={(e) => setJob({ ...job, salaryNegotiable: e.target.checked })}
|
|
className="rounded"
|
|
/>
|
|
<span className="text-muted-foreground">{t.job.salaryNegotiable}</span>
|
|
</label>
|
|
</div>
|
|
|
|
{!job.salaryNegotiable && (
|
|
<>
|
|
{/* Currency and Period Row */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">{t.job.currency}</Label>
|
|
<select
|
|
value={job.currency}
|
|
onChange={(e) => setJob({ ...job, currency: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background text-sm"
|
|
>
|
|
<option value="BRL">R$ (BRL)</option>
|
|
<option value="USD">$ (USD)</option>
|
|
<option value="EUR">€ (EUR)</option>
|
|
<option value="JPY">¥ (JPY)</option>
|
|
<option value="GBP">£ (GBP)</option>
|
|
<option value="CNY">¥ (CNY)</option>
|
|
<option value="AED">د.إ (AED)</option>
|
|
<option value="CAD">$ (CAD)</option>
|
|
<option value="AUD">$ (AUD)</option>
|
|
<option value="CHF">Fr (CHF)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs text-muted-foreground">{t.job.period}</Label>
|
|
<select
|
|
value={job.salaryType}
|
|
onChange={(e) => setJob({ ...job, salaryType: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background text-sm"
|
|
>
|
|
<option value="hourly">{t.options.period.hourly}</option>
|
|
<option value="daily">{t.options.period.daily}</option>
|
|
<option value="weekly">{t.options.period.weekly}</option>
|
|
<option value="monthly">{t.options.period.monthly}</option>
|
|
<option value="yearly">{t.options.period.yearly}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Salary Value(s) */}
|
|
{salaryMode === 'fixed' ? (
|
|
<Input
|
|
type="number"
|
|
value={job.salaryFixed}
|
|
onChange={(e) => setJob({ ...job, salaryFixed: e.target.value })}
|
|
placeholder="Valor"
|
|
/>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Input
|
|
type="number"
|
|
value={job.salaryMin}
|
|
onChange={(e) => setJob({ ...job, salaryMin: e.target.value })}
|
|
placeholder="Mín"
|
|
/>
|
|
<Input
|
|
type="number"
|
|
value={job.salaryMax}
|
|
onChange={(e) => setJob({ ...job, salaryMax: e.target.value })}
|
|
placeholder="Máx"
|
|
/>
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => setSalaryMode(salaryMode === 'fixed' ? 'range' : 'fixed')}
|
|
className="flex items-center gap-1 text-sm text-primary hover:underline"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
{salaryMode === 'fixed' ? 'Faixa salarial' : 'Salário fixo'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<Label>{t.job.contractType}</Label>
|
|
<select
|
|
value={job.employmentType}
|
|
onChange={(e) => setJob({ ...job, employmentType: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="">{t.options.any}</option>
|
|
<option value="permanent">{t.options.contract.permanent}</option>
|
|
<option value="contract">{t.options.contract.contract}</option>
|
|
<option value="training">{t.options.contract.training}</option>
|
|
<option value="temporary">{t.options.contract.temporary}</option>
|
|
<option value="voluntary">{t.options.contract.voluntary}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>{t.job.workingHours}</Label>
|
|
<select
|
|
value={job.workingHours}
|
|
onChange={(e) => setJob({ ...job, workingHours: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="">{t.options.any}</option>
|
|
<option value="full-time">{t.options.hours.fullTime}</option>
|
|
<option value="part-time">{t.options.hours.partTime}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>{t.job.workMode}</Label>
|
|
<select
|
|
value={job.workMode}
|
|
onChange={(e) => setJob({ ...job, workMode: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg"
|
|
>
|
|
<option value="remote">{t.options.mode.remote}</option>
|
|
<option value="hybrid">{t.options.mode.hybrid}</option>
|
|
<option value="onsite">{t.options.mode.onsite}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Canal de candidatura</Label>
|
|
<select
|
|
value={job.applicationChannel}
|
|
onChange={(e) => setJob({ ...job, applicationChannel: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="email">E-mail</option>
|
|
<option value="url">Link externo</option>
|
|
<option value="phone">Telefone</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>Currículo</Label>
|
|
<select
|
|
value={job.resumeRequirement}
|
|
onChange={(e) => setJob({ ...job, resumeRequirement: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="required">Obrigatório</option>
|
|
<option value="optional">Opcional</option>
|
|
<option value="none">Não solicitado</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{job.applicationChannel === "email" && (
|
|
<div>
|
|
<Label>E-mail para candidatura</Label>
|
|
<Input
|
|
type="email"
|
|
value={job.applicationEmail}
|
|
onChange={(e) => setJob({ ...job, applicationEmail: e.target.value })}
|
|
placeholder="jobs@empresa.com"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{job.applicationChannel === "url" && (
|
|
<div>
|
|
<Label>URL externa (HTTPS)</Label>
|
|
<Input
|
|
value={job.applicationUrl}
|
|
onChange={(e) => setJob({ ...job, applicationUrl: e.target.value })}
|
|
placeholder="https://empresa.com/carreiras"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{job.applicationChannel === "phone" && (
|
|
<div>
|
|
<Label>Telefone com DDI</Label>
|
|
<Input
|
|
value={job.applicationPhone}
|
|
onChange={(e) => setJob({ ...job, applicationPhone: e.target.value })}
|
|
placeholder="+55 11999998888"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Área de atuação</Label>
|
|
<select
|
|
value={job.jobCategory}
|
|
onChange={(e) => setJob({ ...job, jobCategory: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-lg bg-background"
|
|
>
|
|
<option value="">Selecione</option>
|
|
{JOB_CATEGORIES.map((category) => (
|
|
<option key={category} value={category}>{category}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>Benefícios</Label>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{BENEFIT_OPTIONS.map((benefit) => {
|
|
const checked = job.benefits.includes(benefit);
|
|
return (
|
|
<label key={benefit} className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={(e) => {
|
|
const nextBenefits = e.target.checked
|
|
? [...job.benefits, benefit]
|
|
: job.benefits.filter((item) => item !== benefit);
|
|
setJob({ ...job, benefits: nextBenefits });
|
|
}}
|
|
/>
|
|
{benefit}
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button onClick={handleNext} className="w-full">
|
|
{t.buttons.next}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Questions */}
|
|
{step === 2 && (
|
|
<div className="space-y-6">
|
|
<div className="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded text-sm mb-4">
|
|
<p><strong>Crie formulários inteligentes:</strong> Faça perguntas específicas para filtrar os melhores candidatos.</p>
|
|
</div>
|
|
|
|
<JobFormBuilder
|
|
questions={questions}
|
|
onChange={setQuestions}
|
|
/>
|
|
|
|
<div className="flex gap-3 pt-6">
|
|
<Button variant="outline" onClick={() => setStep(1)} className="flex-1">
|
|
{t.buttons.back}
|
|
</Button>
|
|
<Button onClick={handleNext} className="flex-1">
|
|
Ir para pré-visualização
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Preview */}
|
|
{step === 3 && (
|
|
<div className="space-y-6">
|
|
<div className="bg-muted/50 rounded-lg p-4">
|
|
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
|
<Building2 className="h-4 w-4" /> {t.common.company}
|
|
</h3>
|
|
<p><strong>{t.common.name}:</strong> {company.name}</p>
|
|
<p><strong>{t.common.email}:</strong> {company.email}</p>
|
|
{company.phone && <p><strong>{t.common.phone}:</strong> {company.ddi} {company.phone}</p>}
|
|
</div>
|
|
<div className="bg-muted/50 rounded-lg p-4">
|
|
<h3 className="font-semibold mb-2 flex items-center gap-2">
|
|
<Briefcase className="h-4 w-4" /> {t.common.job}
|
|
</h3>
|
|
<p><strong>{t.common.title}:</strong> {job.title}</p>
|
|
<p><strong>{t.common.location}:</strong> {job.location || "Não informado"}</p>
|
|
<p><strong>País:</strong> {job.country || "Não informado"}</p>
|
|
<p><strong>Idioma:</strong> {job.descriptionLanguage || "Não informado"}</p>
|
|
<p><strong>{t.common.salary}:</strong> {
|
|
job.salaryNegotiable
|
|
? t.job.salaryNegotiable
|
|
: salaryMode === 'fixed'
|
|
? (job.salaryFixed ? `${getCurrencySymbol(job.currency)} ${job.salaryFixed} ${getSalaryPeriodLabel(job.salaryType)}` : t.job.salaryNegotiable)
|
|
: (job.salaryMin && job.salaryMax ? `${getCurrencySymbol(job.currency)} ${job.salaryMin} - ${job.salaryMax} ${getSalaryPeriodLabel(job.salaryType)}` : t.job.salaryNegotiable)
|
|
}</p>
|
|
<p><strong>Perguntas Personalizadas:</strong> {questions.length}</p>
|
|
<p><strong>Canal de candidatura:</strong> {job.applicationChannel}</p>
|
|
<p><strong>Currículo:</strong> {job.resumeRequirement}</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>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> {
|
|
(job.employmentType ? (t.options.contract[job.employmentType as keyof typeof t.options.contract] || job.employmentType) : t.options.any)
|
|
} / {
|
|
job.workingHours === 'full-time' ? t.options.hours.fullTime : job.workingHours === 'part-time' ? t.options.hours.partTime : t.options.any
|
|
} / {
|
|
job.workMode === 'remote' ? t.options.mode.remote : job.workMode === 'hybrid' ? t.options.mode.hybrid : t.options.mode.onsite
|
|
}</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
|
{t.buttons.back}
|
|
</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">
|
|
{loading ? t.buttons.publishing : t.buttons.publish}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</main >
|
|
|
|
<Footer />
|
|
</div >
|
|
);
|
|
}
|