refatoração fluxo de vagas e correção login automático - Frontend: - Implementa componente JobFormBuilder para perguntas dinâmicas - Atualiza página /post-job com fluxo de 3 etapas e integração do builder - Corrige payload de registro (auth.ts) enviando campo password corretamente - Implementa auto-login após cadastro da empresa (redirecionamento e token) - Remove páginas obsoletas de registro de candidato - Backend: - Atualiza CreateCompanyUseCase para retornar token JWT - Ajusta JobService para persistência correta de campos JSON (Questions, Benefits) - Atualiza DTOs de Job e Company para refletir novas estruturas - Adiciona migração (033) para novas colunas de refatoração - Ajustes nos repositórios para suporte aos novos modelos Ref: #refactor-jobs #fix-auth
200 lines
8.1 KiB
TypeScript
200 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Plus, Trash2, GripVertical, X } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
export type QuestionType = "text" | "long_text" | "yes_no" | "multiple_choice" | "select";
|
|
|
|
export interface Question {
|
|
id: string;
|
|
type: QuestionType;
|
|
label: string;
|
|
required: boolean;
|
|
options?: string[]; // For multiple_choice or select
|
|
}
|
|
|
|
interface JobFormBuilderProps {
|
|
questions: Question[];
|
|
onChange: (questions: Question[]) => void;
|
|
maxQuestions?: number;
|
|
}
|
|
|
|
export function JobFormBuilder({ questions, onChange, maxQuestions = 7 }: JobFormBuilderProps) {
|
|
const [activeQuestion, setActiveQuestion] = useState<string | null>(null);
|
|
|
|
const addQuestion = () => {
|
|
if (questions.length >= maxQuestions) return;
|
|
|
|
const newQuestion: Question = {
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
type: "text",
|
|
label: "",
|
|
required: false,
|
|
};
|
|
|
|
onChange([...questions, newQuestion]);
|
|
setActiveQuestion(newQuestion.id);
|
|
};
|
|
|
|
const removeQuestion = (id: string) => {
|
|
onChange(questions.filter((q) => q.id !== id));
|
|
if (activeQuestion === id) setActiveQuestion(null);
|
|
};
|
|
|
|
const updateQuestion = (id: string, updates: Partial<Question>) => {
|
|
onChange(
|
|
questions.map((q) => (q.id === id ? { ...q, ...updates } : q))
|
|
);
|
|
};
|
|
|
|
const addOption = (qId: string) => {
|
|
const q = questions.find(q => q.id === qId);
|
|
if (!q) return;
|
|
const options = q.options || [];
|
|
updateQuestion(qId, { options: [...options, ""] });
|
|
};
|
|
|
|
const updateOption = (qId: string, idx: number, val: string) => {
|
|
const q = questions.find(q => q.id === qId);
|
|
if (!q || !q.options) return;
|
|
const newOptions = [...q.options];
|
|
newOptions[idx] = val;
|
|
updateQuestion(qId, { options: newOptions });
|
|
};
|
|
|
|
const removeOption = (qId: string, idx: number) => {
|
|
const q = questions.find(q => q.id === qId);
|
|
if (!q || !q.options) return;
|
|
const newOptions = q.options.filter((_, i) => i !== idx);
|
|
updateQuestion(qId, { options: newOptions });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-medium">Formulário de Candidatura</h3>
|
|
<p className="text-sm text-muted-foreground">Personalize as perguntas para os candidatos (Máx: {maxQuestions})</p>
|
|
</div>
|
|
<Button onClick={addQuestion} disabled={questions.length >= maxQuestions} size="sm" className="gap-2">
|
|
<Plus className="w-4 h-4" /> Adicionar Pergunta
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Fixed CV Field */}
|
|
<Card className="bg-muted/30 border-dashed">
|
|
<CardContent className="p-4 flex items-center gap-4">
|
|
<div className="bg-primary/10 p-2 rounded text-primary">
|
|
<GripVertical className="w-4 h-4 opacity-50" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="font-medium">Currículo (CV)</p>
|
|
<p className="text-xs text-muted-foreground">Arquivo PDF, DOCX (Obrigatório)</p>
|
|
</div>
|
|
<Badge variant="secondary">Fixo</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{questions.map((q, index) => (
|
|
<Card key={q.id} className="relative group transition-all hover:border-primary/50">
|
|
<CardHeader className="p-4 pb-2 flex flex-row items-start gap-4 space-y-0">
|
|
<div className="bg-muted p-2 rounded cursor-move mt-2">
|
|
<GripVertical className="w-4 h-4 text-muted-foreground" />
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-4">
|
|
<div className="flex gap-4">
|
|
<div className="flex-1">
|
|
<Label className="text-xs text-muted-foreground mb-1 block">Pergunta</Label>
|
|
<Input
|
|
value={q.label}
|
|
onChange={(e) => updateQuestion(q.id, { label: e.target.value })}
|
|
placeholder="Ex: Por que você quer trabalhar aqui?"
|
|
/>
|
|
</div>
|
|
<div className="w-[180px]">
|
|
<Label className="text-xs text-muted-foreground mb-1 block">Tipo de Resposta</Label>
|
|
<Select value={q.type} onValueChange={(val) => updateQuestion(q.id, { type: val as QuestionType })}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">Texto Curto</SelectItem>
|
|
<SelectItem value="long_text">Texto Longo</SelectItem>
|
|
<SelectItem value="yes_no">Sim/Não</SelectItem>
|
|
<SelectItem value="multiple_choice">Múltipla Escolha</SelectItem>
|
|
<SelectItem value="select">Lista Suspensa</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Options for Multiple Choice / Select */}
|
|
{(q.type === 'multiple_choice' || q.type === 'select') && (
|
|
<div className="pl-4 border-l-2 ml-1 space-y-2">
|
|
<Label className="text-xs">Opções</Label>
|
|
{q.options?.map((opt, i) => (
|
|
<div key={i} className="flex gap-2">
|
|
<Input
|
|
value={opt}
|
|
onChange={(e) => updateOption(q.id, i, e.target.value)}
|
|
className="h-8 text-sm"
|
|
placeholder={`Opção ${i + 1}`}
|
|
/>
|
|
<Button size="icon" variant="ghost" className="h-8 w-8 text-muted-foreground hover:text-destructive" onClick={() => removeOption(q.id, i)}>
|
|
<X className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<Button variant="outline" size="sm" onClick={() => addOption(q.id)} className="h-7 text-xs">
|
|
+ Adicionar Opção
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 pt-2">
|
|
<Switch
|
|
checked={q.required}
|
|
onCheckedChange={(checked) => updateQuestion(q.id, { required: checked })}
|
|
id={`req-${q.id}`}
|
|
/>
|
|
<Label htmlFor={`req-${q.id}`} className="text-sm font-normal">Obrigatório</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => removeQuestion(q.id)}
|
|
className="text-muted-foreground hover:text-destructive self-start -mt-1 -mr-2"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</CardHeader>
|
|
</Card>
|
|
))}
|
|
|
|
{questions.length === 0 && (
|
|
<div className="text-center py-8 border-2 border-dashed rounded-lg text-muted-foreground">
|
|
<p>Nenhuma pergunta personalizada adicionada.</p>
|
|
<p className="text-sm">Clique em "Adicionar Pergunta" para criar seu formulário.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|