diff --git a/frontend/src/app/dashboard/settings/page.tsx b/frontend/src/app/dashboard/settings/page.tsx index 68bc9df..969f499 100644 --- a/frontend/src/app/dashboard/settings/page.tsx +++ b/frontend/src/app/dashboard/settings/page.tsx @@ -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 = { - 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(null) + // Backoffice States + const [audits, setAudits] = useState([]) + const [companies, setCompanies] = useState([]) + const [tags, setTags] = useState([]) + const [creatingTag, setCreatingTag] = useState(false) + const [tagForm, setTagForm] = useState({ name: "", category: "area" as any }) + const [stats, setStats] = useState(null) + const [plans, setPlans] = useState([]) + const [isPlanDialogOpen, setIsPlanDialogOpen] = useState(false) + const [planForm, setPlanForm] = useState({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] }) + const [editingPlanId, setEditingPlanId] = useState(null) + const [deletePlanId, setDeletePlanId] = useState(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,768 +113,226 @@ 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 = { - 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({}) - - const isSelectedConfigured = selectedService ? configuredMap.has(selectedService) : false - - // State migrated from backoffice - const [roles, setRoles] = useState([]) - const [audits, setAudits] = useState([]) - const [companies, setCompanies] = useState([]) - const [jobs, setJobs] = useState([]) - const [tags, setTags] = useState([]) - const [creatingTag, setCreatingTag] = useState(false) - const [tagForm, setTagForm] = useState({ name: "", category: "area" as "area" | "level" | "stack" }) - const [stats, setStats] = useState(null) - const [plans, setPlans] = useState([]) - const [isPlanDialogOpen, setIsPlanDialogOpen] = useState(false) - const [planForm, setPlanForm] = useState({ name: "", description: "", monthlyPrice: 0, yearlyPrice: 0, features: [] }) - const [editingPlanId, setEditingPlanId] = useState(null) - const [deletePlanId, setDeletePlanId] = useState(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 { - await Promise.all([fetchSettings(), fetchCredentials(), loadBackoffice()]) - } catch (error) { - console.error("Error loading settings:", error) - } finally { - setLoading(false) - } + setLoading(true) + await Promise.all([fetchSettings(), fetchCredentials(), loadBackoffice()]) + setLoading(false) } init() }, []) - if (loading) { - return
- } + if (loading) return
return ( -
- {/* Header ... */} -
-

System Settings

-

Manage application appearance and integrations.

+
+
+ +

Configurações do Sistema

+

Gerencie a aparência, integrações e dados da plataforma

+
+
- - - - Branding & Theme - Integrations & Credentials - Backoffice + + + Branding & Tema + Integrações + Backoffice - {/* Theme Tab Content (unchanged) */} - - - - Branding & Theme - Customize the look and feel of your dashboard. + {/* Tab: Tema */} + + + + Identidade Visual + Personalize o nome, logo e cores do dashboard - +
- - setConfig({ ...config, companyName: e.target.value })} - /> + + setConfig({ ...config, companyName: e.target.value })} />
-
- +
- setConfig({ ...config, logoUrl: e.target.value })} - /> - {config.logoUrl && ( -
- Preview e.currentTarget.style.display = 'none'} /> -
- )} -
-

Enter a public URL for your logo.

-
- -
- -
- setConfig({ ...config, primaryColor: e.target.value })} - /> -
- Sample Button + setConfig({ ...config, logoUrl: e.target.value })} /> +
+ Logo
- -
- -
- - - - - - - External Services - Manage credentials for third-party integrations securely. Keys are encrypted before storage. - - -
- {servicesToRender.map((svc) => ( -
-
-
-

{schemas[svc.service_name]?.label || svc.service_name}

- {svc.is_configured ? ( - - Active - - ) : ( - Pending - )} -
-

- {svc.is_configured - ? `Updated ${new Date(svc.updated_at).toLocaleDateString()}` - : 'Not configured'} -

-
-
- {svc.is_configured && ( - - )} -
- - {svc.is_configured && ( - - )} -
-
+
+ +
+ setConfig({ ...config, primaryColor: e.target.value })} /> +
+ Preview de Botão
- ))} -
- - - - - - - - Dashboard - Plans - Stripe - System - - - - {/* Stats Overview */} - {stats && ( -
- - - Total Revenue - $ - - -
${stats.monthlyRevenue?.toLocaleString() || '0'}
-

{stats.revenueGrowth ? `+${stats.revenueGrowth}% from last month` : 'This month'}

-
-
- - - Active Subscriptions - - - -
{stats.activeSubscriptions || 0}
-

{stats.subscriptionGrowth ? `+${stats.subscriptionGrowth} this week` : 'Current active'}

-
-
- - - Companies -
- - -
{stats.totalCompanies || 0}
-

Platform total

-
- - - - New (Month) - - - -
+{stats.newCompaniesThisMonth || 0}
-

Since start of month

-
-
- )} - -
- - - Empresas pendentes - Aprovação e verificação de empresas. - - - - - - Empresa - Status - Ações - - - - {companies.slice(0, 5).map((company) => ( - - {company.name} - - {company.verified ? Verificada : Pendente} - - - - - - ))} - -
-
-
- - - Auditoria Recente - Últimos acessos. - - -
- {audits.slice(0, 5).map((audit) => ( -
-
-

{audit.identifier}

-

{auditDateFormatter.format(new Date(audit.createdAt))}

-
-
{audit.roles}
-
- ))} -
-
-
- - - -
-
- - - Plans Management - Configure subscription plans. + + +
+ + {/* Tab: Integrações */} + +
+ {servicesToRender.map((svc) => ( + + +
+ {schemas[svc.service_name]?.label || svc.service_name} + {svc.is_configured ? Ativo : Pendente} +
- - - - - Name - Monthly - Yearly - Actions + +

+ {svc.is_configured ? `Atualizado em ${new Date(svc.updated_at).toLocaleDateString()}` : "Serviço ainda não configurado para uso."} +

+
+ {svc.is_configured && ( + + )} +
+ + {svc.is_configured && } +
+
+
+ + ))} + + + + {/* Tab: Backoffice */} + + {/* Stats ... */} + {stats && ( +
+ + Receita Mensal +
${stats.monthlyRevenue?.toLocaleString()}
+
+ + Assinaturas Ativas +
{stats.activeSubscriptions}
+
+
+ )} + +
+ + + Verificações Pendentes + + +
+ + {companies.slice(0, 5).map(c => ( + + {c.name} + + + - + ))} + +
+
+
+ + + + Gerenciar Tags + + +
+ setTagForm({ ...tagForm, name: e.target.value })} /> + +
+
+ - {plans.map((plan) => ( - - {plan.name} - ${plan.monthlyPrice} - ${plan.yearlyPrice} - - - - + {tags.map(t => ( + + {t.name} + {t.active ? "Ativa" : "Inativa"} ))}
- - - - - - - {editingPlanId ? 'Edit Plan' : 'Create Plan'} - -
-
- - setPlanForm({ ...planForm, name: e.target.value })} /> -
-
- - setPlanForm({ ...planForm, description: e.target.value })} /> -
-
-
- - setPlanForm({ ...planForm, monthlyPrice: e.target.value })} /> -
-
- - setPlanForm({ ...planForm, yearlyPrice: e.target.value })} /> -
-
-
- -