fix: refactor dashboard urls, fix layout duplication and resolve backend api errors

This commit is contained in:
Tiago Yamamoto 2025-12-09 20:29:49 -03:00
parent 7934afcf0d
commit a505726786
40 changed files with 2374 additions and 6421 deletions

View file

@ -17,15 +17,17 @@ type CoreHandlers struct {
createUserUC *user.CreateUserUseCase
listUsersUC *user.ListUsersUseCase
deleteUserUC *user.DeleteUserUseCase
listCompaniesUC *tenant.ListCompaniesUseCase
}
func NewCoreHandlers(l *auth.LoginUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase) *CoreHandlers {
func NewCoreHandlers(l *auth.LoginUseCase, c *tenant.CreateCompanyUseCase, u *user.CreateUserUseCase, list *user.ListUsersUseCase, del *user.DeleteUserUseCase, lc *tenant.ListCompaniesUseCase) *CoreHandlers {
return &CoreHandlers{
loginUC: l,
createCompanyUC: c,
createUserUC: u,
listUsersUC: list,
deleteUserUC: del,
listCompaniesUC: lc,
}
}
@ -85,6 +87,26 @@ func (h *CoreHandlers) CreateCompany(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(resp)
}
// ListCompanies returns all companies (superadmin or public depending on rule, usually superadmin).
// @Summary List Companies
// @Description Returns a list of all companies.
// @Tags Companies
// @Accept json
// @Produce json
// @Success 200 {array} dto.CompanyResponse
// @Failure 500 {string} string "Internal Server Error"
// @Router /api/v1/companies [get]
func (h *CoreHandlers) ListCompanies(w http.ResponseWriter, r *http.Request) {
resp, err := h.listCompaniesUC.Execute(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// CreateUser creates a new user within the authenticated tenant.
// @Summary Create User
// @Description Creates a new user under the current tenant. Requires Admin role.
@ -184,6 +206,7 @@ func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("User deleted"))
json.NewEncoder(w).Encode(map[string]string{"message": "User deleted"})
}

View file

@ -9,6 +9,7 @@ import (
type CompanyRepository interface {
Save(ctx context.Context, company *entity.Company) (*entity.Company, error)
FindByID(ctx context.Context, id string) (*entity.Company, error)
FindAll(ctx context.Context) ([]*entity.Company, error)
Update(ctx context.Context, company *entity.Company) (*entity.Company, error)
Delete(ctx context.Context, id string) error
}

View file

@ -0,0 +1,37 @@
package tenant
import (
"context"
"github.com/rede5/gohorsejobs/backend/internal/core/dto"
"github.com/rede5/gohorsejobs/backend/internal/core/ports"
)
type ListCompaniesUseCase struct {
companyRepo ports.CompanyRepository
}
func NewListCompaniesUseCase(companyRepo ports.CompanyRepository) *ListCompaniesUseCase {
return &ListCompaniesUseCase{
companyRepo: companyRepo,
}
}
func (uc *ListCompaniesUseCase) Execute(ctx context.Context) ([]dto.CompanyResponse, error) {
companies, err := uc.companyRepo.FindAll(ctx)
if err != nil {
return nil, err
}
var response []dto.CompanyResponse
for _, c := range companies {
response = append(response, dto.CompanyResponse{
ID: c.ID,
Name: c.Name,
Status: c.Status,
CreatedAt: c.CreatedAt,
})
}
return response, nil
}

View file

@ -80,3 +80,22 @@ func (r *CompanyRepository) Delete(ctx context.Context, id string) error {
_, err := r.db.ExecContext(ctx, query, id)
return err
}
func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, error) {
query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var companies []*entity.Company
for rows.Next() {
c := &entity.Company{}
if err := rows.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, err
}
companies = append(companies, c)
}
return companies, nil
}

View file

@ -47,12 +47,13 @@ func NewRouter() http.Handler {
// UseCases
loginUC := authUC.NewLoginUseCase(userRepo, authService)
createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService)
listCompaniesUC := tenantUC.NewListCompaniesUseCase(companyRepo)
createUserUC := userUC.NewCreateUserUseCase(userRepo, authService)
listUsersUC := userUC.NewListUsersUseCase(userRepo)
deleteUserUC := userUC.NewDeleteUserUseCase(userRepo)
// Handlers & Middleware
coreHandlers := apiHandlers.NewCoreHandlers(loginUC, createCompanyUC, createUserUC, listUsersUC, deleteUserUC)
coreHandlers := apiHandlers.NewCoreHandlers(loginUC, createCompanyUC, createUserUC, listUsersUC, deleteUserUC, listCompaniesUC)
authMiddleware := middleware.NewMiddleware(authService)
// Initialize Legacy Handlers
@ -68,6 +69,7 @@ func NewRouter() http.Handler {
// Public
mux.HandleFunc("POST /api/v1/auth/login", coreHandlers.Login)
mux.HandleFunc("POST /api/v1/companies", coreHandlers.CreateCompany)
mux.HandleFunc("GET /api/v1/companies", coreHandlers.ListCompanies)
// Protected
// Note: In Go 1.22+, we can wrap specific patterns. Or we can just wrap the handler.

View file

@ -1,222 +0,0 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Search, Eye, Mail, Phone, MapPin, Briefcase } from "lucide-react"
import { mockCandidates } from "@/lib/mock-data"
export default function AdminCandidatesPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedCandidate, setSelectedCandidate] = useState<any>(null)
const filteredCandidates = mockCandidates.filter(
(candidate) =>
candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
candidate.email.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="flex min-h-screen bg-background">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Candidatos</h1>
<p className="text-muted-foreground mt-1">Visualize e gerencie todos os candidatos cadastrados</p>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Candidatos</CardDescription>
<CardTitle className="text-3xl">{mockCandidates.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Novos (30 dias)</CardDescription>
<CardTitle className="text-3xl">24</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidaturas Ativas</CardDescription>
<CardTitle className="text-3xl">{"49"}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Taxa de Contratação</CardDescription>
<CardTitle className="text-3xl">8%</CardTitle>
</CardHeader>
</Card>
</div>
{/* Search and Table */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar candidatos por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Candidato</TableHead>
<TableHead>Email</TableHead>
<TableHead>Telefone</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCandidates.map((candidate) => (
<TableRow key={candidate.id}>
<TableCell>
<div className="flex items-center gap-3">
<div>
<div className="font-medium">{candidate.name}</div>
<div className="text-sm text-muted-foreground">{candidate.title}</div>
</div>
</div>
</TableCell>
<TableCell>{candidate.email}</TableCell>
<TableCell>{candidate.phone}</TableCell>
<TableCell>{candidate.location}</TableCell>
<TableCell>
<Badge variant="secondary">{candidate.applications.length}</Badge>
</TableCell>
<TableCell className="text-right">
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => setSelectedCandidate(candidate)}>
<Eye className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Perfil do Candidato</DialogTitle>
<DialogDescription>Informações detalhadas sobre {candidate.name}</DialogDescription>
</DialogHeader>
{selectedCandidate && (
<div className="space-y-6">
<div className="flex items-start gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={selectedCandidate.avatar || "/placeholder.svg"} />
<AvatarFallback>
{selectedCandidate.name
.split(" ")
.map((n: string) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="text-xl font-semibold">{selectedCandidate.name}</h3>
<p className="text-muted-foreground">{selectedCandidate.title}</p>
<div className="flex flex-wrap gap-2 mt-3">
{selectedCandidate.skills.map((skill: string) => (
<Badge key={skill} variant="secondary">
{skill}
</Badge>
))}
</div>
</div>
</div>
<div className="grid gap-4">
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.email}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.phone}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.location}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Briefcase className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.experience}</span>
</div>
</div>
<div>
<h4 className="font-semibold mb-2">Sobre</h4>
<p className="text-sm text-muted-foreground">{selectedCandidate.bio}</p>
</div>
<div>
<h4 className="font-semibold mb-2">Candidaturas Recentes</h4>
<div className="space-y-2">
{selectedCandidate.applications.map((app: any) => (
<div
key={app.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<div className="font-medium text-sm">{app.jobTitle}</div>
<div className="text-xs text-muted-foreground">{app.company}</div>
</div>
<Badge
variant={
app.status === "accepted"
? "default"
: app.status === "rejected"
? "destructive"
: "secondary"
}
>
{app.status === "pending" && "Pendente"}
{app.status === "accepted" && "Aceito"}
{app.status === "rejected" && "Rejeitado"}
</Badge>
</div>
))}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</main>
</div>
)
}

View file

@ -1,165 +0,0 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent } from "@/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Search, Mail, Eye, UserCheck } from "lucide-react"
const mockCandidates = [
{
id: "1",
name: "João Silva",
email: "joao@example.com",
area: "Desenvolvimento Full Stack",
applications: 3,
status: "active",
},
{
id: "2",
name: "Maria Santos",
email: "maria@example.com",
area: "Design UX/UI",
applications: 5,
status: "active",
},
{
id: "3",
name: "Carlos Oliveira",
email: "carlos@example.com",
area: "Engenharia de Dados",
applications: 2,
status: "active",
},
{
id: "4",
name: "Ana Costa",
email: "ana@example.com",
area: "Product Management",
applications: 4,
status: "active",
},
{
id: "5",
name: "Pedro Alves",
email: "pedro@example.com",
area: "Desenvolvimento Mobile",
applications: 6,
status: "active",
},
]
export default function AdminCandidatosPage() {
const [searchTerm, setSearchTerm] = useState("")
const filteredCandidates = mockCandidates.filter(
(candidate) =>
candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
candidate.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
candidate.area.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="flex min-h-screen bg-background">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-6xl mx-auto space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Gestão de Candidatos</h1>
<p className="text-muted-foreground">Visualize e gerencie todos os candidatos</p>
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar candidatos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">1,834</div>
<p className="text-sm text-muted-foreground">Total de candidatos</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">156</div>
<p className="text-sm text-muted-foreground">Novos este mês</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">89</div>
<p className="text-sm text-muted-foreground">Com candidaturas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">23</div>
<p className="text-sm text-muted-foreground">Em entrevista</p>
</CardContent>
</Card>
</div>
{/* Candidates List */}
<div className="space-y-4">
{filteredCandidates.map((candidate) => (
<Card key={candidate.id}>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row md:items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src="/placeholder.svg" />
<AvatarFallback>{candidate.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-foreground">{candidate.name}</h3>
<Badge variant="secondary">Ativo</Badge>
</div>
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
{candidate.email}
</div>
<div className="flex items-center gap-2">
<UserCheck className="h-4 w-4" />
{candidate.area}
</div>
<div>{candidate.applications} candidaturas</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Ver perfil
</Button>
<Button variant="outline" size="sm">
<Mail className="h-4 w-4 mr-2" />
Contatar
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</main>
</div>
)
}

View file

@ -1,223 +0,0 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Edit, Trash2, Eye } from "lucide-react"
import { mockJobs } from "@/lib/mock-data"
export default function AdminJobsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [jobs, setJobs] = useState(mockJobs)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const filteredJobs = jobs.filter(
(job) =>
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
job.company.toLowerCase().includes(searchTerm.toLowerCase()),
)
const handleDeleteJob = (id: string) => {
setJobs(jobs.filter((job) => job.id !== id))
}
return (
<div className="flex min-h-screen bg-background">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Vagas</h1>
<p className="text-muted-foreground mt-1">Gerencie todas as vagas publicadas na plataforma</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Nova Vaga
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Criar Nova Vaga</DialogTitle>
<DialogDescription>Preencha os detalhes da nova vaga de emprego</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="title">Título da Vaga</Label>
<Input id="title" placeholder="Ex: Desenvolvedor Full Stack" />
</div>
<div className="grid gap-2">
<Label htmlFor="company">Empresa</Label>
<Input id="company" placeholder="Nome da empresa" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="location">Localização</Label>
<Input id="location" placeholder="São Paulo, SP" />
</div>
<div className="grid gap-2">
<Label htmlFor="type">Tipo</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="full-time">Tempo Integral</SelectItem>
<SelectItem value="part-time">Meio Período</SelectItem>
<SelectItem value="contract">Contrato</SelectItem>
<SelectItem value="Remoto">Remoto</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="salary">Salário</Label>
<Input id="salary" placeholder="R$ 8.000 - R$ 12.000" />
</div>
<div className="grid gap-2">
<Label htmlFor="level">Nível</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="junior">Júnior</SelectItem>
<SelectItem value="pleno">Pleno</SelectItem>
<SelectItem value="senior">Sênior</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Descrição</Label>
<Textarea
id="description"
placeholder="Descreva as responsabilidades e requisitos da vaga..."
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancelar
</Button>
<Button onClick={() => setIsDialogOpen(false)}>Publicar Vaga</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Vagas</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Vagas Ativas</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidaturas</CardDescription>
<CardTitle className="text-3xl">{"436"}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Taxa de Conversão</CardDescription>
<CardTitle className="text-3xl">12%</CardTitle>
</CardHeader>
</Card>
</div>
{/* Search and Filter */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar vagas por título ou empresa..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vaga</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredJobs.map((job) => (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</TableCell>
<TableCell>{job.location}</TableCell>
<TableCell>
<Badge variant="secondary">{job.type}</Badge>
</TableCell>
<TableCell>{Math.floor(Math.random() * 50) + 10}</TableCell>
<TableCell>
<Badge variant="default">Ativa</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="icon">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDeleteJob(job.id)}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</main>
</div>
)
}

View file

@ -1,234 +0,0 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Textarea } from "@/components/ui/textarea"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Search, Send, Paperclip } from "lucide-react"
const mockConversations = [
{
id: "1",
name: "Ana Silva",
avatar: "/professional-woman-diverse.png",
lastMessage: "Obrigada pela resposta sobre a vaga!",
timestamp: "10:30",
unread: 2,
},
{
id: "2",
name: "Carlos Santos",
avatar: "/professional-man.jpg",
lastMessage: "Quando posso esperar um retorno?",
timestamp: "Ontem",
unread: 0,
},
{
id: "3",
name: "Maria Oliveira",
avatar: "/professional-woman-smiling.png",
lastMessage: "Gostaria de mais informações sobre os benefícios",
timestamp: "2 dias",
unread: 1,
},
]
const mockMessages = [
{
id: "1",
sender: "Ana Silva",
content: "Olá! Gostaria de saber mais sobre a vaga de Desenvolvedor Full Stack.",
timestamp: "10:15",
isAdmin: false,
},
{
id: "2",
sender: "Você",
content: "Olá Ana! Claro, ficarei feliz em ajudar. A vaga é para trabalho remoto e oferece benefícios completos.",
timestamp: "10:20",
isAdmin: true,
},
{
id: "3",
sender: "Ana Silva",
content: "Obrigada pela resposta sobre a vaga!",
timestamp: "10:30",
isAdmin: false,
},
]
export default function AdminMessagesPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedConversation, setSelectedConversation] = useState(mockConversations[0])
const [messageText, setMessageText] = useState("")
const filteredConversations = mockConversations.filter((conv) =>
conv.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
const handleSendMessage = () => {
if (messageText.trim()) {
console.log("[v0] Sending message:", messageText)
setMessageText("")
}
}
return (
<div className="flex min-h-screen bg-background">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Mensagens</h1>
<p className="text-muted-foreground mt-1">Comunique-se com candidatos e empresas</p>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Conversas</CardDescription>
<CardTitle className="text-3xl">{mockConversations.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Não Lidas</CardDescription>
<CardTitle className="text-3xl">
{mockConversations.reduce((acc, conv) => acc + conv.unread, 0)}
</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Respondidas Hoje</CardDescription>
<CardTitle className="text-3xl">12</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Tempo Médio de Resposta</CardDescription>
<CardTitle className="text-3xl">2h</CardTitle>
</CardHeader>
</Card>
</div>
{/* Messages Interface */}
<Card className="h-[600px]">
<div className="grid grid-cols-[350px_1fr] h-full">
{/* Conversations List */}
<div className="border-r border-border">
<CardHeader className="border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar conversas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</CardHeader>
<ScrollArea className="h-[calc(600px-80px)]">
<div className="p-2">
{filteredConversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => setSelectedConversation(conversation)}
className={`w-full flex items-start gap-3 p-3 rounded-lg hover:bg-muted transition-colors ${
selectedConversation.id === conversation.id ? "bg-muted" : ""
}`}
>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate">{conversation.name}</span>
<span className="text-xs text-muted-foreground">{conversation.timestamp}</span>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground truncate">{conversation.lastMessage}</p>
{conversation.unread > 0 && (
<Badge
variant="default"
className="ml-2 h-5 w-5 p-0 flex items-center justify-center rounded-full"
>
{conversation.unread}
</Badge>
)}
</div>
</div>
</button>
))}
</div>
</ScrollArea>
</div>
{/* Chat Area */}
<div className="flex flex-col">
{/* Chat Header */}
<CardHeader className="border-b border-border">
<div className="flex items-center gap-3">
<div>
<CardTitle className="text-base">{selectedConversation.name}</CardTitle>
<CardDescription className="text-xs">Online</CardDescription>
</div>
</div>
</CardHeader>
{/* Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{mockMessages.map((message) => (
<div key={message.id} className={`flex ${message.isAdmin ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[70%] rounded-lg p-3 ${
message.isAdmin ? "bg-primary text-primary-foreground" : "bg-muted"
}`}
>
<p className="text-sm">{message.content}</p>
<span className="text-xs opacity-70 mt-1 block">{message.timestamp}</span>
</div>
</div>
))}
</div>
</ScrollArea>
{/* Message Input */}
<div className="border-t border-border p-4">
<div className="flex items-end gap-2">
<Button variant="outline" size="icon">
<Paperclip className="h-4 w-4" />
</Button>
<Textarea
placeholder="Digite sua mensagem..."
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}}
className="min-h-[60px] resize-none"
/>
<Button onClick={handleSendMessage} size="icon">
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</Card>
</div>
</main>
</div>
)
}

View file

