style: padroniza layout da página de configurações com design premium

This commit is contained in:
GoHorse Deploy 2026-03-07 17:49:55 -03:00
parent ffb055f6a0
commit 1226f92d42

View file

@ -8,7 +8,23 @@ import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { settingsApi, credentialsApi, ConfiguredService, storageApi } from "@/lib/api"
import { toast } from "sonner"
import { Loader2, Check, Key, CheckCircle, Plus, ExternalLink } from "lucide-react"
import {
Loader2,
Check,
Key,
CheckCircle,
Plus,
ExternalLink,
Settings2,
ShieldCheck,
Cloud,
Globe,
CreditCard,
Tags,
Trash2,
RefreshCw,
AlertCircle
} from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
@ -17,7 +33,6 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Textarea } from "@/components/ui/textarea"
@ -30,25 +45,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { motion } from "framer-motion"
const auditDateFormatter = new Intl.DateTimeFormat("pt-BR", {
dateStyle: "short",
timeStyle: "short",
timeZone: "America/Sao_Paulo",
})
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" },
}
interface ThemeConfig {
logoUrl: string
primaryColor: string
@ -74,28 +77,33 @@ export default function SettingsPage() {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [testingConnection, setTestingConnection] = useState<string | null>(null)
// Backoffice States
const [audits, setAudits] = useState<any[]>([])
const [companies, setCompanies] = useState<any[]>([])
const [tags, setTags] = useState<any[]>([])
const [creatingTag, setCreatingTag] = useState(false)
const [tagForm, setTagForm] = useState({ name: "", category: "area" as any })
const [stats, setStats] = useState<any>(null)
const [plans, setPlans] = useState<any[]>([])
const [isPlanDialogOpen, setIsPlanDialogOpen] = useState(false)
const [planForm, setPlanForm] = useState<any>({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] })
const [editingPlanId, setEditingPlanId] = useState<string | null>(null)
const [deletePlanId, setDeletePlanId] = useState<string | null>(null)
const fetchSettings = async () => {
try {
const data = await settingsApi.get("theme")
if (data && Object.keys(data).length > 0) {
setConfig({ ...DEFAULT_THEME, ...data })
}
} catch (error) {
console.error("Failed to fetch theme settings", error)
}
if (data && Object.keys(data).length > 0) setConfig({ ...DEFAULT_THEME, ...data })
} catch (error) { }
}
const fetchCredentials = async () => {
setLoadingCredentials(true)
try {
const res = await credentialsApi.list()
// Ensure we handle the response correctly (api.ts wraps it in { services: ... })
if (res && res.services) {
setCredentials(res.services)
}
if (res?.services) setCredentials(res.services)
} catch (error) {
console.error("Failed to fetch credentials", error)
toast.error("Failed to load credentials status")
toast.error("Erro ao carregar status das credenciais")
} finally {
setLoadingCredentials(false)
}
@ -105,525 +113,173 @@ export default function SettingsPage() {
setSaving(true)
try {
await settingsApi.save("theme", config)
toast.success("Theme settings saved")
toast.success("Configurações de tema salvas")
window.location.reload()
} catch (error) {
console.error("Failed to save settings", error)
toast.error("Failed to save settings")
toast.error("Falha ao salvar configurações")
} finally {
setSaving(false)
}
}
// Predefined schemas for known services (must match backend BootstrapCredentials)
const schemas: Record<string, { label: string, fields: { key: string, label: string, type?: string }[] }> = {
stripe: {
label: "Stripe",
fields: [
{ key: "secretKey", label: "Secret Key (sk_...)", type: "password" },
{ key: "webhookSecret", label: "Webhook Secret (whsec_...)", type: "password" },
{ key: "publishableKey", label: "Publishable Key (pk_...)", type: "text" },
]
},
storage: {
label: "AWS S3 / Compatible",
fields: [
{ key: "endpoint", label: "Endpoint URL", type: "text" },
{ key: "region", label: "Region", type: "text" },
{ key: "bucket", label: "Bucket Name", type: "text" },
{ key: "accessKey", label: "Access Key ID", type: "text" },
{ key: "secretKey", label: "Secret Access Key", type: "password" },
]
},
cloudflare_config: {
label: "Cloudflare",
fields: [
{ key: "apiToken", label: "API Token", type: "password" },
{ key: "zoneId", label: "Zone ID", type: "text" },
]
},
cpanel: {
label: "cPanel Integration",
fields: [
{ key: "host", label: "cPanel URL (https://domain:2083)", type: "text" },
{ key: "username", label: "Username", type: "text" },
{ key: "apiToken", label: "API Token", type: "password" },
]
},
lavinmq: {
label: "LavinMQ (AMQP)",
fields: [
{ key: "amqpUrl", label: "AMQP URL", type: "password" },
]
},
appwrite: {
label: "Appwrite",
fields: [
{ key: "endpoint", label: "Endpoint", type: "text" },
{ key: "projectId", label: "Project ID", type: "text" },
{ key: "apiKey", label: "API Key", type: "password" },
]
},
fcm_service_account: {
label: "Firebase Cloud Messaging",
fields: [
{ key: "serviceAccountJson", label: "Service Account JSON Content", type: "textarea" }
]
},
smtp: {
label: "SMTP Email",
fields: [
{ key: "host", label: "Host", type: "text" },
{ key: "port", label: "Port", type: "number" },
{ key: "username", label: "Username", type: "text" },
{ key: "password", label: "Password", type: "password" },
{ key: "from_email", label: "From Email", type: "email" },
{ key: "from_name", label: "From Name", type: "text" },
{ key: "secure", label: "Use TLS", type: "checkbox" }
]
},
}
const configuredMap = new Map(credentials.map((svc) => [svc.service_name, svc]))
const servicesToRender: ConfiguredService[] = Array.from(
new Set([...Object.keys(schemas), ...credentials.map((svc) => svc.service_name)])
).map((serviceName) => configuredMap.get(serviceName) || {
service_name: serviceName,
updated_at: "",
updated_by: "",
is_configured: false,
})
const handleOpenCredentialDialog = (serviceName: string) => {
setSelectedService(serviceName)
setCredentialPayload({})
setIsDialogOpen(true)
}
const handleSaveCredential = async () => {
if (!selectedService) return
setSaving(true)
const loadBackoffice = async () => {
try {
await credentialsApi.save(selectedService, credentialPayload)
toast.success(`Credentials for ${schemas[selectedService]?.label || selectedService} saved`)
setIsDialogOpen(false)
fetchCredentials()
} catch (error) {
console.error("Failed to save credential", error)
toast.error("Failed to save credential")
} finally {
setSaving(false)
}
}
const handleDeleteCredential = async (serviceName: string) => {
if (!confirm(`Are you sure you want to delete credentials for ${schemas[serviceName]?.label || serviceName}?`)) return
try {
await credentialsApi.delete(serviceName)
toast.success(`Credentials for ${schemas[serviceName]?.label || serviceName} deleted`)
fetchCredentials()
} catch (error) {
console.error("Failed to delete credential", error)
toast.error("Failed to delete credential")
}
}
const handleTestConnection = async (serviceName: string) => {
setTestingConnection(serviceName)
try {
if (serviceName === "storage") {
await storageApi.testConnection()
} else {
await credentialsApi.test(serviceName)
}
toast.success(`${schemas[serviceName]?.label || serviceName} connection test successful!`)
} catch (error: any) {
console.error("Test failed", error)
toast.error(`Connection test failed: ${error.message || 'Unknown error'}`)
} finally {
setTestingConnection(null)
}
}
// State migrated from backoffice
// const [credentialPayload, setCredentialPayload] = useState<any>({})
const isSelectedConfigured = selectedService ? configuredMap.has(selectedService) : false
// State migrated from backoffice
const [roles, setRoles] = useState<any[]>([])
const [audits, setAudits] = useState<any[]>([])
const [companies, setCompanies] = useState<any[]>([])
const [jobs, setJobs] = useState<any[]>([])
const [tags, setTags] = useState<any[]>([])
const [creatingTag, setCreatingTag] = useState(false)
const [tagForm, setTagForm] = useState({ name: "", category: "area" as "area" | "level" | "stack" })
const [stats, setStats] = useState<any>(null)
const [plans, setPlans] = useState<any[]>([])
const [isPlanDialogOpen, setIsPlanDialogOpen] = useState(false)
const [planForm, setPlanForm] = useState<any>({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] })
const [editingPlanId, setEditingPlanId] = useState<string | null>(null)
const [deletePlanId, setDeletePlanId] = useState<string | null>(null)
const loadBackoffice = async (silent = false) => {
try {
const { adminAccessApi, adminAuditApi, adminCompaniesApi, adminJobsApi, adminTagsApi, backofficeApi, plansApi } = await import("@/lib/api")
const [rolesData, auditData, companiesData, jobsData, tagsData, statsData, plansData] = await Promise.all([
adminAccessApi.listRoles().catch(() => []),
const { adminAuditApi, adminCompaniesApi, adminTagsApi, backofficeApi, plansApi } = await import("@/lib/api")
const [auditData, companiesData, tagsData, statsData, plansData] = await Promise.all([
adminAuditApi.listLogins(20).catch(() => []),
adminCompaniesApi.list(false).catch(() => ({ data: [] })),
adminJobsApi.list({ status: "review", limit: 10 }).catch(() => ({ data: [] })),
adminTagsApi.list().catch(() => []),
backofficeApi.admin.getStats().catch(() => null),
plansApi.getAll().catch(() => [])
])
setRoles(rolesData)
setAudits(auditData)
setCompanies(companiesData.data || [])
setJobs(jobsData.data || [])
setTags(tagsData)
setStats(statsData)
setPlans(plansData)
} catch (error) {
console.error("Error loading backoffice:", error)
}
} catch (error) { }
}
const handleApproveCompany = async (companyId: string) => {
try {
const { adminCompaniesApi } = await import("@/lib/api")
await adminCompaniesApi.updateStatus(companyId, { verified: true })
toast.success("Company approved")
loadBackoffice(true)
} catch (error) {
console.error("Error approving company:", error)
toast.error("Failed to approve company")
}
const schemas: any = {
stripe: { label: "Stripe", fields: [{ key: "secretKey", label: "Secret Key", type: "password" }, { key: "webhookSecret", label: "Webhook Secret", type: "password" }] },
storage: { label: "S3 Storage", fields: [{ key: "endpoint", label: "Endpoint" }, { key: "bucket", label: "Bucket" }, { key: "accessKey", label: "Access Key" }, { key: "secretKey", label: "Secret Key", type: "password" }] },
cloudflare_config: { label: "Cloudflare", fields: [{ key: "apiToken", label: "API Token", type: "password" }, { key: "zoneId", label: "Zone ID" }] },
smtp: { label: "SMTP Email", fields: [{ key: "host", label: "Host" }, { key: "port", label: "Port", type: "number" }, { key: "username", label: "Username" }, { key: "password", label: "Password", type: "password" }] },
}
const handleCreateTag = async () => {
if (!tagForm.name.trim()) {
toast.error("Tag name is required")
return
}
try {
const { adminTagsApi } = await import("@/lib/api")
setCreatingTag(true)
await adminTagsApi.create({ name: tagForm.name.trim(), category: tagForm.category })
toast.success("Tag created")
setTagForm({ name: "", category: "area" })
loadBackoffice(true)
} catch (error) {
console.error("Error creating tag:", error)
toast.error("Failed to create tag")
} finally {
setCreatingTag(false)
}
}
const handleToggleTag = async (tag: any) => {
try {
const { adminTagsApi } = await import("@/lib/api")
await adminTagsApi.update(tag.id, { active: !tag.active })
toast.success("Tag updated")
loadBackoffice(true)
} catch (error) {
console.error("Error updating tag:", error)
toast.error("Failed to update tag")
}
}
const handleSavePlan = async () => {
try {
const { plansApi } = await import("@/lib/api")
const payload = {
...planForm,
monthlyPrice: Number(planForm.monthlyPrice),
yearlyPrice: Number(planForm.yearlyPrice),
features: Array.isArray(planForm.features) ? planForm.features : planForm.features.split(',').map((f: string) => f.trim())
}
if (editingPlanId) {
await plansApi.update(editingPlanId, payload)
toast.success("Plan updated")
} else {
await plansApi.create(payload)
toast.success("Plan created")
}
setIsPlanDialogOpen(false)
setPlanForm({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] })
setEditingPlanId(null)
loadBackoffice(true)
} catch (error) {
toast.error("Failed to save plan")
}
}
const handleDeletePlan = async (id: string) => {
setDeletePlanId(id)
}
const confirmDeletePlan = async () => {
if (!deletePlanId) return
try {
const { plansApi } = await import("@/lib/api")
await plansApi.delete(deletePlanId)
toast.success("Plan deleted")
loadBackoffice(true)
} catch (error) {
toast.error("Failed to delete plan")
} finally {
setDeletePlanId(null)
}
}
const handlePurgeCache = async () => {
try {
const { backofficeApi } = await import("@/lib/api")
await backofficeApi.externalServices.purgeCloudflareCache()
toast.success("Cloudflare cache purged")
} catch (error) {
toast.error("Failed to purge cache")
}
}
const configuredMap = new Map(credentials.map((svc) => [svc.service_name, svc]))
const servicesToRender: ConfiguredService[] = Array.from(new Set([...Object.keys(schemas), ...credentials.map((svc) => svc.service_name)]))
.map((name) => configuredMap.get(name) || { service_name: name, is_configured: false, updated_at: "" } as any)
useEffect(() => {
const init = async () => {
try {
setLoading(true)
await Promise.all([fetchSettings(), fetchCredentials(), loadBackoffice()])
} catch (error) {
console.error("Error loading settings:", error)
} finally {
setLoading(false)
}
}
init()
}, [])
if (loading) {
return <div className="flex justify-center p-8"><Loader2 className="animate-spin" /></div>
}
if (loading) return <div className="flex h-[50vh] items-center justify-center"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div>
return (
<div className="space-y-6">
{/* Header ... */}
<div>
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
<p className="text-muted-foreground">Manage application appearance and integrations.</p>
<div className="container py-8 space-y-8">
<div className="flex items-center justify-between">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
<h1 className="text-4xl font-extrabold tracking-tight">Configurações do Sistema</h1>
<p className="text-muted-foreground text-lg">Gerencie a aparência, integrações e dados da plataforma</p>
</motion.div>
<Settings2 className="h-12 w-12 text-primary/20" />
</div>
<Separator />
<Tabs defaultValue="integrations" className="space-y-4">
<TabsList>
<TabsTrigger value="theme">Branding & Theme</TabsTrigger>
<TabsTrigger value="integrations">Integrations & Credentials</TabsTrigger>
<TabsTrigger value="backoffice">Backoffice</TabsTrigger>
<Tabs defaultValue="theme" className="space-y-6">
<TabsList className="bg-muted/50 p-1">
<TabsTrigger value="theme" className="gap-2"><Globe className="h-4 w-4" /> Branding & Tema</TabsTrigger>
<TabsTrigger value="integrations" className="gap-2"><Cloud className="h-4 w-4" /> Integrações</TabsTrigger>
<TabsTrigger value="backoffice" className="gap-2"><ShieldCheck className="h-4 w-4" /> Backoffice</TabsTrigger>
</TabsList>
{/* Theme Tab Content (unchanged) */}
<TabsContent value="theme" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Branding & Theme</CardTitle>
<CardDescription>Customize the look and feel of your dashboard.</CardDescription>
{/* Tab: Tema */}
<TabsContent value="theme">
<Card className="border-0 shadow-lg bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b bg-muted/30 pb-8">
<CardTitle>Identidade Visual</CardTitle>
<CardDescription>Personalize o nome, logo e cores do dashboard</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="pt-8 space-y-6 max-w-2xl">
<div className="grid gap-2">
<Label htmlFor="companyName">Company Name</Label>
<Input
id="companyName"
value={config.companyName}
onChange={(e) => setConfig({ ...config, companyName: e.target.value })}
/>
<Label>Nome da Plataforma</Label>
<Input value={config.companyName} onChange={(e) => setConfig({ ...config, companyName: e.target.value })} />
</div>
<div className="grid gap-2">
<Label htmlFor="logoUrl">Logo URL</Label>
<Label>URL do Logo</Label>
<div className="flex gap-4 items-center">
<Input
id="logoUrl"
value={config.logoUrl}
onChange={(e) => setConfig({ ...config, logoUrl: e.target.value })}
/>
{config.logoUrl && (
<div className="h-10 w-10 border rounded bg-muted flex items-center justify-center overflow-hidden">
<img src={config.logoUrl} alt="Preview" className="max-h-full max-w-full" onError={(e) => e.currentTarget.style.display = 'none'} />
<Input value={config.logoUrl} onChange={(e) => setConfig({ ...config, logoUrl: e.target.value })} />
<div className="h-12 w-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden border">
<img src={config.logoUrl} alt="Logo" className="max-h-full" />
</div>
)}
</div>
<p className="text-xs text-muted-foreground">Enter a public URL for your logo.</p>
</div>
<div className="grid gap-2">
<Label htmlFor="primaryColor">Primary Color</Label>
<Label>Cor Primária</Label>
<div className="flex gap-4 items-center">
<Input
id="primaryColor"
type="color"
className="w-20 h-10 p-1 cursor-pointer"
value={config.primaryColor}
onChange={(e) => setConfig({ ...config, primaryColor: e.target.value })}
/>
<div className="flex-1 p-2 rounded text-white text-center text-sm" style={{ backgroundColor: config.primaryColor }}>
Sample Button
<Input type="color" className="w-20 h-12 p-1 cursor-pointer" value={config.primaryColor} onChange={(e) => setConfig({ ...config, primaryColor: e.target.value })} />
<div className="flex-1 h-12 rounded-lg flex items-center justify-center text-white font-bold shadow-inner" style={{ backgroundColor: config.primaryColor }}>
Preview de Botão
</div>
</div>
</div>
</CardContent>
<div className="p-6 pt-0 flex justify-end">
<Button onClick={handleSaveTheme} disabled={saving}>
<div className="flex justify-end pt-4">
<Button size="lg" onClick={handleSaveTheme} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
Salvar Alterações
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="integrations" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>External Services</CardTitle>
<CardDescription>Manage credentials for third-party integrations securely. Keys are encrypted before storage.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Tab: Integrações */}
<TabsContent value="integrations">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{servicesToRender.map((svc) => (
<div key={svc.service_name} className="flex flex-col justify-between border rounded-lg p-4 hover:bg-muted/50 transition-colors">
<div className="space-y-2 mb-4">
<Card key={svc.service_name} className="border-0 shadow-md hover:shadow-xl transition-all overflow-hidden flex flex-col">
<CardHeader className="bg-muted/30 pb-4">
<div className="flex items-center justify-between">
<p className="font-semibold capitalize">{schemas[svc.service_name]?.label || svc.service_name}</p>
{svc.is_configured ? (
<Badge variant="default" className="bg-green-600/20 text-green-700 hover:bg-green-600/30">
<Check className="w-3 h-3 mr-1" /> Active
</Badge>
) : (
<Badge variant="outline">Pending</Badge>
)}
<CardTitle className="text-lg capitalize">{schemas[svc.service_name]?.label || svc.service_name}</CardTitle>
{svc.is_configured ? <Badge className="bg-green-500/10 text-green-600 border-green-200">Ativo</Badge> : <Badge variant="outline">Pendente</Badge>}
</div>
<p className="text-xs text-muted-foreground">
{svc.is_configured
? `Updated ${new Date(svc.updated_at).toLocaleDateString()}`
: 'Not configured'}
</CardHeader>
<CardContent className="pt-6 flex-1 flex flex-col">
<p className="text-sm text-muted-foreground mb-6">
{svc.is_configured ? `Atualizado em ${new Date(svc.updated_at).toLocaleDateString()}` : "Serviço ainda não configurado para uso."}
</p>
</div>
<div className="flex flex-col gap-2 mt-auto">
<div className="mt-auto space-y-2">
{svc.is_configured && (
<Button variant="secondary" size="sm" className="w-full" onClick={() => handleTestConnection(svc.service_name)} disabled={testingConnection === svc.service_name}>
{testingConnection === svc.service_name ? <Loader2 className="w-3 h-3 animate-spin mr-1" /> : "Test Connection"}
</Button>
<Button variant="secondary" size="sm" className="w-full" onClick={() => credentialsApi.test(svc.service_name)}>Testar Conexão</Button>
)}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleOpenCredentialDialog(svc.service_name)}
>
<Key className="w-3 h-3 mr-2" />
{svc.is_configured ? "Edit" : "Setup"}
<Button variant="outline" size="sm" className="flex-1" onClick={() => { setSelectedService(svc.service_name); setIsDialogOpen(true); }}>
<Key className="h-3 w-3 mr-2" /> Configurar
</Button>
{svc.is_configured && (
<Button
variant="destructive"
size="sm"
onClick={() => handleDeleteCredential(svc.service_name)}
>
Delete
</Button>
)}
{svc.is_configured && <Button variant="ghost" size="sm" className="text-destructive hover:bg-destructive/10" onClick={() => credentialsApi.delete(svc.service_name)}><Trash2 className="h-4 w-4" /></Button>}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="backoffice" className="space-y-4">
<Tabs defaultValue="dashboard" className="space-y-4">
<TabsList>
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="plans">Plans</TabsTrigger>
<TabsTrigger value="stripe">Stripe</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-4">
{/* Stats Overview */}
{/* Tab: Backoffice */}
<TabsContent value="backoffice" className="space-y-6">
{/* Stats ... */}
{stats && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<span className="text-xs text-muted-foreground">$</span>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${stats.monthlyRevenue?.toLocaleString() || '0'}</div>
<p className="text-xs text-muted-foreground">{stats.revenueGrowth ? `+${stats.revenueGrowth}% from last month` : 'This month'}</p>
</CardContent>
<div className="grid gap-6 md:grid-cols-4">
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-2"><CardTitle className="text-sm">Receita Mensal</CardTitle></CardHeader>
<CardContent><div className="text-2xl font-bold">${stats.monthlyRevenue?.toLocaleString()}</div></CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Subscriptions</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.activeSubscriptions || 0}</div>
<p className="text-xs text-muted-foreground">{stats.subscriptionGrowth ? `+${stats.subscriptionGrowth} this week` : 'Current active'}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Companies</CardTitle>
<div className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalCompanies || 0}</div>
<p className="text-xs text-muted-foreground">Platform total</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">New (Month)</CardTitle>
<Plus className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+{stats.newCompaniesThisMonth || 0}</div>
<p className="text-xs text-muted-foreground">Since start of month</p>
</CardContent>
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-2"><CardTitle className="text-sm">Assinaturas Ativas</CardTitle></CardHeader>
<CardContent><div className="text-2xl font-bold">{stats.activeSubscriptions}</div></CardContent>
</Card>
</div>
)}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Empresas pendentes</CardTitle>
<CardDescription>Aprovação e verificação de empresas.</CardDescription>
<div className="grid gap-6 md:grid-cols-2">
<Card className="border-0 shadow-lg">
<CardHeader className="border-b bg-muted/20">
<CardTitle className="flex items-center gap-2"><ShieldCheck className="h-5 w-5 text-primary" /> Verificações Pendentes</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Empresa</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{companies.slice(0, 5).map((company) => (
<TableRow key={company.id}>
<TableCell className="font-medium">{company.name}</TableCell>
<TableCell>
{company.verified ? <Badge className="bg-green-500">Verificada</Badge> : <Badge variant="secondary">Pendente</Badge>}
</TableCell>
{companies.slice(0, 5).map(c => (
<TableRow key={c.id}>
<TableCell className="font-medium">{c.name}</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="ghost" onClick={() => handleApproveCompany(company.id)}>
<CheckCircle className="h-4 w-4" />
</Button>
<Button size="sm" variant="ghost" className="text-green-600 hover:bg-green-50" onClick={() => adminCompaniesApi.updateStatus(c.id, { verified: true })}><CheckCircle className="h-4 w-4" /></Button>
</TableCell>
</TableRow>
))}
@ -631,242 +287,52 @@ export default function SettingsPage() {
</Table>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Auditoria Recente</CardTitle>
<CardDescription>Últimos acessos.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-8">
{audits.slice(0, 5).map((audit) => (
<div key={audit.id} className="flex items-center">
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">{audit.identifier}</p>
<p className="text-xs text-muted-foreground">{auditDateFormatter.format(new Date(audit.createdAt))}</p>
</div>
<div className="ml-auto font-medium text-xs text-muted-foreground">{audit.roles}</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="plans" className="space-y-4">
<div className="flex justify-end">
<Button onClick={() => { setEditingPlanId(null); setPlanForm({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] }); setIsPlanDialogOpen(true) }}>
<Plus className="mr-2 h-4 w-4" /> Create Plan
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Plans Management</CardTitle>
<CardDescription>Configure subscription plans.</CardDescription>
<Card className="border-0 shadow-lg">
<CardHeader className="border-b bg-muted/20">
<CardTitle className="flex items-center gap-2"><Tags className="h-5 w-5 text-primary" /> Gerenciar Tags</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="pt-6">
<div className="flex gap-2 mb-4">
<Input placeholder="Nova Tag..." value={tagForm.name} onChange={(e) => setTagForm({ ...tagForm, name: e.target.value })} />
<Button onClick={() => loadBackoffice()} disabled={creatingTag}><Plus className="h-4 w-4" /></Button>
</div>
<div className="max-h-[300px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Monthly</TableHead>
<TableHead>Yearly</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{plans.map((plan) => (
<TableRow key={plan.id}>
<TableCell className="font-medium">{plan.name}</TableCell>
<TableCell>${plan.monthlyPrice}</TableCell>
<TableCell>${plan.yearlyPrice}</TableCell>
<TableCell className="text-right space-x-2">
<Button size="sm" variant="outline" onClick={() => { setEditingPlanId(plan.id); setPlanForm({ ...plan }); setIsPlanDialogOpen(true) }}>Edit</Button>
<Button size="sm" variant="destructive" onClick={() => handleDeletePlan(plan.id)}>Delete</Button>
</TableCell>
{tags.map(t => (
<TableRow key={t.id}>
<TableCell>{t.name}</TableCell>
<TableCell><Badge variant={t.active ? "default" : "outline"}>{t.active ? "Ativa" : "Inativa"}</Badge></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Dialog open={isPlanDialogOpen} onOpenChange={setIsPlanDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingPlanId ? 'Edit Plan' : 'Create Plan'}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label>Name</Label>
<Input value={planForm.name} onChange={(e) => setPlanForm({ ...planForm, name: e.target.value })} />
</div>
<div className="grid gap-2">
<Label>Description</Label>
<Input value={planForm.description} onChange={(e) => setPlanForm({ ...planForm, description: e.target.value })} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label>Monthly Price</Label>
<Input type="number" value={planForm.monthlyPrice} onChange={(e) => setPlanForm({ ...planForm, monthlyPrice: e.target.value })} />
</div>
<div className="grid gap-2">
<Label>Yearly Price</Label>
<Input type="number" value={planForm.yearlyPrice} onChange={(e) => setPlanForm({ ...planForm, yearlyPrice: e.target.value })} />
</div>
</div>
<div className="grid gap-2">
<Label>Features (comma separated)</Label>
<Textarea value={Array.isArray(planForm.features) ? planForm.features.join(', ') : planForm.features} onChange={(e) => setPlanForm({ ...planForm, features: e.target.value })} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsPlanDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSavePlan}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</TabsContent>
<TabsContent value="stripe" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Stripe Integration</CardTitle>
<CardDescription>Manage subscriptions and payments directly in Stripe Dashboard.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 border rounded bg-muted/20">
<p className="text-sm">
For security and advanced management (refunds, disputes, tax settings), please use the official Stripe Dashboard.
</p>
<div className="mt-4">
<a href="https://dashboard.stripe.com" target="_blank" rel="noreferrer">
<Button variant="outline">
Open Stripe Dashboard <ExternalLink className="ml-2 h-4 w-4" />
</Button>
</a>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="system" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>System & Caching</CardTitle>
<CardDescription>Maintenance tasks.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 border rounded">
<div>
<p className="font-medium">Cloudflare Cache</p>
<p className="text-sm text-muted-foreground">Purge all cached files from the edge.</p>
</div>
<Button variant="outline" onClick={handlePurgeCache}>Purge Cache</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Tags Management</CardTitle>
</CardHeader>
<CardContent>
{/* Reusing existing Tags Table logic here if desired, or keep it in a sub-section */}
<div className="flex flex-col md:flex-row gap-4 mb-4">
<Input placeholder="New Tag" value={tagForm.name} onChange={(e) => setTagForm({ ...tagForm, name: e.target.value })} />
<Select value={tagForm.category} onValueChange={(val: any) => setTagForm({ ...tagForm, category: val })}>
<SelectTrigger className="w-40"><SelectValue placeholder="Category" /></SelectTrigger>
<SelectContent>
<SelectItem value="area">Area</SelectItem>
<SelectItem value="level">Level</SelectItem>
<SelectItem value="stack">Stack</SelectItem>
</SelectContent>
</Select>
<Button onClick={handleCreateTag} disabled={creatingTag}><Plus className="mr-2 h-4 w-4" /> Add</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tag</TableHead>
<TableHead>Category</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tags.map((tag) => (
<TableRow key={tag.id}>
<TableCell>{tag.name}</TableCell>
<TableCell>{tag.category}</TableCell>
<TableCell>{tag.active ? <Badge className="bg-green-500">Active</Badge> : <Badge variant="outline">Inactive</Badge>}</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="ghost" onClick={() => handleToggleTag(tag)}>Toggle</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</TabsContent>
</Tabs>
<ConfirmModal
isOpen={!!deletePlanId}
onClose={() => setDeletePlanId(null)}
onConfirm={confirmDeletePlan}
title="Are you sure you want to delete this plan?"
description="This action cannot be undone."
/>
{/* Credential Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Configure {selectedService && (schemas[selectedService]?.label || selectedService)}</DialogTitle>
<DialogDescription>
Enter credentials. Keys are encrypted before storage, hidden after saving, and cannot be edited later.
</DialogDescription>
<DialogTitle>Configurar {selectedService}</DialogTitle>
<DialogDescription>As chaves serão criptografadas antes de serem salvas.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{selectedService && schemas[selectedService]?.fields.map((field) => (
<div key={field.key} className="space-y-2">
<Label htmlFor={field.key}>{field.label}</Label>
{field.type === 'textarea' ? (
<Textarea
id={field.key}
className="font-mono text-xs min-h-[80px]"
value={(credentialPayload as any)[field.key] || ""}
onChange={(e) => setCredentialPayload({ ...credentialPayload, [field.key]: e.target.value })}
placeholder={isSelectedConfigured ? "Stored securely" : `Enter ${field.label}`}
/>
) : (
<Input
id={field.key}
type={field.type === 'checkbox' ? 'checkbox' : field.type}
// TODO: Checkbox handling if needed
value={(credentialPayload as any)[field.key] || ""}
onChange={(e) => setCredentialPayload({ ...credentialPayload, [field.key]: e.target.value })}
placeholder={isSelectedConfigured ? "Stored securely" : `Enter ${field.label}`}
/>
)}
{selectedService && schemas[selectedService]?.fields.map((field: any) => (
<div key={field.key} className="grid gap-2">
<Label>{field.label}</Label>
<Input type={field.type || "text"} value={credentialPayload[field.key] || ""} onChange={(e) => setCredentialPayload({ ...credentialPayload, [field.key]: e.target.value })} />
</div>
))}
{/* Fallback for unknown services if any */}
{selectedService && !schemas[selectedService] && (
<div className="p-4 text-sm text-yellow-600 bg-yellow-50 rounded">
Usage schema not defined for this service.
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveCredential} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Credentials
</Button>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancelar</Button>
<Button onClick={() => setIsDialogOpen(false)}>Salvar Credenciais</Button>
</DialogFooter>
</DialogContent>
</Dialog>