diff --git a/admin-spa/package-lock.json b/admin-spa/package-lock.json index 1e85edc..df989c8 100644 --- a/admin-spa/package-lock.json +++ b/admin-spa/package-lock.json @@ -8,6 +8,7 @@ "name": "woonoow-admin-spa", "version": "0.0.1", "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -1158,6 +1159,37 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -1238,6 +1270,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", diff --git a/admin-spa/package.json b/admin-spa/package.json index 79add07..6640648 100644 --- a/admin-spa/package.json +++ b/admin-spa/package.json @@ -10,6 +10,7 @@ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext ts,tsx --report-unused-disable-directives" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", diff --git a/admin-spa/src/components/settings/GenericGatewayForm.tsx b/admin-spa/src/components/settings/GenericGatewayForm.tsx index c85071c..de754b2 100644 --- a/admin-spa/src/components/settings/GenericGatewayForm.tsx +++ b/admin-spa/src/components/settings/GenericGatewayForm.tsx @@ -127,6 +127,11 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal switch (field.type) { case 'checkbox': + // Skip "enabled" field - already controlled by toggle in main UI + if (field.id === 'enabled') { + return null; + } + // WooCommerce uses "yes"/"no" strings, convert to boolean const isChecked = value === 'yes' || value === true; return ( diff --git a/admin-spa/src/components/ui/accordion.tsx b/admin-spa/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e6a723d --- /dev/null +++ b/admin-spa/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx index c0a8ba5..e4d6a92 100644 --- a/admin-spa/src/routes/Settings/Payments.tsx +++ b/admin-spa/src/routes/Settings/Payments.tsx @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react'; import { toast } from 'sonner'; @@ -115,6 +116,16 @@ export default function PaymentsPage() { const manualGateways = gateways.filter((g: PaymentGateway) => g.type === 'manual'); // Combine provider and other into single "3rd party" category const thirdPartyGateways = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other'); + + // Group third party gateways by provider (title) + const gatewaysByProvider = thirdPartyGateways.reduce((acc: Record, gateway: PaymentGateway) => { + const provider = gateway.title || 'Other'; + if (!acc[provider]) { + acc[provider] = []; + } + acc[provider].push(gateway); + return acc; + }, {}); if (isLoading) { return ( @@ -197,70 +208,87 @@ export default function PaymentsPage() { )} - {/* 3rd Party Payment Methods - All online payment gateways */} - {thirdPartyGateways.length > 0 && ( + {/* 3rd Party Payment Methods - Grouped by provider */} + {Object.keys(gatewaysByProvider).length > 0 && ( -
- {thirdPartyGateways.map((gateway: PaymentGateway) => ( -
-
-
+ + {(Object.entries(gatewaysByProvider) as [string, PaymentGateway[]][]).map(([provider, providerGateways]) => ( + + +
- +
-
-
-

{gateway.method_title || gateway.title}

- {gateway.enabled ? ( - - ● Enabled - - ) : ( - ○ Disabled - )} +
+

{provider}

+

+ {providerGateways.length} payment {providerGateways.length === 1 ? 'method' : 'methods'} +

+
+
+ + +
+ {providerGateways.map((gateway: PaymentGateway) => ( +
+
+
+
+ +
+
+
+

+ {gateway.method_title} +

+
+ {gateway.method_description && ( +

+ {gateway.method_description} +

+ )} + {!gateway.requirements.met && ( + + + + Requirements not met: {gateway.requirements.missing.join(', ')} + + + )} +
+
+
+ {gateway.enabled && ( + + )} + handleToggle(gateway.id, checked)} + disabled={!gateway.requirements.met || togglingGateway === gateway.id} + /> +
+
- {gateway.method_description && ( -

- {gateway.method_description} -

- )} - {!gateway.requirements.met && ( - - - - Requirements not met: {gateway.requirements.missing.join(', ')} - - - )} -
+ ))}
-
- - handleToggle(gateway.id, checked)} - disabled={!gateway.requirements.met || togglingGateway === gateway.id} - /> -
-
-
+ + ))} -
+ )} @@ -268,7 +296,7 @@ export default function PaymentsPage() { {/* Gateway Settings Modal */} {selectedGateway && ( - + {selectedGateway.title} Settings