From 8a906ba756e492d11ced33c86e3da32660ebeae4 Mon Sep 17 00:00:00 2001 From: byteful Date: Thu, 20 Aug 2026 18:52:06 -0500 Subject: [PATCH] perms fix --- client/src/components/layout/MobileNavbar.tsx | 12 +- client/src/components/layout/Sidebar.tsx | 10 +- .../components/settings/GeneralSettings.tsx | 226 ++-------- .../components/settings/StaffRolesCard.tsx | 114 +---- .../settings/TicketAiModerationSettings.tsx | 389 +++++++++++++++++ .../components/settings/TicketSettings.tsx | 385 +--------------- .../src/components/settings/UsageSettings.tsx | 23 +- client/src/hooks/data/server.ts | 17 + client/src/hooks/use-data.tsx | 1 + client/src/hooks/use-permissions.tsx | 120 +---- client/src/hooks/use-realtime.tsx | 6 +- client/src/lib/api.ts | 4 +- client/src/lib/http-error.test.ts | 29 ++ client/src/lib/http-error.ts | 22 + client/src/lib/permissions.ts | 53 +++ client/src/lib/proto-fetch.ts | 11 +- client/src/lib/queryClient.ts | 17 +- client/src/lib/settings-access.test.ts | 129 ++++-- client/src/lib/settings-access.ts | 106 +++-- client/src/pages/audit.tsx | 10 +- client/src/pages/settings.tsx | 411 +++--------------- package.json | 3 +- 22 files changed, 850 insertions(+), 1248 deletions(-) create mode 100644 client/src/components/settings/TicketAiModerationSettings.tsx create mode 100644 client/src/hooks/data/server.ts create mode 100644 client/src/lib/http-error.test.ts create mode 100644 client/src/lib/http-error.ts create mode 100644 client/src/lib/permissions.ts diff --git a/client/src/components/layout/MobileNavbar.tsx b/client/src/components/layout/MobileNavbar.tsx index 8b0af52..855d3a3 100644 --- a/client/src/components/layout/MobileNavbar.tsx +++ b/client/src/components/layout/MobileNavbar.tsx @@ -7,20 +7,12 @@ import { AlertCircle, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useIsFetching } from '@tanstack/react-query'; import { usePermissions, PERMISSIONS } from '@/hooks/use-permissions'; -import { useAuth } from '@/hooks/use-auth'; const MobileNavbar = () => { const { t } = useTranslation(); const [location, navigate] = useLocation(); - const { hasPermission } = usePermissions(); - const { user } = useAuth(); - - // Mirror Sidebar's loading-flash guard so permission-gated items don't appear - // before the user's permissions have loaded. - const fetchingCount = useIsFetching({ queryKey: ['userPermissions', user?.role] }); - const permissionsLoading = fetchingCount > 0; + const { hasPermission, isPermissionsLoading } = usePermissions(); const isActive = (path: string) => { return location === path @@ -65,7 +57,7 @@ const MobileNavbar = () => { // Filter nav items based on permissions, mirroring Sidebar's gating const navItems = allNavItems.filter(item => { if (!item.permission) return true; - if (permissionsLoading) return false; + if (isPermissionsLoading) return false; return hasPermission(item.permission); }); diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx index cdaa825..b363dff 100644 --- a/client/src/components/layout/Sidebar.tsx +++ b/client/src/components/layout/Sidebar.tsx @@ -24,9 +24,7 @@ import serverLogo from "../../assets/server-logo.png"; import { usePublicSettings } from "@/hooks/use-public-settings"; import { usePunishmentLookup } from "@/hooks/use-player-lookup"; import { usePermissions, PERMISSIONS } from "@/hooks/use-permissions"; -import { useAuth } from "@/hooks/use-auth"; import { usePlayerSearch } from "@/hooks/use-data"; -import { useIsFetching } from '@tanstack/react-query'; import { queryClient } from "@/lib/queryClient"; import { protoFetch } from "@/lib/proto-fetch"; import { toNum } from "@/lib/proto-ui"; @@ -61,11 +59,7 @@ const Sidebar = () => { const { openPlayerWindow: openPlayerWindowFromContext } = usePlayerWindow(); const [location, navigate] = useLocation(); const { data: publicSettings } = usePublicSettings(); - const { hasPermission } = usePermissions(); - const { user } = useAuth(); - - const fetchingCount = useIsFetching({ queryKey: ['userPermissions', user?.role] }); - const permissionsLoading = fetchingCount > 0; + const { hasPermission, isPermissionsLoading } = usePermissions(); const [isLookupOpen, setIsLookupOpen] = useState(false); const [isLookupClosing, setIsLookupClosing] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -355,7 +349,7 @@ const Sidebar = () => { // Don't show permission-based items while permissions are loading to prevent flash const navItems = allNavItems.filter(item => { if (!item.permission) return true; // Always show items without permission requirements - if (permissionsLoading) return false; // Hide permission-based items while loading + if (isPermissionsLoading) return false; // Hide permission-based items while loading return hasPermission(item.permission); }); diff --git a/client/src/components/settings/GeneralSettings.tsx b/client/src/components/settings/GeneralSettings.tsx index d4b8c1c..78e8ccf 100644 --- a/client/src/components/settings/GeneralSettings.tsx +++ b/client/src/components/settings/GeneralSettings.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { CreditCard, SettingsIcon, Globe, Key, Upload, Eye, EyeOff, Check, Copy, RefreshCw, Trash2, Plus, ChevronDown, ChevronRight, HardDrive, MessageCircle, Database, Info } from 'lucide-react'; +import { SettingsIcon, Globe, Key, Upload, Eye, EyeOff, Check, Copy, RefreshCw, Trash2, Plus, Info } from 'lucide-react'; import { Button } from '@modl-gg/shared-web/components/ui/button'; import { Input } from '@modl-gg/shared-web/components/ui/input'; import { Label } from '@modl-gg/shared-web/components/ui/label'; @@ -8,10 +8,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Tooltip, TooltipContent, TooltipTrigger } from '@modl-gg/shared-web/components/ui/tooltip'; import { Separator } from '@modl-gg/shared-web/components/ui/separator'; import { SUPPORTED_LANGUAGES } from '@/lib/languages'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@modl-gg/shared-web/components/ui/collapsible'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@modl-gg/shared-web/components/ui/alert-dialog'; -import { useAuth } from '@/hooks/use-auth'; -import { usePermissions } from '@/hooks/use-permissions'; +import { PERMISSIONS, usePermissions } from '@/hooks/use-permissions'; import BillingSettings from './BillingSettings'; import DomainSettings from './DomainSettings'; import UsageSettings from './UsageSettings'; @@ -51,20 +49,12 @@ interface GeneralSettingsProps { copyApiKey: () => void; maskApiKey: (key: string) => string; - // Billing and Usage Data - getBillingSummary: () => string; - getServerConfigSummary: () => string; - getDomainSummary: () => string; - // Webhook Settings webhookSettings?: WebhookSettingsData; - getWebhookSummary: () => string; handleWebhookSave: (settings: WebhookSettingsData) => Promise; savingWebhookSettings?: boolean; - // Optional prop to show only a specific section - // 'billing' | 'usage' | 'server-config' | 'domain' | 'webhooks' | 'migration' | undefined (show all) - visibleSection?: string; + visibleSection: string; } const GeneralSettings = ({ @@ -91,36 +81,19 @@ const GeneralSettings = ({ revealApiKey, copyApiKey, maskApiKey, - getBillingSummary, - getServerConfigSummary, - getDomainSummary, webhookSettings, - getWebhookSummary, handleWebhookSave, savingWebhookSettings, visibleSection }: GeneralSettingsProps) => { - const { user } = useAuth(); - const { hasPermission } = usePermissions(); + const { hasPermission, isSuperAdmin } = usePermissions(); const { t } = useTranslation(); - // When visibleSection is set, show content directly without collapsibles - const showDirectContent = !!visibleSection; - - // Collapsible state (only used when showing all sections) - const [isBillingExpanded, setIsBillingExpanded] = useState(false); - const [isUsageExpanded, setIsUsageExpanded] = useState(false); - const [isServerConfigExpanded, setIsServerConfigExpanded] = useState(false); - const [isDomainExpanded, setIsDomainExpanded] = useState(false); - const [isWebhookExpanded, setIsWebhookExpanded] = useState(false); - const [isMigrationExpanded, setIsMigrationExpanded] = useState(false); - useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const sessionId = urlParams.get('session_id'); if (sessionId) { - setIsBillingExpanded(true); toast({ title: t('settings.general.paymentSuccessful'), description: t('settings.general.subscriptionActivated'), @@ -379,171 +352,34 @@ const GeneralSettings = ({ ); - // When showing a specific section directly (without collapsible) - if (showDirectContent) { - return ( -
- {visibleSection === 'billing' && hasPermission('admin.settings.modify') && ( - - )} - - {visibleSection === 'usage' && ( - - )} - - {visibleSection === 'server-config' && serverConfigContent} - - {visibleSection === 'domain' && hasPermission('admin.settings.view.domain') && ( - - )} - - {visibleSection === 'webhooks' && hasPermission('admin.settings.view') && ( - - )} - - {visibleSection === 'migration' && user?.role === 'Super Admin' && ( - - )} -
- ); - } - - // When showing all sections with collapsibles return ( -
-
- {/* Billing Settings */} - {hasPermission('admin.settings.modify') && ( - - -
- -

{t('settings.general.billingSubscription')}

-
-
- {!isBillingExpanded && ( - {getBillingSummary()} - )} - {isBillingExpanded ? : } -
-
- - - -
- )} - - {/* Usage Section */} - - -
- -