@ -1,218 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { DashboardHeader } from "@/components/dashboard-header"
import { AdminSidebar } from "@/components/admin-sidebar"
import { StatsCard } from "@/components/stats-card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { getCurrentUser } from "@/lib/auth"
import { mockStats, mockJobs } from "@/lib/mock-data"
import { Briefcase, Users, TrendingUp, FileText, Plus, MoreHorizontal } from "lucide-react"
import { motion } from "framer-motion"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
const chartData = [
{ month: "Jan", applications: 45 },
{ month: "Fev", applications: 52 },
{ month: "Mar", applications: 61 },
{ month: "Abr", applications: 73 },
{ month: "Mai", applications: 89 },
{ month: "Jun", applications: 94 },
]
const chartConfig = {
applications: {
label: "Candidaturas",
color: "hsl(var(--chart-1))",
},
}
const mockCandidates = [
{ id: "1", name: "João Silva", position: "Desenvolvedor Full Stack", status: "active" },
{ id: "2", name: "Maria Santos", position: "Designer UX/UI", status: "active" },
{ id: "3", name: "Carlos Oliveira", position: "Product Manager", status: "pending" },
{ id: "4", name: "Ana Costa", position: "Engenheiro de Dados", status: "active" },
{ id: "5", name: "Pedro Alves", position: "DevOps Engineer", status: "inactive" },
]
export default function AdminDashboard() {
const router = useRouter()
const [user, setUser] = useState(getCurrentUser())
useEffect(() => {
const currentUser = getCurrentUser()
if (!currentUser || currentUser.role !== "admin") {
router.push("/login")
} else {
setUser(currentUser)
}
}, [router])
if (!user) {
return null
}
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8"
>
<h1 className="text-3xl font-bold mb-2">Dashboard</h1>
<p className="text-muted-foreground">Visão geral do portal de empregos</p>
</motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<StatsCard
title="Vagas Ativas"
value={mockStats.activeJobs}
icon={Briefcase}
description="Total de vagas publicadas"
/>
<StatsCard
title="Total de Candidatos"
value={mockStats.totalCandidates}
icon={Users}
description="Usuários cadastrados"
/>
<StatsCard
title="Novas Candidaturas"
value={mockStats.newApplications}
icon={FileText}
description="Últimos 7 dias"
/>
<StatsCard title="Taxa de Conversão" value="12.5%" icon={TrendingUp} description="Candidaturas por vaga" />
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Chart */}
{/* Recent Activity */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
</motion.div>
</div>
{/* Jobs Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="mt-8"
>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Gestão de Vagas</CardTitle>
<Button>
<Plus className="mr-2 h-4 w-4" />
Adicionar Vaga
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Título</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Status</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockJobs.slice(0, 5).map((job) => (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</TableCell>
<TableCell>{job.location}</TableCell>
<TableCell>
<Badge variant="secondary">Ativa</Badge>
</TableCell>
<TableCell>{Math.floor(Math.random() * 50) + 10}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
{/* Candidates Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="mt-8"
>
<Card>
<CardHeader>
<CardTitle>Gestão de Candidatos</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>Cargo Pretendido</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockCandidates.map((candidate) => (
<TableRow key={candidate.id}>
<TableCell className="font-medium">{candidate.name}</TableCell>
<TableCell>{candidate.position}</TableCell>
<TableCell>
{candidate.status === "active" && <Badge className="bg-green-500">Ativo</Badge>}
{candidate.status === "pending" && <Badge variant="secondary">Pendente</Badge>}
{candidate.status === "inactive" && <Badge variant="outline">Inativo</Badge>}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
</main>
</div>
</div>
)
}

View file

@ -1,177 +0,0 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { AdminSidebar } from "@/components/admin-sidebar"
import { DashboardHeader } from "@/components/dashboard-header"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ArrowLeft, Upload } from "lucide-react"
import { mockAdminUser } from "@/lib/mock-data"
import { useToast } from "@/hooks/use-toast"
export default function EditAdminProfilePage() {
const router = useRouter()
const { toast } = useToast()
const [formData, setFormData] = useState({
name: mockAdminUser.name,
email: mockAdminUser.email,
phone: mockAdminUser.phone,
department: mockAdminUser.department,
position: mockAdminUser.position,
bio: mockAdminUser.bio,
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Simulate saving
toast({
title: "Perfil atualizado",
description: "Suas informações foram salvas com sucesso.",
})
router.push("/dashboard/admin/profile")
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-3xl mx-auto space-y-8">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-3xl font-bold text-balance">Editar Perfil</h1>
<p className="text-muted-foreground mt-1">Atualize suas informações pessoais</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Avatar Section */}
<Card>
<CardHeader>
<CardTitle>Foto de Perfil</CardTitle>
<CardDescription>Atualize sua foto de perfil</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-6">
<Avatar className="h-24 w-24">
<AvatarImage src={mockAdminUser.avatar || "/placeholder.svg"} alt={formData.name} />
<AvatarFallback className="text-2xl">{formData.name[0]}</AvatarFallback>
</Avatar>
<Button type="button" variant="outline">
<Upload className="h-4 w-4 mr-2" />
Carregar nova foto
</Button>
</div>
</CardContent>
</Card>
{/* Personal Information */}
<Card>
<CardHeader>
<CardTitle>Informações Pessoais</CardTitle>
<CardDescription>Atualize seus dados pessoais</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name">Nome Completo</Label>
<Input id="name" name="name" value={formData.name} onChange={handleChange} required />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="phone">Telefone</Label>
<Input
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="+55 11 99999-9999"
/>
</div>
</CardContent>
</Card>
{/* Professional Information */}
<Card>
<CardHeader>
<CardTitle>Informações Profissionais</CardTitle>
<CardDescription>Atualize seus dados profissionais</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="position">Cargo</Label>
<Input id="position" name="position" value={formData.position} onChange={handleChange} required />
</div>
<div className="space-y-2">
<Label htmlFor="department">Departamento</Label>
<Input
id="department"
name="department"
value={formData.department}
onChange={handleChange}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bio">Sobre</Label>
<Textarea
id="bio"
name="bio"
value={formData.bio}
onChange={handleChange}
rows={4}
placeholder="Conte um pouco sobre sua experiência profissional..."
/>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex gap-4 justify-end">
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancelar
</Button>
<Button type="submit">Salvar Alterações</Button>
</div>
</form>
</div>
</main>
</div>
</div>
)
}

View file

@ -1,165 +0,0 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { AdminSidebar } from "@/components/admin-sidebar"
import { DashboardHeader } from "@/components/dashboard-header"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Mail, Phone, Calendar, Briefcase, Edit } from "lucide-react"
import { mockAdminUser } from "@/lib/mock-data"
export default function AdminProfilePage() {
const router = useRouter()
const [admin] = useState(mockAdminUser)
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* Profile Header */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-6 items-start">
<Avatar className="h-32 w-32">
<AvatarImage src={admin.avatar || "/placeholder.svg"} alt={admin.name} />
<AvatarFallback className="text-3xl">{admin.name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-4">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-balance">Olá, {admin.name}!</h1>
<p className="text-lg text-muted-foreground mt-1">{admin.position}</p>
</div>
<Button onClick={() => router.push("/dashboard/admin/profile/edit")} className="cursor-pointer">
<Edit className="h-4 w-4 mr-2" />
Editar Perfil
</Button>
</div>
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
{admin.email}
</div>
<div className="flex items-center gap-2">
<Phone className="h-4 w-4" />
{admin.phone}
</div>
<div className="flex items-center gap-2">
<Briefcase className="h-4 w-4" />
{admin.department}
</div>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Desde {new Date(admin.joinedAt).toLocaleDateString("pt-BR", { month: "long", year: "numeric" })}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Bio Section */}
<Card>
<CardHeader>
<CardTitle>Sobre</CardTitle>
<CardDescription>Informações profissionais</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground leading-relaxed">{admin.bio}</p>
</CardContent>
</Card>
{/* Stats Section */}
<div className="grid gap-6 md:grid-cols-3">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Vagas Ativas</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">156</div>
<p className="text-xs text-muted-foreground mt-1">+12 este mês</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Candidatos</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">1,834</div>
<p className="text-xs text-muted-foreground mt-1">+234 este mês</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Contratações</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">47</div>
<p className="text-xs text-muted-foreground mt-1">+8 este mês</p>
</CardContent>
</Card>
</div>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle>Atividade Recente</CardTitle>
<CardDescription>Suas ações mais recentes na plataforma</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[
{
action: "Publicou nova vaga",
detail: "Desenvolvedor Full Stack Sênior",
time: "Há 2 horas",
},
{
action: "Aprovou candidato",
detail: "Ana Silva para Designer UX/UI",
time: "Há 5 horas",
},
{
action: "Respondeu mensagem",
detail: "Carlos Santos",
time: "Ontem",
},
{
action: "Atualizou vaga",
detail: "Engenheiro de Dados",
time: "Há 2 dias",
},
].map((activity, index) => (
<div key={index}>
<div className="flex items-start justify-between">
<div>
<p className="font-medium">{activity.action}</p>
<p className="text-sm text-muted-foreground">{activity.detail}</p>
</div>
<Badge variant="outline" className="text-xs">
{activity.time}
</Badge>
</div>
{index < 3 && <Separator className="mt-4" />}
</div>
))}
</div>
</CardContent>
</Card>
</div>
</main>
</div>
</div>
)
}

View file

