gohorsejobs/frontend/src/app/dashboard/backoffice/page.tsx

424 lines
19 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
adminAccessApi,
adminAuditApi,
adminCompaniesApi,
adminJobsApi,
adminTagsApi,
type AdminCompany,
type AdminJob,
type AdminLoginAudit,
type AdminRoleAccess,
type AdminTag,
} from "@/lib/api"
import { getCurrentUser, isAdminUser } from "@/lib/auth"
import { toast } from "sonner"
import { Archive, CheckCircle, Copy, PauseCircle, Plus, RefreshCw, XCircle } from "lucide-react"
const auditDateFormatter = new Intl.DateTimeFormat("pt-BR", {
dateStyle: "short",
timeStyle: "short",
timeZone: "UTC",
})
const jobStatusBadge: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
draft: { label: "Draft", variant: "outline" },
review: { label: "Review", variant: "secondary" },
published: { label: "Published", variant: "default" },
paused: { label: "Paused", variant: "outline" },
expired: { label: "Expired", variant: "destructive" },
archived: { label: "Archived", variant: "outline" },
reported: { label: "Reported", variant: "destructive" },
open: { label: "Open", variant: "default" },
closed: { label: "Closed", variant: "outline" },
}
export default function BackofficePage() {
const router = useRouter()
const [roles, setRoles] = useState<AdminRoleAccess[]>([])
const [audits, setAudits] = useState<AdminLoginAudit[]>([])
const [companies, setCompanies] = useState<AdminCompany[]>([])
const [jobs, setJobs] = useState<AdminJob[]>([])
const [tags, setTags] = useState<AdminTag[]>([])
const [loading, setLoading] = useState(true)
const [creatingTag, setCreatingTag] = useState(false)
const [tagForm, setTagForm] = useState({ name: "", category: "area" as "area" | "level" | "stack" })
useEffect(() => {
const user = getCurrentUser()
if (!isAdminUser(user)) {
router.push("/dashboard")
return
}
loadBackoffice()
}, [router])
const loadBackoffice = async () => {
try {
setLoading(true)
const [rolesData, auditData, companiesData, jobsData, tagsData] = await Promise.all([
adminAccessApi.listRoles(),
adminAuditApi.listLogins(20),
adminCompaniesApi.list(false),
adminJobsApi.list({ status: "review", limit: 10 }),
adminTagsApi.list(),
])
setRoles(rolesData)
setAudits(auditData)
setCompanies(companiesData.data || [])
setJobs(jobsData.data || [])
setTags(tagsData)
} catch (error) {
console.error("Error loading backoffice:", error)
toast.error("Failed to load backoffice data")
} finally {
setLoading(false)
}
}
const handleApproveCompany = async (companyId: string) => {
try {
await adminCompaniesApi.updateStatus(companyId, { verified: true })
toast.success("Company approved")
loadBackoffice()
} catch (error) {
console.error("Error approving company:", error)
toast.error("Failed to approve company")
}
}
const handleDeactivateCompany = async (companyId: string) => {
try {
await adminCompaniesApi.updateStatus(companyId, { active: false })
toast.success("Company deactivated")
loadBackoffice()
} catch (error) {
console.error("Error deactivating company:", error)
toast.error("Failed to deactivate company")
}
}
const handleJobStatus = async (jobId: string, status: string) => {
try {
await adminJobsApi.updateStatus(jobId, status)
toast.success("Job status updated")
loadBackoffice()
} catch (error) {
console.error("Error updating job status:", error)
toast.error("Failed to update job status")
}
}
const handleDuplicateJob = async (jobId: string) => {
try {
await adminJobsApi.duplicate(jobId)
toast.success("Job duplicated as draft")
loadBackoffice()
} catch (error) {
console.error("Error duplicating job:", error)
toast.error("Failed to duplicate job")
}
}
const handleCreateTag = async () => {
if (!tagForm.name.trim()) {
toast.error("Tag name is required")
return
}
try {
setCreatingTag(true)
await adminTagsApi.create({ name: tagForm.name.trim(), category: tagForm.category })
toast.success("Tag created")
setTagForm({ name: "", category: "area" })
loadBackoffice()
} catch (error) {
console.error("Error creating tag:", error)
toast.error("Failed to create tag")
} finally {
setCreatingTag(false)
}
}
const handleToggleTag = async (tag: AdminTag) => {
try {
await adminTagsApi.update(tag.id, { active: !tag.active })
toast.success("Tag updated")
loadBackoffice()
} catch (error) {
console.error("Error updating tag:", error)
toast.error("Failed to update tag")
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}
return (
<div className="space-y-10">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Backoffice</h1>
<p className="text-muted-foreground mt-1">Controle administrativo do GoHorse Jobs</p>
</div>
<Button variant="outline" onClick={loadBackoffice} className="gap-2">
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Gestão de usuários & acesso</CardTitle>
<CardDescription>Perfis, permissões e ações disponíveis no RBAC.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Perfil</TableHead>
<TableHead>Descrição</TableHead>
<TableHead>Ações principais</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{roles.map((role) => (
<TableRow key={role.role}>
<TableCell className="font-medium">{role.role}</TableCell>
<TableCell>{role.description}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
{role.actions.map((action) => (
<Badge key={action} variant="secondary">{action}</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Auditoria de login</CardTitle>
<CardDescription>Histórico recente de acessos ao painel administrativo.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Usuário</TableHead>
<TableHead>Roles</TableHead>
<TableHead>IP</TableHead>
<TableHead>Data</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{audits.map((audit) => (
<TableRow key={audit.id}>
<TableCell className="font-medium">{audit.identifier}</TableCell>
<TableCell>{audit.roles}</TableCell>
<TableCell>{audit.ipAddress || "-"}</TableCell>
<TableCell>{auditDateFormatter.format(new Date(audit.createdAt))}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Empresas pendentes</CardTitle>
<CardDescription>Aprovação e verificação de empresas.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Empresa</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{companies.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Nenhuma empresa pendente.
</TableCell>
</TableRow>
)}
{companies.map((company) => (
<TableRow key={company.id}>
<TableCell className="font-medium">{company.name}</TableCell>
<TableCell>{company.email || "-"}</TableCell>
<TableCell>
{company.verified ? (
<Badge className="bg-green-500">Verificada</Badge>
) : (
<Badge variant="secondary">Pendente</Badge>
)}
</TableCell>
<TableCell className="text-right space-x-2">
<Button size="sm" variant="outline" onClick={() => handleApproveCompany(company.id)}>
<CheckCircle className="h-4 w-4 mr-2" />
Aprovar
</Button>
<Button size="sm" variant="destructive" onClick={() => handleDeactivateCompany(company.id)}>
<XCircle className="h-4 w-4 mr-2" />
Desativar
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Moderação de vagas</CardTitle>
<CardDescription>Fluxo: rascunho revisão publicada expirada/arquivada.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Título</TableHead>
<TableHead>Empresa</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jobs.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Nenhuma vaga aguardando revisão.
</TableCell>
</TableRow>
)}
{jobs.map((job) => {
const statusConfig = jobStatusBadge[job.status] || { label: job.status, variant: "outline" }
return (
<TableRow key={job.id}>
<TableCell className="font-medium">{job.title}</TableCell>
<TableCell>{job.companyName || "-"}</TableCell>
<TableCell>
<Badge variant={statusConfig.variant}>{statusConfig.label}</Badge>
</TableCell>
<TableCell className="text-right space-x-2">
<Button size="sm" variant="outline" onClick={() => handleJobStatus(job.id, "published")}>
<CheckCircle className="h-4 w-4 mr-2" />
Publicar
</Button>
<Button size="sm" variant="outline" onClick={() => handleJobStatus(job.id, "paused")}>
<PauseCircle className="h-4 w-4 mr-2" />
Pausar
</Button>
<Button size="sm" variant="outline" onClick={() => handleJobStatus(job.id, "archived")}>
<Archive className="h-4 w-4 mr-2" />
Arquivar
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDuplicateJob(job.id)}>
<Copy className="h-4 w-4 mr-2" />
Duplicar
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Tags e categorias</CardTitle>
<CardDescription>Áreas, níveis e stacks customizáveis.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Input
placeholder="Nova tag"
value={tagForm.name}
onChange={(event) => setTagForm({ ...tagForm, name: event.target.value })}
/>
<Select
value={tagForm.category}
onValueChange={(value: "area" | "level" | "stack") => setTagForm({ ...tagForm, category: value })}
>
<SelectTrigger className="md:w-48">
<SelectValue placeholder="Categoria" />
</SelectTrigger>
<SelectContent>
<SelectItem value="area">Área</SelectItem>
<SelectItem value="level">Nível</SelectItem>
<SelectItem value="stack">Stack</SelectItem>
</SelectContent>
</Select>
<Button onClick={handleCreateTag} disabled={creatingTag} className="gap-2">
<Plus className="h-4 w-4" />
Criar tag
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tag</TableHead>
<TableHead>Categoria</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tags.map((tag) => (
<TableRow key={tag.id}>
<TableCell className="font-medium">{tag.name}</TableCell>
<TableCell>{tag.category}</TableCell>
<TableCell>
{tag.active ? (
<Badge className="bg-green-500">Ativa</Badge>
) : (
<Badge variant="outline">Inativa</Badge>
)}
</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="outline" onClick={() => handleToggleTag(tag)}>
{tag.active ? "Desativar" : "Ativar"}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}