import { useState, useMemo, useEffect, useCallback, memo } from 'react' import { Pencil, Plus, Trash2, GripVertical, ChevronDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { safeJsonParse } from '../utils/json-parser' type GroupRatioVisualEditorProps = { groupRatio: string topupGroupRatio: string userUsableGroups: string groupGroupRatio: string autoGroups: string onChange: (field: string, value: string) => void } type SimpleGroup = { name: string value: string } type GroupPricingRow = { _id: string name: string ratio: number selectable: boolean description: string } type GroupOverride = { targetGroup: string ratio: number } const sectionCardClassName = 'relative shadow-sm ring-0 before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:border before:border-border/90' const sectionHeaderClassName = 'border-b bg-muted/20' let groupPricingIdCounter = 0 function createGroupPricingId() { groupPricingIdCounter += 1 return `gpr_${groupPricingIdCounter}` } function normalizeRatio(value: unknown): number { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : 1 } function buildGroupPricingRows( groupRatio: string, userUsableGroups: string ): GroupPricingRow[] { const ratioMap = safeJsonParse>(groupRatio, { fallback: {}, context: 'group ratios', }) const usableMap = safeJsonParse>(userUsableGroups, { fallback: {}, context: 'user usable groups', }) const names = new Set([...Object.keys(ratioMap), ...Object.keys(usableMap)]) return Array.from(names).map((name) => ({ _id: createGroupPricingId(), name, ratio: normalizeRatio(ratioMap[name]), selectable: Object.prototype.hasOwnProperty.call(usableMap, name), description: String(usableMap[name] ?? ''), })) } function serializeGroupPricingRows(rows: GroupPricingRow[]) { const groupRatio: Record = {} const userUsableGroups: Record = {} for (const row of rows) { const name = row.name.trim() if (!name) continue groupRatio[name] = normalizeRatio(row.ratio) if (row.selectable) { userUsableGroups[name] = row.description } } return { GroupRatio: JSON.stringify(groupRatio, null, 2), UserUsableGroups: JSON.stringify(userUsableGroups, null, 2), } } function groupPricingSignature(rows: GroupPricingRow[]): string { const serialized = serializeGroupPricingRows(rows) return JSON.stringify({ groupRatio: safeJsonParse(serialized.GroupRatio, { fallback: {}, silent: true, }), userUsableGroups: safeJsonParse(serialized.UserUsableGroups, { fallback: {}, silent: true, }), }) } function sourceGroupPricingSignature( groupRatio: string, userUsableGroups: string ): string { return JSON.stringify({ groupRatio: safeJsonParse(groupRatio, { fallback: {}, silent: true }), userUsableGroups: safeJsonParse(userUsableGroups, { fallback: {}, silent: true, }), }) } export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ groupRatio, topupGroupRatio, userUsableGroups, groupGroupRatio, autoGroups, onChange, }: GroupRatioVisualEditorProps) { const { t } = useTranslation() const [simpleDialogOpen, setSimpleDialogOpen] = useState(false) const [simpleDialogType, setSimpleDialogType] = useState< 'groupRatio' | 'topupGroupRatio' | null >(null) const [simpleEditData, setSimpleEditData] = useState(null) const [autoGroupDialogOpen, setAutoGroupDialogOpen] = useState(false) const [autoGroupInput, setAutoGroupInput] = useState('') const [groupOverrideDialogOpen, setGroupOverrideDialogOpen] = useState(false) const [groupOverrideUserGroup, setGroupOverrideUserGroup] = useState< string | null >(null) const [groupOverrideEditData, setGroupOverrideEditData] = useState(null) const [userGroupDialogOpen, setUserGroupDialogOpen] = useState(false) const [userGroupInput, setUserGroupInput] = useState('') // Parse topup group ratios const topupRatioList = useMemo(() => { const map = safeJsonParse>(topupGroupRatio, { fallback: {}, context: 'topup group ratios', }) return Object.entries(map).map(([name, value]) => ({ name, value: String(value), })) }, [topupGroupRatio]) // Parse auto groups const autoGroupsList = useMemo(() => { return safeJsonParse(autoGroups, { fallback: [], context: 'auto groups', }) }, [autoGroups]) // Parse group-group ratios const groupGroupRatioList = useMemo(() => { const map = safeJsonParse>>( groupGroupRatio, { fallback: {}, context: 'group-group ratios', } ) return Object.entries(map).map(([userGroup, overrides]) => ({ userGroup, overrides: Object.entries(overrides).map(([targetGroup, ratio]) => ({ targetGroup, ratio, })), })) }, [groupGroupRatio]) // Simple group handlers (for groupRatio and topupGroupRatio) const handleSimpleAdd = (type: 'groupRatio' | 'topupGroupRatio') => { setSimpleDialogType(type) setSimpleEditData(null) setSimpleDialogOpen(true) } const handleSimpleEdit = ( type: 'groupRatio' | 'topupGroupRatio', group: SimpleGroup ) => { setSimpleDialogType(type) setSimpleEditData(group) setSimpleDialogOpen(true) } const handleSimpleSave = (name: string, value: string) => { if (!simpleDialogType) return const fieldName = simpleDialogType === 'groupRatio' ? groupRatio : topupGroupRatio const map = safeJsonParse>(fieldName, { fallback: {}, silent: true, }) if (simpleEditData && simpleEditData.name !== name) { delete map[simpleEditData.name] } map[name] = parseFloat(value) const field = simpleDialogType === 'groupRatio' ? 'GroupRatio' : 'TopupGroupRatio' onChange(field, JSON.stringify(map, null, 2)) setSimpleDialogOpen(false) } const handleSimpleDelete = ( type: 'groupRatio' | 'topupGroupRatio', name: string ) => { const fieldName = type === 'groupRatio' ? groupRatio : topupGroupRatio const map = safeJsonParse>(fieldName, { fallback: {}, silent: true, }) delete map[name] const field = type === 'groupRatio' ? 'GroupRatio' : 'TopupGroupRatio' onChange(field, JSON.stringify(map, null, 2)) } // Auto groups handlers const handleAutoGroupAdd = () => { setAutoGroupInput('') setAutoGroupDialogOpen(true) } const handleAutoGroupSave = () => { if (!autoGroupInput.trim()) return const list = [...autoGroupsList, autoGroupInput.trim()] onChange('AutoGroups', JSON.stringify(list, null, 2)) setAutoGroupDialogOpen(false) } const handleAutoGroupDelete = (index: number) => { const list = autoGroupsList.filter((_, i) => i !== index) onChange('AutoGroups', JSON.stringify(list, null, 2)) } const handleAutoGroupMove = (index: number, direction: 'up' | 'down') => { const list = [...autoGroupsList] const newIndex = direction === 'up' ? index - 1 : index + 1 if (newIndex < 0 || newIndex >= list.length) return ;[list[index], list[newIndex]] = [list[newIndex], list[index]] onChange('AutoGroups', JSON.stringify(list, null, 2)) } // Group-group ratio handlers const handleUserGroupAdd = () => { setUserGroupInput('') setUserGroupDialogOpen(true) } const handleUserGroupSave = () => { if (!userGroupInput.trim()) return const map = safeJsonParse>>( groupGroupRatio, { fallback: {}, silent: true, } ) if (!map[userGroupInput.trim()]) { map[userGroupInput.trim()] = {} } onChange('GroupGroupRatio', JSON.stringify(map, null, 2)) setUserGroupDialogOpen(false) } const handleUserGroupDelete = (userGroup: string) => { const map = safeJsonParse>>( groupGroupRatio, { fallback: {}, silent: true, } ) delete map[userGroup] onChange('GroupGroupRatio', JSON.stringify(map, null, 2)) } const handleOverrideAdd = (userGroup: string) => { setGroupOverrideUserGroup(userGroup) setGroupOverrideEditData(null) setGroupOverrideDialogOpen(true) } const handleOverrideEdit = (userGroup: string, override: GroupOverride) => { setGroupOverrideUserGroup(userGroup) setGroupOverrideEditData(override) setGroupOverrideDialogOpen(true) } const handleOverrideSave = ( targetGroup: string, ratio: number, oldTargetGroup?: string ) => { if (!groupOverrideUserGroup) return const map = safeJsonParse>>( groupGroupRatio, { fallback: {}, silent: true, } ) if (!map[groupOverrideUserGroup]) { map[groupOverrideUserGroup] = {} } if (oldTargetGroup && oldTargetGroup !== targetGroup) { delete map[groupOverrideUserGroup][oldTargetGroup] } map[groupOverrideUserGroup][targetGroup] = ratio onChange('GroupGroupRatio', JSON.stringify(map, null, 2)) setGroupOverrideDialogOpen(false) } const handleOverrideDelete = (userGroup: string, targetGroup: string) => { const map = safeJsonParse>>( groupGroupRatio, { fallback: {}, silent: true, } ) if (map[userGroup]) { delete map[userGroup][targetGroup] if (Object.keys(map[userGroup]).length === 0) { delete map[userGroup] } } onChange('GroupGroupRatio', JSON.stringify(map, null, 2)) } return (
{/* Topup Group Ratios */} {t('Top-up group ratios')} {t('Multipliers for recharge pricing based on user groups.')}
{topupRatioList.length > 0 && (
{t('Group name')} {t('Multiplier')} {t('Actions')} {topupRatioList.map((group) => ( {group.name} {group.value}
))}
)}
{/* Inter-group ratio overrides */} {t('Inter-group ratio overrides')} {t( 'Custom multipliers when specific user groups use specific token groups. Example: VIP users get 0.9x rate when using "edit_this" group tokens.' )}
{groupGroupRatioList.length > 0 && (
{groupGroupRatioList.map((userGroupData) => (
} > {userGroupData.userGroup} {t('{{count}} override', { count: userGroupData.overrides.length, })}
{userGroupData.overrides.length > 0 && (
{t('Target group')} {t('Ratio')} {t('Actions')} {userGroupData.overrides.map((override) => ( {override.targetGroup} {override.ratio}
))}
)}
))}
)}
{/* Auto Groups */} {t('Auto assignment order')} {t( 'Priority order for automatic group assignment. New tokens rotate through this list.' )}
{autoGroupsList.length > 0 && (
{autoGroupsList.map((group, index) => (
{group}
))}
)}
{/* Simple Group Dialog */} {/* Auto Group Dialog */} {t('Add auto group')} {t('Add a group identifier to the auto assignment list.')}
setAutoGroupInput(e.target.value)} placeholder={t('default')} />
{/* User Group Dialog */} {t('Add user group')} {t('Create a new user group to configure ratio overrides for.')}
setUserGroupInput(e.target.value)} placeholder={t('vip')} />
{/* Group Override Dialog */}
) }) type GroupPricingTableProps = { groupRatio: string userUsableGroups: string onChange: (field: string, value: string) => void } function GroupPricingTable({ groupRatio, userUsableGroups, onChange, }: GroupPricingTableProps) { const { t } = useTranslation() const [rows, setRows] = useState(() => buildGroupPricingRows(groupRatio, userUsableGroups) ) useEffect(() => { const incomingSignature = sourceGroupPricingSignature( groupRatio, userUsableGroups ) setRows((currentRows) => { if (groupPricingSignature(currentRows) === incomingSignature) { return currentRows } return buildGroupPricingRows(groupRatio, userUsableGroups) }) }, [groupRatio, userUsableGroups]) const emitRows = useCallback( (nextRows: GroupPricingRow[]) => { setRows(nextRows) const serialized = serializeGroupPricingRows(nextRows) onChange('GroupRatio', serialized.GroupRatio) onChange('UserUsableGroups', serialized.UserUsableGroups) }, [onChange] ) const updateRow = useCallback( ( id: string, field: Exclude, value: string | number | boolean ) => { emitRows( rows.map((row) => (row._id === id ? { ...row, [field]: value } : row)) ) }, [emitRows, rows] ) const addRow = useCallback(() => { const existingNames = new Set(rows.map((row) => row.name)) let index = 1 let name = `group_${index}` while (existingNames.has(name)) { index += 1 name = `group_${index}` } emitRows([ ...rows, { _id: createGroupPricingId(), name, ratio: 1, selectable: true, description: '', }, ]) }, [emitRows, rows]) const removeRow = useCallback( (id: string) => { emitRows(rows.filter((row) => row._id !== id)) }, [emitRows, rows] ) const duplicateNames = useMemo(() => { const counts = new Map() for (const row of rows) { const name = row.name.trim() if (!name) continue counts.set(name, (counts.get(name) ?? 0) + 1) } return Array.from(counts.entries()) .filter(([, count]) => count > 1) .map(([name]) => name) }, [rows]) return (
{t('Pricing groups')} {t('Edit billing ratios and user-selectable groups in one table.')}
{t('Group name')} {t('Ratio')} {t('User selectable')} {t('Description')} {t('Actions')} {rows.length === 0 ? ( {t('No groups yet. Add a group to get started.')} ) : ( rows.map((row) => ( updateRow(row._id, 'name', event.target.value) } aria-invalid={duplicateNames.includes( row.name.trim() )} /> updateRow( row._id, 'ratio', normalizeRatio(event.target.value) ) } />
updateRow( row._id, 'selectable', checked === true ) } aria-label={t('User selectable')} />
{row.selectable ? ( updateRow( row._id, 'description', event.target.value ) } /> ) : ( - )}
)) )}
{duplicateNames.length > 0 && (

{t('Duplicate group names: {{names}}', { names: duplicateNames.join(', '), })}

)}
) } // Simple Group Dialog Component type SimpleGroupDialogProps = { open: boolean onOpenChange: (open: boolean) => void onSave: (name: string, value: string) => void editData: SimpleGroup | null type: 'groupRatio' | 'topupGroupRatio' | null } function SimpleGroupDialog({ open, onOpenChange, onSave, editData, type, }: SimpleGroupDialogProps) { const { t } = useTranslation() const [name, setName] = useState('') const [value, setValue] = useState('') const title = type === 'groupRatio' ? t('group ratio') : t('top-up ratio') useEffect(() => { if (!open) { setName('') setValue('') return } setName(editData?.name ?? '') setValue(editData?.value ?? '') }, [editData, open]) const handleSave = () => { if (!name.trim() || !value.trim()) return onSave(name.trim(), value.trim()) setName('') setValue('') } return ( {editData ? t('Edit {{title}}', { title }) : t('Add {{title}}', { title })} {t('Configure the ratio for this group.')}
setName(e.target.value)} placeholder={t('default')} disabled={!!editData} />
{ const val = e.target.value if (val === '' || !isNaN(parseFloat(val))) { setValue(val) } }} placeholder='1.0' />
) } // Group Override Dialog Component type GroupOverrideDialogProps = { open: boolean onOpenChange: (open: boolean) => void onSave: (targetGroup: string, ratio: number, oldTargetGroup?: string) => void editData: GroupOverride | null userGroup: string | null } function GroupOverrideDialog({ open, onOpenChange, onSave, editData, userGroup, }: GroupOverrideDialogProps) { const { t } = useTranslation() const [targetGroup, setTargetGroup] = useState('') const [ratio, setRatio] = useState('') useEffect(() => { if (!open) { setTargetGroup('') setRatio('') return } setTargetGroup(editData?.targetGroup ?? '') setRatio(editData ? String(editData.ratio) : '') }, [editData, open]) const handleSave = () => { if (!targetGroup.trim() || !ratio.trim()) return const parsedRatio = parseFloat(ratio) if (isNaN(parsedRatio)) return onSave(targetGroup.trim(), parsedRatio, editData?.targetGroup) setTargetGroup('') setRatio('') } return ( {editData ? t('Edit ratio override') : t('Add ratio override')} {userGroup ? t( 'Configure a custom ratio for "{{userGroup}}" users when using a specific token group.', { userGroup } ) : t( 'Configure a custom ratio for when users use a specific token group.' )}
setTargetGroup(e.target.value)} placeholder={t('edit_this')} disabled={!!editData} />

{t('The token group that will have a custom ratio')}

{ const val = e.target.value if (val === '' || !isNaN(parseFloat(val))) { setRatio(val) } }} placeholder='0.9' />

{t('Multiplier applied when {{userGroup}} uses {{targetGroup}}', { userGroup: userGroup || t('this user group'), targetGroup: targetGroup || t('this token group'), })}

) }