@ -1,131 +0,0 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { mockJobs } from "@/lib/mock-data"
import { Search, Plus, Edit, Trash2, Eye, MapPin, DollarSign } from "lucide-react"
import Link from "next/link"
export default function AdminVagasPage() {
const [searchTerm, setSearchTerm] = useState("")
const filteredJobs = mockJobs.filter(
(job) =>
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
job.company.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="flex min-h-screen bg-background">
<AdminSidebar />
<main className="flex-1 p-8">
<div className="max-w-6xl mx-auto space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Gestão de Vagas</h1>
<p className="text-muted-foreground">Gerencie todas as vagas publicadas</p>
</div>
<Button>
<Plus className="h-4 w-4 mr-2" />
Nova vaga
</Button>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar vagas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">{mockJobs.length}</div>
<p className="text-sm text-muted-foreground">Total de vagas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">{mockJobs.length}</div>
<p className="text-sm text-muted-foreground">Vagas ativas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">89</div>
<p className="text-sm text-muted-foreground">Candidaturas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">12</div>
<p className="text-sm text-muted-foreground">Vagas preenchidas</p>
</CardContent>
</Card>
</div>
{/* Jobs List */}
<div className="space-y-4">
{filteredJobs.map((job) => (
<Card key={job.id}>
<CardHeader>
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div className="flex-1">
<div className="flex items-start gap-3 mb-2">
<div className="flex-1">
<CardTitle className="text-xl">{job.title}</CardTitle>
<CardDescription>{job.company}</CardDescription>
</div>
<Badge>Ativa</Badge>
</div>
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
{job.location}
</div>
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4" />
{job.salary}
</div>
<div>Publicado: {new Date(job.postedAt).toLocaleDateString("pt-BR")}</div>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Link href={`/vagas/${job.id}`}>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Visualizar
</Button>
</Link>
<Button variant="outline" size="sm">
<Edit className="h-4 w-4 mr-2" />
Editar
</Button>
<Button variant="outline" size="sm">
<Trash2 className="h-4 w-4 mr-2" />
Excluir
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</main>
</div>
)
}

View file

@ -0,0 +1,216 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Search, Eye, Mail, Phone, MapPin, Briefcase } from "lucide-react"
import { mockCandidates } from "@/lib/mock-data"
export default function AdminCandidatesPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedCandidate, setSelectedCandidate] = useState<any>(null)
const filteredCandidates = mockCandidates.filter(
(candidate) =>
candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
candidate.email.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Candidatos</h1>
<p className="text-muted-foreground mt-1">Visualize e gerencie todos os candidatos cadastrados</p>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Candidatos</CardDescription>
<CardTitle className="text-3xl">{mockCandidates.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Novos (30 dias)</CardDescription>
<CardTitle className="text-3xl">24</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidaturas Ativas</CardDescription>
<CardTitle className="text-3xl">{"49"}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Taxa de Contratação</CardDescription>
<CardTitle className="text-3xl">8%</CardTitle>
</CardHeader>
</Card>
</div>
{/* Search and Table */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar candidatos por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Candidato</TableHead>
<TableHead>Email</TableHead>
<TableHead>Telefone</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCandidates.map((candidate) => (
<TableRow key={candidate.id}>
<TableCell>
<div className="flex items-center gap-3">
<div>
<div className="font-medium">{candidate.name}</div>
<div className="text-sm text-muted-foreground">{candidate.title}</div>
</div>
</div>
</TableCell>
<TableCell>{candidate.email}</TableCell>
<TableCell>{candidate.phone}</TableCell>
<TableCell>{candidate.location}</TableCell>
<TableCell>
<Badge variant="secondary">{candidate.applications.length}</Badge>
</TableCell>
<TableCell className="text-right">
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => setSelectedCandidate(candidate)}>
<Eye className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Perfil do Candidato</DialogTitle>
<DialogDescription>Informações detalhadas sobre {candidate.name}</DialogDescription>
</DialogHeader>
{selectedCandidate && (
<div className="space-y-6">
<div className="flex items-start gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={selectedCandidate.avatar || "/placeholder.svg"} />
<AvatarFallback>
{selectedCandidate.name
.split(" ")
.map((n: string) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="text-xl font-semibold">{selectedCandidate.name}</h3>
<p className="text-muted-foreground">{selectedCandidate.title}</p>
<div className="flex flex-wrap gap-2 mt-3">
{selectedCandidate.skills.map((skill: string) => (
<Badge key={skill} variant="secondary">
{skill}
</Badge>
))}
</div>
</div>
</div>
<div className="grid gap-4">
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.email}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.phone}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.location}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Briefcase className="h-4 w-4 text-muted-foreground" />
<span>{selectedCandidate.experience}</span>
</div>
</div>
<div>
<h4 className="font-semibold mb-2">Sobre</h4>
<p className="text-sm text-muted-foreground">{selectedCandidate.bio}</p>
</div>
<div>
<h4 className="font-semibold mb-2">Candidaturas Recentes</h4>
<div className="space-y-2">
{selectedCandidate.applications.map((app: any) => (
<div
key={app.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<div className="font-medium text-sm">{app.jobTitle}</div>
<div className="text-xs text-muted-foreground">{app.company}</div>
</div>
<Badge
variant={
app.status === "accepted"
? "default"
: app.status === "rejected"
? "destructive"
: "secondary"
}
>
{app.status === "pending" && "Pendente"}
{app.status === "accepted" && "Aceito"}
{app.status === "rejected" && "Rejeitado"}
</Badge>
</div>
))}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View file

@ -1,186 +0,0 @@
"use client";
import { DashboardHeader } from "@/components/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { mockApplications, mockJobs } from "@/lib/mock-data";
import {
Search,
Calendar,
Building2,
MapPin,
ExternalLink,
} from "lucide-react";
import { useState } from "react";
import Link from "next/link";
const statusMap = {
pending: { label: "Pendente", variant: "secondary" as const },
reviewing: { label: "Em análise", variant: "default" as const },
interview: { label: "Entrevista", variant: "default" as const },
rejected: { label: "Rejeitado", variant: "destructive" as const },
accepted: { label: "Aceito", variant: "default" as const },
};
export default function CandidaturasPage() {
const [searchTerm, setSearchTerm] = useState("");
const filteredApplications = mockApplications.filter(
(app) =>
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.company.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="max-w-5xl mx-auto space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">
Minhas Candidaturas
</h1>
<p className="text-muted-foreground">
Acompanhe o status das suas candidaturas
</p>
</div>
<div className="relative w-full md:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar candidaturas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{mockApplications.length}
</div>
<p className="text-sm text-muted-foreground">Total</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{
mockApplications.filter((a) => a.status === "reviewing")
.length
}
</div>
<p className="text-sm text-muted-foreground">Em análise</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{
mockApplications.filter((a) => a.status === "interview")
.length
}
</div>
<p className="text-sm text-muted-foreground">Entrevistas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{
mockApplications.filter((a) => a.status === "pending")
.length
}
</div>
<p className="text-sm text-muted-foreground">Pendentes</p>
</CardContent>
</Card>
</div>
{/* Applications List */}
<div className="space-y-4">
{filteredApplications.length > 0 ? (
filteredApplications.map((application) => {
const job = mockJobs.find((j) => j.id === application.jobId);
const status = statusMap[application.status];
return (
<Card key={application.id}>
<CardHeader>
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div className="flex-1">
<div className="flex items-start gap-3">
<div className="flex-1">
<CardTitle className="text-xl mb-1">
{application.jobTitle}
</CardTitle>
<CardDescription className="flex items-center gap-2">
<Building2 className="h-4 w-4" />
{application.company}
</CardDescription>
</div>
<Badge variant={status.variant}>
{status.label}
</Badge>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Candidatura:{" "}
{new Date(application.appliedAt).toLocaleDateString(
"pt-BR"
)}
</div>
{job && (
<>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
{job.location}
</div>
</>
)}
</div>
<Link href={`/vagas/${application.jobId}`}>
<Button variant="outline" size="sm">
Ver vaga
<ExternalLink className="h-4 w-4 ml-2" />
</Button>
</Link>
</div>
</CardContent>
</Card>
);
})
) : (
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
Nenhuma candidatura encontrada.
</p>
</CardContent>
</Card>
)}
</div>
</div>
</main>
</div>
);
}

View file

@ -1,279 +0,0 @@
"use client";
import { DashboardHeader } from "@/components/dashboard-header";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Bell,
Check,
CheckCheck,
Trash2,
AlertCircle,
CheckCircle,
Info,
AlertTriangle,
Archive,
} from "lucide-react";
import { useNotifications } from "@/contexts/notification-context";
import { formatDistanceToNow } from "date-fns";
import { ptBR } from "date-fns/locale";
import { motion } from "framer-motion";
export default function NotificationsPage() {
const {
notifications,
unreadCount,
markAsRead,
markAllAsRead,
removeNotification,
clearAllNotifications,
} = useNotifications();
const getNotificationIcon = (type: string) => {
switch (type) {
case "success":
return <CheckCircle className="h-5 w-5 text-green-500" />;
case "error":
return <AlertCircle className="h-5 w-5 text-red-500" />;
case "warning":
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
default:
return <Info className="h-5 w-5 text-blue-500" />;
}
};
const getNotificationBgColor = (type: string, read: boolean) => {
if (read) return "bg-muted/50";
switch (type) {
case "success":
return "bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-900";
case "error":
return "bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-900";
case "warning":
return "bg-yellow-50 dark:bg-yellow-950/20 border-yellow-200 dark:border-yellow-900";
default:
return "bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-900";
}
};
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Notificações
</h1>
<p className="text-muted-foreground">
{unreadCount > 0 ? (
<>
Você tem{" "}
<span className="font-semibold text-foreground">
{unreadCount}
</span>{" "}
{unreadCount === 1
? "notificação não lida"
: "notificações não lidas"}
</>
) : (
"Você está em dia com suas notificações"
)}
</p>
</div>
{notifications.length > 0 && (
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<Button
variant="outline"
size="sm"
onClick={markAllAsRead}
className="gap-2"
>
<CheckCheck className="h-4 w-4" />
Marcar todas como lidas
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={clearAllNotifications}
className="gap-2 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
Limpar todas
</Button>
</div>
)}
</div>
{/* Notifications List */}
{notifications.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<Bell className="h-16 w-16 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-semibold mb-2">
Nenhuma notificação
</h3>
<p className="text-muted-foreground">
Quando você receber notificações, elas aparecerão aqui
</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{notifications.map((notification, index) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card
className={`relative overflow-hidden transition-all hover:shadow-md ${getNotificationBgColor(
notification.type,
notification.read
)}`}
>
<CardContent className="p-4 sm:p-6">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="shrink-0 mt-1">
{getNotificationIcon(notification.type)}
</div>
{/* Content */}
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h3
className={`font-semibold text-base mb-1 ${
notification.read
? "text-muted-foreground"
: "text-foreground"
}`}
>
{notification.title}
</h3>
<p
className={`text-sm leading-relaxed ${
notification.read
? "text-muted-foreground"
: "text-foreground/80"
}`}
>
{notification.message}
</p>
</div>
{/* Unread indicator */}
{!notification.read && (
<div className="shrink-0">
<div className="h-2 w-2 bg-primary rounded-full" />
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between gap-4 pt-2">
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(
new Date(notification.createdAt),
{
addSuffix: true,
locale: ptBR,
}
)}
</span>
{/* Actions */}
<div className="flex items-center gap-1">
{!notification.read && (
<Button
variant="ghost"
size="sm"
onClick={() => markAsRead(notification.id)}
className="h-8 px-3 gap-2"
>
<Check className="h-3 w-3" />
<span className="hidden sm:inline">
Marcar como lida
</span>
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() =>
removeNotification(notification.id)
}
className="h-8 px-3 gap-2 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
<span className="hidden sm:inline">
Excluir
</span>
</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
)}
{/* Stats */}
{notifications.length > 0 && (
<Card>
<CardContent className="p-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-foreground mb-1">
{notifications.length}
</div>
<div className="text-xs text-muted-foreground">Total</div>
</div>
<div>
<div className="text-2xl font-bold text-primary mb-1">
{unreadCount}
</div>
<div className="text-xs text-muted-foreground">
Não lidas
</div>
</div>
<div>
<div className="text-2xl font-bold text-green-600 mb-1">
{notifications.filter((n) => n.type === "success").length}
</div>
<div className="text-xs text-muted-foreground">Sucesso</div>
</div>
<div>
<div className="text-2xl font-bold text-yellow-600 mb-1">
{notifications.filter((n) => n.type === "warning").length}
</div>
<div className="text-xs text-muted-foreground">Avisos</div>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</main>
</div>
);
}

View file

@ -1,221 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { DashboardHeader } from "@/components/dashboard-header";
import { StatsCard } from "@/components/stats-card";
import { JobCard } from "@/components/job-card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getCurrentUser } from "@/lib/auth";
import { mockJobs, mockApplications, mockNotifications } from "@/lib/mock-data";
import {
Bell,
FileText,
Clock,
CheckCircle,
XCircle,
AlertCircle,
Edit,
} from "lucide-react";
import { motion } from "framer-motion";
export default function CandidateDashboard() {
const router = useRouter();
const [user, setUser] = useState(getCurrentUser());
useEffect(() => {
const currentUser = getCurrentUser();
if (!currentUser || currentUser.role !== "candidate") {
router.push("/login");
} else {
setUser(currentUser);
}
}, [router]);
if (!user) {
return null;
}
const recommendedJobs = mockJobs.slice(0, 3);
const unreadNotifications = mockNotifications.filter((n) => !n.read);
const getStatusBadge = (status: string) => {
switch (status) {
case "pending":
return (
<Badge variant="secondary">
<Clock className="h-3 w-3 mr-1" />
Em análise
</Badge>
);
case "reviewing":
return (
<Badge variant="secondary">
<AlertCircle className="h-3 w-3 mr-1" />
Em análise
</Badge>
);
case "interview":
return (
<Badge className="bg-blue-500 hover:bg-blue-600">
<CheckCircle className="h-3 w-3 mr-1" />
Entrevista
</Badge>
);
case "accepted":
return (
<Badge className="bg-green-500 hover:bg-green-600">
<CheckCircle className="h-3 w-3 mr-1" />
Aprovado
</Badge>
);
case "rejected":
return (
<Badge variant="destructive">
<XCircle className="h-3 w-3 mr-1" />
Rejeitado
</Badge>
);
default:
return <Badge variant="outline">{status}</Badge>;
}
};
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Profile Summary */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="mb-8">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div className="flex-1">
<h1 className="text-2xl font-bold mb-2">Olá, {user.name}!</h1>
<p className="text-muted-foreground">{user.area}</p>
</div>
<Button className="cursor-pointer">
<Edit className="mr-2 h-4 w-4" />
Editar Perfil
</Button>
</div>
</CardContent>
</Card>
</motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"
>
<StatsCard
title="Candidaturas"
value={mockApplications.length}
icon={FileText}
description="Total de vagas aplicadas"
/>
<StatsCard
title="Em processo"
value={
mockApplications.filter(
(a) => a.status === "reviewing" || a.status === "interview"
).length
}
icon={Clock}
description="Aguardando resposta"
/>
<StatsCard
title="Notificações"
value={unreadNotifications.length}
icon={Bell}
description="Novas atualizações"
/>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-1 gap-8">
{/* Main Content */}
<div className="space-y-8">
{/* Recommended Jobs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card>
<CardHeader>
<CardTitle>Vagas recomendadas</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{recommendedJobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</CardContent>
</Card>
</motion.div>
{/* Applications */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<Card>
<CardHeader>
<CardTitle>Minhas candidaturas</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vaga</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Status</TableHead>
<TableHead>Data</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockApplications.map((application) => (
<TableRow key={application.id}>
<TableCell className="font-medium">
{application.jobTitle}
</TableCell>
<TableCell>{application.company}</TableCell>
<TableCell>
{getStatusBadge(application.status)}
</TableCell>
<TableCell className="text-muted-foreground">
{new Date(application.appliedAt).toLocaleDateString(
"pt-BR"
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</main>
</div>
);
}

View file

@ -1,224 +0,0 @@
"use client"
import { useState } from "react"
import { DashboardHeader } from "@/components/dashboard-header"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { mockUser } from "@/lib/mock-data"
import { User, Briefcase, GraduationCap, Award, Plus, X } from "lucide-react"
export default function PerfilPage() {
const [skills, setSkills] = useState(["React", "TypeScript", "Node.js", "Python"])
const [newSkill, setNewSkill] = useState("")
const [saved, setSaved] = useState(false)
const addSkill = () => {
if (newSkill.trim() && !skills.includes(newSkill.trim())) {
setSkills([...skills, newSkill.trim()])
setNewSkill("")
}
}
const removeSkill = (skill: string) => {
setSkills(skills.filter((s) => s !== skill))
}
const handleSave = () => {
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="max-w-4xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Meu Perfil</h1>
<p className="text-muted-foreground">Mantenha suas informações atualizadas</p>
</div>
{/* Profile Picture */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Foto de Perfil
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-6">
<Avatar className="h-24 w-24">
<AvatarImage src="/placeholder.svg" />
<AvatarFallback className="text-2xl">{mockUser.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-2">
<Button variant="outline">Alterar foto</Button>
<p className="text-sm text-muted-foreground">JPG, PNG ou GIF. Máximo 2MB.</p>
</div>
</div>
</CardContent>
</Card>
{/* Personal Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Informações Pessoais
</CardTitle>
<CardDescription>Suas informações básicas</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nome completo</Label>
<Input id="name" defaultValue={mockUser.name} />
</div>
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<Input id="email" type="email" defaultValue={mockUser.email} />
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="phone">Telefone</Label>
<Input id="phone" placeholder="(11) 99999-9999" />
</div>
<div className="space-y-2">
<Label htmlFor="location">Localização</Label>
<Input id="location" placeholder="São Paulo, SP" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bio">Sobre mim</Label>
<Textarea id="bio" placeholder="Conte um pouco sobre você e sua experiência profissional..." rows={4} />
</div>
</CardContent>
</Card>
{/* Professional Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Briefcase className="h-5 w-5" />
Informações Profissionais
</CardTitle>
<CardDescription>Sua experiência e área de atuação</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="area">Área de atuação</Label>
<Input id="area" defaultValue={mockUser.area} />
</div>
<div className="space-y-2">
<Label htmlFor="experience">Anos de experiência</Label>
<Input id="experience" type="number" placeholder="5" />
</div>
<div className="space-y-2">
<Label htmlFor="current-role">Cargo atual</Label>
<Input id="current-role" placeholder="Desenvolvedor Full Stack Sênior" />
</div>
<div className="space-y-2">
<Label htmlFor="linkedin">LinkedIn</Label>
<Input id="linkedin" placeholder="https://linkedin.com/in/seu-perfil" />
</div>
<div className="space-y-2">
<Label htmlFor="portfolio">Portfolio / GitHub</Label>
<Input id="portfolio" placeholder="https://github.com/seu-usuario" />
</div>
</CardContent>
</Card>
{/* Skills */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Award className="h-5 w-5" />
Habilidades
</CardTitle>
<CardDescription>Adicione suas principais competências</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
{skills.map((skill) => (
<Badge key={skill} variant="secondary" className="gap-1">
{skill}
<button onClick={() => removeSkill(skill)} className="ml-1 hover:text-destructive">
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
placeholder="Adicionar habilidade..."
value={newSkill}
onChange={(e) => setNewSkill(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && (e.preventDefault(), addSkill())}
/>
<Button onClick={addSkill} variant="outline">
<Plus className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Education */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
Formação Acadêmica
</CardTitle>
<CardDescription>Sua educação e certificações</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="degree">Grau de escolaridade</Label>
<Input id="degree" placeholder="Bacharelado em Ciência da Computação" />
</div>
<div className="space-y-2">
<Label htmlFor="institution">Instituição</Label>
<Input id="institution" placeholder="Universidade de São Paulo" />
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="start-year">Ano de início</Label>
<Input id="start-year" type="number" placeholder="2015" />
</div>
<div className="space-y-2">
<Label htmlFor="end-year">Ano de conclusão</Label>
<Input id="end-year" type="number" placeholder="2019" />
</div>
</div>
</CardContent>
</Card>
{/* Save Button */}
<div className="flex justify-end gap-4">
<Button variant="outline">Cancelar</Button>
<Button onClick={handleSave} disabled={saved}>
{saved ? "Salvo!" : "Salvar alterações"}
</Button>
</div>
</div>
</main>
</div>
)
}

View file

@ -0,0 +1,267 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle } from "lucide-react"
import { companiesApi, type ApiCompany } from "@/lib/api"
import { getCurrentUser } from "@/lib/auth"
import { toast } from "sonner"
export default function AdminCompaniesPage() {
const router = useRouter()
const [companies, setCompanies] = useState<ApiCompany[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [creating, setCreating] = useState(false)
const [formData, setFormData] = useState({
name: "",
slug: "",
email: "",
})
useEffect(() => {
const user = getCurrentUser()
if (!user || (!user.roles?.includes("superadmin") && user.role !== "admin")) {
router.push("/dashboard")
return
}
loadCompanies()
}, [router])
const loadCompanies = async () => {
try {
setLoading(true)
const data = await companiesApi.list()
setCompanies(data || [])
} catch (error) {
console.error("Error loading companies:", error)
toast.error("Erro ao carregar empresas")
} finally {
setLoading(false)
}
}
const handleCreate = async () => {
try {
setCreating(true)
await companiesApi.create(formData)
toast.success("Empresa criada com sucesso!")
setIsDialogOpen(false)
setFormData({ name: "", slug: "", email: "" })
loadCompanies()
} catch (error) {
console.error("Error creating company:", error)
toast.error("Erro ao criar empresa")
} finally {
setCreating(false)
}
}
const generateSlug = (name: string) => {
return name
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "")
}
const filteredCompanies = companies.filter(
(company) =>
company.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
company.email?.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Empresas</h1>
<p className="text-muted-foreground mt-1">Gerencie todas as empresas cadastradas</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={loadCompanies} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Atualizar
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Nova Empresa
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Cadastrar Nova Empresa</DialogTitle>
<DialogDescription>Preencha os dados da empresa</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Nome da Empresa</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
setFormData({
...formData,
name: e.target.value,
slug: generateSlug(e.target.value),
})
}
placeholder="Empresa XYZ"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="slug">Slug (URL)</Label>
<Input
id="slug"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
placeholder="empresa-xyz"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="contato@empresa.com"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancelar</Button>
<Button onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar Empresa
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Empresas</CardDescription>
<CardTitle className="text-3xl">{companies.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Empresas Ativas</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => c.active).length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Verificadas</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => c.verified).length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Pendentes</CardDescription>
<CardTitle className="text-3xl">{companies.filter((c) => !c.verified).length}</CardTitle>
</CardHeader>
</Card>
</div>
{/* Table */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar empresas por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Empresa</TableHead>
<TableHead>Slug</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Verificada</TableHead>
<TableHead>Data Criação</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCompanies.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
Nenhuma empresa encontrada
</TableCell>
</TableRow>
) : (
filteredCompanies.map((company) => (
<TableRow key={company.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4 text-muted-foreground" />
{company.name}
</div>
</TableCell>
<TableCell className="font-mono text-sm">{company.slug}</TableCell>
<TableCell>{company.email || "-"}</TableCell>
<TableCell>
<Badge variant={company.active ? "default" : "secondary"}>
{company.active ? "Ativa" : "Inativa"}
</Badge>
</TableCell>
<TableCell>
{company.verified ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<XCircle className="h-5 w-5 text-muted-foreground" />
)}
</TableCell>
<TableCell>
{company.created_at ? new Date(company.created_at).toLocaleDateString("pt-BR") : "-"}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

View file

@ -1,575 +0,0 @@
"use client";
import { useState } from "react";
import { DashboardHeader } from "@/components/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Search,
Filter,
Eye,
Mail,
Phone,
MapPin,
Calendar,
Briefcase,
CheckCircle2,
XCircle,
Clock,
MessageSquare,
FileText,
ExternalLink,
} from "lucide-react";
import Link from "next/link";
export default function CompanyApplicationsPage() {
const [searchTerm, setSearchTerm] = useState("");
const [jobFilter, setJobFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
const applications = [
{
id: "1",
candidate: {
id: "1",
name: "Ana Silva",
email: "ana.silva@email.com",
phone: "(11) 98765-4321",
location: "São Paulo, SP",
avatar: "",
experience: "5 anos",
title: "Desenvolvedora Full Stack",
},
jobId: "1",
jobTitle: "Desenvolvedor Full Stack Sênior",
status: "pending",
appliedAt: "2025-11-18T10:30:00",
resumeUrl: "#",
},
{
id: "2",
candidate: {
id: "2",
name: "Carlos Santos",
email: "carlos.santos@email.com",
phone: "(11) 91234-5678",
location: "Rio de Janeiro, RJ",
avatar: "",
experience: "3 anos",
title: "Designer UX/UI",
},
jobId: "2",
jobTitle: "Designer UX/UI",
status: "reviewing",
appliedAt: "2025-11-18T09:15:00",
resumeUrl: "#",
},
{
id: "3",
candidate: {
id: "3",
name: "Maria Oliveira",
email: "maria.oliveira@email.com",
phone: "(21) 99876-5432",
location: "Belo Horizonte, MG",
avatar: "",
experience: "7 anos",
title: "Engenheira de Dados",
},
jobId: "3",
jobTitle: "Product Manager",
status: "interview",
appliedAt: "2025-11-17T14:20:00",
resumeUrl: "#",
},
{
id: "4",
candidate: {
id: "4",
name: "Pedro Costa",
email: "pedro.costa@email.com",
phone: "(31) 98765-1234",
location: "Curitiba, PR",
avatar: "",
experience: "6 anos",
title: "Product Manager",
},
jobId: "1",
jobTitle: "Desenvolvedor Full Stack Sênior",
status: "rejected",
appliedAt: "2025-11-16T16:45:00",
resumeUrl: "#",
},
{
id: "5",
candidate: {
id: "5",
name: "Juliana Ferreira",
email: "juliana.ferreira@email.com",
phone: "(41) 91234-8765",
location: "Porto Alegre, RS",
avatar: "",
experience: "4 anos",
title: "DevOps Engineer",
},
jobId: "2",
jobTitle: "Designer UX/UI",
status: "accepted",
appliedAt: "2025-11-15T11:00:00",
resumeUrl: "#",
},
];
const statusConfig = {
pending: {
label: "Pendente",
color: "bg-yellow-500",
variant: "secondary" as const,
},
reviewing: {
label: "Em Análise",
color: "bg-blue-500",
variant: "default" as const,
},
interview: {
label: "Entrevista",
color: "bg-purple-500",
variant: "default" as const,
},
accepted: {
label: "Aceito",
color: "bg-green-500",
variant: "default" as const,
},
rejected: {
label: "Rejeitado",
color: "bg-red-500",
variant: "destructive" as const,
},
};
const filteredApplications = applications.filter((app) => {
const matchesSearch =
app.candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase());
const matchesJob = jobFilter === "all" || app.jobId === jobFilter;
const matchesStatus = statusFilter === "all" || app.status === statusFilter;
return matchesSearch && matchesJob && matchesStatus;
});
const groupedByStatus = {
pending: filteredApplications.filter((a) => a.status === "pending"),
reviewing: filteredApplications.filter((a) => a.status === "reviewing"),
interview: filteredApplications.filter((a) => a.status === "interview"),
accepted: filteredApplications.filter((a) => a.status === "accepted"),
rejected: filteredApplications.filter((a) => a.status === "rejected"),
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return "Agora há pouco";
if (diffHours < 24) return `${diffHours}h`;
if (diffDays === 1) return "Ontem";
if (diffDays < 7) return `${diffDays} dias`;
return date.toLocaleDateString("pt-BR");
};
const ApplicationCard = ({ app }: { app: (typeof applications)[0] }) => (
<Card className="hover:shadow-md transition-shadow">
<CardContent className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex items-start gap-3 flex-1 min-w-0">
<Avatar className="h-12 w-12 shrink-0">
<AvatarImage src={app.candidate.avatar} />
<AvatarFallback className="bg-primary/10 text-primary">
{app.candidate.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-2">
<div className="min-w-0">
<h3 className="font-semibold text-base sm:text-lg truncate">
{app.candidate.name}
</h3>
<p className="text-sm text-muted-foreground truncate">
{app.candidate.title}
</p>
</div>
<Badge
variant={
statusConfig[app.status as keyof typeof statusConfig]
.variant
}
className="shrink-0"
>
{statusConfig[app.status as keyof typeof statusConfig].label}
</Badge>
</div>
<div className="space-y-1.5 text-sm text-muted-foreground mb-3">
<div className="flex items-center gap-2">
<Briefcase className="h-4 w-4 shrink-0" />
<span className="truncate">{app.jobTitle}</span>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1">
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4 shrink-0" />
<span className="truncate">{app.candidate.location}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">
{formatDate(app.appliedAt)}
</span>
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">
{app.candidate.experience}
</span>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Eye className="h-4 w-4 mr-1" />
Ver Perfil
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Perfil do Candidato</DialogTitle>
<DialogDescription>
Informações detalhadas sobre {app.candidate.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-start gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={app.candidate.avatar} />
<AvatarFallback className="bg-primary/10 text-primary text-xl">
{app.candidate.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="font-bold text-xl">
{app.candidate.name}
</h3>
<p className="text-muted-foreground">
{app.candidate.title}
</p>
<Badge
className="mt-2"
variant={
statusConfig[
app.status as keyof typeof statusConfig
].variant
}
>
{
statusConfig[
app.status as keyof typeof statusConfig
].label
}
</Badge>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-4 border-t">
<div>
<Label className="text-xs text-muted-foreground">
Email
</Label>
<p className="text-sm font-medium">
{app.candidate.email}
</p>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Telefone
</Label>
<p className="text-sm font-medium">
{app.candidate.phone}
</p>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Localização
</Label>
<p className="text-sm font-medium">
{app.candidate.location}
</p>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Experiência
</Label>
<p className="text-sm font-medium">
{app.candidate.experience}
</p>
</div>
</div>
<div className="pt-4 border-t">
<Label className="text-xs text-muted-foreground mb-2 block">
Vaga aplicada
</Label>
<p className="text-sm font-medium">{app.jobTitle}</p>
<p className="text-xs text-muted-foreground mt-1">
Candidatura enviada em {formatDate(app.appliedAt)}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<Button className="flex-1" asChild>
<a href={`mailto:${app.candidate.email}`}>
<Mail className="h-4 w-4 mr-2" />
Enviar Email
</a>
</Button>
<Button variant="outline" className="flex-1" asChild>
<a
href={app.resumeUrl}
target="_blank"
rel="noopener noreferrer"
>
<FileText className="h-4 w-4 mr-2" />
Ver Currículo
</a>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Button size="sm" variant="outline" asChild>
<a href={`mailto:${app.candidate.email}`}>
<Mail className="h-4 w-4 mr-1" />
Contato
</a>
</Button>
{app.status === "pending" && (
<>
<Button size="sm" variant="default">
<CheckCircle2 className="h-4 w-4 mr-1" />
Aprovar
</Button>
<Button size="sm" variant="destructive">
<XCircle className="h-4 w-4 mr-1" />
Rejeitar
</Button>
</>
)}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Candidaturas
</h1>
<p className="text-muted-foreground">
Gerencie todas as candidaturas recebidas
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{applications.length}
</div>
<p className="text-xs text-muted-foreground">Total</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-yellow-600">
{groupedByStatus.pending.length}
</div>
<p className="text-xs text-muted-foreground">Pendentes</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-blue-600">
{groupedByStatus.reviewing.length}
</div>
<p className="text-xs text-muted-foreground">Em Análise</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-purple-600">
{groupedByStatus.interview.length}
</div>
<p className="text-xs text-muted-foreground">Entrevistas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-green-600">
{groupedByStatus.accepted.length}
</div>
<p className="text-xs text-muted-foreground">Aceitos</p>
</CardContent>
</Card>
</div>
{/* Filters */}
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="relative sm:col-span-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar candidato ou vaga..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={jobFilter} onValueChange={setJobFilter}>
<SelectTrigger>
<SelectValue placeholder="Filtrar por vaga" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as vagas</SelectItem>
<SelectItem value="1">
Desenvolvedor Full Stack Sênior
</SelectItem>
<SelectItem value="2">Designer UX/UI</SelectItem>
<SelectItem value="3">Product Manager</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue placeholder="Filtrar por status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os status</SelectItem>
<SelectItem value="pending">Pendente</SelectItem>
<SelectItem value="reviewing">Em Análise</SelectItem>
<SelectItem value="interview">Entrevista</SelectItem>
<SelectItem value="accepted">Aceito</SelectItem>
<SelectItem value="rejected">Rejeitado</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Applications by Status */}
<Tabs defaultValue="all" className="space-y-4">
<TabsList className="grid w-full grid-cols-3 sm:grid-cols-6">
<TabsTrigger value="all">Todas</TabsTrigger>
<TabsTrigger value="pending">Pendentes</TabsTrigger>
<TabsTrigger value="reviewing">Em Análise</TabsTrigger>
<TabsTrigger value="interview">Entrevista</TabsTrigger>
<TabsTrigger value="accepted">Aceitos</TabsTrigger>
<TabsTrigger value="rejected">Rejeitados</TabsTrigger>
</TabsList>
<TabsContent value="all" className="space-y-4">
{filteredApplications.map((app) => (
<ApplicationCard key={app.id} app={app} />
))}
</TabsContent>
{Object.entries(groupedByStatus).map(([status, apps]) => (
<TabsContent key={status} value={status} className="space-y-4">
{apps.length > 0 ? (
apps.map((app) => <ApplicationCard key={app.id} app={app} />)
) : (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">
Nenhuma candidatura com status "
{
statusConfig[status as keyof typeof statusConfig]
.label
}
"
</p>
</CardContent>
</Card>
)}
</TabsContent>
))}
</Tabs>
</div>
</main>
</div>
);
}
function Label({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return <label className={className}>{children}</label>;
}

View file

@ -1,434 +0,0 @@
"use client";
import { useState } from "react";
import { DashboardHeader } from "@/components/dashboard-header";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Search,
Send,
Paperclip,
MoreVertical,
Star,
Archive,
Trash2,
ArrowLeft,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface Message {
id: string;
content: string;
timestamp: string;
sender: "company" | "candidate";
}
interface Conversation {
id: string;
candidateId: string;
candidateName: string;
candidateAvatar: string;
lastMessage: string;
lastMessageTime: string;
unread: number;
jobTitle: string;
messages: Message[];
}
export default function CompanyMessagesPage() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedConversation, setSelectedConversation] = useState<
string | null
>("1");
const [newMessage, setNewMessage] = useState("");
const [showMobileList, setShowMobileList] = useState(true);
const conversations: Conversation[] = [
{
id: "1",
candidateId: "1",
candidateName: "Ana Silva",
candidateAvatar: "",
lastMessage: "Obrigada pela oportunidade! Quando podemos agendar?",
lastMessageTime: "2025-11-18T10:30:00",
unread: 2,
jobTitle: "Desenvolvedor Full Stack Sênior",
messages: [
{
id: "1",
content:
"Olá Ana! Parabéns, você foi selecionada para a próxima fase do processo seletivo.",
timestamp: "2025-11-18T09:00:00",
sender: "company",
},
{
id: "2",
content: "Que ótima notícia! Muito obrigada pela oportunidade.",
timestamp: "2025-11-18T09:15:00",
sender: "candidate",
},
{
id: "3",
content:
"Gostaríamos de agendar uma entrevista técnica. Você tem disponibilidade esta semana?",
timestamp: "2025-11-18T10:00:00",
sender: "company",
},
{
id: "4",
content: "Obrigada pela oportunidade! Quando podemos agendar?",
timestamp: "2025-11-18T10:30:00",
sender: "candidate",
},
],
},
{
id: "2",
candidateId: "2",
candidateName: "Carlos Santos",
candidateAvatar: "",
lastMessage: "Sim, posso fazer a entrevista quinta-feira às 14h",
lastMessageTime: "2025-11-17T16:20:00",
unread: 0,
jobTitle: "Designer UX/UI",
messages: [
{
id: "1",
content: "Olá Carlos! Gostamos muito do seu portfólio.",
timestamp: "2025-11-17T14:00:00",
sender: "company",
},
{
id: "2",
content: "Muito obrigado! Fico feliz em saber.",
timestamp: "2025-11-17T14:30:00",
sender: "candidate",
},
{
id: "3",
content: "Podemos agendar uma call para quinta-feira?",
timestamp: "2025-11-17T15:00:00",
sender: "company",
},
{
id: "4",
content: "Sim, posso fazer a entrevista quinta-feira às 14h",
timestamp: "2025-11-17T16:20:00",
sender: "candidate",
},
],
},
{
id: "3",
candidateId: "3",
candidateName: "Maria Oliveira",
candidateAvatar: "",
lastMessage: "Perfeito! Estarei preparada para a entrevista.",
lastMessageTime: "2025-11-16T11:45:00",
unread: 0,
jobTitle: "Product Manager",
messages: [
{
id: "1",
content: "Olá Maria! Seu perfil chamou nossa atenção.",
timestamp: "2025-11-16T10:00:00",
sender: "company",
},
{
id: "2",
content: "Obrigada! Estou muito interessada na vaga.",
timestamp: "2025-11-16T10:30:00",
sender: "candidate",
},
{
id: "3",
content: "Ótimo! Vamos agendar uma entrevista para amanhã às 15h?",
timestamp: "2025-11-16T11:00:00",
sender: "company",
},
{
id: "4",
content: "Perfeito! Estarei preparada para a entrevista.",
timestamp: "2025-11-16T11:45:00",
sender: "candidate",
},
],
},
];
const filteredConversations = conversations.filter(
(conv) =>
conv.candidateName.toLowerCase().includes(searchTerm.toLowerCase()) ||
conv.jobTitle.toLowerCase().includes(searchTerm.toLowerCase())
);
const activeConversation = conversations.find(
(c) => c.id === selectedConversation
);
const handleSendMessage = () => {
if (newMessage.trim()) {
// Aqui você adicionaria a lógica para enviar a mensagem
console.log("Enviando mensagem:", newMessage);
setNewMessage("");
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return "Agora";
if (diffHours < 24) return `${diffHours}h atrás`;
if (diffDays === 1) return "Ontem";
if (diffDays < 7) return `${diffDays}d atrás`;
return date.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
});
};
const formatMessageTime = (timestamp: string) => {
return new Date(timestamp).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
});
};
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-7xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Mensagens
</h1>
<p className="text-muted-foreground">Converse com os candidatos</p>
</div>
<Card className="overflow-hidden">
<div className="grid lg:grid-cols-[350px_1fr] h-[calc(100vh-250px)] min-h-[500px]">
{/* Conversations List */}
<div className={`border-r bg-muted/10 ${showMobileList ? 'block' : 'hidden lg:block'}`}>
<div className="p-4 border-b">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar conversas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<ScrollArea className="h-[calc(100%-73px)]">
<div className="p-2">
{filteredConversations.map((conv) => (
<button
key={conv.id}
onClick={() => {
setSelectedConversation(conv.id)
setShowMobileList(false)
}}
className={`w-full p-3 rounded-lg text-left hover:bg-muted/50 transition-colors mb-1 ${
selectedConversation === conv.id ? "bg-muted" : ""
}`}
>
<div className="flex items-start gap-3">
<div className="relative">
<Avatar className="h-12 w-12">
<AvatarImage src={conv.candidateAvatar} />
<AvatarFallback className="bg-primary/10 text-primary">
{conv.candidateName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
{conv.unread > 0 && (
<span className="absolute -top-1 -right-1 h-5 w-5 bg-primary text-primary-foreground text-xs rounded-full flex items-center justify-center">
{conv.unread}
</span>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h3 className="font-semibold text-sm truncate">
{conv.candidateName}
</h3>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatTime(conv.lastMessageTime)}
</span>
</div>
<p className="text-xs text-muted-foreground truncate mb-1">
{conv.jobTitle}
</p>
<p
className={`text-sm truncate ${
conv.unread > 0
? "font-medium"
: "text-muted-foreground"
}`}
>
{conv.lastMessage}
</p>
</div>
</div>
</button>
))}
</div>
</ScrollArea>
</div>
{/* Messages Area */}
{activeConversation ? (
<div className={`flex flex-col ${!showMobileList ? 'block' : 'hidden lg:flex'}`}>
{/* Chat Header */}
<div className="p-4 border-b flex items-center justify-between gap-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Button
variant="ghost"
size="icon"
className="lg:hidden shrink-0"
onClick={() => setShowMobileList(true)}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<Avatar className="h-10 w-10 shrink-0">
<AvatarImage src={activeConversation.candidateAvatar} />
<AvatarFallback className="bg-primary/10 text-primary">
{activeConversation.candidateName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<h3 className="font-semibold truncate">
{activeConversation.candidateName}
</h3>
<p className="text-xs text-muted-foreground truncate">
{activeConversation.jobTitle}
</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Star className="h-4 w-4 mr-2" />
Favoritar
</DropdownMenuItem>
<DropdownMenuItem>
<Archive className="h-4 w-4 mr-2" />
Arquivar
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{activeConversation.messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.sender === "company"
? "justify-end"
: "justify-start"
}`}
>
<div
className={`max-w-[70%] rounded-lg p-3 ${
message.sender === "company"
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
>
<p className="text-sm">{message.content}</p>
<p
className={`text-xs mt-1 ${
message.sender === "company"
? "text-primary-foreground/70"
: "text-muted-foreground"
}`}
>
{formatMessageTime(message.timestamp)}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
{/* Message Input */}
<div className="p-3 sm:p-4 border-t">
<div className="flex gap-2">
<Button variant="ghost" size="icon" className="shrink-0 hidden sm:flex">
<Paperclip className="h-5 w-5" />
</Button>
<Input
placeholder="Digite sua mensagem..."
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyPress={(e) =>
e.key === "Enter" && handleSendMessage()
}
className="flex-1"
/>
<Button
onClick={handleSendMessage}
size="icon"
className="shrink-0"
>
<Send className="h-5 w-5" />
</Button>
</div>
</div>
</div>
) : (
<div className={`flex items-center justify-center h-full ${!showMobileList ? 'flex' : 'hidden lg:flex'}`}>
<div className="text-center text-muted-foreground">
<p>Selecione uma conversa para começar</p>
</div>
</div>
)}
</div>
</Card>
</div>
</main>
</div>
);
}

View file

@ -1,249 +0,0 @@
"use client"
import { DashboardHeader } from "@/components/dashboard-header"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
Bell,
Check,
CheckCheck,
Trash2,
AlertCircle,
CheckCircle,
Info,
AlertTriangle,
Archive
} from "lucide-react"
import { useNotifications } from "@/contexts/notification-context"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { motion } from "framer-motion"
export default function CompanyNotificationsPage() {
const {
notifications,
unreadCount,
markAsRead,
markAllAsRead,
removeNotification,
clearAllNotifications,
} = useNotifications()
const getNotificationIcon = (type: string) => {
switch (type) {
case 'success':
return <CheckCircle className="h-5 w-5 text-green-500" />
case 'error':
return <AlertCircle className="h-5 w-5 text-red-500" />
case 'warning':
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
default:
return <Info className="h-5 w-5 text-blue-500" />
}
}
const getNotificationBgColor = (type: string, read: boolean) => {
if (read) return 'bg-muted/50'
switch (type) {
case 'success':
return 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-900'
case 'error':
return 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-900'
case 'warning':
return 'bg-yellow-50 dark:bg-yellow-950/20 border-yellow-200 dark:border-yellow-900'
default:
return 'bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-900'
}
}
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Notificações
</h1>
<p className="text-muted-foreground">
{unreadCount > 0 ? (
<>Você tem <span className="font-semibold text-foreground">{unreadCount}</span> {unreadCount === 1 ? 'notificação não lida' : 'notificações não lidas'}</>
) : (
'Você está em dia com suas notificações'
)}
</p>
</div>
{notifications.length > 0 && (
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<Button
variant="outline"
size="sm"
onClick={markAllAsRead}
className="gap-2"
>
<CheckCheck className="h-4 w-4" />
Marcar todas como lidas
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={clearAllNotifications}
className="gap-2 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
Limpar todas
</Button>
</div>
)}
</div>
{/* Notifications List */}
{notifications.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<Bell className="h-16 w-16 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-semibold mb-2">Nenhuma notificação</h3>
<p className="text-muted-foreground">
Quando você receber notificações, elas aparecerão aqui
</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{notifications.map((notification, index) => (
<motion.div
key={notification.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card
className={`relative overflow-hidden transition-all hover:shadow-md ${
getNotificationBgColor(notification.type, notification.read)
}`}
>
<CardContent className="p-4 sm:p-6">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="shrink-0 mt-1">
{getNotificationIcon(notification.type)}
</div>
{/* Content */}
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className={`font-semibold text-base mb-1 ${
notification.read ? 'text-muted-foreground' : 'text-foreground'
}`}>
{notification.title}
</h3>
<p className={`text-sm leading-relaxed ${
notification.read ? 'text-muted-foreground' : 'text-foreground/80'
}`}>
{notification.message}
</p>
</div>
{/* Unread indicator */}
{!notification.read && (
<div className="shrink-0">
<div className="h-2 w-2 bg-primary rounded-full" />
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between gap-4 pt-2">
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(notification.createdAt), {
addSuffix: true,
locale: ptBR
})}
</span>
{/* Actions */}
<div className="flex items-center gap-1">
{!notification.read && (
<Button
variant="ghost"
size="sm"
onClick={() => markAsRead(notification.id)}
className="h-8 px-3 gap-2"
>
<Check className="h-3 w-3" />
<span className="hidden sm:inline">Marcar como lida</span>
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => removeNotification(notification.id)}
className="h-8 px-3 gap-2 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
<span className="hidden sm:inline">Excluir</span>
</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
)}
{/* Stats */}
{notifications.length > 0 && (
<Card>
<CardContent className="p-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-foreground mb-1">
{notifications.length}
</div>
<div className="text-xs text-muted-foreground">Total</div>
</div>
<div>
<div className="text-2xl font-bold text-primary mb-1">
{unreadCount}
</div>
<div className="text-xs text-muted-foreground">Não lidas</div>
</div>
<div>
<div className="text-2xl font-bold text-green-600 mb-1">
{notifications.filter(n => n.type === 'success').length}
</div>
<div className="text-xs text-muted-foreground">Sucesso</div>
</div>
<div>
<div className="text-2xl font-bold text-yellow-600 mb-1">
{notifications.filter(n => n.type === 'warning').length}
</div>
<div className="text-xs text-muted-foreground">Avisos</div>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</main>
</div>
)
}

View file

@ -1,397 +0,0 @@
"use client";
import { DashboardHeader } from "@/components/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
Briefcase,
Users,
Eye,
TrendingUp,
Plus,
MoreVertical,
Calendar,
MapPin,
DollarSign,
MessageSquare,
BarChart3,
} from "lucide-react";
import Link from "next/link";
export default function CompanyDashboardPage() {
const companyStats = {
activeJobs: 12,
totalApplications: 234,
totalViews: 1542,
thisMonth: 89,
};
const recentJobs = [
{
id: "1",
title: "Desenvolvedor Full Stack Sênior",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 12.000 - R$ 18.000",
applications: 45,
views: 320,
postedAt: "2 dias atrás",
status: "active",
},
{
id: "2",
title: "Designer UX/UI",
type: "Remoto",
location: "Remoto",
salary: "R$ 8.000 - R$ 12.000",
applications: 32,
views: 256,
postedAt: "5 dias atrás",
status: "active",
},
{
id: "3",
title: "Product Manager",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 15.000 - R$ 20.000",
applications: 28,
views: 189,
postedAt: "1 semana atrás",
status: "active",
},
];
const recentApplications = [
{
id: "1",
candidateName: "Ana Silva",
candidateAvatar: "",
jobTitle: "Desenvolvedor Full Stack Sênior",
appliedAt: "Há 2 horas",
status: "pending",
},
{
id: "2",
candidateName: "Carlos Santos",
candidateAvatar: "",
jobTitle: "Designer UX/UI",
appliedAt: "Há 5 horas",
status: "pending",
},
{
id: "3",
candidateName: "Maria Oliveira",
candidateAvatar: "",
jobTitle: "Product Manager",
appliedAt: "Há 1 dia",
status: "reviewing",
},
];
const statusColors = {
pending: "bg-yellow-500",
reviewing: "bg-blue-500",
accepted: "bg-green-500",
rejected: "bg-red-500",
};
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-7xl mx-auto space-y-6 sm:space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Dashboard
</h1>
<p className="text-muted-foreground">
Bem-vindo de volta, TechCorp! 👋
</p>
</div>
<Link href="/dashboard/empresa/vagas/nova">
<Button size="lg" className="w-full sm:w-auto">
<Plus className="h-5 w-5 mr-2" />
Nova Vaga
</Button>
</Link>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Vagas Ativas
</CardTitle>
<Briefcase className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.activeJobs}
</div>
<p className="text-xs text-muted-foreground mt-1">
Publicadas no momento
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Candidaturas
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.totalApplications}
</div>
<p className="text-xs text-muted-foreground mt-1">
+{companyStats.thisMonth} este mês
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Visualizações
</CardTitle>
<Eye className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.totalViews}
</div>
<p className="text-xs text-muted-foreground mt-1">
Nas suas vagas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Taxa de Conversão
</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">15.2%</div>
<p className="text-xs text-green-600 mt-1">
+2.5% vs mês passado
</p>
</CardContent>
</Card>
</div>
<div className="grid lg:grid-cols-3 gap-6">
{/* Vagas Recentes */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Vagas Recentes</CardTitle>
<CardDescription>
Suas últimas vagas publicadas
</CardDescription>
</div>
<Link href="/dashboard/empresa/vagas">
<Button variant="ghost" size="sm">
Ver todas
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="space-y-4">
{recentJobs.map((job) => (
<div
key={job.id}
className="border rounded-lg p-4 hover:bg-muted/50 transition-colors"
>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base sm:text-lg mb-2 truncate">
{job.title}
</h3>
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground mb-3">
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4 shrink-0" />
<span className="truncate">{job.location}</span>
</div>
<Badge
variant="secondary"
className="whitespace-nowrap"
>
{job.type}
</Badge>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">
{job.salary}
</span>
</div>
</div>
<div className="flex flex-wrap gap-4 text-sm">
<span className="flex items-center gap-1">
<Users className="h-4 w-4" />
{job.applications} candidaturas
</span>
<span className="flex items-center gap-1">
<Eye className="h-4 w-4" />
{job.views} visualizações
</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{job.postedAt}
</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="shrink-0"
>
<MoreVertical className="h-4 w-4" />
</Button>
</div>
</div>
))}
</CardContent>
</Card>
</div>
{/* Candidaturas Recentes */}
<div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Candidaturas</CardTitle>
<CardDescription>Novas candidaturas</CardDescription>
</div>
<Link href="/dashboard/empresa/candidaturas">
<Button variant="ghost" size="sm">
Ver todas
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="space-y-4">
{recentApplications.map((application) => (
<div
key={application.id}
className="flex items-start gap-3 p-3 rounded-lg border hover:bg-muted/50 transition-colors cursor-pointer"
>
<div className="relative">
<Avatar className="h-10 w-10">
<AvatarImage src={application.candidateAvatar} />
<AvatarFallback className="bg-primary/10 text-primary text-sm">
{application.candidateName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<span
className={`absolute -top-1 -right-1 h-3 w-3 rounded-full ${
statusColors[
application.status as keyof typeof statusColors
]
} border-2 border-background`}
/>
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{application.candidateName}
</p>
<p className="text-xs text-muted-foreground truncate">
{application.jobTitle}
</p>
<p className="text-xs text-muted-foreground mt-1">
{application.appliedAt}
</p>
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Link href="/dashboard/empresa/mensagens">
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-500/10 rounded-lg">
<MessageSquare className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="font-semibold">Mensagens</h3>
<p className="text-sm text-muted-foreground">
3 não lidas
</p>
</div>
</div>
</CardContent>
</Card>
</Link>
<Link href="/dashboard/empresa/relatorios">
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-500/10 rounded-lg">
<BarChart3 className="h-6 w-6 text-purple-600" />
</div>
<div>
<h3 className="font-semibold">Relatórios</h3>
<p className="text-sm text-muted-foreground">
Ver analytics
</p>
</div>
</div>
</CardContent>
</Card>
</Link>
<Link href="/dashboard/empresa/perfil">
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-green-500/10 rounded-lg">
<Briefcase className="h-6 w-6 text-green-600" />
</div>
<div>
<h3 className="font-semibold">Perfil</h3>
<p className="text-sm text-muted-foreground">
Editar empresa
</p>
</div>
</div>
</CardContent>
</Card>
</Link>
</div>
</div>
</main>
</div>
);
}

View file

@ -1,620 +0,0 @@
"use client";
import { useState } from "react";
import { DashboardHeader } from "@/components/dashboard-header";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Building2,
MapPin,
Globe,
Linkedin,
Instagram,
Facebook,
Upload,
Save,
X,
Plus,
Camera,
} from "lucide-react";
import { ProfilePictureUpload } from "@/components/profile-picture-upload-v2";
export default function CompanyProfilePage() {
const [logoUrl, setLogoUrl] = useState<string>("");
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
const [formData, setFormData] = useState({
name: "TechCorp",
email: "contato@techcorp.com",
phone: "(11) 3456-7890",
description:
"Empresa líder em soluções tecnológicas inovadoras. Trabalhamos com desenvolvimento de software, cloud computing e inteligência artificial.",
website: "www.techcorp.com",
linkedin: "linkedin.com/company/techcorp",
instagram: "@techcorp",
facebook: "facebook.com/techcorp",
industry: "Tecnologia",
companySize: "100-500",
founded: "2015",
cnpj: "12.345.678/0001-90",
street: "Av. Paulista",
number: "1000",
complement: "Sala 1501",
neighborhood: "Bela Vista",
city: "São Paulo",
state: "SP",
zipCode: "01310-100",
culture:
"Cultura de inovação, colaboração e crescimento contínuo. Valorizamos a diversidade e o equilíbrio entre vida pessoal e profissional.",
});
const [benefits, setBenefits] = useState([
"Vale refeição",
"Vale transporte",
"Plano de saúde",
"Plano odontológico",
"Home office flexível",
"Auxílio educação",
]);
const [newBenefit, setNewBenefit] = useState("");
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const addBenefit = () => {
if (newBenefit.trim() && !benefits.includes(newBenefit.trim())) {
setBenefits([...benefits, newBenefit.trim()]);
setNewBenefit("");
}
};
const removeBenefit = (benefit: string) => {
setBenefits(benefits.filter((b) => b !== benefit));
};
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validar tamanho (max 2MB)
if (file.size > 2 * 1024 * 1024) {
alert("❌ Arquivo muito grande! Máximo 2MB.");
return;
}
// Validar tipo
if (!file.type.startsWith("image/")) {
alert("❌ Arquivo inválido! Use PNG ou JPG.");
return;
}
setIsUploadingLogo(true);
// Converter para base64
const reader = new FileReader();
reader.onload = (event) => {
const base64 = event.target?.result as string;
setLogoUrl(base64);
// Salvar no localStorage
localStorage.setItem("company_logo", base64);
setIsUploadingLogo(false);
alert("✅ Logo atualizado com sucesso!");
};
reader.onerror = () => {
setIsUploadingLogo(false);
alert("❌ Erro ao fazer upload do logo.");
};
reader.readAsDataURL(file);
};
const removeLogoHandler = () => {
setLogoUrl("");
localStorage.removeItem("company_logo");
alert("✅ Logo removido com sucesso!");
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert("✅ Perfil da empresa salvo com sucesso!");
};
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-5xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Perfil da Empresa
</h1>
<p className="text-muted-foreground">
Mantenha as informações da sua empresa atualizadas
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Informações Básicas */}
<Card>
<CardHeader>
<CardTitle>Informações Básicas</CardTitle>
<CardDescription>Dados principais da empresa</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Logo */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="relative">
<Avatar className="h-24 w-24 rounded-lg">
<AvatarImage src={logoUrl} alt={formData.name} />
<AvatarFallback className="bg-primary/10 text-primary font-bold text-2xl rounded-lg">
<Building2 className="h-12 w-12" />
</AvatarFallback>
</Avatar>
{logoUrl && (
<Button
type="button"
variant="destructive"
size="icon"
className="absolute -top-2 -right-2 h-6 w-6 rounded-full"
onClick={removeLogoHandler}
>
<X className="h-3 w-3" />
</Button>
)}
</div>
<div className="flex-1">
<input
type="file"
id="logo-upload"
accept="image/png,image/jpeg,image/jpg"
className="hidden"
onChange={handleLogoUpload}
disabled={isUploadingLogo}
/>
<label htmlFor="logo-upload">
<Button
type="button"
variant="outline"
size="sm"
disabled={isUploadingLogo}
onClick={() =>
document.getElementById("logo-upload")?.click()
}
>
<Upload className="h-4 w-4 mr-2" />
{isUploadingLogo ? "Enviando..." : "Upload Logo"}
</Button>
</label>
<p className="text-xs text-muted-foreground mt-2">
PNG, JPG até 2MB. Recomendado: 400x400px
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="name">Nome da Empresa *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
handleInputChange("name", e.target.value)
}
placeholder="Nome da empresa"
required
/>
</div>
<div>
<Label htmlFor="cnpj">CNPJ</Label>
<Input
id="cnpj"
value={formData.cnpj}
onChange={(e) =>
handleInputChange("cnpj", e.target.value)
}
placeholder="00.000.000/0000-00"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="email">Email de Contato *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) =>
handleInputChange("email", e.target.value)
}
placeholder="contato@empresa.com"
required
/>
</div>
<div>
<Label htmlFor="phone">Telefone *</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) =>
handleInputChange("phone", e.target.value)
}
placeholder="(11) 3456-7890"
required
/>
</div>
</div>
<div>
<Label htmlFor="description">Sobre a Empresa *</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
placeholder="Descreva sua empresa, missão e valores..."
rows={4}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label htmlFor="industry">Setor *</Label>
<Select
value={formData.industry}
onValueChange={(value) =>
handleInputChange("industry", value)
}
>
<SelectTrigger id="industry">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Tecnologia">Tecnologia</SelectItem>
<SelectItem value="Saúde">Saúde</SelectItem>
<SelectItem value="Educação">Educação</SelectItem>
<SelectItem value="Finanças">Finanças</SelectItem>
<SelectItem value="Varejo">Varejo</SelectItem>
<SelectItem value="Indústria">Indústria</SelectItem>
<SelectItem value="Serviços">Serviços</SelectItem>
<SelectItem value="Outro">Outro</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="companySize">Tamanho da Empresa *</Label>
<Select
value={formData.companySize}
onValueChange={(value) =>
handleInputChange("companySize", value)
}
>
<SelectTrigger id="companySize">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1-10">1-10 funcionários</SelectItem>
<SelectItem value="11-50">
11-50 funcionários
</SelectItem>
<SelectItem value="51-200">
51-200 funcionários
</SelectItem>
<SelectItem value="201-500">
201-500 funcionários
</SelectItem>
<SelectItem value="501-1000">
501-1000 funcionários
</SelectItem>
<SelectItem value="1000+">
1000+ funcionários
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="founded">Ano de Fundação</Label>
<Input
id="founded"
value={formData.founded}
onChange={(e) =>
handleInputChange("founded", e.target.value)
}
placeholder="2015"
maxLength={4}
/>
</div>
</div>
</CardContent>
</Card>
{/* Endereço */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MapPin className="h-5 w-5" />
Endereço
</CardTitle>
<CardDescription>
Localização da sede da empresa
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="sm:col-span-2">
<Label htmlFor="street">Rua/Avenida *</Label>
<Input
id="street"
value={formData.street}
onChange={(e) =>
handleInputChange("street", e.target.value)
}
placeholder="Av. Paulista"
required
/>
</div>
<div>
<Label htmlFor="number">Número *</Label>
<Input
id="number"
value={formData.number}
onChange={(e) =>
handleInputChange("number", e.target.value)
}
placeholder="1000"
required
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="complement">Complemento</Label>
<Input
id="complement"
value={formData.complement}
onChange={(e) =>
handleInputChange("complement", e.target.value)
}
placeholder="Sala 1501"
/>
</div>
<div>
<Label htmlFor="neighborhood">Bairro *</Label>
<Input
id="neighborhood"
value={formData.neighborhood}
onChange={(e) =>
handleInputChange("neighborhood", e.target.value)
}
placeholder="Bela Vista"
required
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<Label htmlFor="city">Cidade *</Label>
<Input
id="city"
value={formData.city}
onChange={(e) =>
handleInputChange("city", e.target.value)
}
placeholder="São Paulo"
required
/>
</div>
<div>
<Label htmlFor="state">Estado *</Label>
<Input
id="state"
value={formData.state}
onChange={(e) =>
handleInputChange("state", e.target.value)
}
placeholder="SP"
maxLength={2}
required
/>
</div>
<div>
<Label htmlFor="zipCode">CEP *</Label>
<Input
id="zipCode"
value={formData.zipCode}
onChange={(e) =>
handleInputChange("zipCode", e.target.value)
}
placeholder="01310-100"
required
/>
</div>
</div>
</CardContent>
</Card>
{/* Redes Sociais */}
<Card>
<CardHeader>
<CardTitle>Redes Sociais e Website</CardTitle>
<CardDescription>Links para seus canais online</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="website" className="flex items-center gap-2">
<Globe className="h-4 w-4" />
Website
</Label>
<Input
id="website"
value={formData.website}
onChange={(e) =>
handleInputChange("website", e.target.value)
}
placeholder="www.empresa.com"
/>
</div>
<div>
<Label htmlFor="linkedin" className="flex items-center gap-2">
<Linkedin className="h-4 w-4" />
LinkedIn
</Label>
<Input
id="linkedin"
value={formData.linkedin}
onChange={(e) =>
handleInputChange("linkedin", e.target.value)
}
placeholder="linkedin.com/company/empresa"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label
htmlFor="instagram"
className="flex items-center gap-2"
>
<Instagram className="h-4 w-4" />
Instagram
</Label>
<Input
id="instagram"
value={formData.instagram}
onChange={(e) =>
handleInputChange("instagram", e.target.value)
}
placeholder="@empresa"
/>
</div>
<div>
<Label
htmlFor="facebook"
className="flex items-center gap-2"
>
<Facebook className="h-4 w-4" />
Facebook
</Label>
<Input
id="facebook"
value={formData.facebook}
onChange={(e) =>
handleInputChange("facebook", e.target.value)
}
placeholder="facebook.com/empresa"
/>
</div>
</div>
</CardContent>
</Card>
{/* Benefícios */}
<Card>
<CardHeader>
<CardTitle>Benefícios</CardTitle>
<CardDescription>
Benefícios oferecidos aos colaboradores
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col sm:flex-row gap-2">
<Input
value={newBenefit}
onChange={(e) => setNewBenefit(e.target.value)}
placeholder="Digite um benefício"
onKeyPress={(e) =>
e.key === "Enter" && (e.preventDefault(), addBenefit())
}
/>
<Button
type="button"
onClick={addBenefit}
variant="outline"
className="sm:w-auto"
>
<Plus className="h-4 w-4 mr-2" />
Adicionar
</Button>
</div>
<div className="flex flex-wrap gap-2">
{benefits.map((benefit) => (
<Badge
key={benefit}
variant="secondary"
className="text-sm py-1.5 px-3"
>
{benefit}
<button
type="button"
onClick={() => removeBenefit(benefit)}
className="ml-2 hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</CardContent>
</Card>
{/* Cultura */}
<Card>
<CardHeader>
<CardTitle>Cultura da Empresa</CardTitle>
<CardDescription>
Descreva a cultura e valores da empresa
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
id="culture"
value={formData.culture}
onChange={(e) => handleInputChange("culture", e.target.value)}
placeholder="Descreva a cultura organizacional, valores e ambiente de trabalho..."
rows={4}
/>
</CardContent>
</Card>
{/* Botões */}
<div className="flex flex-col sm:flex-row gap-4 justify-end">
<Button type="button" variant="outline" className="sm:w-auto">
Cancelar
</Button>
<Button type="submit" size="lg" className="sm:w-auto">
<Save className="h-4 w-4 mr-2" />
Salvar Perfil
</Button>
</div>
</form>
</div>
</main>
</div>
);
}

View file

@ -1,441 +0,0 @@
"use client";
import { useState } from "react";
import { DashboardHeader } from "@/components/dashboard-header";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
TrendingUp,
TrendingDown,
Users,
Briefcase,
Eye,
UserCheck,
Clock,
Target,
Download,
Calendar,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
export default function CompanyReportsPage() {
const [period, setPeriod] = useState("30");
const stats = [
{
title: "Total de Vagas Ativas",
value: "8",
change: "+2",
changeType: "positive" as const,
icon: Briefcase,
description: "2 novas vagas este mês",
},
{
title: "Candidaturas Recebidas",
value: "156",
change: "+23%",
changeType: "positive" as const,
icon: Users,
description: "vs. mês anterior",
},
{
title: "Taxa de Conversão",
value: "12.8%",
change: "+3.2%",
changeType: "positive" as const,
icon: Target,
description: "candidatos contratados",
},
{
title: "Tempo Médio de Contratação",
value: "18 dias",
change: "-3 dias",
changeType: "positive" as const,
icon: Clock,
description: "vs. mês anterior",
},
];
const jobPerformance = [
{
title: "Desenvolvedor Full Stack Sênior",
views: 234,
applications: 45,
conversionRate: 19.2,
status: "Ativa",
daysOpen: 12,
},
{
title: "Designer UX/UI",
views: 189,
applications: 38,
conversionRate: 20.1,
status: "Ativa",
daysOpen: 8,
},
{
title: "Product Manager",
views: 167,
applications: 29,
conversionRate: 17.4,
status: "Ativa",
daysOpen: 15,
},
{
title: "DevOps Engineer",
views: 145,
applications: 22,
conversionRate: 15.2,
status: "Pausada",
daysOpen: 20,
},
{
title: "Data Scientist",
views: 198,
applications: 34,
conversionRate: 17.2,
status: "Ativa",
daysOpen: 10,
},
];
const funnelData = [
{
stage: "Visualizações",
count: 1250,
percentage: 100,
color: "bg-blue-500",
},
{
stage: "Candidaturas",
count: 156,
percentage: 12.5,
color: "bg-green-500",
},
{ stage: "Em Análise", count: 89, percentage: 7.1, color: "bg-yellow-500" },
{
stage: "Entrevistas",
count: 34,
percentage: 2.7,
color: "bg-orange-500",
},
{
stage: "Contratados",
count: 20,
percentage: 1.6,
color: "bg-purple-500",
},
];
const topSources = [
{ source: "LinkedIn", applications: 67, percentage: 43 },
{ source: "Busca Orgânica", applications: 45, percentage: 29 },
{ source: "Indeed", applications: 28, percentage: 18 },
{ source: "Indicações", applications: 16, percentage: 10 },
];
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Relatórios e Analytics
</h1>
<p className="text-muted-foreground">
Acompanhe o desempenho das suas vagas
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Últimos 7 dias</SelectItem>
<SelectItem value="30">Últimos 30 dias</SelectItem>
<SelectItem value="90">Últimos 90 dias</SelectItem>
<SelectItem value="365">Último ano</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="gap-2">
<Download className="h-4 w-4" />
Exportar
</Button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<Card key={index}>
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="p-2 bg-primary/10 rounded-lg">
<Icon className="h-5 w-5 text-primary" />
</div>
<Badge
variant={
stat.changeType === "positive"
? "default"
: "destructive"
}
>
{stat.changeType === "positive" ? (
<TrendingUp className="h-3 w-3 mr-1" />
) : (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{stat.change}
</Badge>
</div>
<h3 className="text-2xl font-bold mb-1">{stat.value}</h3>
<p className="text-sm font-medium text-foreground mb-1">
{stat.title}
</p>
<p className="text-xs text-muted-foreground">
{stat.description}
</p>
</CardContent>
</Card>
);
})}
</div>
{/* Funnel */}
<Card>
<CardHeader>
<CardTitle>Funil de Conversão</CardTitle>
<CardDescription>
Acompanhe o fluxo de candidatos em cada etapa do processo
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{funnelData.map((item, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className={`h-3 w-3 rounded-full ${item.color}`} />
<span className="font-medium">{item.stage}</span>
</div>
<div className="flex items-center gap-4">
<span className="text-muted-foreground">
{item.count} candidatos
</span>
<span className="font-medium min-w-[60px] text-right">
{item.percentage}%
</span>
</div>
</div>
<Progress value={item.percentage} className="h-2" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-muted/50 rounded-lg">
<div className="flex items-start gap-2">
<Target className="h-5 w-5 text-primary mt-0.5" />
<div>
<p className="font-medium text-sm mb-1">Taxa de Sucesso</p>
<p className="text-sm text-muted-foreground">
1.6% dos visitantes se tornam contratações. A média do
mercado é 1.2%.
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="grid lg:grid-cols-2 gap-6">
{/* Job Performance */}
<Card>
<CardHeader>
<CardTitle>Desempenho por Vaga</CardTitle>
<CardDescription>
Métricas das vagas mais visualizadas
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{jobPerformance.map((job, index) => (
<div
key={index}
className="p-4 border rounded-lg space-y-3"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm mb-1 truncate">
{job.title}
</h4>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge
variant={
job.status === "Ativa" ? "default" : "secondary"
}
className="text-xs"
>
{job.status}
</Badge>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{job.daysOpen} dias
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<div className="flex items-center gap-1 text-muted-foreground mb-1">
<Eye className="h-3 w-3" />
<span className="text-xs">Visualizações</span>
</div>
<p className="text-lg font-semibold">{job.views}</p>
</div>
<div>
<div className="flex items-center gap-1 text-muted-foreground mb-1">
<Users className="h-3 w-3" />
<span className="text-xs">Candidaturas</span>
</div>
<p className="text-lg font-semibold">
{job.applications}
</p>
</div>
<div>
<div className="flex items-center gap-1 text-muted-foreground mb-1">
<Target className="h-3 w-3" />
<span className="text-xs">Taxa</span>
</div>
<p className="text-lg font-semibold">
{job.conversionRate}%
</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Top Sources */}
<Card>
<CardHeader>
<CardTitle>Principais Fontes</CardTitle>
<CardDescription>De onde vêm suas candidaturas</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{topSources.map((source, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{source.source}</span>
<div className="flex items-center gap-4">
<span className="text-muted-foreground">
{source.applications} candidaturas
</span>
<span className="font-medium min-w-[50px] text-right">
{source.percentage}%
</span>
</div>
</div>
<Progress value={source.percentage} className="h-2" />
</div>
))}
</div>
<div className="mt-6 p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-muted-foreground">
<strong className="text-foreground">LinkedIn</strong> é sua
fonte mais efetiva, gerando 43% de todas as candidaturas.
</p>
</div>
</CardContent>
</Card>
</div>
{/* Time to Hire Chart */}
<Card>
<CardHeader>
<CardTitle>Tempo de Contratação por Cargo</CardTitle>
<CardDescription>
Tempo médio desde a publicação até a contratação
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[
{ role: "Desenvolvedor Full Stack", days: 15, maxDays: 30 },
{ role: "Designer UX/UI", days: 12, maxDays: 30 },
{ role: "Product Manager", days: 22, maxDays: 30 },
{ role: "DevOps Engineer", days: 18, maxDays: 30 },
{ role: "Data Scientist", days: 20, maxDays: 30 },
].map((item, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{item.role}</span>
<span className="text-muted-foreground">
{item.days} dias
</span>
</div>
<Progress
value={(item.days / item.maxDays) * 100}
className="h-2"
/>
</div>
))}
</div>
<div className="mt-6 grid grid-cols-2 gap-4">
<div className="p-4 bg-green-500/10 border border-green-500/20 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Clock className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium">Mais Rápido</span>
</div>
<p className="text-2xl font-bold">12 dias</p>
<p className="text-xs text-muted-foreground">
Designer UX/UI
</p>
</div>
<div className="p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Clock className="h-4 w-4 text-orange-600" />
<span className="text-sm font-medium">Mais Lento</span>
</div>
<p className="text-2xl font-bold">22 dias</p>
<p className="text-xs text-muted-foreground">
Product Manager
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View file

@ -1,874 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import {
ChevronRight,
ChevronLeft,
Briefcase,
FileText,
DollarSign,
CheckCircle2,
Plus,
Trash2,
Settings,
ArrowLeft,
Eye,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
import { useNotify } from "@/contexts/notification-context";
import { Switch } from "@/components/ui/switch";
// Steps definition
const steps = [
{ id: 1, title: "Informações Básicas", icon: Briefcase },
{ id: 2, title: "Requisitos", icon: FileText },
{ id: 3, title: "Benefícios e Salário", icon: DollarSign },
{ id: 4, title: "Configuração do Formulário", icon: Settings },
{ id: 5, title: "Revisão", icon: Eye },
];
export default function NewJobPage() {
const router = useRouter();
const notify = useNotify();
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
// Form State
const [formData, setFormData] = useState({
// Step 1: Basic Info
title: "",
description: "",
location: "",
type: "", // remote, hybrid, onsite
contract: "", // clt, pj, etc
// Step 2: Requirements
responsibilities: [""] as string[],
requirements: [""] as string[],
// Step 3: Benefits & Salary
salaryMin: "",
salaryMax: "",
benefits: [""] as string[],
// Step 4: Form Config
includeResume: true,
includeCoverLetter: false,
includePortfolio: false,
includeLinkedIn: true,
customQuestions: [] as {
id: string;
question: string;
type: string;
required: boolean;
}[],
});
const handleInputChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleArrayChange = (
field: "responsibilities" | "requirements" | "benefits",
index: number,
value: string
) => {
const newArray = [...formData[field]];
newArray[index] = value;
setFormData((prev) => ({ ...prev, [field]: newArray }));
};
const addArrayItem = (
field: "responsibilities" | "requirements" | "benefits"
) => {
setFormData((prev) => ({ ...prev, [field]: [...prev[field], ""] }));
};
const removeArrayItem = (
field: "responsibilities" | "requirements" | "benefits",
index: number
) => {
const newArray = [...formData[field]];
newArray.splice(index, 1);
setFormData((prev) => ({ ...prev, [field]: newArray }));
};
// Custom Questions Logic
const addCustomQuestion = () => {
setFormData((prev) => ({
...prev,
customQuestions: [
...prev.customQuestions,
{
id: crypto.randomUUID(),
question: "",
type: "text",
required: false,
},
],
}));
};
const updateCustomQuestion = (index: number, field: string, value: any) => {
const newQuestions = [...formData.customQuestions];
// @ts-ignore
newQuestions[index][field] = value;
setFormData((prev) => ({ ...prev, customQuestions: newQuestions }));
};
const removeCustomQuestion = (index: number) => {
const newQuestions = [...formData.customQuestions];
newQuestions.splice(index, 1);
setFormData((prev) => ({ ...prev, customQuestions: newQuestions }));
};
const validateStep = (step: number) => {
switch (step) {
case 1:
if (
!formData.title ||
!formData.description ||
!formData.location ||
!formData.type ||
!formData.contract
) {
notify.error(
"Campos obrigatórios",
"Preencha todas as informações básicas."
);
return false;
}
return true;
case 2:
if (
formData.requirements.some((r) => !r.trim()) ||
formData.requirements.length === 0
) {
// Optional: enforce at least one requirement
}
return true;
case 3:
// Salary optional? Let's say yes for now, or enforce at least one
return true;
case 4:
// Check if custom questions have text
if (formData.customQuestions.some((q) => !q.question.trim())) {
notify.error(
"Pergunta vazia",
"Todas as perguntas personalizadas devem ter um texto."
);
return false;
}
return true;
default:
return true;
}
};
const handleNext = () => {
if (validateStep(currentStep)) {
if (currentStep < steps.length) {
setCurrentStep((prev) => prev + 1);
window.scrollTo(0, 0);
} else {
handleSubmit();
}
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep((prev) => prev - 1);
window.scrollTo(0, 0);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
notify.success(
"Vaga publicada!",
`A vaga ${formData.title} foi criada com sucesso.`
);
router.push("/dashboard/empresa/vagas");
};
const progress = (currentStep / steps.length) * 100;
return (
<div className="min-h-screen flex flex-col bg-muted/30">
<Navbar />
<main className="flex-1 py-8">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
{/* Header */}
<div className="mb-8">
<Link
href="/dashboard/empresa/vagas"
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary mb-4 transition-colors"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para minhas vagas
</Link>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
Nova Vaga
</h1>
<p className="text-muted-foreground mt-1">
Preencha os detalhes para publicar uma nova oportunidade.
</p>
</div>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex justify-between mb-2">
<span className="text-sm font-medium text-muted-foreground">
Etapa {currentStep} de {steps.length}:{" "}
<span className="text-foreground">
{steps[currentStep - 1].title}
</span>
</span>
<span className="text-sm font-medium text-primary">
{Math.round(progress)}%
</span>
</div>
<Progress value={progress} className="h-2" />
{/* Desktop Steps Indicator */}
<div className="hidden md:flex justify-between mt-4 px-2">
{steps.map((step) => {
const Icon = step.icon;
const isActive = step.id === currentStep;
const isCompleted = step.id < currentStep;
return (
<div
key={step.id}
className={`flex flex-col items-center gap-2 ${
isActive
? "text-primary"
: isCompleted
? "text-primary/60"
: "text-muted-foreground"
}`}
>
<div
className={`
w-8 h-8 rounded-full flex items-center justify-center border-2 transition-colors
${
isActive
? "border-primary bg-primary text-primary-foreground"
: isCompleted
? "border-primary bg-primary/10 text-primary"
: "border-muted-foreground/30 bg-background"
}
`}
>
{isCompleted ? (
<CheckCircle2 className="h-5 w-5" />
) : (
<Icon className="h-4 w-4" />
)}
</div>
<span className="text-xs font-medium">{step.title}</span>
</div>
);
})}
</div>
</div>
{/* Form Content */}
<div className="grid gap-6">
<AnimatePresence mode="wait">
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<Card className="border-t-4 border-t-primary">
<CardHeader>
<CardTitle>{steps[currentStep - 1].title}</CardTitle>
<CardDescription>
Preencha as informações abaixo.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* STEP 1: BASIC INFO */}
{currentStep === 1 && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Título da Vaga *</Label>
<Input
id="title"
placeholder="Ex: Desenvolvedor Frontend Senior"
value={formData.title}
onChange={(e) =>
handleInputChange("title", e.target.value)
}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="location">Localização *</Label>
<Input
id="location"
placeholder="Ex: São Paulo, SP"
value={formData.location}
onChange={(e) =>
handleInputChange("location", e.target.value)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="type">Modelo de Trabalho *</Label>
<Select
value={formData.type}
onValueChange={(val) =>
handleInputChange("type", val)
}
>
<SelectTrigger>
<SelectValue placeholder="Selecione..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="Presencial">
Presencial
</SelectItem>
<SelectItem value="Híbrido">Híbrido</SelectItem>
<SelectItem value="Remoto">Remoto</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="contract">Tipo de Contrato *</Label>
<Select
value={formData.contract}
onValueChange={(val) =>
handleInputChange("contract", val)
}
>
<SelectTrigger>
<SelectValue placeholder="Selecione..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="CLT">CLT</SelectItem>
<SelectItem value="PJ">PJ</SelectItem>
<SelectItem value="Estágio">Estágio</SelectItem>
<SelectItem value="Temporário">
Temporário
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description">
Descrição da Vaga *
</Label>
<Textarea
id="description"
placeholder="Descreva as atividades, cultura da empresa, etc..."
className="min-h-[200px]"
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
/>
</div>
</div>
)}
{/* STEP 2: REQUIREMENTS */}
{currentStep === 2 && (
<div className="space-y-6">
<div className="space-y-3">
<div className="flex justify-between items-center">
<Label>Responsabilidades</Label>
<Button
variant="outline"
size="sm"
onClick={() => addArrayItem("responsibilities")}
>
<Plus className="h-4 w-4 mr-2" /> Adicionar
</Button>
</div>
{formData.responsibilities.map((item, index) => (
<div key={index} className="flex gap-2">
<Input
value={item}
onChange={(e) =>
handleArrayChange(
"responsibilities",
index,
e.target.value
)
}
placeholder="Ex: Desenvolver interfaces..."
/>
<Button
variant="ghost"
size="icon"
onClick={() =>
removeArrayItem("responsibilities", index)
}
disabled={
formData.responsibilities.length === 1
}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
</div>
<Separator />
<div className="space-y-3">
<div className="flex justify-between items-center">
<Label>Requisitos Obrigatórios</Label>
<Button
variant="outline"
size="sm"
onClick={() => addArrayItem("requirements")}
>
<Plus className="h-4 w-4 mr-2" /> Adicionar
</Button>
</div>
{formData.requirements.map((item, index) => (
<div key={index} className="flex gap-2">
<Input
value={item}
onChange={(e) =>
handleArrayChange(
"requirements",
index,
e.target.value
)
}
placeholder="Ex: Experiência com React..."
/>
<Button
variant="ghost"
size="icon"
onClick={() =>
removeArrayItem("requirements", index)
}
disabled={formData.requirements.length === 1}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
</div>
</div>
)}
{/* STEP 3: BENEFITS & SALARY */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="salaryMin">
Salário Mínimo (R$)
</Label>
<Input
id="salaryMin"
type="number"
placeholder="0,00"
value={formData.salaryMin}
onChange={(e) =>
handleInputChange("salaryMin", e.target.value)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="salaryMax">
Salário Máximo (R$)
</Label>
<Input
id="salaryMax"
type="number"
placeholder="0,00"
value={formData.salaryMax}
onChange={(e) =>
handleInputChange("salaryMax", e.target.value)
}
/>
</div>
</div>
<Separator />
<div className="space-y-3">
<div className="flex justify-between items-center">
<Label>Benefícios</Label>
<Button
variant="outline"
size="sm"
onClick={() => addArrayItem("benefits")}
>
<Plus className="h-4 w-4 mr-2" /> Adicionar
</Button>
</div>
{formData.benefits.map((item, index) => (
<div key={index} className="flex gap-2">
<Input
value={item}
onChange={(e) =>
handleArrayChange(
"benefits",
index,
e.target.value
)
}
placeholder="Ex: Vale Refeição..."
/>
<Button
variant="ghost"
size="icon"
onClick={() =>
removeArrayItem("benefits", index)
}
disabled={formData.benefits.length === 1}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
</div>
</div>
)}
{/* STEP 4: FORM CONFIGURATION */}
{currentStep === 4 && (
<div className="space-y-6">
<div className="bg-muted/50 p-4 rounded-lg space-y-4">
<h3 className="font-medium">Campos Padrão</h3>
<div className="grid gap-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Currículo (Upload)</Label>
<p className="text-xs text-muted-foreground">
Solicitar arquivo de CV
</p>
</div>
<Switch
checked={formData.includeResume}
onCheckedChange={(val) =>
handleInputChange("includeResume", val)
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Carta de Apresentação</Label>
<p className="text-xs text-muted-foreground">
Campo de texto livre
</p>
</div>
<Switch
checked={formData.includeCoverLetter}
onCheckedChange={(val) =>
handleInputChange("includeCoverLetter", val)
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>LinkedIn</Label>
<p className="text-xs text-muted-foreground">
URL do perfil
</p>
</div>
<Switch
checked={formData.includeLinkedIn}
onCheckedChange={(val) =>
handleInputChange("includeLinkedIn", val)
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Portfólio</Label>
<p className="text-xs text-muted-foreground">
URL do portfólio/site
</p>
</div>
<Switch
checked={formData.includePortfolio}
onCheckedChange={(val) =>
handleInputChange("includePortfolio", val)
}
/>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-medium">
Perguntas Personalizadas
</h3>
<Button
variant="outline"
size="sm"
onClick={addCustomQuestion}
>
<Plus className="h-4 w-4 mr-2" /> Nova Pergunta
</Button>
</div>
{formData.customQuestions.length === 0 && (
<div className="text-center py-8 border-2 border-dashed rounded-lg text-muted-foreground">
Nenhuma pergunta personalizada adicionada.
</div>
)}
{formData.customQuestions.map((q, index) => (
<Card key={q.id} className="p-4 relative">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 text-muted-foreground hover:text-destructive"
onClick={() => removeCustomQuestion(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
<div className="grid gap-4 pr-8">
<div className="space-y-2">
<Label>Pergunta</Label>
<Input
value={q.question}
onChange={(e) =>
updateCustomQuestion(
index,
"question",
e.target.value
)
}
placeholder="Ex: Por que você quer trabalhar aqui?"
/>
</div>
<div className="flex gap-4">
<div className="space-y-2 flex-1">
<Label>Tipo de Resposta</Label>
<Select
value={q.type}
onValueChange={(val) =>
updateCustomQuestion(index, "type", val)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">
Texto Curto
</SelectItem>
<SelectItem value="textarea">
Texto Longo
</SelectItem>
<SelectItem value="yes_no">
Sim/Não
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2 pt-8">
<Checkbox
id={`req-${q.id}`}
checked={q.required}
onCheckedChange={(val) =>
updateCustomQuestion(
index,
"required",
val
)
}
/>
<Label htmlFor={`req-${q.id}`}>
Obrigatória
</Label>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
)}
{/* STEP 5: REVIEW */}
{currentStep === 5 && (
<div className="space-y-6">
<div className="bg-muted/30 p-6 rounded-lg space-y-4">
<div>
<h3 className="font-bold text-lg">
{formData.title}
</h3>
<p className="text-muted-foreground">
{formData.location} {formData.type} {" "}
{formData.contract}
</p>
</div>
<Separator />
<div>
<h4 className="font-medium mb-2">Descrição</h4>
<p className="text-sm whitespace-pre-wrap">
{formData.description}
</p>
</div>
<Separator />
<div className="grid md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium mb-2">Requisitos</h4>
<ul className="list-disc list-inside text-sm space-y-1">
{formData.requirements
.filter((r) => r)
.map((r, i) => (
<li key={i}>{r}</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Benefícios</h4>
<ul className="list-disc list-inside text-sm space-y-1">
{formData.benefits
.filter((b) => b)
.map((b, i) => (
<li key={i}>{b}</li>
))}
</ul>
</div>
</div>
</div>
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-4 flex items-center">
<Settings className="h-4 w-4 mr-2" />
Configuração do Formulário
</h4>
<div className="text-sm space-y-2">
<p>
<strong>Campos Padrão:</strong>{" "}
{[
formData.includeResume && "Currículo",
formData.includeCoverLetter &&
"Carta de Apresentação",
formData.includeLinkedIn && "LinkedIn",
formData.includePortfolio && "Portfólio",
]
.filter(Boolean)
.join(", ")}
</p>
{formData.customQuestions.length > 0 && (
<div>
<p className="font-medium mt-2">
Perguntas Personalizadas:
</p>
<ul className="list-disc list-inside mt-1 text-muted-foreground">
{formData.customQuestions.map((q, i) => (
<li key={i}>
{q.question} (
{q.type === "text"
? "Texto"
: q.type === "textarea"
? "Texto Longo"
: "Sim/Não"}
)
{q.required && (
<span className="text-destructive ml-1">
*
</span>
)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
)}
</CardContent>
<CardFooter className="flex justify-between border-t pt-6">
<Button
variant="outline"
onClick={handleBack}
disabled={currentStep === 1 || isSubmitting}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Voltar
</Button>
<Button
onClick={handleNext}
disabled={isSubmitting}
className="min-w-[120px]"
>
{isSubmitting ? (
"Publicando..."
) : currentStep === steps.length ? (
<>
Publicar Vaga{" "}
<CheckCircle2 className="ml-2 h-4 w-4" />
</>
) : (
<>
Próxima Etapa{" "}
<ChevronRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</CardFooter>
</Card>
</motion.div>
</AnimatePresence>
</div>
</div>
</main>
<Footer />
</div>
);
}

View file

@ -1,314 +0,0 @@
"use client";
import { DashboardHeader } from "@/components/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Plus,
Search,
MoreVertical,
Edit,
Eye,
Trash2,
MapPin,
Briefcase,
DollarSign,
Users,
Calendar,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function CompanyJobsPage() {
const [searchTerm, setSearchTerm] = useState("");
const jobs = [
{
id: "1",
title: "Desenvolvedor Full Stack Sênior",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 12.000 - R$ 18.000",
applications: 45,
views: 320,
postedAt: "2 dias atrás",
status: "active",
},
{
id: "2",
title: "Designer UX/UI",
type: "Remoto",
location: "Remoto",
salary: "R$ 8.000 - R$ 12.000",
applications: 32,
views: 256,
postedAt: "5 dias atrás",
status: "active",
},
{
id: "3",
title: "Product Manager",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 15.000 - R$ 20.000",
applications: 28,
views: 189,
postedAt: "1 semana atrás",
status: "active",
},
{
id: "4",
title: "Engenheiro de Dados",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 10.000 - R$ 15.000",
applications: 19,
views: 145,
postedAt: "2 semanas atrás",
status: "paused",
},
];
const filteredJobs = jobs.filter((job) =>
job.title.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-background">
<DashboardHeader />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Minhas Vagas
</h1>
<p className="text-muted-foreground">
Gerencie todas as suas vagas publicadas
</p>
</div>
<Link href="/dashboard/empresa/vagas/nova">
<Button size="lg" className="w-full sm:w-auto">
<Plus className="h-5 w-5 mr-2" />
Nova Vaga
</Button>
</Link>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{jobs.filter((j) => j.status === "active").length}
</div>
<p className="text-sm text-muted-foreground">Vagas Ativas</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{jobs.reduce((sum, j) => sum + j.applications, 0)}
</div>
<p className="text-sm text-muted-foreground">
Total de Candidaturas
</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{jobs.reduce((sum, j) => sum + j.views, 0)}
</div>
<p className="text-sm text-muted-foreground">
Total de Visualizações
</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold text-foreground">
{Math.round(
(jobs.reduce((sum, j) => sum + j.applications, 0) /
jobs.reduce((sum, j) => sum + j.views, 0)) *
100
)}
%
</div>
<p className="text-sm text-muted-foreground">
Taxa de Conversão
</p>
</CardContent>
</Card>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar vagas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Jobs List */}
<div className="space-y-4">
{filteredJobs.length > 0 ? (
filteredJobs.map((job) => (
<Card
key={job.id}
className="hover:shadow-md transition-shadow"
>
<CardContent className="p-4 sm:p-6">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
<div className="flex-1 min-w-0 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg sm:text-xl mb-2 truncate">
{job.title}
</h3>
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4 shrink-0" />
<span className="truncate">{job.location}</span>
</div>
<Badge
variant="secondary"
className="whitespace-nowrap"
>
{job.type}
</Badge>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">
{job.salary}
</span>
</div>
</div>
</div>
<Badge
variant={
job.status === "active" ? "default" : "secondary"
}
className="shrink-0"
>
{job.status === "active" ? "Ativa" : "Pausada"}
</Badge>
</div>
<div className="flex flex-wrap gap-4 text-sm">
<span className="flex items-center gap-1">
<Users className="h-4 w-4" />
<span className="font-medium">
{job.applications}
</span>{" "}
candidaturas
</span>
<span className="flex items-center gap-1">
<Eye className="h-4 w-4" />
<span className="font-medium">
{job.views}
</span>{" "}
visualizações
</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{job.postedAt}
</span>
</div>
</div>
<div className="flex sm:flex-row gap-2 lg:flex-col">
<Link
href={`/vagas/${job.id}`}
className="flex-1 sm:flex-initial"
>
<Button
variant="outline"
size="sm"
className="w-full"
>
<Eye className="h-4 w-4 mr-2" />
Ver
</Button>
</Link>
<Link
href={`/dashboard/empresa/vagas/${job.id}/editar`}
className="flex-1 sm:flex-initial"
>
<Button
variant="outline"
size="sm"
className="w-full"
>
<Edit className="h-4 w-4 mr-2" />
Editar
</Button>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
{job.status === "active"
? "Pausar vaga"
: "Ativar vaga"}
</DropdownMenuItem>
<DropdownMenuItem>Duplicar vaga</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardContent>
</Card>
))
) : (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Briefcase className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">
Nenhuma vaga encontrada
</h3>
<p className="text-sm text-muted-foreground mb-4">
Tente ajustar seus filtros ou crie uma nova vaga
</p>
<Link href="/dashboard/empresa/vagas/nova">
<Button>
<Plus className="h-4 w-4 mr-2" />
Criar Vaga
</Button>
</Link>
</CardContent>
</Card>
)}
</div>
</div>
</main>
</div>
);
}

View file

@ -0,0 +1,216 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Edit, Trash2, Eye } from "lucide-react"
import { mockJobs } from "@/lib/mock-data"
export default function AdminJobsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [jobs, setJobs] = useState(mockJobs)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const filteredJobs = jobs.filter(
(job) =>
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
job.company.toLowerCase().includes(searchTerm.toLowerCase()),
)
const handleDeleteJob = (id: string) => {
setJobs(jobs.filter((job) => job.id !== id))
}
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Vagas</h1>
<p className="text-muted-foreground mt-1">Gerencie todas as vagas publicadas na plataforma</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Nova Vaga
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Criar Nova Vaga</DialogTitle>
<DialogDescription>Preencha os detalhes da nova vaga de emprego</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="title">Título da Vaga</Label>
<Input id="title" placeholder="Ex: Desenvolvedor Full Stack" />
</div>
<div className="grid gap-2">
<Label htmlFor="company">Empresa</Label>
<Input id="company" placeholder="Nome da empresa" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="location">Localização</Label>
<Input id="location" placeholder="São Paulo, SP" />
</div>
<div className="grid gap-2">
<Label htmlFor="type">Tipo</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="full-time">Tempo Integral</SelectItem>
<SelectItem value="part-time">Meio Período</SelectItem>
<SelectItem value="contract">Contrato</SelectItem>
<SelectItem value="Remoto">Remoto</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="salary">Salário</Label>
<Input id="salary" placeholder="R$ 8.000 - R$ 12.000" />
</div>
<div className="grid gap-2">
<Label htmlFor="level">Nível</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="junior">Júnior</SelectItem>
<SelectItem value="pleno">Pleno</SelectItem>
<SelectItem value="senior">Sênior</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Descrição</Label>
<Textarea
id="description"
placeholder="Descreva as responsabilidades e requisitos da vaga..."
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancelar
</Button>
<Button onClick={() => setIsDialogOpen(false)}>Publicar Vaga</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Vagas</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Vagas Ativas</CardDescription>
<CardTitle className="text-3xl">{jobs.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidaturas</CardDescription>
<CardTitle className="text-3xl">{"436"}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Taxa de Conversão</CardDescription>
<CardTitle className="text-3xl">12%</CardTitle>
</CardHeader>
</Card>
</div>
{/* Search and Filter */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar vagas por título ou empresa..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vaga</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Tipo</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredJobs.map((job) => (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</TableCell>
<TableCell>{job.location}</TableCell>
<TableCell>
<Badge variant="secondary">{job.type}</Badge>
</TableCell>
<TableCell>{((job.id.charCodeAt(0) * 7) % 50) + 10}</TableCell>
<TableCell>
<Badge variant="default">Ativa</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="icon">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDeleteJob(job.id)}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View file

@ -0,0 +1,62 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { Sidebar } from "@/components/sidebar"
import { DashboardHeader } from "@/components/dashboard-header"
import { getCurrentUser } from "@/lib/auth"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const router = useRouter()
const [isAuthorized, setIsAuthorized] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
// Simple auth check for dashboard access
const user = getCurrentUser()
if (!user) {
router.push("/login")
} else {
setIsAuthorized(true)
}
}, [router])
// Prevent hydration mismatch by returning null on first render
if (!mounted) return null
// Optional: Loading state while checking auth
if (!isAuthorized) {
return (
// Simple loading screen to match background
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}
// Canonical Layout Structure
return (
<div className="flex h-screen overflow-hidden bg-background">
{/* Sidebar - Fixed Left, Fixed Width */}
<Sidebar />
{/* Main Content Area - Flex Column */}
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
{/* Header - Fixed Top */}
<div className="h-16 shrink-0 z-50">
<DashboardHeader />
</div>
{/* Scrollable Main Content */}
<main className="flex-1 overflow-y-auto p-6 scroll-smooth bg-muted/10">
{children}
</main>
</div>
</div>
)
}

View file

@ -0,0 +1,226 @@
"use client"
import { useState } from "react"
import { AdminSidebar } from "@/components/admin-sidebar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Textarea } from "@/components/ui/textarea"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Search, Send, Paperclip } from "lucide-react"
const mockConversations = [
{
id: "1",
name: "Ana Silva",
avatar: "/professional-woman-diverse.png",
lastMessage: "Obrigada pela resposta sobre a vaga!",
timestamp: "10:30",
unread: 2,
},
{
id: "2",
name: "Carlos Santos",
avatar: "/professional-man.jpg",
lastMessage: "Quando posso esperar um retorno?",
timestamp: "Ontem",
unread: 0,
},
{
id: "3",
name: "Maria Oliveira",
avatar: "/professional-woman-smiling.png",
lastMessage: "Gostaria de mais informações sobre os benefícios",
timestamp: "2 dias",
unread: 1,
},
]
const mockMessages = [
{
id: "1",
sender: "Ana Silva",
content: "Olá! Gostaria de saber mais sobre a vaga de Desenvolvedor Full Stack.",
timestamp: "10:15",
isAdmin: false,
},
{
id: "2",
sender: "Você",
content: "Olá Ana! Claro, ficarei feliz em ajudar. A vaga é para trabalho remoto e oferece benefícios completos.",
timestamp: "10:20",
isAdmin: true,
},
{
id: "3",
sender: "Ana Silva",
content: "Obrigada pela resposta sobre a vaga!",
timestamp: "10:30",
isAdmin: false,
},
]
export default function AdminMessagesPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedConversation, setSelectedConversation] = useState(mockConversations[0])
const [messageText, setMessageText] = useState("")
const filteredConversations = mockConversations.filter((conv) =>
conv.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
const handleSendMessage = () => {
if (messageText.trim()) {
console.log("[v0] Sending message:", messageText)
setMessageText("")
}
}
return (
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-foreground">Mensagens</h1>
<p className="text-muted-foreground mt-1">Comunique-se com candidatos e empresas</p>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Conversas</CardDescription>
<CardTitle className="text-3xl">{mockConversations.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Não Lidas</CardDescription>
<CardTitle className="text-3xl">
{mockConversations.reduce((acc, conv) => acc + conv.unread, 0)}
</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Respondidas Hoje</CardDescription>
<CardTitle className="text-3xl">12</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Tempo Médio de Resposta</CardDescription>
<CardTitle className="text-3xl">2h</CardTitle>
</CardHeader>
</Card>
</div>
{/* Messages Interface */}
<Card className="h-[600px]">
<div className="grid grid-cols-[350px_1fr] h-full">
{/* Conversations List */}
<div className="border-r border-border">
<CardHeader className="border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar conversas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</CardHeader>
<ScrollArea className="h-[calc(600px-80px)]">
<div className="p-2">
{filteredConversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => setSelectedConversation(conversation)}
className={`w-full flex items-start gap-3 p-3 rounded-lg hover:bg-muted transition-colors ${selectedConversation.id === conversation.id ? "bg-muted" : ""
}`}
>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm truncate">{conversation.name}</span>
<span className="text-xs text-muted-foreground">{conversation.timestamp}</span>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground truncate">{conversation.lastMessage}</p>
{conversation.unread > 0 && (
<Badge
variant="default"
className="ml-2 h-5 w-5 p-0 flex items-center justify-center rounded-full"
>
{conversation.unread}
</Badge>
)}
</div>
</div>
</button>
))}
</div>
</ScrollArea>
</div>
{/* Chat Area */}
<div className="flex flex-col">
{/* Chat Header */}
<CardHeader className="border-b border-border">
<div className="flex items-center gap-3">
<div>
<CardTitle className="text-base">{selectedConversation.name}</CardTitle>
<CardDescription className="text-xs">Online</CardDescription>
</div>
</div>
</CardHeader>
{/* Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{mockMessages.map((message) => (
<div key={message.id} className={`flex ${message.isAdmin ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[70%] rounded-lg p-3 ${message.isAdmin ? "bg-primary text-primary-foreground" : "bg-muted"
}`}
>
<p className="text-sm">{message.content}</p>
<span className="text-xs opacity-70 mt-1 block">{message.timestamp}</span>
</div>
</div>
))}
</div>
</ScrollArea>
{/* Message Input */}
<div className="border-t border-border p-4">
<div className="flex items-end gap-2">
<Button variant="outline" size="icon">
<Paperclip className="h-4 w-4" />
</Button>
<Textarea
placeholder="Digite sua mensagem..."
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}}
className="min-h-[60px] resize-none"
/>
<Button onClick={handleSendMessage} size="icon">
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</Card>
</div>
)
}

View file

@ -0,0 +1,16 @@
"use client"
import { DashboardHeader } from "@/components/dashboard-header" // Keep header if we want consistent layout, though global layout handles it
// Actually global layout handles sidebar/header.
// We just need the content.
// But wait, "My Jobs" page doesn't exist yet!
// I'll create a placeholder for now to prevent 404s on the new links.
export default function MyJobsPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Minhas Vagas</h1>
<p>Funcionalidade em desenvolvimento.</p>
</div>
)
}

View file

@ -0,0 +1,51 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { getCurrentUser } from "@/lib/auth"
import { AdminDashboardContent } from "@/components/dashboard-contents/admin-dashboard"
import { CompanyDashboardContent } from "@/components/dashboard-contents/company-dashboard"
import { CandidateDashboardContent } from "@/components/dashboard-contents/candidate-dashboard"
export default function DashboardPage() {
const router = useRouter()
const [user, setUser] = useState(getCurrentUser())
const [loading, setLoading] = useState(true)
useEffect(() => {
const currentUser = getCurrentUser()
if (!currentUser) {
router.push("/login")
return
}
setUser(currentUser)
setLoading(false)
}, [router])
if (loading) {
return null
}
if (!user) return null
// Role-based rendering
if (user.role === "admin" || user.roles?.includes("superadmin")) {
return <AdminDashboardContent />
}
if (user.role === "company" || user.roles?.includes("companyAdmin")) {
return <CompanyDashboardContent />
}
if (user.role === "candidate" || user.role === "jobSeeker") {
return <CandidateDashboardContent />
}
// Fallback
return (
<div className="p-8 text-center">
<h1 className="text-2xl font-bold">Acesso não configurado</h1>
<p>Seu perfil não possui um dashboard associado.</p>
</div>
)
}

View file

@ -0,0 +1,302 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Trash2, Loader2, RefreshCw } from "lucide-react"
import { usersApi, type ApiUser } from "@/lib/api"
import { getCurrentUser } from "@/lib/auth"
import { toast } from "sonner"
export default function AdminUsersPage() {
const router = useRouter()
const [users, setUsers] = useState<ApiUser[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [creating, setCreating] = useState(false)
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
role: "jobSeeker",
})
useEffect(() => {
const user = getCurrentUser()
if (!user || (!user.roles?.includes("superadmin") && user.role !== "admin")) {
router.push("/dashboard")
return
}
loadUsers()
}, [router])
const loadUsers = async () => {
try {
setLoading(true)
const data = await usersApi.list()
setUsers(data || [])
} catch (error) {
console.error("Error loading users:", error)
toast.error("Erro ao carregar usuários")
} finally {
setLoading(false)
}
}
const handleCreate = async () => {
try {
setCreating(true)
await usersApi.create(formData)
toast.success("Usuário criado com sucesso!")
setIsDialogOpen(false)
setFormData({ name: "", email: "", password: "", role: "jobSeeker" })
loadUsers()
} catch (error) {
console.error("Error creating user:", error)
toast.error("Erro ao criar usuário")
} finally {
setCreating(false)
}
}
const handleDelete = async (id: string) => {
if (!confirm("Tem certeza que deseja excluir este usuário?")) return
try {
await usersApi.delete(id)
toast.success("Usuário excluído!")
loadUsers()
} catch (error) {
console.error("Error deleting user:", error)
toast.error("Erro ao excluir usuário")
}
}
const filteredUsers = users.filter(
(user) =>
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase())
)
const getRoleBadge = (role: string) => {
const labels: Record<string, string> = {
superadmin: "Super Admin",
companyAdmin: "Admin Empresa",
recruiter: "Recrutador",
jobSeeker: "Candidato",
admin: "Admin",
company: "Empresa"
}
const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
superadmin: "destructive",
companyAdmin: "default",
recruiter: "secondary",
jobSeeker: "outline",
admin: "destructive",
company: "default"
}
const label = labels[role] || role || "Usuário"
return <Badge variant={colors[role] || "outline"}>{label}</Badge>
}
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Gestão de Usuários</h1>
<p className="text-muted-foreground mt-1">Gerencie todos os usuários da plataforma</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={loadUsers} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Atualizar
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Novo Usuário
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Criar Novo Usuário</DialogTitle>
<DialogDescription>Preencha os dados do novo usuário</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Nome</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Nome completo"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="email@exemplo.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Senha</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="Senha segura"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="role">Função</Label>
<Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="superadmin">Super Admin</SelectItem>
<SelectItem value="companyAdmin">Admin Empresa</SelectItem>
<SelectItem value="recruiter">Recrutador</SelectItem>
<SelectItem value="jobSeeker">Candidato</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancelar</Button>
<Button onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar Usuário
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total de Usuários</CardDescription>
<CardTitle className="text-3xl">{users.length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Admins</CardDescription>
<CardTitle className="text-3xl">
{users.filter((u) => u.role === "superadmin" || u.role === "companyAdmin").length}
</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Recrutadores</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "recruiter").length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidatos</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "jobSeeker").length}</CardTitle>
</CardHeader>
</Card>
</div>
{/* Table */}
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar usuários por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>Email</TableHead>
<TableHead>Função</TableHead>
<TableHead>Status</TableHead>
<TableHead>Data Criação</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
Nenhum usuário encontrado
</TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{getRoleBadge(user.role)}</TableCell>
<TableCell>
<Badge variant={user.status === "active" ? "default" : "secondary"}>
{user.status === "active" ? "Ativo" : user.status}
</Badge>
</TableCell>
<TableCell>
{user.created_at ? new Date(user.created_at).toLocaleDateString("pt-BR") : "-"}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(user.id)}
disabled={user.role === "superadmin"}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

View file

@ -1,60 +0,0 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import { LayoutDashboard, Briefcase, Users, MessageSquare } from "lucide-react"
const sidebarItems = [
{
title: "Dashboard",
href: "/dashboard/admin",
icon: LayoutDashboard,
},
{
title: "Vagas",
href: "/dashboard/admin/jobs",
icon: Briefcase,
},
{
title: "Candidatos",
href: "/dashboard/admin/candidates",
icon: Users,
},
{
title: "Mensagens",
href: "/dashboard/admin/messages",
icon: MessageSquare,
},
]
export function AdminSidebar() {
const pathname = usePathname()
return (
<aside className="w-64 border-r border-border bg-muted/30 min-h-[calc(100vh-4rem)] p-4">
<nav className="space-y-2">
{sidebarItems.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors bg-background text-background",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Icon className="h-5 w-5" />
{item.title}
</Link>
)
})}
</nav>
</aside>
)
}

View file

@ -0,0 +1,157 @@
"use client"
import { useRouter } from "next/navigation"
import { StatsCard } from "@/components/stats-card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { mockStats, mockJobs } from "@/lib/mock-data"
import { Briefcase, Users, TrendingUp, FileText, Plus, MoreHorizontal } from "lucide-react"
import { motion } from "framer-motion"
const mockCandidates = [
{ id: "1", name: "João Silva", position: "Desenvolvedor Full Stack", status: "active" },
{ id: "2", name: "Maria Santos", position: "Designer UX/UI", status: "active" },
{ id: "3", name: "Carlos Oliveira", position: "Product Manager", status: "pending" },
{ id: "4", name: "Ana Costa", position: "Engenheiro de Dados", status: "active" },
{ id: "5", name: "Pedro Alves", position: "DevOps Engineer", status: "inactive" },
]
export function AdminDashboardContent() {
const router = useRouter()
return (
<div className="space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-3xl font-bold mb-2">Dashboard</h1>
<p className="text-muted-foreground">Visão geral do portal de empregos</p>
</motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
>
<StatsCard
title="Vagas Ativas"
value={mockStats.activeJobs}
icon={Briefcase}
description="Total de vagas publicadas"
/>
<StatsCard
title="Total de Candidatos"
value={mockStats.totalCandidates}
icon={Users}
description="Usuários cadastrados"
/>
<StatsCard
title="Novas Candidaturas"
value={mockStats.newApplications}
icon={FileText}
description="Últimos 7 dias"
/>
<StatsCard title="Taxa de Conversão" value="12.5%" icon={TrendingUp} description="Candidaturas por vaga" />
</motion.div>
{/* Jobs Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Gestão de Vagas</CardTitle>
<Button onClick={() => router.push('/dashboard/jobs')}>
<Plus className="mr-2 h-4 w-4" />
Adicionar Vaga
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Título</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Localização</TableHead>
<TableHead>Status</TableHead>
<TableHead>Candidaturas</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockJobs.slice(0, 5).map((job) => (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.company}</TableCell>
<TableCell>{job.location}</TableCell>
<TableCell>
<Badge variant="secondary">Ativa</Badge>
</TableCell>
<TableCell>{((job.id.charCodeAt(0) * 7) % 50) + 10}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
{/* Candidates Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<Card>
<CardHeader>
<CardTitle>Gestão de Candidatos</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>Cargo Pretendido</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockCandidates.map((candidate) => (
<TableRow key={candidate.id}>
<TableCell className="font-medium">{candidate.name}</TableCell>
<TableCell>{candidate.position}</TableCell>
<TableCell>
{candidate.status === "active" && <Badge className="bg-green-500">Ativo</Badge>}
{candidate.status === "pending" && <Badge variant="secondary">Pendente</Badge>}
{candidate.status === "inactive" && <Badge variant="outline">Inativo</Badge>}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
</div>
)
}

View file

@ -0,0 +1,198 @@
"use client"
import { StatsCard } from "@/components/stats-card"
import { JobCard } from "@/components/job-card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { mockJobs, mockApplications, mockNotifications } from "@/lib/mock-data"
import {
Bell,
FileText,
Clock,
CheckCircle,
XCircle,
AlertCircle,
Edit,
} from "lucide-react"
import { motion } from "framer-motion"
import { getCurrentUser } from "@/lib/auth"
export function CandidateDashboardContent() {
const user = getCurrentUser()
const recommendedJobs = mockJobs.slice(0, 3)
const unreadNotifications = mockNotifications.filter((n) => !n.read)
const getStatusBadge = (status: string) => {
switch (status) {
case "pending":
return (
<Badge variant="secondary">
<Clock className="h-3 w-3 mr-1" />
Em análise
</Badge>
)
case "reviewing":
return (
<Badge variant="secondary">
<AlertCircle className="h-3 w-3 mr-1" />
Em análise
</Badge>
)
case "interview":
return (
<Badge className="bg-blue-500 hover:bg-blue-600">
<CheckCircle className="h-3 w-3 mr-1" />
Entrevista
</Badge>
)
case "accepted":
return (
<Badge className="bg-green-500 hover:bg-green-600">
<CheckCircle className="h-3 w-3 mr-1" />
Aprovado
</Badge>
)
case "rejected":
return (
<Badge variant="destructive">
<XCircle className="h-3 w-3 mr-1" />
Rejeitado
</Badge>
)
default:
return <Badge variant="outline">{status}</Badge>
}
}
return (
<div className="space-y-8">
{/* Profile Summary */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="mb-8">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div className="flex-1">
<h1 className="text-2xl font-bold mb-2">Olá, {user?.name || "Candidato"}!</h1>
<p className="text-muted-foreground">{user?.area || "Desenvolvimento"}</p>
</div>
<Button className="cursor-pointer">
<Edit className="mr-2 h-4 w-4" />
Editar Perfil
</Button>
</div>
</CardContent>
</Card>
</motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"
>
<StatsCard
title="Candidaturas"
value={mockApplications.length}
icon={FileText}
description="Total de vagas aplicadas"
/>
<StatsCard
title="Em processo"
value={
mockApplications.filter(
(a) => a.status === "reviewing" || a.status === "interview"
).length
}
icon={Clock}
description="Aguardando resposta"
/>
<StatsCard
title="Notificações"
value={unreadNotifications.length}
icon={Bell}
description="Novas atualizações"
/>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-1 gap-8">
{/* Main Content */}
<div className="space-y-8">
{/* Recommended Jobs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card>
<CardHeader>
<CardTitle>Vagas recomendadas</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{recommendedJobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</CardContent>
</Card>
</motion.div>
{/* Applications */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<Card>
<CardHeader>
<CardTitle>Minhas candidaturas</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vaga</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Status</TableHead>
<TableHead>Data</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockApplications.map((application) => (
<TableRow key={application.id}>
<TableCell className="font-medium">
{application.jobTitle}
</TableCell>
<TableCell>{application.company}</TableCell>
<TableCell>
{getStatusBadge(application.status)}
</TableCell>
<TableCell className="text-muted-foreground">
{new Date(application.appliedAt).toLocaleDateString(
"pt-BR"
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,332 @@
"use client"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import {
Briefcase,
Users,
Eye,
TrendingUp,
Plus,
MoreVertical,
Calendar,
MapPin,
DollarSign,
MessageSquare,
BarChart3,
} from "lucide-react"
import Link from "next/link"
export function CompanyDashboardContent() {
const companyStats = {
activeJobs: 12,
totalApplications: 234,
totalViews: 1542,
thisMonth: 89,
}
const recentJobs = [
{
id: "1",
title: "Desenvolvedor Full Stack Sênior",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 12.000 - R$ 18.000",
applications: 45,
views: 320,
postedAt: "2 dias atrás",
status: "active",
},
{
id: "2",
title: "Designer UX/UI",
type: "Remoto",
location: "Remoto",
salary: "R$ 8.000 - R$ 12.000",
applications: 32,
views: 256,
postedAt: "5 dias atrás",
status: "active",
},
{
id: "3",
title: "Product Manager",
type: "Tempo Integral",
location: "São Paulo, SP",
salary: "R$ 15.000 - R$ 20.000",
applications: 28,
views: 189,
postedAt: "1 semana atrás",
status: "active",
},
]
const recentApplications = [
{
id: "1",
candidateName: "Ana Silva",
candidateAvatar: "",
jobTitle: "Desenvolvedor Full Stack Sênior",
appliedAt: "Há 2 horas",
status: "pending",
},
{
id: "2",
candidateName: "Carlos Santos",
candidateAvatar: "",
jobTitle: "Designer UX/UI",
appliedAt: "Há 5 horas",
status: "pending",
},
{
id: "3",
candidateName: "Maria Oliveira",
candidateAvatar: "",
jobTitle: "Product Manager",
appliedAt: "Há 1 dia",
status: "reviewing",
},
]
const statusColors = {
pending: "bg-yellow-500",
reviewing: "bg-blue-500",
accepted: "bg-green-500",
rejected: "bg-red-500",
}
return (
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
Dashboard
</h1>
<p className="text-muted-foreground">
Bem-vindo de volta, TechCorp! 👋
</p>
</div>
<Link href="/dashboard/my-jobs">
<Button size="lg" className="w-full sm:w-auto">
<Plus className="h-5 w-5 mr-2" />
Nova Vaga
</Button>
</Link>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Vagas Ativas
</CardTitle>
<Briefcase className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.activeJobs}
</div>
<p className="text-xs text-muted-foreground mt-1">
Publicadas no momento
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Candidaturas
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.totalApplications}
</div>
<p className="text-xs text-muted-foreground mt-1">
+{companyStats.thisMonth} este mês
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Visualizações
</CardTitle>
<Eye className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{companyStats.totalViews}
</div>
<p className="text-xs text-muted-foreground mt-1">
Nas suas vagas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Taxa de Conversão
</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">15.2%</div>
<p className="text-xs text-green-600 mt-1">
+2.5% vs mês passado
</p>
</CardContent>
</Card>
</div>
<div className="grid lg:grid-cols-3 gap-6">
{/* Vagas Recentes */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Vagas Recentes</CardTitle>
<CardDescription>
Suas últimas vagas publicadas
</CardDescription>
</div>
<Link href="/dashboard/jobs">
<Button variant="ghost" size="sm">
Ver todas
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="space-y-4">
{recentJobs.map((job) => (
<div
key={job.id}
className="border rounded-lg p-4 hover:bg-muted/50 transition-colors"
>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base sm:text-lg mb-2 truncate">
{job.title}
</h3>
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground mb-3">
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4 shrink-0" />
<span className="truncate">{job.location}</span>
</div>
<Badge
variant="secondary"
className="whitespace-nowrap"
>
{job.type}
</Badge>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 shrink-0" />
<span className="whitespace-nowrap">
{job.salary}
</span>
</div>
</div>
<div className="flex flex-wrap gap-4 text-sm">
<span className="flex items-center gap-1">
<Users className="h-4 w-4" />
{job.applications} candidaturas
</span>
<span className="flex items-center gap-1">
<Eye className="h-4 w-4" />
{job.views} visualizações
</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{job.postedAt}
</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="shrink-0"
>
<MoreVertical className="h-4 w-4" />
</Button>
</div>
</div>
))}
</CardContent>
</Card>
</div>
{/* Candidaturas Recentes */}
<div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Candidaturas</CardTitle>
<CardDescription>Novas candidaturas</CardDescription>
</div>
<Link href="/dashboard/candidates">
<Button variant="ghost" size="sm">
Ver todas
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="space-y-4">
{recentApplications.map((application) => (
<div
key={application.id}
className="flex items-start gap-3 p-3 rounded-lg border hover:bg-muted/50 transition-colors cursor-pointer"
>
<div className="relative">
<Avatar className="h-10 w-10">
<AvatarImage src={application.candidateAvatar} />
<AvatarFallback className="bg-primary/10 text-primary text-sm">
{application.candidateName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<span
className={`absolute -top-1 -right-1 h-3 w-3 rounded-full ${statusColors[
application.status as keyof typeof statusColors
]
} border-2 border-background`}
/>
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{application.candidateName}
</p>
<p className="text-xs text-muted-foreground truncate">
{application.jobTitle}
</p>
<p className="text-xs text-muted-foreground mt-1">
{application.appliedAt}
</p>
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
</div>
)
}

View file

@ -47,15 +47,12 @@ export function DashboardHeader() {
<header className="border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<Link href="/" className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity">
<Image
src="/logohorse.png"
alt="GoHorse Jobs"
width={64}
height={64}
/>
<span className="text-xl font-bold tracking-tight hidden sm:inline-block">GoHorse Jobs</span>
</Link>
{/* Logo removed as it is in Sidebar */}
<div className="flex items-center gap-3 md:hidden">
{/* Mobile Toggle could go here */}
<span className="font-bold">GoHorse Jobs</span>
</div>
<div className="hidden md:block"></div> {/* Spacer */}
<div className="flex items-center gap-4">
<NotificationDropdown />

View file

@ -0,0 +1,138 @@
"use client"
import Link from "next/link"
import Image from "next/image"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import { LayoutDashboard, Briefcase, Users, MessageSquare, Building2, FileText } from "lucide-react"
import { getCurrentUser } from "@/lib/auth"
const adminItems = [
{
title: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
title: "Vagas",
href: "/dashboard/jobs",
icon: Briefcase,
},
{
title: "Candidatos",
href: "/dashboard/candidates",
icon: Users,
},
{
title: "Usuários",
href: "/dashboard/users",
icon: Users,
},
{
title: "Empresas",
href: "/dashboard/companies",
icon: Building2,
},
{
title: "Mensagens",
href: "/dashboard/messages",
icon: MessageSquare,
},
]
const companyItems = [
{
title: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
title: "Minhas Vagas",
href: "/dashboard/my-jobs",
icon: Briefcase,
},
{
title: "Candidaturas",
href: "/dashboard/applications",
icon: Users,
},
]
const candidateItems = [
{
title: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
title: "Vagas",
href: "/jobs", // Public search
icon: Briefcase,
},
{
title: "Minhas Candidaturas",
href: "/dashboard/my-applications",
icon: FileText,
},
]
export function Sidebar() {
const pathname = usePathname()
const user = getCurrentUser()
let items = candidateItems
if (user?.role === "admin" || user?.roles?.includes("superadmin")) {
items = adminItems
} else if (user?.role === "company") {
items = companyItems
}
return (
<aside className="w-64 shrink-0 border-r border-border bg-muted/30 min-h-screen flex flex-col">
{/* Branding Header with CORRECT Padding (pl-6) */}
<div className="flex h-16 shrink-0 items-center px-6 gap-3 border-b border-border">
<Link href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
<Image
src="/logohorse.png"
alt="GoHorse Jobs"
width={40}
height={40}
className="rounded-lg"
/>
<span className="font-bold text-lg text-foreground">GoHorse Jobs</span>
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
{items.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Icon className="h-5 w-5" />
{item.title}
</Link>
)
})}
</nav>
{/* Footer / User Info could go here */}
<div className="p-4 border-t border-border mt-auto">
<div className="text-xs text-muted-foreground text-center">
v1.0.0
</div>
</div>
</aside>
)
}

102
frontend/src/lib/api.ts Normal file
View file

@ -0,0 +1,102 @@
import { getToken } from "./auth";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
export interface ApiUser {
id: string;
name: string;
email: string;
identifier: string;
phone?: string;
role: string;
status: string;
created_at: string;
}
export interface ApiCompany {
id: string;
name: string;
slug: string;
email?: string;
phone?: string;
website?: string;
address?: string;
active: boolean;
verified: boolean;
created_at: string;
}
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getToken();
// Sanitize API_URL: remove trailing slash
let baseUrl = API_URL.replace(/\/+$/, "");
// Sanitize endpoint: ensure leading slash
let cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
// Detect and fix double prefixing of /api/v1
// Case 1: BaseURL ends with /api/v1 AND endpoint starts with /api/v1
if (baseUrl.endsWith("/api/v1") && cleanEndpoint.startsWith("/api/v1")) {
cleanEndpoint = cleanEndpoint.replace("/api/v1", "");
}
// Case 2: Double /api/v1 inside endpoint itself (if passed incorrectly)
if (cleanEndpoint.includes("/api/v1/api/v1")) {
cleanEndpoint = cleanEndpoint.replace("/api/v1/api/v1", "/api/v1");
}
const url = `${baseUrl}${cleanEndpoint}`;
console.log(`[API Request] ${url}`); // Debug log
const res = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!res.ok) {
const error = await res.text();
throw new Error(error || `API Error: ${res.status}`);
}
return res.json();
}
// Users API
export const usersApi = {
list: () => apiRequest<ApiUser[]>("/api/v1/users"),
create: (data: { name: string; email: string; password: string; role: string }) =>
apiRequest<ApiUser>("/api/v1/users", {
method: "POST",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiRequest<void>(`/api/v1/users/${id}`, {
method: "DELETE",
}),
};
// Companies API
export const companiesApi = {
list: () => apiRequest<ApiCompany[]>("/api/v1/companies"),
create: (data: { name: string; slug: string; email?: string }) =>
apiRequest<ApiCompany>("/api/v1/companies", {
method: "POST",
body: JSON.stringify(data),
}),
};
// Jobs API (public)
export const jobsApi = {
list: () => apiRequest<unknown[]>("/jobs"),
};