fix Products, Coupons, and admin pages
This commit is contained in:
@@ -47,6 +47,7 @@ li#toplevel_page_formipay a[href^="admin.php?page=formipay-settings"]:before{
|
|||||||
}
|
}
|
||||||
.wp-submenu-wrap li:has(a[href="admin.php?page=formipay-products"]).current ~ li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]),
|
.wp-submenu-wrap li:has(a[href="admin.php?page=formipay-products"]).current ~ li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]),
|
||||||
.wp-submenu-wrap li:has(a[href="admin.php?page=formipay-products"]):hover ~ li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]),
|
.wp-submenu-wrap li:has(a[href="admin.php?page=formipay-products"]):hover ~ li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]),
|
||||||
|
li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]):hover,
|
||||||
.wp-submenu-wrap li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]).current {
|
.wp-submenu-wrap li:has(a[href="edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product"]).current {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -138,3 +138,11 @@ table.wpcfto-table.inner-table tr > *:is(th, td):first-child {
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.variation-details-content tbody tr:first-child td:nth-child(2) .price-input-wrapper .price-currency {
|
||||||
|
background-color: #AA1C1C;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.variation-details-content tbody .price-input-wrapper .price-currency {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
jQuery(function ($) {
|
jQuery(function ($) {
|
||||||
|
|
||||||
|
console.log(product_details)
|
||||||
|
|
||||||
$('a[href="admin.php?page=formipay-products"]').addClass('current').closest('li').addClass('current');
|
$('a[href="admin.php?page=formipay-products"]').addClass('current').closest('li').addClass('current');
|
||||||
|
|
||||||
function autoset_variation_name() {
|
function autoset_variation_name() {
|
||||||
@@ -47,16 +49,12 @@ jQuery(function ($) {
|
|||||||
Vue.component('price-input', {
|
Vue.component('price-input', {
|
||||||
// Gunakan 'value' sebagai prop, sesuai konvensi v-model Vue 2
|
// Gunakan 'value' sebagai prop, sesuai konvensi v-model Vue 2
|
||||||
props: {
|
props: {
|
||||||
value: [Number, String], // Diubah dari modelValue
|
value: [Number, String],
|
||||||
currencySymbol: String,
|
currencySymbol: String,
|
||||||
currencyDecimalDigits: {
|
currencyDecimalDigits: { type: Number, default: 2 },
|
||||||
type: Number,
|
disabled: { type: Boolean, default: false },
|
||||||
default: 2
|
required: { type: Boolean, default: false },
|
||||||
},
|
placeholder: { type: String, default: 'Auto' }
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -79,13 +77,26 @@ jQuery(function ($) {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
stepValue() {
|
stepValue() {
|
||||||
return Math.pow(10, -this.currencyDecimalDigits);
|
const digits = this.currencyDecimalDigits || 0;
|
||||||
|
if (!digits || digits === 0) return 1;
|
||||||
|
return 1 / Math.pow(10, digits);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="price-input-wrapper">
|
<div class="price-input-wrapper">
|
||||||
<span class="price-currency">{{ currencySymbol }}</span>
|
<span class="price-currency">
|
||||||
<input type="number" :value="inputValue" @input="onInput" :step="stepValue" placeholder="0" :disabled="disabled" />
|
{{ currencySymbol }}
|
||||||
|
<span v-if="required" class="required-asterisk" style="color: red; margin-left: 4px;">*</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:value="inputValue"
|
||||||
|
@input="onInput"
|
||||||
|
:step="stepValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:required="required"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
@@ -98,22 +109,27 @@ jQuery(function ($) {
|
|||||||
tableRows: [],
|
tableRows: [],
|
||||||
deletedKeys: [],
|
deletedKeys: [],
|
||||||
isPhysical: window.product_details?.product_is_physical || false,
|
isPhysical: window.product_details?.product_is_physical || false,
|
||||||
pricingMethod: window.product_details?.product_pricing_method || 'auto',
|
|
||||||
manualPrices: [],
|
|
||||||
productHasVariation: window.product_details?.product_has_variation || false,
|
productHasVariation: window.product_details?.product_has_variation || false,
|
||||||
jsonValue: '[]',
|
jsonValue: '[]',
|
||||||
attributeRepeaterWatcher: null,
|
attributeRepeaterWatcher: null,
|
||||||
manualPricesWatcher: null,
|
|
||||||
_debounceTimer: null,
|
_debounceTimer: null,
|
||||||
productType: window.product_details?.product_type || 'digital',
|
productType: window.product_details?.product_type || 'digital',
|
||||||
currencyDecimalDigits: parseInt(window.product_details?.default_currency_decimal_digits) || 2,
|
currencyDecimalDigits: parseInt(window.product_details?.default_currency_decimal_digits) || 2,
|
||||||
currencySymbol: window.product_details?.default_currency_symbol || '$'
|
currencySymbol: window.product_details?.default_currency_symbol || '$',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
showFlatPricing() {
|
||||||
|
const mc = !!(window.product_details && window.product_details.multicurrency);
|
||||||
|
const selected = (window.product_details && window.product_details.global_selected_currencies) || {};
|
||||||
|
const count = Object.keys(selected).length;
|
||||||
|
return !mc || count <= 1; // flat when off or only one currency selected
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.initializeOrClearTable();
|
await this.initializeOrClearTable();
|
||||||
this.setupAttributeRepeaterSync();
|
this.setupAttributeRepeaterSync();
|
||||||
this.setupManualPricesSync();
|
this.$nextTick(() => this.enforcePriceInputStates());
|
||||||
const typeRadios = document.querySelectorAll('input[name="product_type"]');
|
const typeRadios = document.querySelectorAll('input[name="product_type"]');
|
||||||
typeRadios.forEach(radio => {
|
typeRadios.forEach(radio => {
|
||||||
radio.addEventListener('change', e => {
|
radio.addEventListener('change', e => {
|
||||||
@@ -121,13 +137,6 @@ jQuery(function ($) {
|
|||||||
this.isPhysical = e.target.value === 'physical';
|
this.isPhysical = e.target.value === 'physical';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const pricingRadios = document.querySelectorAll('input[name="product_pricing_method"]');
|
|
||||||
pricingRadios.forEach(radio => {
|
|
||||||
radio.addEventListener('change', async e => {
|
|
||||||
this.pricingMethod = e.target.value;
|
|
||||||
await this.rebuildTable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const hasVariationToggle = document.querySelector('input[name="product_has_variation"]');
|
const hasVariationToggle = document.querySelector('input[name="product_has_variation"]');
|
||||||
if (hasVariationToggle) {
|
if (hasVariationToggle) {
|
||||||
hasVariationToggle.addEventListener('change', async (e) => {
|
hasVariationToggle.addEventListener('change', async (e) => {
|
||||||
@@ -135,43 +144,223 @@ jQuery(function ($) {
|
|||||||
await this.initializeOrClearTable();
|
await this.initializeOrClearTable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Add submit validation for missing default currency price
|
||||||
|
const postForm = document.getElementById('post');
|
||||||
|
if (postForm) {
|
||||||
|
postForm.addEventListener('submit', (ev) => {
|
||||||
|
const missing = this.findFirstMissingDefault();
|
||||||
|
if (missing) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopImmediatePropagation();
|
||||||
|
const tmpl = (window.product_details
|
||||||
|
&& window.product_details.variation_table
|
||||||
|
&& window.product_details.variation_table.error_missing_default_price)
|
||||||
|
|| 'Please fill Regular Price for default currency (%1$s) in variation "%2$s".';
|
||||||
|
|
||||||
|
const msg = tmpl.replace('%1$s', missing.currencyCode).replace('%2$s', missing.rowLabel);
|
||||||
|
|
||||||
|
if (typeof window.triggerSwalEmptyRequired === 'function') {
|
||||||
|
window.triggerSwalEmptyRequired(msg);
|
||||||
|
} else if (window.Swal && typeof window.Swal.fire === 'function') {
|
||||||
|
window.Swal.fire({ icon: 'error', title: 'Required field empty', text: msg });
|
||||||
|
} else {
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.attributeRepeaterWatcher) {
|
if (this.attributeRepeaterWatcher) {
|
||||||
this.attributeRepeaterWatcher.disconnect();
|
this.attributeRepeaterWatcher.disconnect();
|
||||||
}
|
}
|
||||||
if (this.manualPricesWatcher) {
|
if (this._attrPoller) {
|
||||||
this.manualPricesWatcher.disconnect();
|
clearInterval(this._attrPoller);
|
||||||
}
|
this._attrPoller = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
defaultEntry(row) {
|
||||||
|
const defCode = this.getDefaultCurrencyCode();
|
||||||
|
const defTriple = (window.product_details && window.product_details.default_currency) || '';
|
||||||
|
const fallback = {
|
||||||
|
currency: defTriple || (defCode ? `${defCode}:::` : 'USD:::'),
|
||||||
|
regular_price: '',
|
||||||
|
sale_price: '',
|
||||||
|
currency_decimal_digits: this.findDigitsForCode(defCode)
|
||||||
|
};
|
||||||
|
if (!row || !Array.isArray(row.prices) || row.prices.length === 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const found = row.prices.find(p => String(p.currency).split(':::')[0] === defCode);
|
||||||
|
return found || row.prices[0] || fallback;
|
||||||
|
},
|
||||||
|
enforcePriceInputStates() {
|
||||||
|
const defTriple = (window.product_details && window.product_details.default_currency) || '';
|
||||||
|
const defCode = String(defTriple).split(':::')[0] || '';
|
||||||
|
const REG_PLACEHOLDER = window.product_details?.variation_table?.child_placeholder_regular || 'Enter Regular Price';
|
||||||
|
const SALE_PLACEHOLDER = window.product_details?.variation_table?.child_placeholder_sale || 'Enter Sale Price';
|
||||||
|
const $scope = $('#product-variables-table');
|
||||||
|
|
||||||
|
if (!Array.isArray(this.tableRows) || this.tableRows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Reset all number inputs to optional + "Auto"
|
||||||
|
$scope.find('input[type="number"]').each(function () {
|
||||||
|
$(this).attr('placeholder', 'Auto').removeAttr('required');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) Flat mode: set placeholders and step for default currency inputs in main row
|
||||||
|
if (this.showFlatPricing) {
|
||||||
|
$scope.find('tbody > tr').each((rowIdx, tr) => {
|
||||||
|
const row = this.tableRows[rowIdx];
|
||||||
|
if (!row) return;
|
||||||
|
const def = this.defaultEntry(row);
|
||||||
|
if (!def) return;
|
||||||
|
const code = String(def.currency || '').split(':::')[0];
|
||||||
|
const digits = Number(this.findDigitsForCode(code) || 0);
|
||||||
|
const step = (!digits || digits === 0) ? 1 : (1 / Math.pow(10, digits));
|
||||||
|
const $price = $(tr).find('td[data-cell="price"] input[type="number"]');
|
||||||
|
const $sale = $(tr).find('td[data-cell="sale"] input[type="number"]');
|
||||||
|
if ($price.length) {
|
||||||
|
$price.attr('placeholder', REG_PLACEHOLDER).attr('step', step).removeAttr('required');
|
||||||
|
}
|
||||||
|
if ($sale.length) {
|
||||||
|
$sale.attr('placeholder', SALE_PLACEHOLDER).attr('step', step).removeAttr('required');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return; // do not process inner tables in flat mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Multi-currency inner tables
|
||||||
|
const detailsRows = $scope.find('.variation-details-row .inner-table tbody');
|
||||||
|
detailsRows.each((rowIdx, tbodyEl) => {
|
||||||
|
const vueRow = this.tableRows[rowIdx];
|
||||||
|
if (!vueRow || !Array.isArray(vueRow.prices)) return;
|
||||||
|
const $tbody = $(tbodyEl);
|
||||||
|
const trs = $tbody.find('tr');
|
||||||
|
trs.each((i, tr) => {
|
||||||
|
const entry = vueRow.prices[i];
|
||||||
|
if (!entry) return;
|
||||||
|
const code = String(entry.currency).split(':::')[0];
|
||||||
|
const digits = Number(this.findDigitsForCode(code) || 0);
|
||||||
|
const step = (!digits || digits === 0) ? 1 : (1 / Math.pow(10, digits));
|
||||||
|
const $inputs = $(tr).find('input[type="number"]');
|
||||||
|
$inputs.attr('step', step);
|
||||||
|
if (code === defCode) {
|
||||||
|
if ($inputs.length) {
|
||||||
|
$inputs.eq(0).removeAttr('required').attr('placeholder', REG_PLACEHOLDER);
|
||||||
|
if ($inputs.length > 1) {
|
||||||
|
$inputs.eq(1).attr('placeholder', SALE_PLACEHOLDER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const $firstTd = $(tr).children('td').eq(0);
|
||||||
|
if ($firstTd.find('.required-asterisk').length === 0) {
|
||||||
|
$firstTd.append($('<span/>', { class: 'required-asterisk', text: ' *', css: { color: 'red', marginLeft: '4px' } }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(tr).children('td').eq(0).find('.required-asterisk').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
findDigitsForCode(codeOrTriple) {
|
||||||
|
// product_details.global_currencies is the 'raw' array from PHP (with decimal_digits)
|
||||||
|
const globalsRaw = (window.product_details && window.product_details.global_currencies) || [];
|
||||||
|
const target = String(codeOrTriple).split(':::')[0];
|
||||||
|
const found = globalsRaw.find(c => String(c.currency).split(':::')[0] === target);
|
||||||
|
const dd = parseInt(found && found.decimal_digits, 10);
|
||||||
|
return Number.isFinite(dd) ? dd : 2;
|
||||||
|
},
|
||||||
|
buildCurrencyPriceSkeleton() {
|
||||||
|
const selectedMap = (window.product_details && window.product_details.global_selected_currencies) || {};
|
||||||
|
let entries = Object.keys(selectedMap || {});
|
||||||
|
// If multicurrency is off or nothing selected, fall back to default currency triple
|
||||||
|
if (!entries.length) {
|
||||||
|
const defTriple = (window.product_details && window.product_details.default_currency) || '';
|
||||||
|
if (defTriple) entries = [defTriple];
|
||||||
|
}
|
||||||
|
return entries.map(val => {
|
||||||
|
const parts = String(val).split(':::');
|
||||||
|
const code = parts[0] || val;
|
||||||
|
return {
|
||||||
|
currency: val,
|
||||||
|
regular_price: '',
|
||||||
|
sale_price: '',
|
||||||
|
currency_decimal_digits: this.findDigitsForCode(code)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
async initializeOrClearTable() {
|
async initializeOrClearTable() {
|
||||||
if (!this.productHasVariation) {
|
if (!this.productHasVariation) {
|
||||||
this.tableRows = [];
|
this.tableRows = [];
|
||||||
this.updateJson();
|
this.updateJson();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.syncManualPrices();
|
|
||||||
await this.loadProductVariables();
|
await this.loadProductVariables();
|
||||||
},
|
},
|
||||||
_ensureRowDataStructure(row) {
|
_ensureRowDataStructure(row) {
|
||||||
if (this.pricingMethod === 'manual') {
|
// Ensure expanded flag
|
||||||
if (typeof row.prices === 'undefined') {
|
if (typeof row.expanded === 'undefined') {
|
||||||
row.prices = JSON.parse(JSON.stringify(this.manualPrices));
|
row.expanded = false;
|
||||||
delete row.price;
|
}
|
||||||
delete row.sale;
|
|
||||||
}
|
// Always use multi-currency child rows
|
||||||
} else {
|
const skeleton = this.buildCurrencyPriceSkeleton();
|
||||||
if (typeof row.price === 'undefined') {
|
|
||||||
row.price = 0;
|
const byCode = (val) => String(val).split(':::')[0];
|
||||||
row.sale = 0;
|
// Initialize or reconcile row.prices against current global selected currencies
|
||||||
delete row.prices;
|
if (!Array.isArray(row.prices)) {
|
||||||
}
|
row.prices = JSON.parse(JSON.stringify(skeleton));
|
||||||
}
|
} else {
|
||||||
if (typeof row.expanded === 'undefined') {
|
const globalCodes = new Set(skeleton.map(p => byCode(p.currency)));
|
||||||
row.expanded = false;
|
// keep only currencies that still exist globally
|
||||||
}
|
row.prices = row.prices.filter(p => p && globalCodes.has(byCode(p.currency)));
|
||||||
return row;
|
// add newly-added global currencies
|
||||||
|
skeleton.forEach(skel => {
|
||||||
|
const code = byCode(skel.currency);
|
||||||
|
const exists = row.prices.some(p => byCode(p.currency) === code);
|
||||||
|
if (!exists) row.prices.push(JSON.parse(JSON.stringify(skel)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still empty (e.g., multicurrency off), seed with skeleton
|
||||||
|
if (!Array.isArray(row.prices) || row.prices.length === 0) {
|
||||||
|
row.prices = JSON.parse(JSON.stringify(skeleton));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize digits and ensure numeric defaults
|
||||||
|
row.prices.forEach(p => {
|
||||||
|
const code = byCode(p.currency);
|
||||||
|
p.currency_decimal_digits = this.findDigitsForCode(code);
|
||||||
|
if (typeof p.regular_price === 'undefined' || p.regular_price === null) p.regular_price = '';
|
||||||
|
if (typeof p.sale_price === 'undefined' || p.sale_price === null) p.sale_price = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort so default currency is always first
|
||||||
|
const defaultCode = this.getDefaultCurrencyCode();
|
||||||
|
row.prices.sort((a, b) => {
|
||||||
|
const byCode = val => String(val).split(':::')[0];
|
||||||
|
if (byCode(a.currency) === defaultCode) return -1;
|
||||||
|
if (byCode(b.currency) === defaultCode) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove any legacy single-currency fields if present
|
||||||
|
delete row.price;
|
||||||
|
delete row.sale;
|
||||||
|
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultCurrencyCode() {
|
||||||
|
// Try multiple sources for robustness
|
||||||
|
const triple = (window.product_details && window.product_details.default_currency) || '';
|
||||||
|
const codeFromTriple = String(triple).split(':::')[0];
|
||||||
|
const fallback = window.product_details && window.product_details.default_currency_code;
|
||||||
|
return codeFromTriple || fallback || '';
|
||||||
},
|
},
|
||||||
async loadProductVariables() {
|
async loadProductVariables() {
|
||||||
if (!this.productHasVariation) {
|
if (!this.productHasVariation) {
|
||||||
@@ -239,29 +428,29 @@ jQuery(function ($) {
|
|||||||
this.tableRows = newRows;
|
this.tableRows = newRows;
|
||||||
this.updateJson();
|
this.updateJson();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error building from attributes:", e);
|
console.warn("Attributes not available; initializing empty variations.");
|
||||||
this.tableRows = [];
|
this.tableRows = [];
|
||||||
this.updateJson();
|
this.updateJson();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAttributeRepeaterData() {
|
getAttributeRepeaterData() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 100;
|
const maxAttempts = 100;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const el = $('#variation-product_variation_attributes');
|
const el = $('input[name="product_variation_attributes"]');
|
||||||
if (el.length && el.val()) {
|
if (el.length && el.val()) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(el.val());
|
const data = JSON.parse(el.val());
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
resolve(Array.isArray(data) ? data : []);
|
resolve(Array.isArray(data) ? data : []);
|
||||||
} catch {
|
} catch (e) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
reject(new Error('Invalid JSON in attribute repeater'));
|
resolve([]);
|
||||||
}
|
}
|
||||||
} else if (++attempts >= maxAttempts) {
|
} else if (++attempts >= maxAttempts) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
reject(new Error('Attribute repeater data not found'));
|
resolve([]);
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
@@ -284,61 +473,109 @@ jQuery(function ($) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
setupAttributeRepeaterSync() {
|
setupAttributeRepeaterSync() {
|
||||||
const target = document.getElementById('variation-product_variation_attributes');
|
const rebuildDebounced = () => {
|
||||||
if (!target) return;
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
clearTimeout(this._debounceTimer);
|
clearTimeout(this._debounceTimer);
|
||||||
this._debounceTimer = setTimeout(() => this.buildFromAttributes(), 300);
|
this._debounceTimer = setTimeout(() => {
|
||||||
});
|
this.buildFromAttributes();
|
||||||
observer.observe(target, {
|
this.updateJson();
|
||||||
attributes: true,
|
}, 200);
|
||||||
attributeFilter: ['value']
|
};
|
||||||
});
|
|
||||||
this.attributeRepeaterWatcher = observer;
|
// Root hidden input that contains the full JSON
|
||||||
},
|
const rootInput = document.querySelector('input[name="product_variation_attributes"]');
|
||||||
async syncManualPrices() {
|
if (rootInput) {
|
||||||
const el = document.getElementById('general-product_prices');
|
// Listen to direct input/change
|
||||||
if (el && el.value) {
|
rootInput.addEventListener('input', rebuildDebounced);
|
||||||
try {
|
rootInput.addEventListener('change', rebuildDebounced);
|
||||||
const pricesData = JSON.parse(el.value);
|
|
||||||
this.manualPrices = Array.isArray(pricesData) ? pricesData : [];
|
// Observe value attribute changes (WPCFTO updates .value programmatically)
|
||||||
} catch (e) {
|
const obs = new MutationObserver(rebuildDebounced);
|
||||||
this.manualPrices = [];
|
obs.observe(rootInput, { attributes: true, attributeFilter: ['value'] });
|
||||||
}
|
this.attributeRepeaterWatcher = obs;
|
||||||
} else {
|
// Fallback poller in case the repeater mutates value without firing events
|
||||||
this.manualPrices = [];
|
this._lastAttrJson = (rootInput && rootInput.value) || '';
|
||||||
|
if (this._attrPoller) clearInterval(this._attrPoller);
|
||||||
|
this._attrPoller = setInterval(() => {
|
||||||
|
const el = document.querySelector('input[name="product_variation_attributes"]');
|
||||||
|
if (!el) return;
|
||||||
|
const curr = el.value || '';
|
||||||
|
if (curr !== this._lastAttrJson) {
|
||||||
|
this._lastAttrJson = curr;
|
||||||
|
rebuildDebounced();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also watch nested variation lists to catch intermediate states before root JSON updates
|
||||||
|
const nestedSelector = 'input[name^="product_variation_attributes_"][name$="_attribute_variations"]';
|
||||||
|
const attachNestedObservers = () => {
|
||||||
|
document.querySelectorAll(nestedSelector).forEach((inp) => {
|
||||||
|
// Skip if already tagged
|
||||||
|
if (inp.__fpObserved) return;
|
||||||
|
inp.__fpObserved = true;
|
||||||
|
inp.addEventListener('input', rebuildDebounced);
|
||||||
|
inp.addEventListener('change', rebuildDebounced);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
attachNestedObservers();
|
||||||
|
|
||||||
|
// Also watch variation title inputs for changes
|
||||||
|
const titleSelector = 'input[name^="product_variation_attributes_"][name$="_variation_label"]';
|
||||||
|
const attachTitleObservers = () => {
|
||||||
|
document.querySelectorAll(titleSelector).forEach(inp => {
|
||||||
|
if (inp.__fpTitleObserved) return;
|
||||||
|
inp.__fpTitleObserved = true;
|
||||||
|
inp.addEventListener('input', rebuildDebounced);
|
||||||
|
inp.addEventListener('change', rebuildDebounced);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
attachTitleObservers();
|
||||||
|
|
||||||
|
// Also watch variation value inputs for changes
|
||||||
|
const valueSelector = 'input[name^="product_variation_attributes_"][name$="_variation_value"]';
|
||||||
|
const attachValueObservers = () => {
|
||||||
|
document.querySelectorAll(valueSelector).forEach(inp => {
|
||||||
|
if (inp.__fpValueObserved) return;
|
||||||
|
inp.__fpValueObserved = true;
|
||||||
|
inp.addEventListener('input', rebuildDebounced);
|
||||||
|
inp.addEventListener('change', rebuildDebounced);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
attachValueObservers();
|
||||||
|
|
||||||
|
// Trigger initial build right away if attributes already present
|
||||||
|
this.buildFromAttributes();
|
||||||
|
|
||||||
|
// Re-attach on WPCFTO repeater events
|
||||||
|
$(document).on('repeater-item-added repeater-item-removed', (ev, repeater) => {
|
||||||
|
if (!repeater) return;
|
||||||
|
if (repeater.field_name && String(repeater.field_name).indexOf('product_variation_attributes') === 0) {
|
||||||
|
attachNestedObservers();
|
||||||
|
attachTitleObservers();
|
||||||
|
attachValueObservers();
|
||||||
|
rebuildDebounced();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure table rebuild when attributes repeater changes
|
||||||
|
$(document).on('change input', '.product_variation_attributes.repeater input, .product_variation_attributes.repeater select', rebuildDebounced);
|
||||||
},
|
},
|
||||||
setupManualPricesSync() {
|
findFirstMissingDefault() {
|
||||||
const target = document.getElementById('general-product_prices');
|
const defCode = this.getDefaultCurrencyCode();
|
||||||
if (!target) return;
|
for (let i = 0; i < this.tableRows.length; i++) {
|
||||||
const observer = new MutationObserver(async () => {
|
const row = this.tableRows[i];
|
||||||
await this.syncManualPrices();
|
if (!row || !Array.isArray(row.prices)) continue;
|
||||||
if (this.pricingMethod === 'manual') {
|
const entry = row.prices.find(p => String(p.currency).split(':::')[0] === defCode);
|
||||||
this.tableRows.forEach(row => {
|
const reg = entry && entry.regular_price != null ? String(entry.regular_price).trim() : '';
|
||||||
const newPricesFromRepeater = this.manualPrices;
|
if (!reg) {
|
||||||
let reconciledPrices = row.prices.filter(existingPrice =>
|
return { index: i, rowLabel: row.name || `Row ${i+1}`, currencyCode: defCode };
|
||||||
newPricesFromRepeater.some(newPrice => newPrice.currency === existingPrice.currency)
|
}
|
||||||
);
|
}
|
||||||
newPricesFromRepeater.forEach(newPrice => {
|
return null;
|
||||||
const isAlreadyThere = reconciledPrices.some(p => p.currency === newPrice.currency);
|
|
||||||
if (!isAlreadyThere) {
|
|
||||||
reconciledPrices.push(JSON.parse(JSON.stringify(newPrice)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.prices = reconciledPrices;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.updateJson();
|
|
||||||
});
|
|
||||||
observer.observe(target, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['value']
|
|
||||||
});
|
|
||||||
this.manualPricesWatcher = observer;
|
|
||||||
},
|
},
|
||||||
updateJson() {
|
updateJson() {
|
||||||
this.jsonValue = JSON.stringify(this.tableRows);
|
this.jsonValue = JSON.stringify(this.tableRows);
|
||||||
|
this.$nextTick(() => this.enforcePriceInputStates());
|
||||||
},
|
},
|
||||||
async rebuildTable() {
|
async rebuildTable() {
|
||||||
await this.initializeOrClearTable();
|
await this.initializeOrClearTable();
|
||||||
|
|||||||
@@ -78,16 +78,20 @@ function formipay_default_currency($return='raw') {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formipay_global_currency_options() {
|
function formipay_global_currency_options($output = 'options_array') {
|
||||||
|
|
||||||
$formipay_settings = get_option('formipay_settings');
|
$formipay_settings = get_option('formipay_settings');
|
||||||
// $currencies = (false !== boolval($formipay_settings['enable_multicurrency'])) ? formipay_default_currency() : [];
|
// $currencies = (false !== boolval($formipay_settings['enable_multicurrency'])) ? formipay_default_currency() : [];
|
||||||
$currencies = [];
|
$currencies = [];
|
||||||
if(false !== boolval($formipay_settings['enable_multicurrency']) && !empty($formipay_settings['multicurrencies'])) {
|
if(false !== boolval($formipay_settings['enable_multicurrency']) && !empty($formipay_settings['multicurrencies'])) {
|
||||||
foreach($formipay_settings['multicurrencies'] as $currency){
|
if($output === 'options_array'){
|
||||||
$currency_value = $currency['currency'];
|
foreach($formipay_settings['multicurrencies'] as $currency){
|
||||||
$currency_label = formipay_get_currency_data_by_value($currency_value, 'title');
|
$currency_value = $currency['currency'];
|
||||||
$currencies[$currency_value] = $currency_label;
|
$currency_label = formipay_get_currency_data_by_value($currency_value, 'title');
|
||||||
|
$currencies[$currency_value] = $currency_label;
|
||||||
|
}
|
||||||
|
}elseif($output == 'raw'){
|
||||||
|
$currencies = $formipay_settings['multicurrencies'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if(empty($currencies)){
|
// if(empty($currencies)){
|
||||||
@@ -98,6 +102,49 @@ function formipay_global_currency_options() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_global_currency_array() {
|
||||||
|
$multicurrency = formipay_is_multi_currency_active();
|
||||||
|
$global_currencies = formipay_global_currency_options('raw');
|
||||||
|
$default_currency = formipay_default_currency();
|
||||||
|
|
||||||
|
$product_currency_group = [];
|
||||||
|
|
||||||
|
$ifSingleCurrency = true;
|
||||||
|
if(boolval($multicurrency)){
|
||||||
|
$ifSingleCurrency = false;
|
||||||
|
if(count($global_currencies) === 1){
|
||||||
|
$ifSingleCurrency = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(false === $ifSingleCurrency){
|
||||||
|
// $currency_sort = [];
|
||||||
|
$default_sort_key = null;
|
||||||
|
foreach($global_currencies as $key => $currency){
|
||||||
|
$currency_value = $currency['currency'];
|
||||||
|
if($currency_value === $default_currency){
|
||||||
|
$default_sort_key = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$currency_sort = [$default_sort_key => $global_currencies[$default_sort_key]];
|
||||||
|
unset($global_currencies[$default_sort_key]);
|
||||||
|
$global_currencies = $currency_sort + $global_currencies;
|
||||||
|
}else{
|
||||||
|
if(false === boolval($multicurrency)){
|
||||||
|
$global_currencies = [
|
||||||
|
[
|
||||||
|
'currency' => formipay_default_currency(),
|
||||||
|
'decimal_digits' => formipay_default_currency('decimal_digits'),
|
||||||
|
'decimal_symbol' => formipay_default_currency('decimal_symbol'),
|
||||||
|
'thousand_separator' => formipay_default_currency('thousand_separator'),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $global_currencies;
|
||||||
|
}
|
||||||
|
|
||||||
function formipay_country_array() {
|
function formipay_country_array() {
|
||||||
|
|
||||||
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/country.json');
|
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/country.json');
|
||||||
|
|||||||
@@ -9,16 +9,10 @@
|
|||||||
|
|
||||||
<th data-cell="weight" v-if="isPhysical">{{ product_details.variation_table.th_weight }}</th>
|
<th data-cell="weight" v-if="isPhysical">{{ product_details.variation_table.th_weight }}</th>
|
||||||
|
|
||||||
<template v-if="pricingMethod !== 'manual'">
|
<th v-if="showFlatPricing" data-cell="price">{{ product_details.variation_table.th_price }}</th>
|
||||||
<th data-cell="price">{{ product_details.variation_table.th_price }}</th>
|
<th v-if="showFlatPricing" data-cell="sale">{{ product_details.variation_table.th_sale }}</th>
|
||||||
<th data-cell="sale">{{ product_details.variation_table.th_sale }}</th>
|
<th v-else data-cell="prices">{{ product_details.variation_table.th_prices_overall }}</th>
|
||||||
</template>
|
<th class="toggle-column" v-if="!showFlatPricing"></th>
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<th data-cell="prices">{{ product_details.variation_table.th_prices_overall }}</th>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<th class="toggle-column"></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody v-if="tableRows.length > 0">
|
<tbody v-if="tableRows.length > 0">
|
||||||
@@ -44,23 +38,30 @@
|
|||||||
<input type="number" v-model.number="row.weight" step="0.01" style="width:100%; text-align:right;" :disabled="!row.active" />
|
<input type="number" v-model.number="row.weight" step="0.01" style="width:100%; text-align:right;" :disabled="!row.active" />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<template v-if="pricingMethod !== 'manual'">
|
<td v-if="showFlatPricing" data-cell="price">
|
||||||
<td data-cell="price">
|
<price-input
|
||||||
<price-input :value="row.price" @input="row.price = $event" :currency-symbol="currencySymbol" :currency-decimal-digits="currencyDecimalDigits" :disabled="!row.active" />
|
:value="defaultEntry(row).regular_price"
|
||||||
</td>
|
@input="defaultEntry(row).regular_price = $event"
|
||||||
<td data-cell="sale">
|
:currency-symbol="defaultEntry(row).currency.split(':::')[2] || defaultEntry(row).currency.split(':::')[0]"
|
||||||
<price-input :value="row.sale" @input="row.sale = $event" :currency-symbol="currencySymbol" :currency-decimal-digits="currencyDecimalDigits" :disabled="!row.active" />
|
:currency-decimal-digits="parseInt(defaultEntry(row).currency_decimal_digits)"
|
||||||
</td>
|
:disabled="!row.active"
|
||||||
</template>
|
/>
|
||||||
|
</td>
|
||||||
|
<td v-if="showFlatPricing" data-cell="sale">
|
||||||
|
<price-input
|
||||||
|
:value="defaultEntry(row).sale_price"
|
||||||
|
@input="defaultEntry(row).sale_price = $event"
|
||||||
|
:currency-symbol="defaultEntry(row).currency.split(':::')[2] || defaultEntry(row).currency.split(':::')[0]"
|
||||||
|
:currency-decimal-digits="parseInt(defaultEntry(row).currency_decimal_digits)"
|
||||||
|
:disabled="!row.active"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
<template v-else>
|
<td v-else data-cell="prices" style="text-align:center;">
|
||||||
<td data-cell="prices" class="manual-price-hint">
|
<span @click="row.expanded = !row.expanded">{{ product_details.variation_table.manual_price_hint }}</span>
|
||||||
<span @click="row.expanded = !row.expanded">{{ product_details.variation_table.manual_price_hint }}</span>
|
</td>
|
||||||
</td>
|
<td class="toggle-column" style="text-align:center;" v-if="!showFlatPricing">
|
||||||
</template>
|
<button v-if="row.prices.length > 0" @click="row.expanded = !row.expanded" class="button-link child-row-toggle" :class="row.expanded ? 'active' : ''" type="button">
|
||||||
|
|
||||||
<td class="toggle-column" style="text-align:center;">
|
|
||||||
<button v-if="pricingMethod === 'manual' && row.prices.length > 0" @click="row.expanded = !row.expanded" class="button-link child-row-toggle" :class="row.expanded ? 'active' : ''" type="button">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr v-if="pricingMethod === 'manual' && row.expanded" :key="row.key + '-details'" class="variation-details-row">
|
<tr v-if="!showFlatPricing && row.expanded" :key="row.key + '-details'" class="variation-details-row">
|
||||||
<td :colspan="isPhysical ? 7 : 6">
|
<td :colspan="isPhysical ? 7 : 6">
|
||||||
<div class="variation-details-content">
|
<div class="variation-details-content">
|
||||||
<table class="wpcfto-table inner-table" v-if="row.prices.length > 0">
|
<table class="wpcfto-table inner-table" v-if="row.prices.length > 0">
|
||||||
|
|||||||
@@ -187,13 +187,111 @@ class Coupon {
|
|||||||
|
|
||||||
public function rule_config($fields) {
|
public function rule_config($fields) {
|
||||||
|
|
||||||
|
$multicurrency = formipay_is_multi_currency_active();
|
||||||
|
$all_currencies = formipay_currency_as_options();
|
||||||
|
$global_selected_currencies = formipay_global_currency_options();
|
||||||
|
$global_currencies = formipay_global_currency_options('raw');
|
||||||
|
$default_currency = formipay_default_currency();
|
||||||
|
|
||||||
// Group : Rules
|
// Group : Rules
|
||||||
$rules_group = array(
|
$rules_group_1 = array(
|
||||||
|
'rules_general_group' => array(
|
||||||
|
'type' => 'group_title',
|
||||||
|
'group' => 'started'
|
||||||
|
),
|
||||||
'active' => array(
|
'active' => array(
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'label' => __( 'Active this coupon', 'formipay' ),
|
'label' => __( 'Active this coupon', 'formipay' ),
|
||||||
'value' => true
|
'value' => true,
|
||||||
),
|
),
|
||||||
|
'type' => array(
|
||||||
|
'type' => 'radio',
|
||||||
|
'label' => __( 'Type', 'formipay' ),
|
||||||
|
'required' => true,
|
||||||
|
'options' => array(
|
||||||
|
'fixed' => __( 'Fixed', 'formipay' ),
|
||||||
|
'percentage' => __( 'Percentage', 'formipay' )
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'amount_percentage' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'label' => __( 'Amount', 'formipay' ),
|
||||||
|
'required' => true,
|
||||||
|
'dependency' => array(
|
||||||
|
'key' => 'type',
|
||||||
|
'value' => 'percentage'
|
||||||
|
),
|
||||||
|
'group' => 'ended'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules_group_2 = [];
|
||||||
|
|
||||||
|
$global_currencies = get_global_currency_array();
|
||||||
|
$default_currency = formipay_default_currency();
|
||||||
|
|
||||||
|
$rules_group_2['discount_amount_wrapper'] = array(
|
||||||
|
'type' => 'group_title',
|
||||||
|
'label' => __( 'Discount Amount', 'formipay' ),
|
||||||
|
'dependency' => array(
|
||||||
|
'key' => 'type',
|
||||||
|
'value' => 'fixed'
|
||||||
|
),
|
||||||
|
'group' => 'started',
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules_group_max['discount_max_amount_wrapper'] = array(
|
||||||
|
'type' => 'group_title',
|
||||||
|
'label' => __( 'Max Discount Amount', 'formipay' ),
|
||||||
|
'description' => __( 'Leave empty to not limit the max discount amount.', 'formipay' ),
|
||||||
|
'group' => 'started',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach($global_currencies as $currency){
|
||||||
|
$default_currency_symbol = formipay_get_currency_data_by_value($default_currency, 'symbol');
|
||||||
|
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||||
|
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
||||||
|
$decimal_digits = intval($currency['decimal_digits']);
|
||||||
|
$step = $decimal_digits * 10;
|
||||||
|
$step = $step > 0 ? 1 / $step : 1;
|
||||||
|
|
||||||
|
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
|
||||||
|
'type' => 'number',
|
||||||
|
'label' => sprintf(
|
||||||
|
__( '<img src="%s" width="18" /> Amount in %s', 'formipay' ),
|
||||||
|
formipay_get_flag_by_currency($currency['currency']),
|
||||||
|
$currency_symbol
|
||||||
|
),
|
||||||
|
'step' => $step,
|
||||||
|
'min' => 0,
|
||||||
|
'required' => true,
|
||||||
|
'placeholder' => __( 'Enter Amount...', 'formipay' ),
|
||||||
|
'dependency' => array(
|
||||||
|
'key' => 'type',
|
||||||
|
'value' => 'fixed'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules_group_max['max_amount_'.$currency_symbol] = array(
|
||||||
|
'type' => 'number',
|
||||||
|
'label' => sprintf(
|
||||||
|
__( '<img src="%s" width="18" /> Max Amount in %s', 'formipay' ),
|
||||||
|
formipay_get_flag_by_currency($currency['currency']),
|
||||||
|
$currency_symbol
|
||||||
|
),
|
||||||
|
'step' => $step,
|
||||||
|
'min' => 0,
|
||||||
|
'placeholder' => __( 'Enter Max Amount...', 'formipay' )
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
$last_rules_group_2 = array_key_last($rules_group_2);
|
||||||
|
$rules_group_2[$last_rules_group_2]['group'] = 'ended';
|
||||||
|
|
||||||
|
$last_rules_group_max = array_key_last($rules_group_max);
|
||||||
|
$rules_group_max[$last_rules_group_max]['group'] = 'ended';
|
||||||
|
|
||||||
|
$rules_group_3 = array(
|
||||||
'rules_group' => array(
|
'rules_group' => array(
|
||||||
'type' => 'group_title',
|
'type' => 'group_title',
|
||||||
'label' => __( 'Rules', 'formipay' ),
|
'label' => __( 'Rules', 'formipay' ),
|
||||||
@@ -209,19 +307,6 @@ class Coupon {
|
|||||||
- <b>save10</b> is invalid<br>
|
- <b>save10</b> is invalid<br>
|
||||||
- <b>Save10</b> is invalid.', 'formipay' )
|
- <b>Save10</b> is invalid.', 'formipay' )
|
||||||
),
|
),
|
||||||
'type' => array(
|
|
||||||
'type' => 'radio',
|
|
||||||
'label' => __( 'Type', 'formipay' ),
|
|
||||||
'options' => array(
|
|
||||||
'fixed' => __( 'Fixed', 'formipay' ),
|
|
||||||
'percentage' => __( 'Percentage', 'formipay' )
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'amount' => array(
|
|
||||||
'type' => 'number',
|
|
||||||
'label' => __( 'Amount', 'formipay' ),
|
|
||||||
'description' => sprintf('<span style="color: red;">' . __( 'Be carefully, this amount is not regarding the currency.', 'formipay') . '</span>' )
|
|
||||||
),
|
|
||||||
'free_shipping' => array(
|
'free_shipping' => array(
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'label' => __( 'Free Shipping', 'formipay' ),
|
'label' => __( 'Free Shipping', 'formipay' ),
|
||||||
@@ -236,13 +321,15 @@ class Coupon {
|
|||||||
'value' => 'fixed'
|
'value' => 'fixed'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'max_discount' => array(
|
// 'max_discount' => array(
|
||||||
'type' => 'number',
|
// 'type' => 'number',
|
||||||
'label' => __( 'Max Discount Amount', 'formipay' ),
|
// 'label' => __( 'Max Discount Amount', 'formipay' ),
|
||||||
'description' => __( 'Leave it empty to not limit the max discount amount.', 'formipay' )
|
// 'description' => __( 'Leave it empty to not limit the max discount amount.', 'formipay' )
|
||||||
),
|
// ),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$rules_group = $rules_group_1 + $rules_group_2 + $rules_group_max + $rules_group_3;
|
||||||
|
|
||||||
$rules_group = apply_filters( 'formipay/coupon-settings/tab:rules/group:rules', $rules_group );
|
$rules_group = apply_filters( 'formipay/coupon-settings/tab:rules/group:rules', $rules_group );
|
||||||
|
|
||||||
$last_rules_group = array_key_last($rules_group);
|
$last_rules_group = array_key_last($rules_group);
|
||||||
@@ -290,8 +377,19 @@ class Coupon {
|
|||||||
'forms' => array(
|
'forms' => array(
|
||||||
'type' => 'autocomplete',
|
'type' => 'autocomplete',
|
||||||
'post_type' => array('formipay-form'),
|
'post_type' => array('formipay-form'),
|
||||||
'label' => __( 'These forms can use the coupon', 'formipay' ),
|
'label' => __( 'Forms', 'formipay' ),
|
||||||
'description' => __( 'Leave it empty to enable all forms to use this coupon. <span style="color: red;">Please be carefully, this coupon amount is not regarding the currency of the selected form.</span>', 'formipay' )
|
'description' => __( 'Only selected form(s) can use the coupon. Leave empty to apply to all forms.', 'formipay' )
|
||||||
|
),
|
||||||
|
'products' => array(
|
||||||
|
'type' => 'autocomplete',
|
||||||
|
'post_type' => array('formipay-product'),
|
||||||
|
'label' => __( 'Products', 'formipay' ),
|
||||||
|
'description' => __( 'Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay' )
|
||||||
|
),
|
||||||
|
'customers' => array(
|
||||||
|
'type' => 'autocomplete',
|
||||||
|
'label' => __( 'Customers', 'formipay' ),
|
||||||
|
'description' => __( 'Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay' )
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class Customer {
|
|||||||
add_filter( 'formipay/global-settings/tab:general', [$this, 'global_settings'], 20 );
|
add_filter( 'formipay/global-settings/tab:general', [$this, 'global_settings'], 20 );
|
||||||
add_filter( 'formipay/form-config', [$this, 'add_menu_on_product_setting'], 100 );
|
add_filter( 'formipay/form-config', [$this, 'add_menu_on_product_setting'], 100 );
|
||||||
|
|
||||||
|
// For Coupon Autocomplete
|
||||||
|
add_filter( 'stm_wpcfto_autocomplete_customers', [$this, 'autocomplete_options'], 100, 2 );
|
||||||
|
|
||||||
// Register new customer by order submitted
|
// Register new customer by order submitted
|
||||||
add_action( 'formipay/order/new', [$this, 'add_customer'] );
|
add_action( 'formipay/order/new', [$this, 'add_customer'] );
|
||||||
}
|
}
|
||||||
@@ -448,4 +451,11 @@ class Customer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function autocomplete_options($r, $args) {
|
||||||
|
|
||||||
|
console.log(print_r($args, true));
|
||||||
|
return $r;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -175,12 +175,11 @@ class Product {
|
|||||||
wp_enqueue_style( 'product-details', FORMIPAY_URL . 'admin/assets/css/admin-product-editor.css', [], FORMIPAY_VERSION, 'all' );
|
wp_enqueue_style( 'product-details', FORMIPAY_URL . 'admin/assets/css/admin-product-editor.css', [], FORMIPAY_VERSION, 'all' );
|
||||||
wp_enqueue_script( 'product-details', FORMIPAY_URL . 'admin/assets/js/admin-product-editor.js', ['jquery', 'vue.js'], FORMIPAY_VERSION, true );
|
wp_enqueue_script( 'product-details', FORMIPAY_URL . 'admin/assets/js/admin-product-editor.js', ['jquery', 'vue.js'], FORMIPAY_VERSION, true );
|
||||||
wp_localize_script( 'product-details', 'product_details', [
|
wp_localize_script( 'product-details', 'product_details', [
|
||||||
|
'multicurrency' => formipay_is_multi_currency_active(),
|
||||||
|
// 'all_currencies' => formipay_currency_as_options(),
|
||||||
|
'global_selected_currencies' => formipay_global_currency_options(),
|
||||||
|
'global_currencies' => formipay_global_currency_options('raw'),
|
||||||
'default_currency' => formipay_default_currency(),
|
'default_currency' => formipay_default_currency(),
|
||||||
'default_currency_title' => formipay_default_currency('title'),
|
|
||||||
'default_currency_symbol' => formipay_default_currency('symbol'),
|
|
||||||
'default_currency_decimal_digits' => formipay_default_currency('decimal_digits'),
|
|
||||||
'default_currency_decimal_symbol' => formipay_default_currency('decimal_symbol'),
|
|
||||||
'default_currency_thousand_separator' => formipay_default_currency('thousand_separator'),
|
|
||||||
'product_type' => formipay_get_post_meta($product_id, 'product_type') ?: 'digital',
|
'product_type' => formipay_get_post_meta($product_id, 'product_type') ?: 'digital',
|
||||||
'product_is_physical' => formipay_get_post_meta($product_id, 'product_type') == 'physical',
|
'product_is_physical' => formipay_get_post_meta($product_id, 'product_type') == 'physical',
|
||||||
'product_pricing_method' => $pricing_method, // Pass pricing method to JS
|
'product_pricing_method' => $pricing_method, // Pass pricing method to JS
|
||||||
@@ -192,12 +191,16 @@ class Product {
|
|||||||
'th_price' => __('Regular Price', 'formipay'),
|
'th_price' => __('Regular Price', 'formipay'),
|
||||||
'th_sale' => __('Sale Price', 'formipay'),
|
'th_sale' => __('Sale Price', 'formipay'),
|
||||||
'th_weight' => __('Weight', 'formipay'),
|
'th_weight' => __('Weight', 'formipay'),
|
||||||
'th_prices_overall' => __('Prices', 'formipay'), // New header for manual mode
|
'th_prices_overall' => __('Prices', 'formipay'),
|
||||||
'manual_price_hint' => __('Prices set manually below', 'formipay'), // New hint for manual mode cell
|
'manual_price_hint' => __('Prices set manually below', 'formipay'),
|
||||||
'child_th_currency' => __('Currency', 'formipay'), // Child table header
|
'child_th_currency' => __('Currency', 'formipay'),
|
||||||
'child_th_regular' => __('Regular Price', 'formipay'), // Child table header
|
'child_th_regular' => __('Regular Price', 'formipay'),
|
||||||
'child_th_sale' => __('Sale Price', 'formipay'), // Child table header
|
'child_th_sale' => __('Sale Price', 'formipay'),
|
||||||
'no_variations' => __( 'No variations available', 'formipay' )
|
'no_currencies' => __( 'No currencies available', 'formipay' ),
|
||||||
|
'no_variations' => __( 'No variations available', 'formipay' ),
|
||||||
|
'child_placeholder_regular' => __( 'Enter Regular Price', 'formipay' ),
|
||||||
|
'child_placeholder_sale' => __( 'Enter Sale Price', 'formipay' ),
|
||||||
|
'error_missing_default_price' => __( 'Please fill Regular Price for default currency (%1$s) in variation "%2$s".', 'formipay' ),
|
||||||
]
|
]
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
@@ -279,103 +282,157 @@ class Product {
|
|||||||
$product_details_group[$last_product_details_group]['group'] = 'ended';
|
$product_details_group[$last_product_details_group]['group'] = 'ended';
|
||||||
|
|
||||||
// Product Currency Group
|
// Product Currency Group
|
||||||
$product_currency_group = array(
|
// $product_currency_group = array(
|
||||||
'setting_product_pricing' => array(
|
// 'setting_product_pricing' => array(
|
||||||
|
// 'type' => 'group_title',
|
||||||
|
// 'label' => __( 'Pricing', 'formipay' ),
|
||||||
|
// 'description' => __( 'Carefully set these options below.', 'formipay' ),
|
||||||
|
// 'group' => 'started',
|
||||||
|
// ),
|
||||||
|
// 'product_pricing_method' => array(
|
||||||
|
// 'type' => 'radio',
|
||||||
|
// 'label' => __('Product Pricing Method', 'formipay'),
|
||||||
|
// 'options' => array(
|
||||||
|
// 'manual' => 'Manual Price Set',
|
||||||
|
// 'auto' => 'Auto via Currency Exchange API'
|
||||||
|
// ),
|
||||||
|
// 'value' => 'manual',
|
||||||
|
// 'required' => true
|
||||||
|
// ),
|
||||||
|
// 'product_regular_price' => array(
|
||||||
|
// 'type' => 'number',
|
||||||
|
// 'label' => sprintf( __('Regular Price (%s)', 'formipay'), formipay_default_currency('symbol') ),
|
||||||
|
// 'value' => 0,
|
||||||
|
// 'step' => 0.01,
|
||||||
|
// 'required' => true,
|
||||||
|
// 'dependency' => [
|
||||||
|
// 'key' => 'product_pricing_method',
|
||||||
|
// 'value' => 'auto'
|
||||||
|
// ]
|
||||||
|
// ),
|
||||||
|
// 'product_sale_price' => array(
|
||||||
|
// 'type' => 'number',
|
||||||
|
// 'label' => sprintf( __('Sale Price (%s)', 'formipay'), formipay_default_currency('symbol') ),
|
||||||
|
// 'value' => 0,
|
||||||
|
// 'step' => 0.01,
|
||||||
|
// 'dependency' => [
|
||||||
|
// 'key' => 'product_pricing_method',
|
||||||
|
// 'value' => 'auto'
|
||||||
|
// ]
|
||||||
|
// ),
|
||||||
|
// 'product_prices' => [
|
||||||
|
// 'type' => 'repeater',
|
||||||
|
// 'label' => __( 'Prices', 'formipay' ),
|
||||||
|
// 'description' => __( 'You can set price with multiple currency', 'formipay' ),
|
||||||
|
// 'fields' => [
|
||||||
|
// 'regular_price' => array(
|
||||||
|
// 'type' => 'number',
|
||||||
|
// 'label' => __('Regular Price', 'formipay'),
|
||||||
|
// 'value' => 0,
|
||||||
|
// 'step' => 0.01,
|
||||||
|
// 'required' => true
|
||||||
|
// ),
|
||||||
|
// 'sale_price' => array(
|
||||||
|
// 'type' => 'number',
|
||||||
|
// 'label' => __('Sale Price', 'formipay'),
|
||||||
|
// 'value' => 0,
|
||||||
|
// 'step' => 0.01
|
||||||
|
// ),
|
||||||
|
// 'currency' => array(
|
||||||
|
// 'type' => 'select',
|
||||||
|
// 'label' => __('Currency', 'formipay'),
|
||||||
|
// 'value' => 'IDR:::Indonesian rupiah:::Rp',
|
||||||
|
// 'options' => formipay_currency_as_options(),
|
||||||
|
// 'required' => true,
|
||||||
|
// 'searchable' => true,
|
||||||
|
// 'is_group_title' => true
|
||||||
|
// ),
|
||||||
|
// 'currency_decimal_digits' => array(
|
||||||
|
// 'type' => 'number',
|
||||||
|
// 'label' => __('Decimal Digits', 'formipay'),
|
||||||
|
// 'value' => '2',
|
||||||
|
// 'required' => true
|
||||||
|
// ),
|
||||||
|
// 'currency_decimal_symbol' => array(
|
||||||
|
// 'type' => 'text',
|
||||||
|
// 'label' => __('Decimal Symbol', 'formipay'),
|
||||||
|
// 'value' => '.',
|
||||||
|
// 'required' => true
|
||||||
|
// ),
|
||||||
|
// 'currency_thousand_separator' => array(
|
||||||
|
// 'type' => 'text',
|
||||||
|
// 'label' => __('Thousand Separator Symbol', 'formipay'),
|
||||||
|
// 'value' => ',',
|
||||||
|
// 'required' => true
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// 'dependency' => [
|
||||||
|
// 'key' => 'product_pricing_method',
|
||||||
|
// 'value' => 'manual'
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
|
||||||
|
$global_currencies = get_global_currency_array();
|
||||||
|
$default_currency = formipay_default_currency();
|
||||||
|
|
||||||
|
foreach($global_currencies as $currency){
|
||||||
|
$default_currency_symbol = formipay_get_currency_data_by_value($default_currency, 'symbol');
|
||||||
|
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||||
|
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
||||||
|
$decimal_digits = intval($currency['decimal_digits']);
|
||||||
|
$step = $decimal_digits * 10;
|
||||||
|
$step = $step > 0 ? 1 / $step : 1;
|
||||||
|
$product_currency_group['setting_product_pricing_'.$currency_symbol] = array(
|
||||||
'type' => 'group_title',
|
'type' => 'group_title',
|
||||||
'label' => __( 'Pricing', 'formipay' ),
|
'label' => sprintf(
|
||||||
'description' => __( 'Carefully set these options below.', 'formipay' ),
|
__( '<img src="%s" width="18" /> Pricing in %s (%s)', 'formipay' ),
|
||||||
'group' => 'started',
|
formipay_get_flag_by_currency($currency['currency']),
|
||||||
),
|
$currency_title,
|
||||||
'product_pricing_method' => array(
|
$currency_symbol
|
||||||
'type' => 'radio',
|
|
||||||
'label' => __('Product Pricing Method', 'formipay'),
|
|
||||||
'options' => array(
|
|
||||||
'manual' => 'Manual Price Set',
|
|
||||||
'auto' => 'Auto via Currency Exchange API'
|
|
||||||
),
|
),
|
||||||
'value' => 'manual',
|
'group' => 'started',
|
||||||
'required' => true
|
);
|
||||||
),
|
$product_currency_group['setting_product_price_regular_'.$currency_symbol] = array(
|
||||||
'product_regular_price' => array(
|
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'label' => sprintf( __('Regular Price (%s)', 'formipay'), formipay_default_currency('symbol') ),
|
'label' => __( 'Regular Price', 'formipay' ),
|
||||||
'value' => 0,
|
'step' => $step,
|
||||||
'step' => 0.01,
|
'min' => 0,
|
||||||
'required' => true,
|
'placeholder' => __( 'Auto', 'formipay' )
|
||||||
'dependency' => [
|
);
|
||||||
'key' => 'product_pricing_method',
|
$product_currency_group['setting_product_price_sale_'.$currency_symbol] = array(
|
||||||
'value' => 'auto'
|
|
||||||
]
|
|
||||||
),
|
|
||||||
'product_sale_price' => array(
|
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'label' => sprintf( __('Sale Price (%s)', 'formipay'), formipay_default_currency('symbol') ),
|
'label' => __( '🎉 Sale Price', 'formipay' ),
|
||||||
'value' => 0,
|
'step' => $step,
|
||||||
'step' => 0.01,
|
'min' => 0,
|
||||||
'dependency' => [
|
'placeholder' => __( 'Auto', 'formipay' ),
|
||||||
'key' => 'product_pricing_method',
|
'group' => 'ended'
|
||||||
'value' => 'auto'
|
);
|
||||||
]
|
|
||||||
),
|
if(count($global_currencies) > 1){
|
||||||
'product_prices' => [
|
if($default_currency_symbol === $currency_symbol){
|
||||||
'type' => 'repeater',
|
$product_currency_group['setting_product_pricing_'.$currency_symbol]['description'] = sprintf(
|
||||||
'label' => __( 'Prices', 'formipay' ),
|
__( 'This is your default currency. <a href="%s" target="_blank">Change in Settings</a>', 'formipay' ),
|
||||||
'description' => __( 'You can set price with multiple currency', 'formipay' ),
|
admin_url('/admin.php?page=formipay-settings')
|
||||||
'fields' => [
|
);
|
||||||
'regular_price' => array(
|
$product_currency_group['setting_product_price_regular_'.$currency_symbol]['required'] = true;
|
||||||
'type' => 'number',
|
$product_currency_group['setting_product_price_regular_'.$currency_symbol]['placeholder'] = __( 'Enter Regular Price...', 'formipay' );
|
||||||
'label' => __('Regular Price', 'formipay'),
|
$product_currency_group['setting_product_price_sale_'.$currency_symbol]['placeholder'] = __( 'Enter Sale Price...', 'formipay' );
|
||||||
'value' => 0,
|
}else{
|
||||||
'step' => 0.01,
|
$product_currency_group['setting_product_pricing_'.$currency_symbol]['description'] = sprintf(
|
||||||
'required' => true
|
__( 'System will calculate the price in %s based on exchange rate against %s when you leave these empty.', 'formipay' ),
|
||||||
),
|
$currency_symbol, $default_currency_symbol
|
||||||
'sale_price' => array(
|
);
|
||||||
'type' => 'number',
|
}
|
||||||
'label' => __('Sale Price', 'formipay'),
|
}else{
|
||||||
'value' => 0,
|
$product_currency_group['setting_product_pricing_'.$currency_symbol]['label'] = __( 'Product Prices', 'formipay' );
|
||||||
'step' => 0.01
|
$product_currency_group['setting_product_price_regular_'.$currency_symbol]['placeholder'] = __( 'Enter Regular Price...', 'formipay' );
|
||||||
),
|
$product_currency_group['setting_product_price_sale_'.$currency_symbol]['placeholder'] = __( 'Enter Sale Price...', 'formipay' );
|
||||||
'currency' => array(
|
}
|
||||||
'type' => 'select',
|
}
|
||||||
'label' => __('Currency', 'formipay'),
|
|
||||||
'value' => 'IDR:::Indonesian rupiah:::Rp',
|
|
||||||
'options' => formipay_currency_as_options(),
|
|
||||||
'required' => true,
|
|
||||||
'searchable' => true,
|
|
||||||
'is_group_title' => true
|
|
||||||
),
|
|
||||||
'currency_decimal_digits' => array(
|
|
||||||
'type' => 'number',
|
|
||||||
'label' => __('Decimal Digits', 'formipay'),
|
|
||||||
'value' => '2',
|
|
||||||
'required' => true
|
|
||||||
),
|
|
||||||
'currency_decimal_symbol' => array(
|
|
||||||
'type' => 'text',
|
|
||||||
'label' => __('Decimal Symbol', 'formipay'),
|
|
||||||
'value' => '.',
|
|
||||||
'required' => true
|
|
||||||
),
|
|
||||||
'currency_thousand_separator' => array(
|
|
||||||
'type' => 'text',
|
|
||||||
'label' => __('Thousand Separator Symbol', 'formipay'),
|
|
||||||
'value' => ',',
|
|
||||||
'required' => true
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'dependency' => [
|
|
||||||
'key' => 'product_pricing_method',
|
|
||||||
'value' => 'manual'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
$product_currency_group = apply_filters( 'formipay/product-settings/tab:general/group:product-currency', $product_currency_group );
|
$product_currency_group = apply_filters( 'formipay/product-settings/tab:general/group:product-currency', $product_currency_group );
|
||||||
|
|
||||||
$last_product_currency_group = array_key_last($product_currency_group);
|
|
||||||
$product_currency_group[$last_product_currency_group]['group'] = 'ended';
|
|
||||||
|
|
||||||
|
|
||||||
$general_all_fields = array_merge($product_details_group, $product_currency_group);
|
$general_all_fields = array_merge($product_details_group, $product_currency_group);
|
||||||
|
|
||||||
$general_all_fields = apply_filters( 'formipay/product-settings/tab:general', $general_all_fields );
|
$general_all_fields = apply_filters( 'formipay/product-settings/tab:general', $general_all_fields );
|
||||||
@@ -420,47 +477,16 @@ class Product {
|
|||||||
'variation_label' => [
|
'variation_label' => [
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'label' => __( 'Title', 'formipay' ),
|
'label' => __( 'Title', 'formipay' ),
|
||||||
'description' => __( 'e.g. Color, Size, etc', 'formipay' ),
|
'description' => __( 'e.g. Red, XL, etc', 'formipay' ),
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'is_group_title' => true
|
'is_group_title' => true
|
||||||
],
|
],
|
||||||
'variation_value_type' => [
|
'variation_value' => [
|
||||||
'type' => 'select',
|
|
||||||
'label' => __( 'Value Type', 'formipay' ),
|
|
||||||
'options' => [
|
|
||||||
'text' => __( 'Text', 'formipay' ),
|
|
||||||
'number' => __( 'Number', 'formipay' ),
|
|
||||||
'color' => __( 'Color', 'formipay' ),
|
|
||||||
],
|
|
||||||
'value' => 'text'
|
|
||||||
],
|
|
||||||
'variation_value_text' => [
|
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'label' => __( 'Value', 'formipay' ),
|
'label' => __( 'Value', 'formipay' ),
|
||||||
'required' => true,
|
'description' => __( 'e.g. red, xl, etc', 'formipay' ),
|
||||||
'dependency' => array(
|
'required' => true
|
||||||
'key' => 'variation_value_type',
|
]
|
||||||
'value' => 'text'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'variation_value_number' => [
|
|
||||||
'type' => 'number',
|
|
||||||
'label' => __( 'Value', 'formipay' ),
|
|
||||||
'required' => true,
|
|
||||||
'dependency' => array(
|
|
||||||
'key' => 'variation_value_type',
|
|
||||||
'value' => 'number'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
'variation_value_color' => [
|
|
||||||
'type' => 'color',
|
|
||||||
'label' => __( 'Value', 'formipay' ),
|
|
||||||
'required' => true,
|
|
||||||
'dependency' => array(
|
|
||||||
'key' => 'variation_value_type',
|
|
||||||
'value' => 'color'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Vue.component('wpcfto_number', {
|
|||||||
data: function data() {
|
data: function data() {
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
|
min: 0,
|
||||||
|
max: '',
|
||||||
step: 1
|
step: 1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -23,6 +25,8 @@ Vue.component('wpcfto_number', {
|
|||||||
:placeholder="fields.placeholder ? fields.placeholder : 'Enter numbers...'"
|
:placeholder="fields.placeholder ? fields.placeholder : 'Enter numbers...'"
|
||||||
:id="field_id"
|
:id="field_id"
|
||||||
:step="step"
|
:step="step"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
:required="fields.required === true"
|
:required="fields.required === true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user