Files
formipay-public/admin/assets/js/admin-product-editor.js
2025-08-21 20:39:34 +07:00

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);
});