diff --git a/frontend/src/app/dashboard/jobs/new/page.tsx b/frontend/src/app/dashboard/jobs/new/page.tsx index 1b15f29..cf186af 100644 --- a/frontend/src/app/dashboard/jobs/new/page.tsx +++ b/frontend/src/app/dashboard/jobs/new/page.tsx @@ -1,14 +1,14 @@ "use client" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Loader2, PlusCircle } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, @@ -16,55 +16,129 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" import { adminCompaniesApi, jobsApi, type AdminCompany, type CreateJobPayload } from "@/lib/api" +type ApplicationChannel = "email" | "url" | "phone" + +type LocationResult = { + id: number + name: string + type: "city" | "state" + country_id: number + state_id?: number + region_name?: string +} + +type ApiCountry = { + id: number + name: string + iso2: string +} + export default function DashboardNewJobPage() { const [loading, setLoading] = useState(false) const [loadingCompanies, setLoadingCompanies] = useState(true) const [companies, setCompanies] = useState([]) const [formData, setFormData] = useState({ + companyId: "", title: "", description: "", location: "", + country: "", + employmentType: "", + workMode: "", + workingHours: "", salaryMin: "", salaryMax: "", salaryType: "monthly", currency: "BRL", - employmentType: "", - workingHours: "", - companyId: "", + salaryNegotiable: false, + languageLevel: "", + applicationChannel: "email" as ApplicationChannel, + applicationEmail: "", + applicationUrl: "", + applicationPhone: "", + resumeRequirement: "optional", + visaSupport: false, + status: "draft", }) + // Location autocomplete state + const [apiCountries, setApiCountries] = useState([]) + const [locationIds, setLocationIds] = useState<{ cityId: number | null; regionId: number | null }>({ cityId: null, regionId: null }) + const [locationResults, setLocationResults] = useState([]) + const [locationSearching, setLocationSearching] = useState(false) + const [showLocationDropdown, setShowLocationDropdown] = useState(false) + const locationRef = useRef(null) + + // Load companies useEffect(() => { - const loadCompanies = async () => { - try { - setLoadingCompanies(true) - const data = await adminCompaniesApi.list(undefined, 1, 100) - setCompanies(data.data ?? []) - } catch (error) { - console.error("Falha ao carregar empresas:", error) - toast.error("Falha ao carregar empresas") - } finally { - setLoadingCompanies(false) + adminCompaniesApi.list(undefined, 1, 100) + .then((data) => setCompanies(data.data ?? [])) + .catch(() => toast.error("Falha ao carregar empresas")) + .finally(() => setLoadingCompanies(false)) + }, []) + + // Load countries for location autocomplete + useEffect(() => { + const apiBase = process.env.NEXT_PUBLIC_API_URL || "" + fetch(`${apiBase}/api/v1/locations/countries`) + .then((r) => r.json()) + .then((data: ApiCountry[]) => setApiCountries(data)) + .catch(() => {}) + }, []) + + const selectedCountryId = useMemo( + () => apiCountries.find((c) => c.iso2 === formData.country)?.id ?? null, + [apiCountries, formData.country] + ) + + const searchLocation = useCallback((query: string, countryId: number) => { + const apiBase = process.env.NEXT_PUBLIC_API_URL || "" + setLocationSearching(true) + fetch(`${apiBase}/api/v1/locations/search?q=${encodeURIComponent(query)}&country_id=${countryId}`) + .then((r) => r.json()) + .then((data) => { + setLocationResults(Array.isArray(data) ? data : []) + setShowLocationDropdown(true) + }) + .catch(() => setLocationResults([])) + .finally(() => setLocationSearching(false)) + }, []) + + useEffect(() => { + const query = formData.location.trim() + if (query.length < 2 || !selectedCountryId) { + setLocationResults([]) + setShowLocationDropdown(false) + return + } + const timer = setTimeout(() => searchLocation(query, selectedCountryId), 350) + return () => clearTimeout(timer) + }, [formData.location, selectedCountryId, searchLocation]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (locationRef.current && !locationRef.current.contains(e.target as Node)) { + setShowLocationDropdown(false) } } - - loadCompanies() + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) }, []) + const set = (field: string, value: string | boolean) => + setFormData((prev) => ({ ...prev, [field]: value })) + const canSubmit = formData.title.trim().length >= 5 && formData.description.trim().length >= 20 && formData.companyId !== "" - const handleInputChange = (field: string, value: string) => { - setFormData((prev) => ({ ...prev, [field]: value })) - } - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (!canSubmit) { toast.error("Preencha os campos obrigatórios") return @@ -77,32 +151,55 @@ export default function DashboardNewJobPage() { title: formData.title, description: formData.description, location: formData.location || undefined, - employmentType: - (formData.employmentType as CreateJobPayload["employmentType"]) || undefined, + ...(locationIds.cityId && { cityId: locationIds.cityId }), + ...(locationIds.regionId && { regionId: locationIds.regionId }), + employmentType: (formData.employmentType as CreateJobPayload["employmentType"]) || undefined, + workMode: (formData.workMode as CreateJobPayload["workMode"]) || undefined, + workingHours: formData.workingHours || undefined, salaryMin: formData.salaryMin ? parseFloat(formData.salaryMin) : undefined, salaryMax: formData.salaryMax ? parseFloat(formData.salaryMax) : undefined, salaryType: (formData.salaryType as CreateJobPayload["salaryType"]) || undefined, currency: (formData.currency as CreateJobPayload["currency"]) || undefined, - workingHours: formData.workingHours || undefined, - status: "draft", + salaryNegotiable: formData.salaryNegotiable, + languageLevel: formData.languageLevel || undefined, + visaSupport: formData.visaSupport, + requirements: { + resumeRequirement: formData.resumeRequirement, + applicationChannel: formData.applicationChannel, + applicationEmail: formData.applicationChannel === "email" ? formData.applicationEmail : null, + applicationUrl: formData.applicationChannel === "url" ? formData.applicationUrl : null, + applicationPhone: formData.applicationChannel === "phone" ? formData.applicationPhone : null, + }, + status: formData.status as CreateJobPayload["status"], } await jobsApi.create(payload) - toast.success("Vaga cadastrada no dashboard com sucesso") + toast.success("Vaga cadastrada com sucesso!") setFormData({ + companyId: "", title: "", description: "", location: "", + country: "", + employmentType: "", + workMode: "", + workingHours: "", salaryMin: "", salaryMax: "", salaryType: "monthly", currency: "BRL", - employmentType: "", - workingHours: "", - companyId: "", + salaryNegotiable: false, + languageLevel: "", + applicationChannel: "email", + applicationEmail: "", + applicationUrl: "", + applicationPhone: "", + resumeRequirement: "optional", + visaSupport: false, + status: "draft", }) + setLocationIds({ cityId: null, regionId: null }) } catch (error: any) { - console.error("Falha ao cadastrar vaga:", error) toast.error(error.message || "Falha ao cadastrar vaga") } finally { setLoading(false) @@ -111,34 +208,76 @@ export default function DashboardNewJobPage() { return (
-
-
-

