style: padroniza layout da página de configurações com design premium
This commit is contained in:
parent
ffb055f6a0
commit
1226f92d42
1 changed files with 188 additions and 722 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue