diff --git a/admin-spa/package-lock.json b/admin-spa/package-lock.json index f2f83a1..73cc019 100644 --- a/admin-spa/package-lock.json +++ b/admin-spa/package-lock.json @@ -8,6 +8,9 @@ "name": "woonoow-admin-spa", "version": "0.0.1", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", @@ -362,6 +365,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/admin-spa/package.json b/admin-spa/package.json index d849818..95fd154 100644 --- a/admin-spa/package.json +++ b/admin-spa/package.json @@ -10,6 +10,9 @@ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext ts,tsx --report-unused-disable-directives" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", diff --git a/admin-spa/src/components/settings/GenericGatewayForm.tsx b/admin-spa/src/components/settings/GenericGatewayForm.tsx index a6ddb85..a87c236 100644 --- a/admin-spa/src/components/settings/GenericGatewayForm.tsx +++ b/admin-spa/src/components/settings/GenericGatewayForm.tsx @@ -51,7 +51,8 @@ interface GenericGatewayFormProps { } // Supported field types (outside component to avoid re-renders) -const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url', 'account']; +// Note: WooCommerce BACS uses 'account' type for bank account repeater +const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url', 'account', 'title', 'multiselect']; // Bank account interface interface BankAccount { @@ -136,6 +137,20 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal } switch (field.type) { + case 'title': + // Title field is just a heading/separator + return ( +
+

{field.title}

+ {field.description && ( +

+ )} +

+ ); + case 'checkbox': // Skip "enabled" field - already controlled by toggle in main UI if (field.id === 'enabled') { @@ -229,7 +244,18 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal case 'account': // Bank account repeater field - const accounts = (value as BankAccount[]) || []; + // Parse value if it's a string (serialized PHP or JSON) + let accounts: BankAccount[] = []; + if (typeof value === 'string' && value) { + try { + accounts = JSON.parse(value); + } catch (e) { + // If not JSON, might be empty or invalid + accounts = []; + } + } else if (Array.isArray(value)) { + accounts = value; + } const addAccount = () => { const newAccounts = [...accounts, { diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx index 9104017..298a7d3 100644 --- a/admin-spa/src/routes/Settings/Payments.tsx +++ b/admin-spa/src/routes/Settings/Payments.tsx @@ -10,9 +10,12 @@ import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react'; +import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle, GripVertical } from 'lucide-react'; import { toast } from 'sonner'; import { useMediaQuery } from '@/hooks/use-media-query'; +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; +import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; interface GatewayField { id: string; @@ -52,13 +55,52 @@ interface PaymentGateway { wc_settings_url: string; } +// Sortable Gateway Item Component +function SortableGatewayItem({ gateway, children }: { gateway: PaymentGateway; children: React.ReactNode }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: gateway.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
+ +
+
+ {children} +
+
+ ); +} + export default function PaymentsPage() { const queryClient = useQueryClient(); const [selectedGateway, setSelectedGateway] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [togglingGateway, setTogglingGateway] = useState(null); + const [manualOrder, setManualOrder] = useState([]); + const [onlineOrder, setOnlineOrder] = useState([]); const isDesktop = useMediaQuery("(min-width: 768px)"); + // DnD sensors + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + // Fetch all payment gateways const { data: gateways = [], isLoading, refetch } = useQuery({ queryKey: ['payment-gateways'],