359 lines
18 KiB
JavaScript
359 lines
18 KiB
JavaScript
jQuery(function ($) {
|
|
|
|
$('a[href="admin.php?page=formipay-products"]').addClass('current').closest('li').addClass('current');
|
|
|
|
function autoset_variation_name() {
|
|
var repeater_single = $('.product_variation_attributes.repeater [parent_repeater="parent"] > .wpcfto-field-content > .wpcfto-repeater-single');
|
|
$.each(repeater_single, function (key, parent) {
|
|
var repeater_child = $(parent).find('[field_native_name="product_variation_attributes"]');
|
|
var attribute_name = repeater_child.find(`[name="product_variation_attributes_${key}_attribute_name"]`).val();
|
|
var attribute_type = repeater_child.find(`[name="product_variation_attributes_${key}_attribute_type"]`).val();
|
|
var repeater_child_single = repeater_child.find('.wpcfto-repeater-single');
|
|
$.each(repeater_child_single, function (index, child) {
|
|
var label_field = $(`input[name="product_variation_attributes_${key}_attribute_variations_${index}_variation_label"]`);
|
|
var name_field = $(`input[name="product_variation_attributes_${key}_attribute_variations_${index}_variation_name"]`);
|
|
var color_field = $(`input[name="product_variation_attributes_${key}_attribute_variations_${index}_variation_color"]`);
|
|
var color_field_row = color_field.closest('.wpcfto-repeater-field');
|
|
if (attribute_type == 'color') {
|
|
color_field_row.show();
|
|
} else {
|
|
color_field_row.hide();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
$(document).on('change blur', '[field_native_name_inner="variation_label"] input', function () {
|
|
autoset_variation_name();
|
|
});
|
|
|
|
$(document).on('click', '.stm_metaboxes_grid .stm_metaboxes_grid__inner .wpcfto-repeater .addArea', function () {
|
|
autoset_variation_name();
|
|
});
|
|
|
|
var onMetaboxLoaded = setInterval(() => {
|
|
var repeater_single = $('.product_variation_attributes.repeater [parent_repeater="parent"] > .wpcfto-field-content > .wpcfto-repeater-single');
|
|
if (repeater_single.length > 0) {
|
|
autoset_variation_name();
|
|
clearInterval(onMetaboxLoaded);
|
|
}
|
|
}, 250);
|
|
|
|
var waitForTable = setInterval(() => {
|
|
if ($('#product-variables-table').length > 0) {
|
|
clearInterval(waitForTable);
|
|
|
|
// --- PERBAIKAN UTAMA DI SINI ---
|
|
Vue.component('price-input', {
|
|
// Gunakan 'value' sebagai prop, sesuai konvensi v-model Vue 2
|
|
props: {
|
|
value: [Number, String], // Diubah dari modelValue
|
|
currencySymbol: String,
|
|
currencyDecimalDigits: {
|
|
type: Number,
|
|
default: 2
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
inputValue: this.value // Diubah dari modelValue
|
|
};
|
|
},
|
|
watch: {
|
|
// Amati perubahan pada prop 'value'
|
|
value(newValue) { // Diubah dari modelValue
|
|
this.inputValue = newValue;
|
|
}
|
|
},
|
|
methods: {
|
|
onInput(e) {
|
|
const value = e.target.value;
|
|
this.inputValue = value;
|
|
// Pancarkan event 'input', sesuai konvensi v-model Vue 2
|
|
this.$emit('input', value); // Diubah dari 'update:modelValue'
|
|
}
|
|
},
|
|
computed: {
|
|
stepValue() {
|
|
return Math.pow(10, -this.currencyDecimalDigits);
|
|
}
|
|
},
|
|
template: `
|
|
<div class="price-input-wrapper">
|
|
<span class="price-currency">{{ currencySymbol }}</span>
|
|
<input type="number" :value="inputValue" @input="onInput" :step="stepValue" placeholder="0" :disabled="disabled" />
|
|
</div>
|
|
`
|
|
});
|
|
// --- AKHIR DARI PERBAIKAN ---
|
|
|
|
new Vue({
|
|
el: '#product-variables-table',
|
|
data() {
|
|
return {
|
|
tableRows: [],
|
|
deletedKeys: [],
|
|
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,
|
|
jsonValue: '[]',
|
|
attributeRepeaterWatcher: null,
|
|
manualPricesWatcher: null,
|
|
_debounceTimer: null,
|
|
productType: window.product_details?.product_type || 'digital',
|
|
currencyDecimalDigits: parseInt(window.product_details?.default_currency_decimal_digits) || 2,
|
|
currencySymbol: window.product_details?.default_currency_symbol || '$'
|
|
};
|
|
},
|
|
async mounted() {
|
|
await this.initializeOrClearTable();
|
|
this.setupAttributeRepeaterSync();
|
|
this.setupManualPricesSync();
|
|
const typeRadios = document.querySelectorAll('input[name="product_type"]');
|
|
typeRadios.forEach(radio => {
|
|
radio.addEventListener('change', e => {
|
|
this.productType = e.target.value;
|
|
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"]');
|
|
if (hasVariationToggle) {
|
|
hasVariationToggle.addEventListener('change', async (e) => {
|
|
this.productHasVariation = e.target.checked;
|
|
await this.initializeOrClearTable();
|
|
});
|
|
}
|
|
},
|
|
beforeDestroy() {
|
|
if (this.attributeRepeaterWatcher) {
|
|
this.attributeRepeaterWatcher.disconnect();
|
|
}
|
|
if (this.manualPricesWatcher) {
|
|
this.manualPricesWatcher.disconnect();
|
|
}
|
|
},
|
|
methods: {
|
|
async initializeOrClearTable() {
|
|
if (!this.productHasVariation) {
|
|
this.tableRows = [];
|
|
this.updateJson();
|
|
return;
|
|
}
|
|
await this.syncManualPrices();
|
|
await this.loadProductVariables();
|
|
},
|
|
_ensureRowDataStructure(row) {
|
|
if (this.pricingMethod === 'manual') {
|
|
if (typeof row.prices === 'undefined') {
|
|
row.prices = JSON.parse(JSON.stringify(this.manualPrices));
|
|
delete row.price;
|
|
delete row.sale;
|
|
}
|
|
} else {
|
|
if (typeof row.price === 'undefined') {
|
|
row.price = 0;
|
|
row.sale = 0;
|
|
delete row.prices;
|
|
}
|
|
}
|
|
if (typeof row.expanded === 'undefined') {
|
|
row.expanded = false;
|
|
}
|
|
return row;
|
|
},
|
|
async loadProductVariables() {
|
|
if (!this.productHasVariation) {
|
|
this.tableRows = [];
|
|
this.updateJson();
|
|
return;
|
|
}
|
|
const postId = window.formipayProductId || $('input[name="post_ID"]').val();
|
|
if (!postId) {
|
|
await this.buildFromAttributes();
|
|
return;
|
|
}
|
|
try {
|
|
const res = await $.ajax({
|
|
url: ajaxurl,
|
|
method: 'POST',
|
|
data: {
|
|
action: 'get_product_variables',
|
|
post_id: postId
|
|
}
|
|
});
|
|
if (res.success && Array.isArray(res.data) && res.data.length) {
|
|
this.tableRows = res.data.map(row => this._ensureRowDataStructure(row));
|
|
this.deletedKeys = [];
|
|
this.updateJson();
|
|
} else {
|
|
await this.buildFromAttributes();
|
|
}
|
|
} catch {
|
|
await this.buildFromAttributes();
|
|
}
|
|
},
|
|
async buildFromAttributes() {
|
|
if (!this.productHasVariation) {
|
|
this.tableRows = [];
|
|
this.updateJson();
|
|
return;
|
|
}
|
|
try {
|
|
const attributes = await this.getAttributeRepeaterData();
|
|
if (!attributes.length) {
|
|
this.tableRows = [];
|
|
this.updateJson();
|
|
return;
|
|
}
|
|
const combinations = this.getAllCombinations(attributes);
|
|
const filtered = combinations.filter(c => !this.deletedKeys.includes(c.key));
|
|
const newRows = filtered.map(c => {
|
|
const existing = this.tableRows.find(r => r.key === c.key);
|
|
if (existing) {
|
|
this._ensureRowDataStructure(existing);
|
|
return Object.assign(existing, {
|
|
name: c.label
|
|
});
|
|
}
|
|
let newRowData = {
|
|
key: c.key,
|
|
name: c.label,
|
|
stock: '',
|
|
weight: 0,
|
|
active: true,
|
|
};
|
|
return this._ensureRowDataStructure(newRowData);
|
|
});
|
|
this.tableRows = newRows;
|
|
this.updateJson();
|
|
} catch (e) {
|
|
console.error("Error building from attributes:", e);
|
|
this.tableRows = [];
|
|
this.updateJson();
|
|
}
|
|
},
|
|
getAttributeRepeaterData() {
|
|
return new Promise((resolve, reject) => {
|
|
let attempts = 0;
|
|
const maxAttempts = 100;
|
|
const interval = setInterval(() => {
|
|
const el = $('#variation-product_variation_attributes');
|
|
if (el.length && el.val()) {
|
|
try {
|
|
const data = JSON.parse(el.val());
|
|
clearInterval(interval);
|
|
resolve(Array.isArray(data) ? data : []);
|
|
} catch {
|
|
clearInterval(interval);
|
|
reject(new Error('Invalid JSON in attribute repeater'));
|
|
}
|
|
} else if (++attempts >= maxAttempts) {
|
|
clearInterval(interval);
|
|
reject(new Error('Attribute repeater data not found'));
|
|
}
|
|
}, 50);
|
|
});
|
|
},
|
|
getAllCombinations(attributes) {
|
|
const attrVars = attributes
|
|
.map(attr => (attr.attribute_variations || []).map(v => ({
|
|
label: v.variation_label
|
|
})))
|
|
.filter(arr => arr.length > 0);
|
|
if (!attrVars.length) return [];
|
|
const combine = (arrs) => arrs.reduce((a, b) => a.flatMap(d => b.map(e => [].concat(d, e))));
|
|
const combos = combine(attrVars);
|
|
return combos.map(combo => {
|
|
const labels = Array.isArray(combo) ? combo.map(c => c.label) : [combo.label];
|
|
return {
|
|
key: labels.join('||'),
|
|
label: labels.join(' - ')
|
|
};
|
|
});
|
|
},
|
|
setupAttributeRepeaterSync() {
|
|
const target = document.getElementById('variation-product_variation_attributes');
|
|
if (!target) return;
|
|
const observer = new MutationObserver(() => {
|
|
clearTimeout(this._debounceTimer);
|
|
this._debounceTimer = setTimeout(() => this.buildFromAttributes(), 300);
|
|
});
|
|
observer.observe(target, {
|
|
attributes: true,
|
|
attributeFilter: ['value']
|
|
});
|
|
this.attributeRepeaterWatcher = observer;
|
|
},
|
|
async syncManualPrices() {
|
|
const el = document.getElementById('general-product_prices');
|
|
if (el && el.value) {
|
|
try {
|
|
const pricesData = JSON.parse(el.value);
|
|
this.manualPrices = Array.isArray(pricesData) ? pricesData : [];
|
|
} catch (e) {
|
|
this.manualPrices = [];
|
|
}
|
|
} else {
|
|
this.manualPrices = [];
|
|
}
|
|
},
|
|
setupManualPricesSync() {
|
|
const target = document.getElementById('general-product_prices');
|
|
if (!target) return;
|
|
const observer = new MutationObserver(async () => {
|
|
await this.syncManualPrices();
|
|
if (this.pricingMethod === 'manual') {
|
|
this.tableRows.forEach(row => {
|
|
const newPricesFromRepeater = this.manualPrices;
|
|
let reconciledPrices = row.prices.filter(existingPrice =>
|
|
newPricesFromRepeater.some(newPrice => newPrice.currency === existingPrice.currency)
|
|
);
|
|
newPricesFromRepeater.forEach(newPrice => {
|
|
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() {
|
|
this.jsonValue = JSON.stringify(this.tableRows);
|
|
},
|
|
async rebuildTable() {
|
|
await this.initializeOrClearTable();
|
|
}
|
|
},
|
|
watch: {
|
|
tableRows: {
|
|
handler() {
|
|
this.updateJson();
|
|
},
|
|
deep: true
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}, 250);
|
|
|
|
}); |