feat: add React metabox island for coupon editor
- Create CouponMetabox React component with WPCFTO design system - Add MetaboxLayout with vertical tabs (Rules, Restrictions) - Implement Rules tab: active toggle, type radio, amount fields, multi-currency support - Implement Restrictions tab: usage limit, date limit, autocomplete for forms/products/customers - Add metabox registration in Coupon.php for formipay-coupon post type - Update ReactAdmin to load assets on post.php edit screens - Add autocomplete AJAX handler for relation fields - Disable old WPCFTO metabox in favor of React island
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '83e1a34ae6a628833355');
|
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => 'f1f91f1cc72d44a0469e');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,8 +19,11 @@ class ReactAdmin {
|
|||||||
|
|
||||||
$screen = get_current_screen();
|
$screen = get_current_screen();
|
||||||
|
|
||||||
// Only load React assets on Formipay admin pages
|
// Load React assets on Formipay admin pages OR on post edit screens for our CPTs
|
||||||
if ( strpos( $screen->id, 'formipay' ) === false ) {
|
$is_formipay_admin = strpos($screen->id, 'formipay') !== false;
|
||||||
|
$is_formipay_cpt = $screen->base === 'post' && in_array($screen->post_type, ['formipay-coupon', 'formipay-product', 'formipay-form']);
|
||||||
|
|
||||||
|
if (!$is_formipay_admin && !$is_formipay_cpt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ class ReactAdmin {
|
|||||||
$build_dir = FORMIPAY_PATH . 'build';
|
$build_dir = FORMIPAY_PATH . 'build';
|
||||||
$build_url = FORMIPAY_URL . 'build';
|
$build_url = FORMIPAY_URL . 'build';
|
||||||
|
|
||||||
if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) {
|
if (!file_exists($build_dir . '/admin.asset.php')) {
|
||||||
error_log('[Formipay] Build files not found at: ' . $build_dir . '/admin.asset.php');
|
error_log('[Formipay] Build files not found at: ' . $build_dir . '/admin.asset.php');
|
||||||
return; // Build not generated yet
|
return; // Build not generated yet
|
||||||
}
|
}
|
||||||
@@ -61,19 +64,19 @@ class ReactAdmin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Localize script with required data
|
// Localize script with required data
|
||||||
$data = apply_filters( 'formipay/admin/data', [
|
$data = apply_filters('formipay/admin/data', [
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
'restUrl' => rest_url( 'formipay/v1' ),
|
'restUrl' => rest_url('formipay/v1'),
|
||||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
'nonce' => wp_create_nonce('formipay-admin'),
|
||||||
'pluginUrl' => FORMIPAY_URL,
|
'pluginUrl' => FORMIPAY_URL,
|
||||||
'siteUrl' => site_url(),
|
'siteUrl' => site_url(),
|
||||||
] );
|
]);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
error_log('[Formipay] Enqueuing React assets on screen: ' . $screen->id);
|
error_log('[Formipay] Enqueuing React assets on screen: ' . $screen->id);
|
||||||
error_log('[Formipay] Page data: ' . wp_json_encode($data));
|
error_log('[Formipay] Page data: ' . wp_json_encode($data));
|
||||||
|
|
||||||
wp_localize_script( 'formipay-admin', 'formipayAdmin', $data );
|
wp_localize_script('formipay-admin', 'formipayAdmin', $data);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ class Coupon {
|
|||||||
add_action( 'wp_ajax_formipay-duplicate-coupon', [$this, 'formipay_duplicate_coupon'] );
|
add_action( 'wp_ajax_formipay-duplicate-coupon', [$this, 'formipay_duplicate_coupon'] );
|
||||||
add_action( 'wp_ajax_formipay-get-coupon', [$this, 'formipay_get_coupon'] );
|
add_action( 'wp_ajax_formipay-get-coupon', [$this, 'formipay_get_coupon'] );
|
||||||
add_action( 'wp_ajax_formipay-save-coupon', [$this, 'formipay_save_coupon'] );
|
add_action( 'wp_ajax_formipay-save-coupon', [$this, 'formipay_save_coupon'] );
|
||||||
|
add_action( 'wp_ajax_formipay-autocomplete-search', [$this, 'formipay_autocomplete_search'] );
|
||||||
|
|
||||||
|
// React Metabox
|
||||||
|
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
|
||||||
|
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
|
||||||
|
|
||||||
// Order
|
// Order
|
||||||
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
||||||
@@ -101,79 +106,29 @@ class Coupon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
// Assets now handled by ReactAdmin class
|
|
||||||
return;
|
|
||||||
|
|
||||||
if($current_screen->id == 'formipay_page_formipay-coupons') {
|
|
||||||
|
|
||||||
wp_enqueue_style( 'page-coupons', FORMIPAY_URL . 'admin/assets/css/page-coupons.css', [], FORMIPAY_VERSION, 'all' );
|
|
||||||
wp_enqueue_script( 'page-coupons', FORMIPAY_URL . 'admin/assets/js/page-coupons.js', ['jquery', 'gridjs'], FORMIPAY_VERSION, true );
|
|
||||||
|
|
||||||
wp_localize_script( 'page-coupons', 'formipay_coupons_page', [
|
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
|
||||||
'site_url' => site_url(),
|
|
||||||
'columns' => [
|
|
||||||
'id' => esc_html__( 'ID', 'formipay' ),
|
|
||||||
'code' => esc_html__( 'Coupon Code', 'formipay' ),
|
|
||||||
'usages' => esc_html__( 'Usages', 'formipay' ),
|
|
||||||
'date_limit' => esc_html__( 'Date Limit', 'formipay' ),
|
|
||||||
'status' => esc_html__( 'Status', 'formipay' ),
|
|
||||||
'type' => esc_html__( 'Type', 'formipay' ),
|
|
||||||
'amount' => esc_html__( 'Amount', 'formipay' )
|
|
||||||
],
|
|
||||||
'filter_form' => [
|
|
||||||
'products' => [
|
|
||||||
'placeholder' => esc_html__( 'Filter by Product', 'formipay' ),
|
|
||||||
'noresult_text' => esc_html__( 'No results found', 'formipay' )
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'modal' => [
|
|
||||||
'add' => [
|
|
||||||
'title' => esc_html__( 'Your New Coupon Code', 'formipay' ),
|
|
||||||
'validation' => esc_html__( 'Coupon code is still empty. Please input the code before continue', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Create New Coupon', 'formipay' )
|
|
||||||
],
|
|
||||||
'delete' => [
|
|
||||||
'question' => esc_html__( 'Do you want to delete the coupon?', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Delete Permanently', 'formipay' )
|
|
||||||
],
|
|
||||||
'bulk_delete' => [
|
|
||||||
'question' => esc_html__( 'Do you want to delete the selected the coupon(s)?', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
|
||||||
],
|
|
||||||
'duplicate' => [
|
|
||||||
'question' => esc_html__( 'Do you want to duplicate the coupon?', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'nonce' => wp_create_nonce('formipay-admin-coupon-page')
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
|
|
||||||
$screen = get_current_screen();
|
$screen = get_current_screen();
|
||||||
|
|
||||||
if ( $screen->post_type === 'formipay-coupon' && $screen->base === 'post' ) {
|
// Enqueue SweetAlert2 for coupon post edit screen
|
||||||
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
|
||||||
wp_enqueue_script( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
wp_enqueue_style('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
||||||
|
wp_enqueue_script('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||||
wp_localize_script( 'sweetalert2', 'formipay_admin', [
|
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
|
||||||
'site_url' => site_url(),
|
|
||||||
] );
|
|
||||||
|
|
||||||
|
// Localize admin data
|
||||||
|
wp_localize_script('formipay-admin', 'formipayAdmin', [
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'siteUrl' => site_url(),
|
||||||
|
'nonce' => wp_create_nonce('formipay-admin'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cpt_post_fields_box($boxes) {
|
public function cpt_post_fields_box($boxes) {
|
||||||
$boxes['formipay_coupon_settings'] = array(
|
// Disabled - using React metabox instead
|
||||||
'post_type' => array('formipay-coupon'),
|
// $boxes['formipay_coupon_settings'] = array(
|
||||||
'label' => __('Details', 'formipay'),
|
// 'post_type' => array('formipay-coupon'),
|
||||||
);
|
// 'label' => __('Details', 'formipay'),
|
||||||
|
// );
|
||||||
|
|
||||||
return $boxes;
|
return $boxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,4 +1001,101 @@ class Coupon {
|
|||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add React metabox for coupon editor
|
||||||
|
*/
|
||||||
|
public function add_react_metabox() {
|
||||||
|
add_meta_box(
|
||||||
|
'formipay_coupon_reactor_metabox',
|
||||||
|
__( 'Coupon Settings', 'formipay' ),
|
||||||
|
[$this, 'render_react_metabox'],
|
||||||
|
'formipay-coupon',
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render React metabox container
|
||||||
|
*/
|
||||||
|
public function render_react_metabox($post) {
|
||||||
|
?>
|
||||||
|
<div
|
||||||
|
data-formipay-metabox="coupon"
|
||||||
|
data-post-id="<?php echo esc_attr($post->ID); ?>"
|
||||||
|
class="formipay-react-metabox-container"
|
||||||
|
>
|
||||||
|
<div class="formipay-loading">
|
||||||
|
<div class="formipay-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render React metabox template (hidden)
|
||||||
|
*/
|
||||||
|
public function render_react_metabox_template() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
if (!$post || $post->post_type !== 'formipay-coupon') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get global currencies
|
||||||
|
$global_currencies = get_global_currency_array();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
|
||||||
|
window.formipayGetFlag = function(currencyRaw) {
|
||||||
|
<?php
|
||||||
|
foreach ($global_currencies as $currency) {
|
||||||
|
echo 'if (currencyRaw === "' . esc_js($currency['currency']) . '") return "' . esc_url(formipay_get_flag_by_currency($currency['currency'])) . '";';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autocomplete search for relation fields
|
||||||
|
*/
|
||||||
|
public function formipay_autocomplete_search() {
|
||||||
|
|
||||||
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : '';
|
||||||
|
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
|
||||||
|
|
||||||
|
if (empty($post_type) || strlen($search) < 2) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = get_posts([
|
||||||
|
'post_type' => $post_type,
|
||||||
|
's' => $search,
|
||||||
|
'posts_per_page' => 20,
|
||||||
|
'post_status' => ['publish', 'draft'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
if (!empty($query)) {
|
||||||
|
foreach ($query as $post) {
|
||||||
|
$results[] = [
|
||||||
|
'value' => $post->ID,
|
||||||
|
'label' => $post->post_title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($results);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
146
src/admin/components/coupons/CouponMetabox.css
Normal file
146
src/admin/components/coupons/CouponMetabox.css
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Coupon Metabox Styles
|
||||||
|
* WPCFTO-inspired design for React metabox island
|
||||||
|
*/
|
||||||
|
|
||||||
|
.formipay-coupon-metabox {
|
||||||
|
margin: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-metabox-actions {
|
||||||
|
padding: 20px 30px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #f0f0f1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radio Group */
|
||||||
|
.formipay-radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid var(--formipay-color-border-dark);
|
||||||
|
border-radius: var(--formipay-radius-lg);
|
||||||
|
transition: all var(--formipay-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio span {
|
||||||
|
font-weight: var(--formipay-font-weight-medium);
|
||||||
|
color: var(--formipay-color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio:hover {
|
||||||
|
border-color: var(--formipay-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio.active {
|
||||||
|
background-color: var(--formipay-color-primary);
|
||||||
|
border-color: var(--formipay-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio.active span {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Autocomplete Field */
|
||||||
|
.formipay-autocomplete {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-selected {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: var(--formipay-color-primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: var(--formipay-font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-remove:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-loading {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--formipay-color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--formipay-color-border-dark);
|
||||||
|
border-radius: var(--formipay-radius-sm);
|
||||||
|
box-shadow: var(--formipay-shadow-md);
|
||||||
|
z-index: 100;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-result {
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--formipay-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-result:hover {
|
||||||
|
background-color: var(--formipay-color-content-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-result:first-child {
|
||||||
|
border-radius: var(--formipay-radius-sm) var(--formipay-radius-sm) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-autocomplete-result:last-child {
|
||||||
|
border-radius: 0 0 var(--formipay-radius-sm) var(--formipay-radius-sm);
|
||||||
|
}
|
||||||
545
src/admin/components/coupons/CouponMetabox.js
Normal file
545
src/admin/components/coupons/CouponMetabox.js
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
/**
|
||||||
|
* Coupon Metabox - React metabox island for post.php editor
|
||||||
|
* Uses WPCFTO design system components
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from '@wordpress/element';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { MetaboxLayout, TabNav, TabPanel, Field, Input, Checkbox, Textarea, Button, Notice, GroupTitle } from '../../design-system';
|
||||||
|
|
||||||
|
// Currency helper functions
|
||||||
|
const getGlobalCurrencies = () => {
|
||||||
|
if (window.formipayGlobalCurrencies) {
|
||||||
|
return window.formipayGlobalCurrencies;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrencySymbol = (currencyRaw) => {
|
||||||
|
const parts = currencyRaw.split(':::');
|
||||||
|
return parts[1] || currencyRaw;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrencyCode = (currencyRaw) => {
|
||||||
|
const parts = currencyRaw.split(':::');
|
||||||
|
return parts[0] || currencyRaw;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFlagByCurrency = (currencyRaw) => {
|
||||||
|
if (window.formipayGetFlag) {
|
||||||
|
return window.formipayGetFlag(currencyRaw);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CouponMetabox({ postId }) {
|
||||||
|
const [activeTab, setActiveTab] = useState('rules');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
active: 'on',
|
||||||
|
type: 'percentage',
|
||||||
|
amount_percentage: '',
|
||||||
|
case_sensitive: '',
|
||||||
|
free_shipping: '',
|
||||||
|
quantity_active: '',
|
||||||
|
use_limit: '',
|
||||||
|
date_limit: '',
|
||||||
|
amounts_fixed: {},
|
||||||
|
max_amounts: {},
|
||||||
|
forms: [],
|
||||||
|
products: [],
|
||||||
|
customers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'rules', label: __('Rules', 'formipay'), icon: 'fa fa-cog' },
|
||||||
|
{ id: 'restriction', label: __('Restrictions', 'formipay'), icon: 'fa fa-lock' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load coupon data
|
||||||
|
useEffect(() => {
|
||||||
|
if (postId > 0) {
|
||||||
|
loadCouponData();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [postId]);
|
||||||
|
|
||||||
|
const loadCouponData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-get-coupon`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
id: postId,
|
||||||
|
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
active: data.active || 'on',
|
||||||
|
type: data.type || 'percentage',
|
||||||
|
amount_percentage: data.amount_percentage || '',
|
||||||
|
case_sensitive: data.case_sensitive || '',
|
||||||
|
free_shipping: data.free_shipping || '',
|
||||||
|
quantity_active: data.quantity_active || '',
|
||||||
|
use_limit: data.use_limit || '',
|
||||||
|
date_limit: data.date_limit || '',
|
||||||
|
amounts_fixed: data.amounts_fixed?.reduce((acc, item) => {
|
||||||
|
acc[item.symbol] = item.amount;
|
||||||
|
return acc;
|
||||||
|
}, {}) || {},
|
||||||
|
max_amounts: data.max_amounts?.reduce((acc, item) => {
|
||||||
|
acc[item.symbol] = item.amount;
|
||||||
|
return acc;
|
||||||
|
}, {}) || {},
|
||||||
|
forms: data.forms || [],
|
||||||
|
products: data.products || [],
|
||||||
|
customers: data.customers || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load coupon:', error);
|
||||||
|
setMessage({ type: 'error', text: __('Failed to load coupon data.', 'formipay') });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (field, value) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
id: postId,
|
||||||
|
title: document.querySelector('#title input')?.value || '',
|
||||||
|
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||||
|
...formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add fixed amounts
|
||||||
|
Object.entries(formData.amounts_fixed).forEach(([symbol, amount]) => {
|
||||||
|
params.append(`amount_fixed_${symbol}`, amount);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add max amounts
|
||||||
|
Object.entries(formData.max_amounts).forEach(([symbol, amount]) => {
|
||||||
|
params.append(`max_amount_${symbol}`, amount);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-save-coupon`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setMessage({ type: 'success', text: result.data.message || __('Coupon saved successfully.', 'formipay') });
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: result.data?.message || __('Failed to save coupon.', 'formipay') });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save coupon:', error);
|
||||||
|
setMessage({ type: 'error', text: __('Failed to save coupon.', 'formipay') });
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPercentage = formData.type === 'percentage';
|
||||||
|
const isFixed = formData.type === 'fixed';
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="formipay-loading">{__('Loading...', 'formipay')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-coupon-metabox">
|
||||||
|
{message && (
|
||||||
|
<Notice
|
||||||
|
type={message.type}
|
||||||
|
onDismiss={() => setMessage(null)}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</Notice>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MetaboxLayout
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
>
|
||||||
|
<TabPanel tabs={tabs} activeTab={activeTab}>
|
||||||
|
{(tab) => {
|
||||||
|
if (tab.id === 'rules') {
|
||||||
|
return (
|
||||||
|
<div className="formipay-tab-content">
|
||||||
|
{/* General Settings */}
|
||||||
|
<GroupTitle title={__('General', 'formipay')} icon="fa fa-cog" />
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Active', 'formipay')}
|
||||||
|
description={__('Enable this coupon.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={formData.active === 'on'}
|
||||||
|
onChange={(e) => handleChange('active', e.target.checked ? 'on' : '')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Type', 'formipay')}
|
||||||
|
description={__('Choose discount type.', 'formipay')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<div className="formipay-radio-group">
|
||||||
|
<label className={`formipay-radio ${isFixed ? 'active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
value="fixed"
|
||||||
|
checked={isFixed}
|
||||||
|
onChange={(e) => handleChange('type', e.target.value)}
|
||||||
|
/>
|
||||||
|
<span>{__('Fixed', 'formipay')}</span>
|
||||||
|
</label>
|
||||||
|
<label className={`formipay-radio ${isPercentage ? 'active' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
value="percentage"
|
||||||
|
checked={isPercentage}
|
||||||
|
onChange={(e) => handleChange('type', e.target.value)}
|
||||||
|
/>
|
||||||
|
<span>{__('Percentage', 'formipay')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{isPercentage && (
|
||||||
|
<Field
|
||||||
|
label={__('Amount', 'formipay')}
|
||||||
|
description={__('Discount percentage.', 'formipay')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.amount_percentage}
|
||||||
|
onChange={(e) => handleChange('amount_percentage', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fixed Amount Section */}
|
||||||
|
{isFixed && (
|
||||||
|
<>
|
||||||
|
<GroupTitle title={__('Discount Amount', 'formipay')} icon="fa fa-money" />
|
||||||
|
|
||||||
|
{getGlobalCurrencies().map((currency) => {
|
||||||
|
const symbol = getCurrencySymbol(currency.currency);
|
||||||
|
const currencyCode = getCurrencyCode(currency.currency);
|
||||||
|
const flag = getFlagByCurrency(currency.currency);
|
||||||
|
const step = currency.decimal_digits > 0 ? 1 / (currency.decimal_digits * 10) : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={currencyCode} className="formipay-generic-field">
|
||||||
|
<div className="formipay-field-aside">
|
||||||
|
<div className="formipay-field-label required">
|
||||||
|
<span className="formipay-field-label-text">
|
||||||
|
{flag && <img src={flag} alt="" width="18" style={{ verticalAlign: 'middle', marginRight: '4px' }} />}
|
||||||
|
{__('Amount in', 'formipay')} {symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="formipay-field-content">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step={step}
|
||||||
|
placeholder={__('Enter Amount...', 'formipay')}
|
||||||
|
value={formData.amounts_fixed[symbol] || ''}
|
||||||
|
onChange={(e) => handleChange('amounts_fixed', {
|
||||||
|
...formData.amounts_fixed,
|
||||||
|
[symbol]: e.target.value
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Max Discount Section */}
|
||||||
|
<GroupTitle
|
||||||
|
title={__('Max Discount Amount', 'formipay')}
|
||||||
|
icon="fa fa-calculator"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{getGlobalCurrencies().map((currency) => {
|
||||||
|
const symbol = getCurrencySymbol(currency.currency);
|
||||||
|
const currencyCode = getCurrencyCode(currency.currency);
|
||||||
|
const flag = getFlagByCurrency(currency.currency);
|
||||||
|
const step = currency.decimal_digits > 0 ? 1 / (currency.decimal_digits * 10) : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
key={`max_${currencyCode}`}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{flag && <img src={flag} alt="" width="18" style={{ verticalAlign: 'middle', marginRight: '4px' }} />}
|
||||||
|
{__('Max Amount in', 'formipay')} {symbol}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description={__('Leave empty to not limit the max discount amount.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step={step}
|
||||||
|
placeholder={__('Enter Max Amount...', 'formipay')}
|
||||||
|
value={formData.max_amounts[symbol] || ''}
|
||||||
|
onChange={(e) => handleChange('max_amounts', {
|
||||||
|
...formData.max_amounts,
|
||||||
|
[symbol]: e.target.value
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Rules Section */}
|
||||||
|
<GroupTitle title={__('Rules', 'formipay')} icon="fa fa-list" />
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Case Sensitive', 'formipay')}
|
||||||
|
description={__('If activated, coupon codes must be entered with the exact capitalization.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={formData.case_sensitive === 'on'}
|
||||||
|
onChange={(e) => handleChange('case_sensitive', e.target.checked ? 'on' : '')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Free Shipping', 'formipay')}
|
||||||
|
description={__('Shipping cost will be free when this coupon is applied.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={formData.free_shipping === 'on'}
|
||||||
|
onChange={(e) => handleChange('free_shipping', e.target.checked ? 'on' : '')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{isFixed && (
|
||||||
|
<Field
|
||||||
|
label={__('Influenced by Quantity', 'formipay')}
|
||||||
|
description={__('Example: when buyer buys 4 items, 4 × discount amount will be applied.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={formData.quantity_active === 'on'}
|
||||||
|
onChange={(e) => handleChange('quantity_active', e.target.checked ? 'on' : '')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.id === 'restriction') {
|
||||||
|
return (
|
||||||
|
<div className="formipay-tab-content">
|
||||||
|
<GroupTitle title={__('Restrictions', 'formipay')} icon="fa fa-lock" />
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Usage Limit', 'formipay')}
|
||||||
|
description={__('Leave empty or 0 (zero) for unlimited usage.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.use_limit}
|
||||||
|
onChange={(e) => handleChange('use_limit', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Date Limit', 'formipay')}
|
||||||
|
description={__('Last day the coupon can be used. Leave empty for no limit.', 'formipay')}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formData.date_limit}
|
||||||
|
onChange={(e) => handleChange('date_limit', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Forms', 'formipay')}
|
||||||
|
description={__('Only selected form(s) can use the coupon. Leave empty to apply to all forms.', 'formipay')}
|
||||||
|
>
|
||||||
|
<AutocompleteField
|
||||||
|
postType="formipay-form"
|
||||||
|
value={formData.forms}
|
||||||
|
onChange={(values) => handleChange('forms', values)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Products', 'formipay')}
|
||||||
|
description={__('Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay')}
|
||||||
|
>
|
||||||
|
<AutocompleteField
|
||||||
|
postType="formipay-product"
|
||||||
|
value={formData.products}
|
||||||
|
onChange={(values) => handleChange('products', values)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={__('Customers', 'formipay')}
|
||||||
|
description={__('Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay')}
|
||||||
|
>
|
||||||
|
<AutocompleteField
|
||||||
|
postType="formipay-customer"
|
||||||
|
value={formData.customers}
|
||||||
|
onChange={(values) => handleChange('customers', values)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<div className="formipay-metabox-actions">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? __('Saving...', 'formipay') : __('Save Coupon', 'formipay')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</MetaboxLayout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Autocomplete Field Component
|
||||||
|
function AutocompleteField({ postType, value, onChange }) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [results, setResults] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedItems = value || [];
|
||||||
|
|
||||||
|
const handleSearch = async (query) => {
|
||||||
|
setSearch(query);
|
||||||
|
if (query.length < 2) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-autocomplete-search`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
post_type: postType,
|
||||||
|
search: query,
|
||||||
|
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
setResults(result.data || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Autocomplete search failed:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (item) => {
|
||||||
|
if (!selectedItems.includes(item.value)) {
|
||||||
|
onChange([...selectedItems, item.value]);
|
||||||
|
}
|
||||||
|
setSearch('');
|
||||||
|
setResults([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (valueToRemove) => {
|
||||||
|
onChange(selectedItems.filter(v => v !== valueToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-autocomplete">
|
||||||
|
<div className="formipay-autocomplete-selected">
|
||||||
|
{selectedItems.map(val => (
|
||||||
|
<span key={val} className="formipay-autocomplete-tag">
|
||||||
|
{val}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="formipay-autocomplete-remove"
|
||||||
|
onClick={() => handleRemove(val)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="formipay-autocomplete-input-wrapper">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
onFocus={() => setOpen(true)}
|
||||||
|
placeholder={__('Search...', 'formipay')}
|
||||||
|
/>
|
||||||
|
{loading && <span className="formipay-autocomplete-loading">...</span>}
|
||||||
|
</div>
|
||||||
|
{open && results.length > 0 && (
|
||||||
|
<div className="formipay-autocomplete-results">
|
||||||
|
{results.map(item => (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className="formipay-autocomplete-result"
|
||||||
|
onClick={() => handleSelect(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
import { render } from '@wordpress/element';
|
import { render } from '@wordpress/element';
|
||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
|
import CouponMetabox from './components/coupons/CouponMetabox';
|
||||||
|
import './components/coupons/CouponMetabox.css';
|
||||||
|
|
||||||
// Mount the React app to all available mount points
|
// Mount the React app to all available mount points
|
||||||
const mountApps = () => {
|
const mountApps = () => {
|
||||||
|
// Mount main admin app pages
|
||||||
const mountPoints = document.querySelectorAll('[data-formipay-mount]');
|
const mountPoints = document.querySelectorAll('[data-formipay-mount]');
|
||||||
|
|
||||||
console.log('[Formipay] Mount points found:', mountPoints.length);
|
console.log('[Formipay] Mount points found:', mountPoints.length);
|
||||||
@@ -28,6 +31,30 @@ const mountApps = () => {
|
|||||||
console.error('[Formipay] Failed to mount:', page, error);
|
console.error('[Formipay] Failed to mount:', page, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mount metabox islands
|
||||||
|
const metaboxPoints = document.querySelectorAll('[data-formipay-metabox]');
|
||||||
|
|
||||||
|
console.log('[Formipay] Metabox points found:', metaboxPoints.length);
|
||||||
|
|
||||||
|
metaboxPoints.forEach((mountPoint) => {
|
||||||
|
const metaboxType = mountPoint.dataset.formipayMetabox;
|
||||||
|
const postId = parseInt(mountPoint.dataset.postId || '0');
|
||||||
|
|
||||||
|
console.log('[Formipay] Mounting metabox:', metaboxType, 'for post:', postId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (metaboxType === 'coupon') {
|
||||||
|
render(
|
||||||
|
<CouponMetabox postId={postId} />,
|
||||||
|
mountPoint
|
||||||
|
);
|
||||||
|
console.log('[Formipay] Successfully mounted coupon metabox');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Formipay] Failed to mount metabox:', metaboxType, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize when DOM is ready
|
// Initialize when DOM is ready
|
||||||
|
|||||||
Reference in New Issue
Block a user