{t('settings.general.usage')}

-
-
- {!isUsageExpanded && ( - {t('settings.general.storageFileManagement')} - )} - {isUsageExpanded ? : } -
-
- - - -
- - {/* Server Configuration */} - - -
- -

{t('settings.general.serverConfiguration')}

-
-
- {!isServerConfigExpanded && ( - {getServerConfigSummary()} - )} - {isServerConfigExpanded ? : } -
-
- - {serverConfigContent} - -
- - {/* Custom Domain Settings */} - {hasPermission('admin.settings.view.domain') && ( - - -
- -

{t('settings.general.customDomain')}

-
-
- {!isDomainExpanded && ( - {getDomainSummary()} - )} - {isDomainExpanded ? : } -
-
- - - -
- )} - - {/* Discord Webhook Settings */} - {hasPermission('admin.settings.view') && ( - - -
- -

{t('settings.general.discordWebhooks')}

-
-
- {!isWebhookExpanded && ( - {getWebhookSummary()} - )} - {isWebhookExpanded ? : } -
-
- - - -
- )} +
+ {visibleSection === 'billing' && isSuperAdmin && ( + + )} + + {visibleSection === 'usage' && ( + + )} + + {visibleSection === 'server-config' && isSuperAdmin && serverConfigContent} + + {visibleSection === 'domain' && hasPermission(PERMISSIONS.ADMIN_SETTINGS_VIEW_DOMAIN) && ( + + )} + + {visibleSection === 'webhooks' && hasPermission(PERMISSIONS.ADMIN_SETTINGS_VIEW) && ( + + )} - {/* Migration Tool - Super Admin Only */} - {user && user.role === "Super Admin" && ( - - -
- -

{t('settings.migration.title')}

-
-
- {!isMigrationExpanded && ( - {t('settings.migration.summary')} - )} - {isMigrationExpanded ? : } -
-
- - - -
- )} -
+ {visibleSection === 'migration' && isSuperAdmin && ( + + )}
); }; diff --git a/client/src/components/settings/StaffRolesCard.tsx b/client/src/components/settings/StaffRolesCard.tsx index a8244ae..1bc535b 100644 --- a/client/src/components/settings/StaffRolesCard.tsx +++ b/client/src/components/settings/StaffRolesCard.tsx @@ -45,98 +45,6 @@ const PERMISSION_CATEGORIES = { staff: 'staff' }; -const DEFAULT_PERMISSIONS: Permission[] = [ - // Admin permissions - { id: 'admin.settings.view', name: 'View Settings', description: 'View all system settings (includes all sub-permissions)', category: 'admin', parentId: null }, - { id: 'admin.settings.view.punishments', name: 'View Punishments Config', description: 'View punishment type configuration', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.view.content', name: 'View Content', description: 'View homepage cards, knowledgebase, media', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.view.domain', name: 'View Domain', description: 'View custom domain configuration', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.view.billing', name: 'View Billing', description: 'View billing, subscription, and payment info', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.view.migration', name: 'View Migration', description: 'View import/export data configuration', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.view.storage', name: 'View Storage', description: 'View storage configuration', category: 'admin', parentId: 'admin.settings.view' }, - { id: 'admin.settings.modify', name: 'Modify Settings', description: 'Full control over system settings (includes all sub-permissions)', category: 'admin', parentId: null }, - { id: 'admin.settings.modify.punishments', name: 'Modify Punishments Config', description: 'Create/edit/delete punishment types', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.settings.modify.content', name: 'Modify Content', description: 'Edit homepage cards, knowledgebase, media', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.settings.modify.domain', name: 'Modify Domain', description: 'Change custom domain configuration', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.settings.modify.billing', name: 'Modify Billing', description: 'Update subscription and payment methods', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.settings.modify.migration', name: 'Modify Migration', description: 'Import/export data between platforms', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.settings.modify.storage', name: 'Modify Storage', description: 'Configure storage backends and limits', category: 'admin', parentId: 'admin.settings.modify' }, - { id: 'admin.staff.manage', name: 'Manage Staff', description: 'Full staff management (includes all sub-permissions)', category: 'admin', parentId: null }, - { id: 'admin.staff.manage.members', name: 'Manage Members', description: 'Invite, remove, and reassign staff', category: 'admin', parentId: 'admin.staff.manage' }, - { id: 'admin.staff.manage.roles', name: 'Manage Roles', description: 'Create/edit/delete roles and permissions', category: 'admin', parentId: 'admin.staff.manage' }, - { id: 'admin.audit.view', name: 'View Audit', description: 'Full audit access (includes all sub-permissions)', category: 'admin', parentId: null }, - { id: 'admin.audit.view.dashboard', name: 'View Dashboard', description: 'View dashboard statistics', category: 'admin', parentId: 'admin.audit.view' }, - { id: 'admin.audit.view.analytics', name: 'View Analytics', description: 'View player and ticket analytics', category: 'admin', parentId: 'admin.audit.view' }, - { id: 'admin.audit.view.logs', name: 'View Logs', description: 'View audit trail of staff actions', category: 'admin', parentId: 'admin.audit.view' }, - - // Punishment permissions - { id: 'punishment.view', name: 'View Punishments', description: 'View player profiles, punishments, and linked accounts', category: 'punishment', parentId: null }, - { id: 'punishment.modify', name: 'Modify Punishments', description: 'Full control over existing punishments (includes all sub-permissions)', category: 'punishment', parentId: null }, - { id: 'punishment.modify.pardon', name: 'Pardon Punishments', description: 'Pardon punishments and clear associated points', category: 'punishment', parentId: 'punishment.modify' }, - { id: 'punishment.modify.duration', name: 'Modify Duration', description: 'Change punishment duration', category: 'punishment', parentId: 'punishment.modify' }, - { id: 'punishment.modify.note', name: 'Add Notes', description: 'Add staff notes to punishments', category: 'punishment', parentId: 'punishment.modify' }, - { id: 'punishment.modify.evidence', name: 'Manage Evidence', description: 'Add and view evidence on punishments', category: 'punishment', parentId: 'punishment.modify' }, - { id: 'punishment.modify.options', name: 'Toggle Options', description: 'Toggle alt-blocking and stat-wipe options', category: 'punishment', parentId: 'punishment.modify' }, - - // Ticket permissions - { id: 'ticket.view.all', name: 'View All Tickets', description: 'View all tickets (includes all sub-permissions)', category: 'ticket', parentId: null }, - { id: 'ticket.view.all.notes', name: 'View Staff Notes', description: 'View internal staff notes on tickets', category: 'ticket', parentId: 'ticket.view.all' }, - { id: 'ticket.reply.all', name: 'Reply to All Tickets', description: 'Reply to all ticket types (includes all sub-permissions)', category: 'ticket', parentId: null }, - { id: 'ticket.reply.all.notes', name: 'Add Staff Notes', description: 'Add staff-only internal notes', category: 'ticket', parentId: 'ticket.reply.all' }, - { id: 'ticket.close.all', name: 'Close/Reopen All Tickets', description: 'Close and reopen all ticket types (includes all sub-permissions)', category: 'ticket', parentId: null }, - { id: 'ticket.close.all.lock', name: 'Lock Tickets', description: 'Lock tickets to prevent further replies', category: 'ticket', parentId: 'ticket.close.all' }, - { id: 'ticket.manage', name: 'Manage Tickets', description: 'Advanced ticket management (includes all sub-permissions)', category: 'ticket', parentId: null }, - { id: 'ticket.manage.tags', name: 'Manage Tags', description: 'Add and remove tags from tickets', category: 'ticket', parentId: 'ticket.manage' }, - { id: 'ticket.manage.hide', name: 'Hide Tickets', description: 'Hide tickets from public view', category: 'ticket', parentId: 'ticket.manage' }, - { id: 'ticket.manage.subscribe', name: 'Manage Subscriptions', description: 'Manage ticket notification subscriptions', category: 'ticket', parentId: 'ticket.manage' }, - { id: 'ticket.delete.all', name: 'Delete Tickets', description: 'Delete tickets from the system', category: 'ticket', parentId: null }, - - // Staff tool permissions - { id: 'staff.chat.toggle', name: 'Toggle Chat', description: 'Toggle server chat on/off', category: 'staff', parentId: null }, - { id: 'staff.chat.clear', name: 'Clear Chat', description: 'Clear server chat', category: 'staff', parentId: null }, - { id: 'staff.chat.slow', name: 'Slow Chat', description: 'Set slow mode on server chat', category: 'staff', parentId: null }, - { id: 'staff.maintenance', name: 'Maintenance Mode', description: 'Toggle server maintenance mode', category: 'staff', parentId: null }, - { id: 'staff.modactions', name: 'Moderation Actions', description: 'Staff mode, vanish, freeze, and target players', category: 'staff', parentId: null }, - { id: 'staff.intercept', name: 'Intercept Chat', description: 'Intercept and view all network chat', category: 'staff', parentId: null }, - { id: 'staff.chatlogs', name: 'Chat Logs', description: 'View player chat history', category: 'staff', parentId: null }, - { id: 'staff.commandlogs', name: 'Command Logs', description: 'View player command history', category: 'staff', parentId: null }, -]; - -const DEFAULT_ROLES: StaffRole[] = [ - { - id: 'super-admin', - name: 'Super Admin', - description: 'Full access to all features and settings', - permissions: ['admin.settings.view', 'admin.settings.modify', 'admin.staff.manage', 'admin.audit.view', 'ticket.view.all', 'ticket.reply.all', 'ticket.close.all', 'ticket.delete.all'], - isDefault: true, - order: 0, - }, - { - id: 'admin', - name: 'Admin', - description: 'Administrative access with some restrictions', - permissions: ['admin.settings.view', 'admin.staff.manage', 'admin.audit.view', 'ticket.view.all', 'ticket.reply.all', 'ticket.close.all'], - isDefault: true, - order: 1, - }, - { - id: 'moderator', - name: 'Moderator', - description: 'Moderation permissions for punishments and tickets', - permissions: ['ticket.view.all', 'ticket.reply.all', 'ticket.close.all'], - isDefault: true, - order: 2, - }, - { - id: 'helper', - name: 'Helper', - description: 'Basic support permissions', - permissions: ['ticket.view.all', 'ticket.reply.all'], - isDefault: true, - order: 3, - }, -]; - // Get role order from the role object (lower order = higher authority) const getRoleOrder = (role: StaffRole): number => { return role.order ?? 999; // Default to high number if order is not set @@ -346,9 +254,7 @@ export default function StaffRolesCard() { const createRoleMutation = useCreateRole(); const updateRoleMutation = useUpdateRole(); const deleteRoleMutation = useDeleteRole(); - const { hasPermission: userHasPermission } = useUserPermissions(); - - const isSuperAdmin = currentUser?.role === 'Super Admin'; + const { hasPermission: userHasPermission, isSuperAdmin } = useUserPermissions(); // Check if the current user can grant a specific permission const canGrantPermission = (permissionId: string): boolean => { @@ -357,25 +263,21 @@ export default function StaffRolesCard() { }; const roles = rolesData?.roles || []; - const permissions = permissionsData?.permissions || []; - const permissionCatalog = permissions.length > 0 ? permissions : DEFAULT_PERMISSIONS; - - // If no roles are loaded from the database, use default roles as fallback - const effectiveRoles = roles.length > 0 ? roles : DEFAULT_ROLES; - + const permissionCatalog = permissionsData?.permissions || []; + // Create role order map from roles data (copying StaffManagementPanel approach) const roleOrderMap = new Map(); - if (effectiveRoles) { - effectiveRoles.forEach((role: StaffRole) => { + if (roles) { + roles.forEach((role: StaffRole) => { roleOrderMap.set(role.name, role.order ?? 999); }); } // Update local roles when server data changes useEffect(() => { - if (effectiveRoles.length > 0) { + if (roles.length > 0) { // Sort roles by order (lower order = higher authority) - const sortedRoles = [...effectiveRoles].sort((a, b) => { + const sortedRoles = [...roles].sort((a, b) => { const aOrder = a.order ?? 999; const bOrder = b.order ?? 999; return aOrder - bOrder; @@ -383,7 +285,7 @@ export default function StaffRolesCard() { setLocalRoles(sortedRoles); setOriginalRoles(sortedRoles); } - }, [effectiveRoles]); + }, [roles]); // Guard placed AFTER all hook declarations so the hook count never changes // between renders (Rules of Hooks). diff --git a/client/src/components/settings/TicketAiModerationSettings.tsx b/client/src/components/settings/TicketAiModerationSettings.tsx new file mode 100644 index 0000000..a70cb7b --- /dev/null +++ b/client/src/components/settings/TicketAiModerationSettings.tsx @@ -0,0 +1,389 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Crown, Plus } from 'lucide-react'; +import { Button } from '@modl-gg/shared-web/components/ui/button'; +import { Label } from '@modl-gg/shared-web/components/ui/label'; +import { Badge } from '@modl-gg/shared-web/components/ui/badge'; +import { Switch } from '@modl-gg/shared-web/components/ui/switch'; +import { Textarea } from '@modl-gg/shared-web/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@modl-gg/shared-web/components/ui/select'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@modl-gg/shared-web/components/ui/dialog'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@modl-gg/shared-web/components/ui/alert-dialog'; +import { StatusBanner } from '@modl-gg/shared-web/components/ui/status-banner'; +import type { PunishmentType } from '@modl-gg/proto/modl/v1/settings_pb.ts'; +import { useServerPremium } from '@/hooks/data/server'; + +export interface AIPunishmentConfig { + id: string; + name: string; + aiDescription: string; + enabled: boolean; +} + +export interface AIModerationSettings { + enableAIReview: boolean; + enableAutomatedActions: boolean; + aiPunishmentConfigs: Record; +} + +interface TicketAiModerationSettingsProps { + aiModerationSettings: AIModerationSettings; + setAiModerationSettings: (value: AIModerationSettings | ((prev: AIModerationSettings) => AIModerationSettings)) => void; + punishmentTypes: PunishmentType[]; +} + +const TicketAiModerationSettings = ({ + aiModerationSettings, + setAiModerationSettings, + punishmentTypes, +}: TicketAiModerationSettingsProps) => { + const { t } = useTranslation(); + const isPremium = useServerPremium(); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [editedConfig, setEditedConfig] = useState(null); + const [configPendingRemoval, setConfigPendingRemoval] = useState<{ id: string; name: string } | null>(null); + + if (isPremium === false) { + return ( + } + title={t('settings.tickets.premiumFeature')} + > + {t('settings.tickets.aiModerationPremiumDesc')} + + ); + } + + return ( +
+

+ {t('settings.tickets.aiModerationDesc')} +

+ +
+
+
+ +

{t('settings.tickets.enableAIModerationDesc')}

+
+ + setAiModerationSettings((prev) => ({ ...prev, enableAIReview: checked })) + } + /> +
+ + {aiModerationSettings.enableAIReview && ( + <> +
+
+ +

{t('settings.tickets.enableAutomatedActionsDesc')}

+
+ + setAiModerationSettings((prev) => ({ ...prev, enableAutomatedActions: checked })) + } + /> +
+ +
+ +

+ {t('settings.tickets.aiPunishmentTypesDesc')} +

+ + {aiModerationSettings.aiPunishmentConfigs && Object.keys(aiModerationSettings.aiPunishmentConfigs).length > 0 ? ( +
+ {Object.values(aiModerationSettings.aiPunishmentConfigs).map((config: AIPunishmentConfig) => ( +
+
+
+ { + setAiModerationSettings((prev) => ({ + ...prev, + aiPunishmentConfigs: { + ...prev.aiPunishmentConfigs, + [config.id]: { ...config, enabled: checked } + } + })); + }} + /> +
{config.name || 'Unknown Type'}
+
+

{config.aiDescription}

+
+
+ + +
+
+ ))} +
+ ) : ( +
+

{t('settings.tickets.noAIPunishmentTypes')}

+
+ )} + + +
+ + )} +
+ + {isAddDialogOpen && ( + { + if (selectedType.ordinal != null) { + const configKey = String(selectedType.ordinal); + setAiModerationSettings((prev) => ({ + ...prev, + aiPunishmentConfigs: { + ...prev.aiPunishmentConfigs, + [configKey]: { + id: configKey, + name: selectedType.name, + aiDescription, + enabled: true + } + } + })); + } + setIsAddDialogOpen(false); + }} + onClose={() => setIsAddDialogOpen(false)} + /> + )} + + { if (!open) setConfigPendingRemoval(null); }}> + + + {t('settings.tickets.removeAIPunishmentTitle')} + + {t('settings.tickets.removeAIPunishmentConfirm', { name: configPendingRemoval?.name })} + + + + {t('common.cancel')} + { + if (configPendingRemoval) { + setAiModerationSettings((prev) => { + const remaining = { ...prev.aiPunishmentConfigs }; + delete remaining[configPendingRemoval.id]; + return { ...prev, aiPunishmentConfigs: remaining }; + }); + } + setConfigPendingRemoval(null); + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {t('common.remove')} + + + + + + {editedConfig && ( + { + setAiModerationSettings((prev) => ({ + ...prev, + aiPunishmentConfigs: { + ...prev.aiPunishmentConfigs, + [editedConfig.id]: { + ...(prev.aiPunishmentConfigs[editedConfig.id] ?? editedConfig), + aiDescription + } + } + })); + setEditedConfig(null); + }} + onClose={() => setEditedConfig(null)} + /> + )} +
+ ); +}; + +interface AddAIPunishmentDialogProps { + punishmentTypes: PunishmentType[]; + existingConfigs: Record; + onEnable: (punishmentType: PunishmentType, aiDescription: string) => void; + onClose: () => void; +} + +const AddAIPunishmentDialog = ({ punishmentTypes, existingConfigs, onEnable, onClose }: AddAIPunishmentDialogProps) => { + const { t } = useTranslation(); + const [selectedTypeId, setSelectedTypeId] = useState(null); + const [description, setDescription] = useState(''); + const selectedType = selectedTypeId != null ? punishmentTypes.find(pt => pt.id === selectedTypeId) : undefined; + + return ( + { if (!open) onClose(); }}> + + + {t('settings.tickets.enableAIPunishmentType')} + + {selectedTypeId != null + ? (selectedType ? t('settings.tickets.configureAIDescFor', { name: selectedType.name }) : t('settings.tickets.configureAIDescSelected')) + : t('settings.tickets.selectPunishmentTypeForAI')} + + + +
+ {selectedType && ( +
+
+
+
{selectedType.name}
+
+ + {selectedType.category} + + + {t('settings.tickets.ordinal')}: {selectedType.ordinal} + +
+
+
+
+ )} + + {selectedTypeId == null && ( +
+ + +
+ )} + + {selectedTypeId != null && ( +
+ +