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();
|
||||
|
||||
// Only load React assets on Formipay admin pages
|
||||
if ( strpos( $screen->id, 'formipay' ) === false ) {
|
||||
// Load React assets on Formipay admin pages OR on post edit screens for our CPTs
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -28,7 +31,7 @@ class ReactAdmin {
|
||||
$build_dir = FORMIPAY_PATH . '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');
|
||||
return; // Build not generated yet
|
||||
}
|
||||
@@ -61,19 +64,19 @@ class ReactAdmin {
|
||||
);
|
||||
|
||||
// Localize script with required data
|
||||
$data = apply_filters( 'formipay/admin/data', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'restUrl' => rest_url( 'formipay/v1' ),
|
||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
||||
$data = apply_filters('formipay/admin/data', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'restUrl' => rest_url('formipay/v1'),
|
||||
'nonce' => wp_create_nonce('formipay-admin'),
|
||||
'pluginUrl' => FORMIPAY_URL,
|
||||
'siteUrl' => site_url(),
|
||||
] );
|
||||
]);
|
||||
|
||||
// Debug logging
|
||||
error_log('[Formipay] Enqueuing React assets on screen: ' . $screen->id);
|
||||
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-get-coupon', [$this, 'formipay_get_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
|
||||
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
||||
@@ -101,79 +106,29 @@ class Coupon {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if ( $screen->post_type === 'formipay-coupon' && $screen->base === 'post' ) {
|
||||
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(),
|
||||
] );
|
||||
// Enqueue SweetAlert2 for coupon post edit screen
|
||||
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
$boxes['formipay_coupon_settings'] = array(
|
||||
'post_type' => array('formipay-coupon'),
|
||||
'label' => __('Details', 'formipay'),
|
||||
);
|
||||
|
||||
// Disabled - using React metabox instead
|
||||
// $boxes['formipay_coupon_settings'] = array(
|
||||
// 'post_type' => array('formipay-coupon'),
|
||||
// 'label' => __('Details', 'formipay'),
|
||||
// );
|
||||
|
||||
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 App from './components/App';
|
||||
import CouponMetabox from './components/coupons/CouponMetabox';
|
||||
import './components/coupons/CouponMetabox.css';
|
||||
|
||||
// Mount the React app to all available mount points
|
||||
const mountApps = () => {
|
||||
// Mount main admin app pages
|
||||
const mountPoints = document.querySelectorAll('[data-formipay-mount]');
|
||||
|
||||
console.log('[Formipay] Mount points found:', mountPoints.length);
|
||||
@@ -28,6 +31,30 @@ const mountApps = () => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user