feat(dashboard): rewrite jobs/new with all backend fields and location autocomplete
- Add workMode, languageLevel, visaSupport, salaryNegotiable to form - Add all 10 currency options and all 8 employment types (including dispatch/voluntary) - Add status selector (draft/review/open/paused/closed) - Add location autocomplete with country dropdown and city/region search - Add application channel (email/url/phone) with conditional inputs - Add resumeRequirement selector and requirements map in payload - Load companies via adminCompaniesApi (no registration — user is already logged in) - Extend CreateJobPayload type with workMode, cityId, regionId, languageLevel, visaSupport, requirements, expanded currency and status options Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
edc1b76cfd
commit
2b98552658
2 changed files with 463 additions and 144 deletions
|
|
@ -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<AdminCompany[]>([])
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Location autocomplete state
|
||||
const [apiCountries, setApiCountries] = useState<ApiCountry[]>([])
|
||||
const [locationIds, setLocationIds] = useState<{ cityId: number | null; regionId: number | null }>({ cityId: null, regionId: null })
|
||||
const [locationResults, setLocationResults] = useState<LocationResult[]>([])
|
||||
const [locationSearching, setLocationSearching] = useState(false)
|
||||
const [showLocationDropdown, setShowLocationDropdown] = useState(false)
|
||||
const locationRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
loadCompanies()
|
||||
// Load companies
|
||||
useEffect(() => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Nova vaga</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Cadastre vagas usando o padrão visual do dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1">Preencha os dados da vaga. Os campos marcados com * são obrigatórios.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Empresa e Status */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<PlusCircle className="h-5 w-5" /> Dados da vaga
|
||||
<PlusCircle className="h-5 w-5" /> Empresa e status
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Informe os detalhes obrigatórios para criar a vaga em rascunho.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<CardContent className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Empresa *</Label>
|
||||
{loadingCompanies ? (
|
||||
<div className="h-10 px-3 flex items-center text-sm text-muted-foreground border rounded-md">
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Carregando...
|
||||
</div>
|
||||
) : (
|
||||
<Select value={formData.companyId} onValueChange={(v) => set("companyId", v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione uma empresa" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.length === 0 ? (
|
||||
<SelectItem value="__none" disabled>Nenhuma empresa disponível</SelectItem>
|
||||
) : (
|
||||
companies.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<Select value={formData.status} onValueChange={(v) => set("status", v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Rascunho</SelectItem>
|
||||
<SelectItem value="review">Em revisão</SelectItem>
|
||||
<SelectItem value="open">Aberta</SelectItem>
|
||||
<SelectItem value="paused">Pausada</SelectItem>
|
||||
<SelectItem value="closed">Encerrada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dados da vaga */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dados da vaga</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="title">Título da vaga *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
maxLength={255}
|
||||
placeholder="Ex: Desenvolvedor(a) Full Stack Sênior"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange("title", e.target.value)}
|
||||
onChange={(e) => set("title", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">{formData.title.length}/255 caracteres</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -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)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Idioma da descrição</Label>
|
||||
<Select value={formData.languageLevel} onValueChange={(v) => set("languageLevel", v)}>
|
||||
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pt">Português</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
<SelectItem value="ja">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Localização */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Localização</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>País</Label>
|
||||
<Select value={formData.country} onValueChange={(v) => {
|
||||
set("country", v)
|
||||
set("location", "")
|
||||
setLocationIds({ cityId: null, regionId: null })
|
||||
}}>
|
||||
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{apiCountries.length > 0
|
||||
? apiCountries.map((c) => (
|
||||
<SelectItem key={c.id} value={c.iso2}>{c.name}</SelectItem>
|
||||
))
|
||||
: ["US","BR","PT","ES","GB","DE","FR","JP"].map((iso) => (
|
||||
<SelectItem key={iso} value={iso}>{iso}</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Cidade / Estado</Label>
|
||||
<div ref={locationRef} className="relative">
|
||||
<Input
|
||||
placeholder={formData.country ? "Digite para buscar..." : "Selecione um país primeiro"}
|
||||
value={formData.location}
|
||||
disabled={!formData.country}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
set("location", e.target.value)
|
||||
setLocationIds({ cityId: null, regionId: null })
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (locationResults.length > 0) setShowLocationDropdown(true)
|
||||
}}
|
||||
/>
|
||||
{locationSearching && (
|
||||
<div className="absolute right-3 top-2.5">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
)}
|
||||
{showLocationDropdown && locationResults.length > 0 && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border bg-white shadow-md max-h-60 overflow-y-auto">
|
||||
{locationResults.map((result) => (
|
||||
<button
|
||||
key={`${result.type}-${result.id}`}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-gray-50"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
const label = result.region_name
|
||||
? `${result.name}, ${result.region_name}`
|
||||
: result.name
|
||||
set("location", label)
|
||||
setLocationIds({
|
||||
cityId: result.type === "city" ? result.id : null,
|
||||
regionId: result.type === "state" ? result.id : (result.state_id ?? null),
|
||||
})
|
||||
setShowLocationDropdown(false)
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="text-sm font-medium">{result.name}</span>
|
||||
{result.region_name && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">— {result.region_name}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`rounded px-1.5 py-0.5 text-xs ${result.type === "city" ? "bg-blue-50 text-blue-600" : "bg-emerald-50 text-emerald-600"}`}>
|
||||
{result.type}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Modo de trabalho</Label>
|
||||
<Select value={formData.workMode} onValueChange={(v) => set("workMode", v)}>
|
||||
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="onsite">Presencial</SelectItem>
|
||||
<SelectItem value="hybrid">Híbrido</SelectItem>
|
||||
<SelectItem value="remote">Remoto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-6">
|
||||
<Checkbox
|
||||
id="visaSupport"
|
||||
checked={formData.visaSupport}
|
||||
onCheckedChange={(v) => set("visaSupport", v === true)}
|
||||
/>
|
||||
<Label htmlFor="visaSupport">Oferece suporte de visto</Label>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contrato e Salário */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contrato e salário</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="location">Localização</Label>
|
||||
<Input
|
||||
id="location"
|
||||
placeholder="Ex: São Paulo/SP ou Remoto"
|
||||
value={formData.location}
|
||||
onChange={(e) => handleInputChange("location", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tipo de contrato</Label>
|
||||
<Select
|
||||
value={formData.employmentType}
|
||||
onValueChange={(v) => handleInputChange("employmentType", v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<Select value={formData.employmentType} onValueChange={(v) => set("employmentType", v)}>
|
||||
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="permanent">Permanente</SelectItem>
|
||||
<SelectItem value="full-time">Tempo integral</SelectItem>
|
||||
<SelectItem value="part-time">Meio período</SelectItem>
|
||||
<SelectItem value="contract">Contrato</SelectItem>
|
||||
<SelectItem value="contract">Contrato (PJ)</SelectItem>
|
||||
<SelectItem value="dispatch">Terceirizado</SelectItem>
|
||||
<SelectItem value="temporary">Temporário</SelectItem>
|
||||
<SelectItem value="training">Estágio/Trainee</SelectItem>
|
||||
<SelectItem value="voluntary">Voluntário</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="salaryMin">Salário mínimo</Label>
|
||||
<Input
|
||||
id="salaryMin"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="Ex: 3000"
|
||||
value={formData.salaryMin}
|
||||
onChange={(e) => handleInputChange("salaryMin", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="salaryMax">Salário máximo</Label>
|
||||
<Label htmlFor="workingHours">Jornada de trabalho</Label>
|
||||
<Input
|
||||
id="salaryMax"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="Ex: 6000"
|
||||
value={formData.salaryMax}
|
||||
onChange={(e) => handleInputChange("salaryMax", e.target.value)}
|
||||
id="workingHours"
|
||||
placeholder="Ex: 9h às 18h, 40h/semana"
|
||||
value={formData.workingHours}
|
||||
onChange={(e) => set("workingHours", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Label>Moeda</Label>
|
||||
<Select value={formData.currency} onValueChange={(v) => handleInputChange("currency", v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<Select value={formData.currency} onValueChange={(v) => set("currency", v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="BRL">BRL - R$</SelectItem>
|
||||
<SelectItem value="USD">USD - $</SelectItem>
|
||||
<SelectItem value="EUR">EUR - €</SelectItem>
|
||||
<SelectItem value="GBP">GBP - £</SelectItem>
|
||||
<SelectItem value="BRL">BRL — R$</SelectItem>
|
||||
<SelectItem value="USD">USD — $</SelectItem>
|
||||
<SelectItem value="EUR">EUR — €</SelectItem>
|
||||
<SelectItem value="GBP">GBP — £</SelectItem>
|
||||
<SelectItem value="JPY">JPY — ¥</SelectItem>
|
||||
<SelectItem value="CNY">CNY — ¥</SelectItem>
|
||||
<SelectItem value="AED">AED — د.إ</SelectItem>
|
||||
<SelectItem value="CAD">CAD — $</SelectItem>
|
||||
<SelectItem value="AUD">AUD — $</SelectItem>
|
||||
<SelectItem value="CHF">CHF — Fr</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Período do salário</Label>
|
||||
<Select value={formData.salaryType} onValueChange={(v) => handleInputChange("salaryType", v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<Label>Período</Label>
|
||||
<Select value={formData.salaryType} onValueChange={(v) => set("salaryType", v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hourly">Por hora</SelectItem>
|
||||
<SelectItem value="daily">Por dia</SelectItem>
|
||||
|
|
@ -243,51 +483,118 @@ export default function DashboardNewJobPage() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="workingHours">Jornada de trabalho</Label>
|
||||
<Label htmlFor="salaryMin">Salário mínimo</Label>
|
||||
<Input
|
||||
id="workingHours"
|
||||
placeholder="Ex: 9h às 18h"
|
||||
value={formData.workingHours}
|
||||
onChange={(e) => handleInputChange("workingHours", e.target.value)}
|
||||
id="salaryMin"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="3000"
|
||||
value={formData.salaryMin}
|
||||
onChange={(e) => set("salaryMin", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="salaryMax">Salário máximo</Label>
|
||||
<Input
|
||||
id="salaryMax"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="6000"
|
||||
value={formData.salaryMax}
|
||||
onChange={(e) => set("salaryMax", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Empresa *</Label>
|
||||
{loadingCompanies ? (
|
||||
<div className="h-10 px-3 flex items-center text-sm text-muted-foreground border rounded-md">
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Carregando empresas...
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="salaryNegotiable"
|
||||
checked={formData.salaryNegotiable}
|
||||
onCheckedChange={(v) => set("salaryNegotiable", v === true)}
|
||||
/>
|
||||
<Label htmlFor="salaryNegotiable">Salário negociável</Label>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={formData.companyId} onValueChange={(v) => handleInputChange("companyId", v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione uma empresa" />
|
||||
</SelectTrigger>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Candidaturas */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Candidaturas</CardTitle>
|
||||
<CardDescription>Como os candidatos devem se candidatar à vaga.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Canal de candidatura</Label>
|
||||
<Select value={formData.applicationChannel} onValueChange={(v) => set("applicationChannel", v as ApplicationChannel)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{companies.length === 0 ? (
|
||||
<SelectItem value="__none" disabled>
|
||||
Nenhuma empresa disponível
|
||||
</SelectItem>
|
||||
) : (
|
||||
companies.map((company) => (
|
||||
<SelectItem key={company.id} value={company.id}>
|
||||
{company.name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
<SelectItem value="email">E-mail</SelectItem>
|
||||
<SelectItem value="url">Link externo</SelectItem>
|
||||
<SelectItem value="phone">Telefone</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={loading || !canSubmit}>
|
||||
{loading ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
|
||||
<div>
|
||||
<Label>Currículo</Label>
|
||||
<Select value={formData.resumeRequirement} onValueChange={(v) => set("resumeRequirement", v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="required">Obrigatório</SelectItem>
|
||||
<SelectItem value="optional">Opcional</SelectItem>
|
||||
<SelectItem value="none">Não solicitado</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.applicationChannel === "email" && (
|
||||
<div>
|
||||
<Label htmlFor="appEmail">E-mail para candidatura</Label>
|
||||
<Input
|
||||
id="appEmail"
|
||||
type="email"
|
||||
placeholder="jobs@empresa.com"
|
||||
value={formData.applicationEmail}
|
||||
onChange={(e) => set("applicationEmail", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.applicationChannel === "url" && (
|
||||
<div>
|
||||
<Label htmlFor="appUrl">Link externo (HTTPS)</Label>
|
||||
<Input
|
||||
id="appUrl"
|
||||
placeholder="https://empresa.com/carreiras"
|
||||
value={formData.applicationUrl}
|
||||
onChange={(e) => set("applicationUrl", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.applicationChannel === "phone" && (
|
||||
<div>
|
||||
<Label htmlFor="appPhone">Telefone (com DDI)</Label>
|
||||
<Input
|
||||
id="appPhone"
|
||||
placeholder="+55 11 99999-8888"
|
||||
value={formData.applicationPhone}
|
||||
onChange={(e) => set("applicationPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button type="submit" disabled={loading || !canSubmit} size="lg">
|
||||
{loading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
Criar vaga
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue