/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { Lightbulb, Pencil, Plus, Search, Trash2 } from 'lucide-react' import { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StaticDataTable } from '@/components/data-table/static/static-data-table' import { StaticRowActions } from '@/components/data-table/static/static-row-actions' import { ReactIconByName } from '@/components/react-icon-by-name' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { safeJsonParseWithValidation } from '../utils/json-parser' import { isArray } from '../utils/json-validators' import { PaymentMethodDialog, type PaymentMethodData, } from './payment-method-dialog' type PaymentMethodsVisualEditorProps = { value: string onChange: (value: string) => void } const PAYMENT_TYPE_ICON_NAMES: Record = { alipay: 'SiAlipay', stripe: 'SiStripe', waffo_pancake: 'LuCreditCard', wxpay: 'SiWechat', } function getDefaultIconName(type: string) { return PAYMENT_TYPE_ICON_NAMES[type] ?? '' } function getEffectiveIconName(method: PaymentMethodData) { return method.icon || getDefaultIconName(method.type) } export function PaymentMethodsVisualEditor({ value, onChange, }: PaymentMethodsVisualEditorProps) { const { t } = useTranslation() const paymentTemplates = [ { name: t('Epay Alipay'), template: { icon: getDefaultIconName('alipay'), name: '支付宝', type: 'alipay', }, }, { name: t('Epay WeChat Pay'), template: { icon: getDefaultIconName('wxpay'), name: '微信', type: 'wxpay', }, }, { name: t('Stripe'), template: { icon: getDefaultIconName('stripe'), min_topup: '10', name: 'Stripe', type: 'stripe', }, }, { name: 'Waffo Pancake', template: { icon: getDefaultIconName('waffo_pancake'), name: 'Waffo Pancake', type: 'waffo_pancake', }, }, { name: t('Custom Epay method'), template: { icon: 'LuCreditCard', min_topup: '50', name: '自定义1', type: 'custom1', }, }, ] const [searchText, setSearchText] = useState('') const [dialogOpen, setDialogOpen] = useState(false) const [editData, setEditData] = useState(null) const paymentMethods = useMemo(() => { const parsed = safeJsonParseWithValidation(value, { fallback: [], validator: isArray, validatorMessage: 'Payment methods must be a JSON array', context: 'payment methods', }) return parsed.filter( (item): item is PaymentMethodData => typeof item === 'object' && item !== null && 'name' in item && 'type' in item && typeof item.name === 'string' && typeof item.type === 'string' && (!('icon' in item) || typeof item.icon === 'string') && (!('min_topup' in item) || typeof item.min_topup === 'string') && (!('color' in item) || typeof item.color === 'string') ) }, [value]) const filteredMethods = useMemo(() => { if (!searchText) return paymentMethods const lowerSearch = searchText.toLowerCase() return paymentMethods.filter( (method) => method.name.toLowerCase().includes(lowerSearch) || method.type.toLowerCase().includes(lowerSearch) || getEffectiveIconName(method).toLowerCase().includes(lowerSearch) ) }, [paymentMethods, searchText]) const handleSave = (data: PaymentMethodData) => { const parsed = safeJsonParseWithValidation(value, { fallback: [], validator: isArray, silent: true, }) const updatedArray = [...parsed] if (editData) { const index = updatedArray.findIndex( (item): item is PaymentMethodData => typeof item === 'object' && item !== null && 'name' in item && 'type' in item && item.name === editData.name && item.type === editData.type ) if (index !== -1) { updatedArray[index] = data } else { updatedArray.push(data) } } else { updatedArray.push(data) } onChange(JSON.stringify(updatedArray, null, 2)) } const handleDelete = (method: PaymentMethodData) => { const parsed = safeJsonParseWithValidation(value, { fallback: [], validator: isArray, silent: true, }) const updatedArray = parsed.filter( (item) => !( typeof item === 'object' && item !== null && 'name' in item && 'type' in item && item.name === method.name && item.type === method.type ) ) onChange(JSON.stringify(updatedArray, null, 2)) } const handleEdit = (method: PaymentMethodData) => { setEditData(method) setDialogOpen(true) } const handleAdd = () => { setEditData(null) setDialogOpen(true) } const handleInsertTemplate = (template: PaymentMethodData) => { const parsed = safeJsonParseWithValidation(value, { fallback: [], validator: isArray, silent: true, }) // Check if template already exists const exists = parsed.some( (item) => typeof item === 'object' && item !== null && 'type' in item && 'name' in item && item.type === template.type && item.name === template.name ) if (!exists) { parsed.push(template) onChange(JSON.stringify(parsed, null, 2)) } } return (
setSearchText(e.target.value)} className='pl-9' />
} > {t('Templates')}

{t('Quick insert payment entries')}

{paymentTemplates.map((item) => ( ))}
{filteredMethods.length === 0 ? (
{searchText ? t('No payment methods match your search') : t( 'No payment methods configured. Click "Add method" or use templates to get started.' )}
) : (
{/* Desktop table view */} `${method.type}-${index}`} columns={[ { id: 'name', header: t('Name'), cellClassName: 'font-medium', cell: (method) => method.name, }, { id: 'type', header: t('Payment type key'), cell: (method) => ( {method.type} ), }, { id: 'icon', header: t('Icon'), cell: (method) => { const iconName = getEffectiveIconName(method) return iconName ? (
{iconName}
) : ( ) }, }, { id: 'min-top-up', header: t('Min Top-up'), cell: (method) => method.min_topup ? ( {method.min_topup} ) : ( ), }, { id: 'actions', header: t('Actions'), className: 'text-right', cellClassName: 'text-right', cell: (method) => ( handleEdit(method)} onDelete={() => handleDelete(method)} /> ), }, ]} /> {/* Mobile card view */}
{filteredMethods.map((method) => { const iconName = getEffectiveIconName(method) const methodKey = [ method.type, method.name, method.icon, method.min_topup, method.color, ] .filter(Boolean) .join('-') return (
{method.name}
{method.type}
{t('Icon')} {iconName ? (
{iconName}
) : ( )}
{method.min_topup && (
{t('Min Top-up:')} {method.min_topup}
)}
) })}
)}
) }