style: padroniza layout da listagem de empresas com design premium
This commit is contained in:
parent
007a708ffe
commit
326644f22f
1 changed files with 119 additions and 421 deletions
|
|
@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
|
|
@ -16,10 +15,25 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, EyeOff, Trash2, Pencil, ChevronLeft, ChevronRight } from "lucide-react"
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
|
Building2,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Eye,
|
||||||
|
Trash2,
|
||||||
|
Pencil,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Users,
|
||||||
|
ShieldCheck,
|
||||||
|
AlertCircle
|
||||||
|
} from "lucide-react"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { adminCompaniesApi, type AdminCompany } from "@/lib/api"
|
import { adminCompaniesApi, type AdminCompany } from "@/lib/api"
|
||||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||||
|
|
@ -27,16 +41,14 @@ import { toast } from "sonner"
|
||||||
import { ConfirmModal } from "@/components/confirm-modal"
|
import { ConfirmModal } from "@/components/confirm-modal"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { useTranslation } from "@/lib/i18n"
|
import { useTranslation } from "@/lib/i18n"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
const companyDateFormatter = new Intl.DateTimeFormat("en-US", {
|
const companyDateFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeZone: "UTC",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to format description (handles JSON or plain text)
|
|
||||||
const formatDescription = (description: string | undefined) => {
|
const formatDescription = (description: string | undefined) => {
|
||||||
if (!description) return null
|
if (!description) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(description)
|
const parsed = JSON.parse(description)
|
||||||
if (typeof parsed === 'object' && parsed !== null) {
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
|
@ -47,39 +59,16 @@ const formatDescription = (description: string | undefined) => {
|
||||||
<dt className="text-xs text-muted-foreground capitalize">
|
<dt className="text-xs text-muted-foreground capitalize">
|
||||||
{key.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ')}
|
{key.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="text-sm">{String(value)}</dd>
|
<dd className="text-sm font-medium">{String(value)}</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { }
|
||||||
// Not JSON, return as plain text
|
|
||||||
}
|
|
||||||
|
|
||||||
return <p className="text-sm mt-1">{description}</p>
|
return <p className="text-sm mt-1">{description}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format CNPJ: 00.000.000/0000-00
|
|
||||||
const formatCNPJ = (value: string) => {
|
|
||||||
return value
|
|
||||||
.replace(/\D/g, "")
|
|
||||||
.replace(/^(\d{2})(\d)/, "$1.$2")
|
|
||||||
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
|
|
||||||
.replace(/\.(\d{3})(\d)/, ".$1/$2")
|
|
||||||
.replace(/(\d{4})(\d)/, "$1-$2")
|
|
||||||
.substring(0, 18)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format Phone: (00) 00000-0000
|
|
||||||
const formatPhone = (value: string) => {
|
|
||||||
return value
|
|
||||||
.replace(/\D/g, "")
|
|
||||||
.replace(/^(\d{2})(\d)/, "($1) $2")
|
|
||||||
.replace(/(\d{5})(\d)/, "$1-$2")
|
|
||||||
.substring(0, 15)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminCompaniesPage() {
|
export default function AdminCompaniesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -121,10 +110,7 @@ export default function AdminCompaniesPage() {
|
||||||
const totalPages = Math.max(1, Math.ceil(totalCompanies / limit))
|
const totalPages = Math.max(1, Math.ceil(totalCompanies / limit))
|
||||||
|
|
||||||
const loadCompanies = async (targetPage = page) => {
|
const loadCompanies = async (targetPage = page) => {
|
||||||
// If coming from onClick event, targetPage might be the event object
|
|
||||||
// Ensure it is a number
|
|
||||||
const pageNum = typeof targetPage === 'number' ? targetPage : page
|
const pageNum = typeof targetPage === 'number' ? targetPage : page
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await adminCompaniesApi.list(undefined, pageNum, limit)
|
const data = await adminCompaniesApi.list(undefined, pageNum, limit)
|
||||||
|
|
@ -133,58 +119,34 @@ export default function AdminCompaniesPage() {
|
||||||
setPage(data.pagination.page)
|
setPage(data.pagination.page)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading companies:", error)
|
console.error("Error loading companies:", error)
|
||||||
toast.error("Failed to load companies")
|
toast.error("Erro ao carregar empresas")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleView = (company: AdminCompany) => {
|
|
||||||
setSelectedCompany(company)
|
|
||||||
setIsViewDialogOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleStatus = async (company: AdminCompany, field: 'active' | 'verified') => {
|
const toggleStatus = async (company: AdminCompany, field: 'active' | 'verified') => {
|
||||||
const newValue = !company[field]
|
const newValue = !company[field]
|
||||||
// Optimistic update
|
|
||||||
const originalCompanies = [...companies]
|
const originalCompanies = [...companies]
|
||||||
setCompanies(companies.map(c => c.id === company.id ? { ...c, [field]: newValue } : c))
|
setCompanies(companies.map(c => c.id === company.id ? { ...c, [field]: newValue } : c))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminCompaniesApi.updateStatus(company.id, { [field]: newValue })
|
await adminCompaniesApi.updateStatus(company.id, { [field]: newValue })
|
||||||
toast.success(t('admin.companies.success.statusUpdated', { field }))
|
toast.success(t('admin.companies.success.statusUpdated', { field }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(`Failed to update ${field}`)
|
toast.error(`Falha ao atualizar ${field}`)
|
||||||
setCompanies(originalCompanies)
|
setCompanies(originalCompanies)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateSlug = (name: string) => {
|
|
||||||
return name
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize("NFD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
|
||||||
.replace(/(^-|-$)/g, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (company: AdminCompany) => {
|
|
||||||
setCompanyToDelete(company)
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!companyToDelete) return
|
if (!companyToDelete) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminCompaniesApi.delete(companyToDelete.id)
|
await adminCompaniesApi.delete(companyToDelete.id)
|
||||||
toast.success(t('admin.companies.success.deleted'))
|
toast.success(t('admin.companies.success.deleted'))
|
||||||
if (selectedCompany?.id === companyToDelete.id) {
|
setIsViewDialogOpen(false)
|
||||||
setIsViewDialogOpen(false)
|
|
||||||
}
|
|
||||||
loadCompanies()
|
loadCompanies()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting company:", error)
|
toast.error("Falha ao deletar empresa")
|
||||||
toast.error("Failed to delete company")
|
|
||||||
} finally {
|
} finally {
|
||||||
setCompanyToDelete(null)
|
setCompanyToDelete(null)
|
||||||
}
|
}
|
||||||
|
|
@ -213,34 +175,18 @@ export default function AdminCompaniesPage() {
|
||||||
if (!selectedCompany) return
|
if (!selectedCompany) return
|
||||||
try {
|
try {
|
||||||
setUpdating(true)
|
setUpdating(true)
|
||||||
|
|
||||||
// Check if status changed
|
|
||||||
if (editFormData.active !== selectedCompany.active || editFormData.verified !== selectedCompany.verified) {
|
if (editFormData.active !== selectedCompany.active || editFormData.verified !== selectedCompany.verified) {
|
||||||
await adminCompaniesApi.updateStatus(selectedCompany.id, {
|
await adminCompaniesApi.updateStatus(selectedCompany.id, {
|
||||||
active: editFormData.active,
|
active: editFormData.active,
|
||||||
verified: editFormData.verified
|
verified: editFormData.verified
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
await adminCompaniesApi.update(selectedCompany.id, editFormData as any)
|
||||||
await adminCompaniesApi.update(selectedCompany.id, {
|
toast.success("Empresa atualizada com sucesso")
|
||||||
name: editFormData.name,
|
|
||||||
slug: editFormData.slug,
|
|
||||||
email: editFormData.email,
|
|
||||||
phone: editFormData.phone,
|
|
||||||
website: editFormData.website,
|
|
||||||
document: editFormData.document,
|
|
||||||
address: editFormData.address,
|
|
||||||
description: editFormData.description,
|
|
||||||
logoUrl: editFormData.logoUrl || undefined,
|
|
||||||
yearsInMarket: editFormData.yearsInMarket || undefined,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
toast.success(t('admin.companies.success.updated'))
|
|
||||||
setIsEditDialogOpen(false)
|
setIsEditDialogOpen(false)
|
||||||
loadCompanies()
|
loadCompanies()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating company:", error)
|
toast.error("Erro ao atualizar empresa")
|
||||||
toast.error("Failed to update company")
|
|
||||||
} finally {
|
} finally {
|
||||||
setUpdating(false)
|
setUpdating(false)
|
||||||
}
|
}
|
||||||
|
|
@ -253,58 +199,70 @@ export default function AdminCompaniesPage() {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="container py-8 space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
|
||||||
<h1 className="text-3xl font-bold text-foreground">{t('admin.companies.title')}</h1>
|
<h1 className="text-4xl font-extrabold tracking-tight">{t('admin.companies.title')}</h1>
|
||||||
<p className="text-muted-foreground mt-1">{t('admin.companies.subtitle')}</p>
|
<p className="text-muted-foreground text-lg">{t('admin.companies.subtitle')}</p>
|
||||||
</div>
|
</motion.div>
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="outline" onClick={() => loadCompanies()} disabled={loading}>
|
<Button variant="outline" size="lg" onClick={() => loadCompanies()} disabled={loading} className="shadow-sm">
|
||||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
||||||
{t('admin.companies.refresh')}
|
{t('admin.companies.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="gap-2" asChild>
|
<Button size="lg" className="gap-2 shadow-md hover:shadow-lg transition-all" asChild>
|
||||||
<Link href="/dashboard/companies/new">
|
<Link href="/dashboard/companies/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-5 w-5" />
|
||||||
{t('admin.companies.newCompany')}
|
{t('admin.companies.newCompany')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats Cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<Card>
|
<Card className="border-l-4 border-l-blue-500 shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<CardDescription>{t('admin.companies.stats.total')}</CardDescription>
|
<CardTitle className="text-sm font-medium">Total de Empresas</CardTitle>
|
||||||
<CardTitle className="text-3xl">{totalCompanies}</CardTitle>
|
<Building2 className="h-4 w-4 text-blue-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalCompanies}</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="border-l-4 border-l-green-500 shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<CardDescription>{t('admin.companies.stats.active')}</CardDescription>
|
<CardTitle className="text-sm font-medium">Ativas</CardTitle>
|
||||||
<CardTitle className="text-3xl">{companies.filter((c) => c.active).length}</CardTitle>
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{companies.filter(c => c.active).length}</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="border-l-4 border-l-indigo-500 shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<CardDescription>{t('admin.companies.stats.verified')}</CardDescription>
|
<CardTitle className="text-sm font-medium">Verificadas</CardTitle>
|
||||||
<CardTitle className="text-3xl">{companies.filter((c) => c.verified).length}</CardTitle>
|
<ShieldCheck className="h-4 w-4 text-indigo-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{companies.filter(c => c.verified).length}</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="border-l-4 border-l-amber-500 shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<CardDescription>{t('admin.companies.stats.pending')}</CardDescription>
|
<CardTitle className="text-sm font-medium">Pendentes</CardTitle>
|
||||||
<CardTitle className="text-3xl">{companies.filter((c) => !c.verified).length}</CardTitle>
|
<AlertCircle className="h-4 w-4 text-amber-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{companies.filter(c => !c.verified).length}</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Content Card */}
|
||||||
<Card>
|
<Card className="border-0 shadow-xl overflow-hidden bg-card/50 backdrop-blur-sm">
|
||||||
<CardHeader>
|
<CardHeader className="border-b bg-muted/30">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
|
@ -312,83 +270,81 @@ export default function AdminCompaniesPage() {
|
||||||
placeholder={t('admin.companies.searchPlaceholder')}
|
placeholder={t('admin.companies.searchPlaceholder')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="pl-10"
|
className="pl-10 h-11 border-muted-foreground/20 focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-0">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="space-y-2 py-4">
|
<div className="p-8 space-y-4">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-16 w-full rounded-lg" />)}
|
||||||
<div key={i} className="flex items-center space-x-4">
|
|
||||||
<Skeleton className="h-12 w-full" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader className="bg-muted/50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('admin.companies.table.company')}</TableHead>
|
<TableHead className="w-[300px] font-bold">Empresa</TableHead>
|
||||||
<TableHead>{t('admin.companies.table.email')}</TableHead>
|
<TableHead className="font-bold">E-mail</TableHead>
|
||||||
<TableHead>{t('admin.companies.table.status')}</TableHead>
|
<TableHead className="font-bold">Status</TableHead>
|
||||||
<TableHead>{t('admin.companies.table.verified')}</TableHead>
|
<TableHead className="font-bold">Verificado</TableHead>
|
||||||
<TableHead>{t('admin.companies.table.created')}</TableHead>
|
<TableHead className="font-bold">Cadastro</TableHead>
|
||||||
<TableHead className="text-right">{t('admin.companies.table.actions')}</TableHead>
|
<TableHead className="text-right font-bold">Ações</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredCompanies.length === 0 ? (
|
{filteredCompanies.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
<TableCell colSpan={6} className="text-center py-20 text-muted-foreground">
|
||||||
{t('admin.companies.table.empty')}
|
Nenhuma empresa encontrada
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredCompanies.map((company) => (
|
filteredCompanies.map((company) => (
|
||||||
<TableRow key={company.id}>
|
<TableRow key={company.id} className="hover:bg-muted/30 transition-colors">
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-semibold text-foreground">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
<div className="p-2 bg-primary/5 rounded-lg">
|
||||||
|
<Building2 className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
{company.name}
|
{company.name}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{company.email || "-"}</TableCell>
|
<TableCell className="text-muted-foreground">{company.email || "-"}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={company.active ? "default" : "secondary"}
|
variant={company.active ? "default" : "secondary"}
|
||||||
className="cursor-pointer hover:opacity-80"
|
className="cursor-pointer transition-all active:scale-95"
|
||||||
onClick={() => toggleStatus(company, 'active')}
|
onClick={() => toggleStatus(company, 'active')}
|
||||||
>
|
>
|
||||||
{company.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
|
{company.active ? "Ativo" : "Inativo"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer hover:opacity-80 inline-flex"
|
className="cursor-pointer hover:bg-muted p-1 rounded-full inline-flex transition-colors"
|
||||||
onClick={() => toggleStatus(company, 'verified')}
|
onClick={() => toggleStatus(company, 'verified')}
|
||||||
>
|
>
|
||||||
{company.verified ? (
|
{company.verified ? (
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<XCircle className="h-5 w-5 text-muted-foreground" />
|
<XCircle className="h-6 w-6 text-muted-foreground/40" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-muted-foreground font-medium">
|
||||||
{company.createdAt ? companyDateFormatter.format(new Date(company.createdAt)) : "-"}
|
{company.createdAt ? companyDateFormatter.format(new Date(company.createdAt)) : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleView(company)}>
|
<Button variant="ghost" size="icon" onClick={() => { setSelectedCompany(company); setIsViewDialogOpen(true); }} className="hover:text-primary">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleEditClick(company)}>
|
<Button variant="ghost" size="icon" onClick={() => handleEditClick(company)} className="hover:text-blue-600">
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="text-destructive hover:text-destructive" onClick={() => handleDelete(company)}>
|
<Button variant="ghost" size="icon" className="hover:text-destructive hover:bg-destructive/10" onClick={() => setCompanyToDelete(company)}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -398,291 +354,33 @@ export default function AdminCompaniesPage() {
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
{!loading && (
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground mt-4">
|
{/* Pagination */}
|
||||||
<span>
|
{!loading && totalPages > 1 && (
|
||||||
{totalCompanies === 0
|
<div className="p-4 border-t bg-muted/10 flex items-center justify-between">
|
||||||
? t('admin.companies.table.empty')
|
<p className="text-sm text-muted-foreground font-medium">
|
||||||
: t('admin.companies.table.showing', {
|
Mostrando <span className="text-foreground">{(page - 1) * limit + 1}</span> a <span className="text-foreground">{Math.min(page * limit, totalCompanies)}</span> de <span className="text-foreground">{totalCompanies}</span> empresas
|
||||||
from: (page - 1) * limit + 1,
|
</p>
|
||||||
to: Math.min(page * limit, totalCompanies),
|
<div className="flex gap-2">
|
||||||
total: totalCompanies
|
<Button variant="outline" size="sm" onClick={() => loadCompanies(page - 1)} disabled={page <= 1}>
|
||||||
})}
|
<ChevronLeft className="h-4 w-4 mr-1" /> Anterior
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => loadCompanies(page - 1)}
|
|
||||||
disabled={page <= 1 || loading}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
Previous
|
|
||||||
</Button>
|
</Button>
|
||||||
<span>
|
<Button variant="outline" size="sm" onClick={() => loadCompanies(page + 1)} disabled={page >= totalPages}>
|
||||||
Page {page} of {totalPages}
|
Próxima <ChevronRight className="h-4 w-4 ml-1" />
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => loadCompanies(page + 1)}
|
|
||||||
disabled={page >= totalPages || loading}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{/* View Company Modal */}
|
|
||||||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
|
||||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
<Building2 className="h-5 w-5" />
|
|
||||||
{selectedCompany?.name}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>{t('admin.companies.details.subtitle')}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{selectedCompany && (
|
|
||||||
<div className="space-y-6 py-4">
|
|
||||||
{/* Status Badges */}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Badge variant={selectedCompany.active ? "default" : "secondary"}>
|
|
||||||
{selectedCompany.active ? t('admin.companies.fields.active') : t('admin.companies.fields.inactive')}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant={selectedCompany.verified ? "default" : "outline"}>
|
|
||||||
{selectedCompany.verified ? t('admin.companies.stats.verified') : "Not Verified"}
|
|
||||||
</Badge>
|
|
||||||
{selectedCompany.type && (
|
|
||||||
<Badge variant="outline">{selectedCompany.type}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Basic Info */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.create.slug')}</Label>
|
|
||||||
<p className="font-mono text-sm">{selectedCompany.slug}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.email')}</Label>
|
|
||||||
<p className="text-sm">{selectedCompany.email || "-"}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.phone')}</Label>
|
|
||||||
<p className="text-sm">{selectedCompany.phone || "-"}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.website')}</Label>
|
|
||||||
<p className="text-sm">
|
|
||||||
{selectedCompany.website ? (
|
|
||||||
<a
|
|
||||||
href={selectedCompany.website}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{selectedCompany.website}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.document')}</Label>
|
|
||||||
<p className="text-sm font-mono">{selectedCompany.document || "-"}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.address')}</Label>
|
|
||||||
<p className="text-sm">{selectedCompany.address || "-"}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
{selectedCompany.description && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.description')}</Label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{formatDescription(selectedCompany.description)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Timestamps */}
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t">
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.createdAt')}</Label>
|
|
||||||
<p className="text-sm">
|
|
||||||
{selectedCompany.createdAt
|
|
||||||
? companyDateFormatter.format(new Date(selectedCompany.createdAt))
|
|
||||||
: "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground text-xs">{t('admin.companies.fields.updatedAt')}</Label>
|
|
||||||
<p className="text-sm">
|
|
||||||
{selectedCompany.updatedAt
|
|
||||||
? companyDateFormatter.format(new Date(selectedCompany.updatedAt))
|
|
||||||
: "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DialogFooter className="flex w-full justify-between sm:justify-between">
|
|
||||||
{selectedCompany && (
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => handleDelete(selectedCompany)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
{t('admin.companies.details.delete')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
|
|
||||||
{t('admin.companies.details.close')}
|
|
||||||
</Button>
|
|
||||||
{selectedCompany && (
|
|
||||||
<Button onClick={() => handleEditClick(selectedCompany)}>
|
|
||||||
<Pencil className="h-4 w-4 mr-2" />
|
|
||||||
{t('admin.companies.details.edit')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
|
||||||
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('admin.companies.edit.title')}</DialogTitle>
|
|
||||||
<DialogDescription>{t('admin.companies.edit.subtitle')}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="flex items-center gap-4 border p-4 rounded-md">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={editFormData.active}
|
|
||||||
onCheckedChange={(checked) => setEditFormData({ ...editFormData, active: checked })}
|
|
||||||
id="edit-active"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="edit-active">{t('admin.companies.fields.active')}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={editFormData.verified}
|
|
||||||
onCheckedChange={(checked) => setEditFormData({ ...editFormData, verified: checked })}
|
|
||||||
id="edit-verified"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="edit-verified">{t('admin.companies.stats.verified')}</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-name">{t('admin.companies.create.name')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-name"
|
|
||||||
value={editFormData.name}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-slug">{t('admin.companies.create.slug')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-slug"
|
|
||||||
value={editFormData.slug}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, slug: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-email">{t('admin.companies.create.email')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-email"
|
|
||||||
value={editFormData.email}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-phone">{t('admin.companies.fields.phone')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-phone"
|
|
||||||
value={editFormData.phone}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, phone: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-website">{t('admin.companies.fields.website')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-website"
|
|
||||||
value={editFormData.website}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, website: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-document">{t('admin.companies.fields.document')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-document"
|
|
||||||
value={editFormData.document}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, document: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-address">{t('admin.companies.fields.address')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-address"
|
|
||||||
value={editFormData.address}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, address: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-description">{t('admin.companies.fields.description')}</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-description"
|
|
||||||
value={editFormData.description}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, description: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-logoUrl">Logo URL</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-logoUrl"
|
|
||||||
value={editFormData.logoUrl}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, logoUrl: e.target.value })}
|
|
||||||
placeholder="https://example.com/logo.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-yearsInMarket">Anos no mercado</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-yearsInMarket"
|
|
||||||
value={editFormData.yearsInMarket}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, yearsInMarket: e.target.value })}
|
|
||||||
placeholder="Ex: 10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('admin.companies.create.cancel')}</Button>
|
|
||||||
<Button onClick={handleUpdate} disabled={updating}>
|
|
||||||
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
||||||
{t('admin.companies.edit.save')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!companyToDelete}
|
isOpen={!!companyToDelete}
|
||||||
onClose={() => setCompanyToDelete(null)}
|
onClose={() => setCompanyToDelete(null)}
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
title={companyToDelete ? t('admin.companies.deleteConfirm', { name: companyToDelete.name }) : ""}
|
title="Excluir Empresa"
|
||||||
description="This action cannot be undone."
|
description={`Tem certeza que deseja excluir ${companyToDelete?.name}? Esta ação não pode ser desfeita.`}
|
||||||
/>
|
/>
|
||||||
</div >
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue