fix: refactor dashboard urls, fix layout duplication and resolve backend api errors
This commit is contained in:
parent
7934afcf0d
commit
a505726786
40 changed files with 2374 additions and 6421 deletions
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
37
backend/internal/core/usecases/tenant/list_companies.go
Normal file
37
backend/internal/core/usecases/tenant/list_companies.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
216
frontend/src/app/dashboard/candidates/page.tsx
Normal file
216
frontend/src/app/dashboard/candidates/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
267
frontend/src/app/dashboard/companies/page.tsx
Normal file
267
frontend/src/app/dashboard/companies/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 `Há ${diffHours}h`;
|
||||
if (diffDays === 1) return "Ontem";
|
||||
if (diffDays < 7) return `Há ${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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
216
frontend/src/app/dashboard/jobs/page.tsx
Normal file
216
frontend/src/app/dashboard/jobs/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
62
frontend/src/app/dashboard/layout.tsx
Normal file
62
frontend/src/app/dashboard/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
226
frontend/src/app/dashboard/messages/page.tsx
Normal file
226
frontend/src/app/dashboard/messages/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
16
frontend/src/app/dashboard/my-jobs/page.tsx
Normal file
16
frontend/src/app/dashboard/my-jobs/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
51
frontend/src/app/dashboard/page.tsx
Normal file
51
frontend/src/app/dashboard/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
302
frontend/src/app/dashboard/users/page.tsx
Normal file
302
frontend/src/app/dashboard/users/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
157
frontend/src/components/dashboard-contents/admin-dashboard.tsx
Normal file
157
frontend/src/components/dashboard-contents/admin-dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
332
frontend/src/components/dashboard-contents/company-dashboard.tsx
Normal file
332
frontend/src/components/dashboard-contents/company-dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
138
frontend/src/components/sidebar.tsx
Normal file
138
frontend/src/components/sidebar.tsx
Normal 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
102
frontend/src/lib/api.ts
Normal 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"),
|
||||
};
|
||||
Loading…
Reference in a new issue