Nova vaga

-

- Cadastre vagas usando o padrão visual do dashboard. -

-
+
+

Nova vaga

+

Preencha os dados da vaga. Os campos marcados com * são obrigatórios.

- - - - Dados da vaga - - - Informe os detalhes obrigatórios para criar a vaga em rascunho. - - - -
+ + {/* Empresa e Status */} + + + + Empresa e status + + + +
+ + {loadingCompanies ? ( +
+ Carregando... +
+ ) : ( + + )} +
+ +
+ + +
+
+
+ + {/* Dados da vaga */} + + + Dados da vaga + +
handleInputChange("title", e.target.value)} + onChange={(e) => set("title", e.target.value)} /> +

{formData.title.length}/255 caracteres

@@ -148,90 +287,191 @@ export default function DashboardNewJobPage() { rows={6} placeholder="Descreva responsabilidades, requisitos e diferenciais..." value={formData.description} - onChange={(e) => handleInputChange("description", e.target.value)} + onChange={(e) => set("description", e.target.value)} />
+
+ + +
+
+
+ + {/* Localização */} + + + Localização + + +
+ + +
+ +
+ +
+ { + set("location", e.target.value) + setLocationIds({ cityId: null, regionId: null }) + }} + onFocus={() => { + if (locationResults.length > 0) setShowLocationDropdown(true) + }} + /> + {locationSearching && ( +
+
+
+ )} + {showLocationDropdown && locationResults.length > 0 && ( +
+ {locationResults.map((result) => ( + + ))} +
+ )} +
+
+ +
+ + +
+ +
+ set("visaSupport", v === true)} + /> + +
+ + + + {/* Contrato e Salário */} + + + Contrato e salário + +
-
- - handleInputChange("location", e.target.value)} - /> -
-
- set("employmentType", v)}> + Permanente Tempo integral Meio período - Contrato + Contrato (PJ) + Terceirizado Temporário Estágio/Trainee + Voluntário
-
- -
-
- - handleInputChange("salaryMin", e.target.value)} - /> -
- + handleInputChange("salaryMax", e.target.value)} + id="workingHours" + placeholder="Ex: 9h às 18h, 40h/semana" + value={formData.workingHours} + onChange={(e) => set("workingHours", e.target.value)} />
-
+
- set("currency", v)}> + - BRL - R$ - USD - $ - EUR - € - GBP - £ + BRL — R$ + USD — $ + EUR — € + GBP — £ + JPY — ¥ + CNY — ¥ + AED — د.إ + CAD — $ + AUD — $ + CHF — Fr
- - set("salaryType", v)}> + Por hora Por dia @@ -243,51 +483,118 @@ export default function DashboardNewJobPage() {
- + handleInputChange("workingHours", e.target.value)} + id="salaryMin" + type="number" + min="0" + placeholder="3000" + value={formData.salaryMin} + onChange={(e) => set("salaryMin", e.target.value)} + /> +
+ +
+ + set("salaryMax", e.target.value)} />
-
- - {loadingCompanies ? ( -
- Carregando empresas... -
- ) : ( - set("applicationChannel", v as ApplicationChannel)}> + - {companies.length === 0 ? ( - - Nenhuma empresa disponível - - ) : ( - companies.map((company) => ( - - {company.name} - - )) - )} + E-mail + Link externo + Telefone - )} +
+ +
+ + +
- - -
-
+ {formData.applicationChannel === "email" && ( +
+ + set("applicationEmail", e.target.value)} + /> +
+ )} + + {formData.applicationChannel === "url" && ( +
+ + set("applicationUrl", e.target.value)} + /> +
+ )} + + {formData.applicationChannel === "phone" && ( +
+ + set("applicationPhone", e.target.value)} + /> +
+ )} + + + + +
) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7a7ed3e..6ea4c71 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -364,15 +364,27 @@ export interface CreateJobPayload { companyId: string; title: string; description: string; + location?: string; + cityId?: number; + regionId?: number; + employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent'; + workMode?: 'onsite' | 'hybrid' | 'remote'; + workingHours?: string; salaryMin?: number; salaryMax?: number; salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly'; - currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY'; + currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY' | 'CNY' | 'AED' | 'CAD' | 'AUD' | 'CHF'; salaryNegotiable?: boolean; - employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent'; - workingHours?: string; - location?: string; - status: 'draft' | 'published' | 'open'; + languageLevel?: string; + visaSupport?: boolean; + requirements?: { + resumeRequirement?: string; + applicationChannel?: string; + applicationEmail?: string | null; + applicationUrl?: string | null; + applicationPhone?: string | null; + }; + status: 'draft' | 'review' | 'open' | 'paused' | 'closed' | 'published'; } export const jobsApi = {