Files
REG_IBL/one-ui/agreement/one-md-packet-v8/components/oneMdPacketPxList.vue
2026-05-25 20:01:37 +07:00

1184 lines
55 KiB
Vue

<template>
<v-layout class="fill-height" column>
<v-card class="mb-2 pa-2">
<!-- Loading indicator -->
<v-progress-linear
:active="isLoading"
:indeterminate="isLoading"
color="blue"
height="3"
class="ma-0"
style="position: absolute; top: 0; left: 0; right: 0;"
></v-progress-linear>
<v-layout row wrap>
<v-flex xs6 v-if="state_add">
<h3 class="title mb-2">{{ packet_name }} ({{ packet_type }})</h3>
<v-layout row wrap align-center>
<v-flex xs5 class="pr-2">
<v-layout row align-center>
<v-flex :xs11="packet_type_code == 'PN'" :xs12="packet_type_code == 'PR'">
<v-text-field
label="HARGA PAKET"
v-model="packet_price"
class="text-xs-right"
reverse
:disabled="packet_type_code == 'PR' || !edit_packet_price_mode"
@input="onPacketPriceChange"
:hint="is_packet_price_locked && packet_type_code == 'PN' ? 'Harga dikunci, tidak berubah otomatis' : ''"
:persistent-hint="is_packet_price_locked && packet_type_code == 'PN'"
></v-text-field>
</v-flex>
<v-flex xs1 class="text-xs-center" v-if="packet_type_code == 'PN'">
<!-- Edit button -->
<v-tooltip bottom v-if="!edit_packet_price_mode">
<template v-slot:activator="{ on }">
<v-btn
icon
color="blue"
@click="enableEditPacketPrice"
v-on="on"
small
>
<v-icon small>edit</v-icon>
</v-btn>
</template>
<span>Edit Harga Paket</span>
</v-tooltip>
<!-- Save button -->
<v-tooltip bottom v-if="edit_packet_price_mode">
<template v-slot:activator="{ on }">
<v-btn
icon
color="orange"
@click="savePacketPrice"
v-on="on"
small
>
<v-icon small>save</v-icon>
</v-btn>
</template>
<span>Simpan Harga Paket</span>
</v-tooltip>
</v-flex>
</v-layout>
</v-flex>
<v-flex xs5>
<v-text-field
label="TOTAL HARGA"
hide-details
:value="one_money(total_price)"
readonly
class="text-xs-right"
reverse
></v-text-field>
</v-flex>
<v-flex xs2 pl-2 v-if="packet_type_code == 'PR' || is_packet_price_locked">
<v-chip v-if="packet_type_code == 'PR'" small color="info" text-color="white" label>
<v-icon small left>info</v-icon>
Profile
</v-chip>
<v-tooltip bottom v-if="is_packet_price_locked && packet_type_code == 'PN'">
<template v-slot:activator="{ on }">
<v-chip small color="orange" text-color="white" label v-on="on">
<v-icon small left>lock</v-icon>
Dikunci
</v-chip>
</template>
<span>Harga paket dikunci. Tidak akan berubah otomatis saat add/remove item.<br/>Klik icon Edit Harga untuk mengubah.</span>
</v-tooltip>
</v-flex>
</v-layout>
<v-layout row wrap pt-2 v-if="is_data_saved">
<v-flex xs12>
<v-alert type="success" :value="true" dense class="ma-0 mb-2">
<v-icon small left>check_circle</v-icon>
<strong>Data berhasil disimpan!</strong> Klik "Kembali" untuk kembali ke list paket.
</v-alert>
</v-flex>
</v-layout>
<v-layout row wrap pt-2>
<v-flex xs12>
<v-btn color="grey" dark @click="goBack" class="mr-2">
<v-icon small left>arrow_back</v-icon>
Kembali
</v-btn>
<v-btn color="success" dark @click="save" class="mr-2" v-if="!is_data_saved">
<v-icon small left>save</v-icon>
Simpan Paket
</v-btn>
<v-btn
v-if="edit_detail_discount_mode && packet_type_code == 'PN'"
color="primary"
dark
@click="saveDetailDiscounts"
class="mr-2"
>
<v-icon small left>save</v-icon>
Simpan Diskon Detail
</v-btn>
<v-btn color="red" dark @click="cancel" v-if="!is_data_saved">
<v-icon small left>close</v-icon>
Batal & Kembali
</v-btn>
</v-flex>
</v-layout>
</v-flex>
<v-flex v-bind:class="state_add? 'xs6':'xs8'" class="searchbox">
<v-layout row wrap>
<v-text-field class="xs2 ma-1" label="" placeholder="Pencarian" single-line outline v-model="query_px" hide-details></v-text-field>
<v-btn class="xs2 ma-1 one-btn-icon v-btn--search" color="success" @click="search_px" :disabled="!btn_enabled" :dark="btn_enabled">
<span class="icon-search"></span>
</v-btn>
<v-btn class="xs2 ma-1 one-btn-icon v-btn--search" color="orange" @click="imports" :disabled="!btn_enabled || selected_packet.isverif == 'Y' && pxs.length > 0"
:dark="btn_enabled && selected_packet.isverif != 'Y'"
v-show="!state_add">
<span class="icon-import"></span>
</v-btn>
<v-btn class="xs2 ma-1 one-btn-icon v-btn--search" color="primary" @click="add" :disabled="!btn_enabled || selected_packet.isverif == 'Y'"
:dark="btn_enabled && selected_packet.isverif != 'Y'"
v-show="!state_add">
<span class="icon-edit"></span>
</v-btn>
</v-layout>
<!-- <v-layout row wrap>
<v-btn color="success" @click="imports">Impor ...</v-btn>
</v-layout> -->
</v-flex>
<!-- <v-flex xs2 pl-3 v-show="!state_add">
<v-text-field label="Harga Bruto" :value="selected_packet?one_money(selected_packet.T_PacketOriginalBruto):0" reverse readonly
hide-details></v-text-field>
</v-flex> -->
<v-flex xs2 pl-3 v-show="!state_add">
<v-text-field label="Harga Normal" :value="selected_packet?one_money(selected_packet.T_PacketOriginalPrice):0" reverse readonly
hide-details></v-text-field>
</v-flex>
<v-flex xs2 pl-4 v-show="!state_add">
<v-text-field label="Harga Paket" :value="selected_packet?one_money(selected_packet.T_PacketPrice):0" reverse readonly hide-details></v-text-field>
</v-flex>
<!-- <v-btn class="xs3 ma-1" color="info" @click="set" >Baru</v-btn> -->
</v-layout>
</v-card>
<!-- <v-card>
<v-layout row wrap>
<v-flex xs12>
<one-md-packet-px-new></one-md-packet-px-new>
</v-flex>
</v-layout>
</v-card> -->
<v-card>
<v-layout row>
<v-flex xs12 pl-2 pr-2 pt-2 pb-2>
<v-layout row wrap mb-2 align-center v-if="state_add && packet_type_code == 'PN'">
<v-flex xs12>
<v-btn
v-if="!edit_detail_discount_mode"
color="blue"
dark
@click="enableEditDetailDiscount"
class="ma-0"
>
<v-icon small left>edit</v-icon>
Edit Diskon Detail Paket
</v-btn>
<v-btn
v-if="edit_detail_discount_mode"
color="orange"
dark
@click="cancelEditDetailDiscount"
class="ma-0"
>
<v-icon small left>close</v-icon>
Batal Edit Diskon
</v-btn>
<span v-if="edit_detail_discount_mode" class="ml-3 grey--text">
<v-icon small color="info">info</v-icon>
Edit diskon persen (%) dan/atau diskon rupiah (Rp) untuk setiap item
</span>
</v-flex>
</v-layout>
<v-layout row wrap mb-2 v-if="state_add && packet_type_code == 'PR'">
<v-flex xs12>
<v-alert type="info" dense :value="true" class="ma-0">
<v-icon small left>info</v-icon>
<strong>Profile:</strong> Harga paket adalah summary dari total detail item. Tidak dapat mengubah harga paket atau diskon detail.
</v-alert>
</v-flex>
</v-layout>
<v-data-table :headers="headers" :items="pxs" :loading="isLoading" :pagination.sync="pagination" class="elevation-1">
<template slot="items" slot-scope="props">
<td class="text-xs-left pa-2" v-bind:class="{'amber lighten-4':isSelected(props.item)}">{{ props.item.T_TestCode }}</td>
<td class="text-xs-left pa-2" v-bind:class="{'amber lighten-4':isSelected(props.item)}">{{ props.item.T_TestName }}</td>
<td class="text-xs-right pa-2" v-bind:class="{'amber lighten-4':isSelected(props.item)}">{{ one_money(props.item.T_PacketDetailPriceAmount) }}</td>
<td class="text-xs-right pa-1" v-bind:class="{'amber lighten-4':isSelected(props.item)}">
<v-text-field
v-if="edit_detail_discount_mode && state_add && packet_type_code == 'PN'"
v-model.number="props.item.T_PacketDetailPriceDisc"
type="number"
class="text-xs-right compact-input"
reverse
hide-details
single-line
solo
flat
@input="onDetailDiscountChange(props.item)"
style="max-width: 100px; margin: auto;"
></v-text-field>
<span v-else>{{ props.item.T_PacketDetailPriceDisc ? one_money(props.item.T_PacketDetailPriceDisc) + '%' : '0%' }}</span>
</td>
<td class="text-xs-right pa-1" v-bind:class="{'amber lighten-4':isSelected(props.item)}">
<v-text-field
v-if="edit_detail_discount_mode && state_add && packet_type_code == 'PN'"
v-model.number="props.item.T_PacketDetailPriceDiscRp"
type="number"
class="text-xs-right compact-input"
reverse
hide-details
single-line
solo
flat
@input="onDetailDiscountChange(props.item)"
style="max-width: 120px; margin: auto;"
></v-text-field>
<span v-else>{{ one_money(props.item.T_PacketDetailPriceDiscRp) }}</span>
</td>
<td class="text-xs-right pa-2" v-bind:class="{'amber lighten-4':isSelected(props.item)}">{{ one_money(props.item.T_PacketDetailPrice) }}</td>
<td class="text-xs-center pa-0" v-bind:class="{'amber lighten-4':isSelected(props.item)}">
<v-btn color="red" dark class="one-btn-icon" small ma-0 @click="remove(props.item)" v-show="state_add">«</v-btn>
</td>
</template>
<template>
<div class="text-xs-center">
<v-pagination :length="15" :total-visible="7"></v-pagination>
</div>
</template>
</v-data-table>
</v-flex>
</v-layout>
</v-card>
</v-layout>
</template>
<style scoped>
/* .searchbox .v-input.v-text-field .v-input__slot{
min-height:60px;
} */
.searchbox .v-btn--search {
min-height: 50px;
}
table.v-table tbody td,
table.v-table tbody th {
height: 40px;
}
table.v-table thead tr {
height: 40px;
}
.one-btn-icon {
font-size: 1.5em
}
/* Overwrite */
.v-text-field--box>.v-input__control>.v-input__slot,
.v-text-field--full-width>.v-input__control>.v-input__slot,
.v-text-field--outline>.v-input__control>.v-input__slot {
align-items: stretch;
min-height: 50px
}
.v-text-field--box .v-label,
.v-text-field--full-width .v-label,
.v-text-field--outline .v-label {
top: 12px;
}
.v-text-field.v-text-field--enclosed .v-input__append-inner {
margin-top: 10px;
}
.v-text-field--outline.v-text-field--single-line input {
margin-top: 6px;
}
/* Button styling improvements */
.v-btn {
text-transform: none !important;
letter-spacing: normal;
}
.v-btn .v-icon {
margin-right: 4px;
}
/* Compact input field in table styling */
.compact-input.v-text-field {
margin: 0 !important;
padding: 0 !important;
font-size: 13px;
}
.compact-input.v-text-field .v-input__control {
min-height: 32px !important;
}
.compact-input.v-text-field .v-input__slot {
min-height: 32px !important;
padding: 0 8px !important;
margin-bottom: 0 !important;
}
.compact-input.v-text-field input {
font-size: 13px;
padding: 6px 0 !important;
max-height: 32px;
}
.compact-input.v-text-field.v-text-field--solo .v-input__control {
min-height: 32px !important;
}
/* Remove box shadow from solo flat inputs */
.compact-input.v-text-field--solo.v-text-field--solo-flat > .v-input__control > .v-input__slot {
box-shadow: none;
border: 1px solid #ddd;
}
.compact-input.v-text-field--solo.v-text-field--solo-flat > .v-input__control > .v-input__slot:focus-within {
border-color: #1976d2;
}
/* Icon button styling */
.v-btn--icon.v-size--small {
width: 32px;
height: 32px;
}
</style>
<script>
module.exports = {
components: {
// 'one-md-price-new-dialog': httpVueLoader('./oneMdPriceNewDialog.vue'),
// 'one-md-price-new-addon-dialog': httpVueLoader('./oneMdPriceNewAddonDialog.vue')
},
mounted() {
},
methods: {
one_money(p) {
return window.one_money(p)
},
isSelected(p) {
return false
},
search_px() {
this.$store.dispatch("px/search_px")
},
selectMe(doc) {
// this.$store.commit('price/update_selected_price', doc)
},
add() {
// Reset edit modes and saved state
this.edit_detail_discount_mode = false;
this.edit_packet_price_mode = false;
this.is_data_saved = false; // Reset saved state when entering edit mode
this.original_pxs = [];
this.original_packet_price = 0;
this.$store.commit('test/update_state_add', true)
this.$store.dispatch('test/search_test')
// Calculate total price from current packet details
let pxs = this.$store.state.px.pxs
let calculatedTotalBruto = 0;
for (let i in pxs) {
// Use BRUTO (original price before discounts) for total price
calculatedTotalBruto += parseFloat(pxs[i].T_PacketDetailPriceAmount) || parseFloat(pxs[i].T_PriceAmount) || 0;
}
let selectedPacket = this.$store.state.packet.selected_packet
let existingPacketPrice = parseFloat(selectedPacket.T_PacketPrice) || 0;
// Determine if this is fresh data or already saved in DB
// Check if any item has T_PacketDetailID > 0 (means saved in DB)
let hasDetailFromDB = false;
for (let i in pxs) {
if (pxs[i].T_PacketDetailID && parseInt(pxs[i].T_PacketDetailID) > 0) {
hasDetailFromDB = true;
break;
}
}
let isDataFromDB = hasDetailFromDB && existingPacketPrice > 0;
// Lock/Unlock logic:
// - If data from DB: LOCK (don't auto-update when add/remove items)
// - If fresh/new: UNLOCK (auto-update)
this.is_packet_price_locked = isDataFromDB;
console.log('=== ADD MODE - DATA LOADED ===');
console.log('Data from DB:', isDataFromDB, 'Locked:', this.is_packet_price_locked);
console.log('Existing packet price:', existingPacketPrice, 'Calculated total bruto:', calculatedTotalBruto);
console.log('PXS Data loaded:');
for (let i in pxs) {
console.log(` - ${pxs[i].T_TestName}: Disc % = ${pxs[i].T_PacketDetailPriceDisc || pxs[i].T_PriceDisc || 0}, Disc Rp = ${pxs[i].T_PacketDetailPriceDiscRp || pxs[i].T_PriceDiscRp || 0}`);
}
// Set total price (BRUTO - before discounts)
this.$store.commit('px/update_estimate_total_price', calculatedTotalBruto)
// Set packet price:
// - If from DB: keep existing price (LOCKED)
// - If fresh: use calculated total bruto (UNLOCKED)
if (isDataFromDB) {
this.$store.commit('px/update_packet_price', existingPacketPrice)
} else {
this.$store.commit('px/update_packet_price', calculatedTotalBruto)
}
// update nat tests
// refresh nat_tests
let nt = []
for (let i in pxs) {
for (let j in pxs[i].nat_tests) {
nt.push(pxs[i].nat_tests[j])
}
}
this.$store.commit('px/update_nat_tests', nt)
},
cancel() {
if (!confirm('Yakin ingin membatalkan perubahan dan kembali?')) {
return;
}
// Reset all states
this.edit_detail_discount_mode = false;
this.edit_packet_price_mode = false;
this.is_packet_price_locked = false; // Unlock
this.is_data_saved = false; // Reset saved state
this.original_pxs = [];
this.original_packet_price = 0;
// Exit add mode first
this.$store.commit('test/update_state_add', false)
// Refresh packet list and px list to discard changes
this.$store.dispatch('px/search_px')
this.$store.dispatch('packet/search_packet')
},
save() {
console.log('=== BEFORE SAVE - CHECKING DATA ===');
console.log('PXS Array Length:', this.pxs.length);
console.log('PXS Data:', JSON.parse(JSON.stringify(this.pxs)));
// Check if no items (all deleted)
if (this.pxs.length === 0) {
if (!confirm('Tidak ada detail test di paket ini. Apakah Anda yakin ingin menyimpan paket kosong? Semua detail sebelumnya akan dihapus.')) {
return;
}
console.log('⚠️ SAVING EMPTY PACKET - All details will be marked as inactive');
}
// For Profile (PR): packet price must equal total price
let finalPacketPrice = this.packet_price;
if (this.packet_type_code == "PR") {
finalPacketPrice = this.total_price;
}
// Validation: packet price should not exceed total price
if(parseInt(finalPacketPrice) > parseInt(this.total_price)){
alert('Harga paket tidak boleh melebihi harga normal')
console.log(finalPacketPrice)
console.log(this.total_price)
}else{
let x = this.$store.state.packet.selected_packet
x.T_PacketPrice = finalPacketPrice
x.T_PacketOriginalPrice = this.total_price
this.$store.commit('packet/update_selected_packet', x)
// Save to backend
this.$store.dispatch('px/save_px')
// Wait a bit for save to complete, then update UI
setTimeout(() => {
console.log('=== SAVE COMPLETED ===');
// Reset edit modes
this.edit_detail_discount_mode = false;
this.edit_packet_price_mode = false;
// LOCK packet price after save
this.is_packet_price_locked = true;
// Mark as saved - THIS WILL SHOW SUCCESS ALERT AND HIDE BUTTONS
this.is_data_saved = true;
console.log('✅ is_data_saved:', this.is_data_saved);
console.log('✅ Alert should show now');
console.log('✅ "Simpan Paket" button should hide');
console.log('✅ "Batal & Kembali" button should hide');
// Alert user
alert('✅ Data berhasil disimpan! Klik "Kembali" untuk kembali ke list paket.');
}, 500);
}
},
goBack() {
console.log('=== GO BACK CLICKED ===');
// Reset all local states first
this.edit_detail_discount_mode = false;
this.edit_packet_price_mode = false;
this.is_packet_price_locked = false;
this.is_data_saved = false;
this.original_pxs = [];
this.original_packet_price = 0;
// Exit add mode
this.$store.commit('test/update_state_add', false)
// Reset loading states
this.$store.commit('px/update_search_status', 0)
this.$store.commit('test/update_search_status', 0)
// Refresh packet list to get latest data from backend
this.$store.dispatch('packet/search_packet').then(() => {
console.log('Packet list refreshed successfully');
}).catch((error) => {
console.error('Error refreshing data:', error);
});
},
remove(x) {
// Reset saved state - user is making changes
this.is_data_saved = false;
// go to state add
let z = this.$store.state.test.state_add
if (!z) {
// Use BRUTO (original price before discounts) not NETTO
let brutoTotal = this.$store.state.packet.selected_packet.T_PacketOriginalBruto || 0
this.$store.commit('px/update_estimate_total_price', brutoTotal)
this.$store.commit('test/update_state_add', true)
}
// add to test
let te = this.$store.state.test.exclude
let ti = this.$store.state.test.include
let td = te.indexOf(x.T_TestID)
if (td > -1)
te.splice(td, 1)
else
ti.push(x.T_TestID)
this.$store.commit('test/update_exclude', te)
this.$store.commit('test/update_include', ti)
// search test
this.$store.dispatch('test/search_test')
console.log('=== REMOVE ITEM ===');
console.log('Removing test ID:', x.T_TestID, x.T_TestName);
console.log('Current PXS length:', this.pxs.length);
let pxs = this.pxs
let priceBruto = 0
let priceNetto = 0
let total = pxs.length
let total_price_bruto = parseFloat(this.$store.state.px.estimate_total_price)
let removed = false;
for (let i in pxs) {
if (pxs[i].T_TestID == x.T_TestID) {
console.log('Found item at index', i, '- removing...');
// Get BRUTO price (before discounts) for total_price
priceBruto = parseFloat(pxs[i].T_PacketDetailPriceAmount) || parseFloat(pxs[i].T_PriceAmount) || 0
// Get NETTO price (after discounts) for packet_price
priceNetto = parseFloat(pxs[i].T_PacketDetailPrice) || 0
pxs.splice(i, 1)
removed = true;
break; // Important: break after splice to avoid skipping items
}
}
if (!removed) {
console.log('⚠️ WARNING: Item not found in array!');
}
total_price_bruto -= priceBruto
if (total_price_bruto < 0) total_price_bruto = 0
console.log('After remove - PXS length:', pxs.length);
console.log('Updating store with:', {total: total - 1, records: pxs});
this.pxs = {
total: total - 1,
records: pxs
}
// Update total price (BRUTO)
this.$store.commit('px/update_estimate_total_price', total_price_bruto)
console.log('=== UPDATE PACKET PRICE AFTER REMOVE ===');
console.log('Packet Type:', this.packet_type_code);
console.log('Is Locked:', this.is_packet_price_locked);
console.log('Is Edit Mode:', this.edit_packet_price_mode);
console.log('Remaining items:', pxs.length);
// Calculate new packet price (NETTO) by summing remaining items
let newPacketPriceNetto = 0;
for (let i in pxs) {
newPacketPriceNetto += parseFloat(pxs[i].T_PacketDetailPrice) || 0;
}
// IMPORTANT: If array is EMPTY (all items deleted), ALWAYS set packet price to 0
// regardless of lock state, because it doesn't make sense to have price without items
if (pxs.length === 0) {
console.log('→ Array EMPTY: Force updating packet price to 0 (overriding lock)');
this.$store.commit('px/update_packet_price', 0);
}
// For Profile (PR): packet price ALWAYS = sum of netto
else if (this.packet_type_code == "PR") {
console.log('→ Profile: UPDATING packet price to', newPacketPriceNetto);
this.$store.commit('px/update_packet_price', newPacketPriceNetto)
}
// For Panel (PN): only update if NOT locked and NOT in edit mode
else if (this.packet_type_code == "PN" && !this.is_packet_price_locked && !this.edit_packet_price_mode) {
console.log('→ Panel UNLOCKED: UPDATING packet price to', newPacketPriceNetto);
this.$store.commit('px/update_packet_price', newPacketPriceNetto)
} else if (this.packet_type_code == "PN" && this.is_packet_price_locked) {
console.log('→ Panel LOCKED: NOT updating packet price');
console.log(' Current packet price remains:', this.packet_price);
console.log(' 🔒 With', pxs.length, 'items remaining, price stays locked');
} else if (this.packet_type_code == "PN" && this.edit_packet_price_mode) {
console.log('→ Panel EDIT MODE: NOT updating');
}
// refresh nat_tests
let nt = []
for (let i in pxs) {
for (let j in pxs[i].nat_tests) {
nt.push(pxs[i].nat_tests[j])
}
}
this.$store.commit('px/update_nat_tests', nt)
},
imports() {
this.$store.commit('packet_copy/update_dialog_copy', true)
},
// Enable edit mode for detail discounts (only for Panel type)
enableEditDetailDiscount() {
if (this.packet_type_code == "PR") {
alert('Profile: Harga paket adalah summary dari total detail. Tidak bisa edit diskon detail.');
return;
}
// Save original data for cancel
this.original_pxs = JSON.parse(JSON.stringify(this.pxs));
// Reset saved state - user is making changes
this.is_data_saved = false;
// Use nextTick to avoid DOM conflicts
this.$nextTick(() => {
this.edit_detail_discount_mode = true;
});
},
// Cancel edit detail discount
cancelEditDetailDiscount() {
// Restore original data first
if (this.original_pxs.length > 0) {
this.$store.commit('px/update_pxs', {
records: JSON.parse(JSON.stringify(this.original_pxs)),
total: this.original_pxs.length
});
}
// Use nextTick to avoid DOM conflicts
this.$nextTick(() => {
this.edit_detail_discount_mode = false;
});
},
// When detail discount changes, recalculate detail price and packet price (only for Panel)
onDetailDiscountChange(item) {
// Profile type cannot edit detail discounts
if (this.packet_type_code == "PR") return;
// Calculate: T_PacketDetailPriceAmount - (T_PacketDetailPriceAmount * T_PacketDetailPriceDisc/100) - T_PacketDetailPriceDiscRp
let amount = parseFloat(item.T_PacketDetailPriceAmount) || 0;
let discPerc = parseFloat(item.T_PacketDetailPriceDisc) || 0;
let discRp = parseFloat(item.T_PacketDetailPriceDiscRp) || 0;
// Apply percentage discount first
let afterPercDisc = amount - (amount * discPerc / 100);
// Then subtract nominal discount
let finalPrice = afterPercDisc - discRp;
if (finalPrice < 0) finalPrice = 0;
item.T_PacketDetailPrice = finalPrice;
item.T_PacketDetailPriceSubTotal = finalPrice;
// Recalculate total price and packet price (will auto-update unless in edit packet price mode)
this.recalculateTotalPrice();
},
// Recalculate total price from all details
recalculateTotalPrice() {
// IMPORTANT: Total price = SUM of BRUTO (before discounts)
// NOT from T_PacketDetailPrice (which is after discounts)
let totalBruto = 0;
let totalNetto = 0;
let pxs = this.pxs;
for (let i in pxs) {
// Sum BRUTO (original price before any discounts)
totalBruto += parseFloat(pxs[i].T_PacketDetailPriceAmount) || 0;
// Also calculate netto for reference
totalNetto += parseFloat(pxs[i].T_PacketDetailPrice) || 0;
}
console.log('Recalculate: Total Bruto =', totalBruto, 'Total Netto =', totalNetto, 'Array length =', pxs.length);
// estimate_total_price = TOTAL BRUTO (harga normal sebelum diskon)
this.$store.commit('px/update_estimate_total_price', totalBruto);
// IMPORTANT: If array is EMPTY, ALWAYS set packet price to 0
if (pxs.length === 0) {
console.log('Recalculate: Array EMPTY - force packet price to 0');
this.$store.commit('px/update_packet_price', 0);
}
// For Profile (PR): packet price ALWAYS = total NETTO (after discounts)
else if (this.packet_type_code == "PR") {
this.$store.commit('px/update_packet_price', totalNetto);
}
// For Panel (PN): only update if not in edit packet price mode AND not locked
else if (this.packet_type_code == "PN" && !this.edit_packet_price_mode && !this.is_packet_price_locked) {
this.$store.commit('px/update_packet_price', totalNetto);
}
},
// Save detail discounts to backend (only for Panel)
saveDetailDiscounts() {
// Profile type cannot edit detail discounts
if (this.packet_type_code == "PR") return;
// Clear backup since changes are confirmed
this.original_pxs = [];
// Use nextTick to avoid DOM conflicts
this.$nextTick(() => {
// User clicked save detail discounts, just close the edit mode
// The actual save will happen when user clicks "Simpan Paket"
this.edit_detail_discount_mode = false;
alert('Diskon detail dikonfirmasi. Klik "Simpan Paket" untuk menyimpan ke database.');
});
},
// Enable edit mode for packet price (only for Panel type)
enableEditPacketPrice() {
if (this.packet_type_code == "PR") {
alert('Profile: Harga paket adalah summary dari total detail item. Tidak bisa diubah manual.');
return;
}
// Show info if data is locked
if (this.is_packet_price_locked) {
console.log('Edit Harga: Data was LOCKED, now entering edit mode');
}
// Reset saved state - user is making changes
this.is_data_saved = false;
// Store original value and enter edit mode
this.original_packet_price = this.packet_price;
this.edit_packet_price_mode = true;
},
// When packet price changes, distribute discount pro-rata (only for Panel)
onPacketPriceChange() {
// Profile type cannot edit packet price
if (this.packet_type_code == "PR") return;
if (!this.edit_packet_price_mode) return;
let newPacketPrice = parseFloat(this.packet_price) || 0;
let totalPrice = parseFloat(this.total_price) || 0;
// Don't process if packet price is 0 or invalid
if (newPacketPrice === 0 || isNaN(newPacketPrice)) return;
if (newPacketPrice < totalPrice) {
console.log('=== PRO-RATA DISTRIBUTION ===');
console.log('Packet Price:', newPacketPrice);
console.log('Total Price:', totalPrice);
// Calculate difference
let difference = totalPrice - newPacketPrice;
console.log('Difference to distribute:', difference);
// Distribute pro-rata to T_PacketDetailPriceDiscRp
// Clear all percentage discounts
let pxs = this.pxs;
let totalAmount = 0;
// Calculate total amount (bruto)
for (let i in pxs) {
totalAmount += parseFloat(pxs[i].T_PacketDetailPriceAmount) || 0;
}
if (totalAmount > 0) {
let totalDiscDistributed = 0;
let pxsLength = pxs.length;
for (let i = 0; i < pxsLength; i++) {
let amount = parseFloat(pxs[i].T_PacketDetailPriceAmount) || 0;
// IMPORTANT: Clear percentage discount to 0
pxs[i].T_PacketDetailPriceDisc = 0;
// Calculate pro-rata discount in Rp
let proRataDisc;
// For last item, use remaining difference to ensure exact total
if (i === pxsLength - 1) {
proRataDisc = difference - totalDiscDistributed;
} else {
proRataDisc = Math.round((amount / totalAmount) * difference);
totalDiscDistributed += proRataDisc;
}
pxs[i].T_PacketDetailPriceDiscRp = proRataDisc;
// Recalculate detail price
pxs[i].T_PacketDetailPrice = amount - pxs[i].T_PacketDetailPriceDiscRp;
pxs[i].T_PacketDetailPriceSubTotal = pxs[i].T_PacketDetailPrice;
console.log(`Item ${i+1}: ${pxs[i].T_TestName}`);
console.log(` - Bruto: ${amount}`);
console.log(` - Disc %: ${pxs[i].T_PacketDetailPriceDisc} (CLEARED)`);
console.log(` - Disc Rp: ${pxs[i].T_PacketDetailPriceDiscRp}`);
console.log(` - Final Price: ${pxs[i].T_PacketDetailPrice}`);
}
// Update store
this.$store.commit('px/update_pxs', {
records: pxs,
total: pxs.length
});
console.log('=== PRO-RATA COMPLETED ===');
}
} else if (newPacketPrice > totalPrice) {
alert('Harga paket tidak boleh melebihi total harga normal!');
this.packet_price = this.original_packet_price;
} else if (newPacketPrice == totalPrice) {
// If equal, clear all discounts
let pxs = this.pxs;
for (let i in pxs) {
pxs[i].T_PacketDetailPriceDisc = 0;
pxs[i].T_PacketDetailPriceDiscRp = 0;
let amount = parseFloat(pxs[i].T_PacketDetailPriceAmount) || 0;
pxs[i].T_PacketDetailPrice = amount;
pxs[i].T_PacketDetailPriceSubTotal = amount;
}
this.$store.commit('px/update_pxs', {
records: pxs,
total: pxs.length
});
}
},
// Save packet price
savePacketPrice() {
// Clear backup since changes are confirmed
this.original_packet_price = 0;
// Use nextTick to avoid DOM conflicts
this.$nextTick(() => {
// User clicked save packet price, just close the edit mode
// Lock packet price so it won't auto-update when adding/removing items
this.edit_packet_price_mode = false;
this.is_packet_price_locked = true; // Lock the price
alert('Harga paket dikonfirmasi dan dikunci.\nHarga paket tidak akan berubah otomatis saat menambah/menghapus item.\nKlik "Simpan Paket" untuk menyimpan ke database.');
});
}
},
computed: {
pxs: {
get() {
return this.$store.state.px.pxs
},
set(v) {
this.$store.commit('px/update_pxs', v)
}
},
selected_px: {
get() {
return this.$store.state.px.selected_px
},
set(v) {
this.$store.commit('px/update_selected_px', v)
}
},
query_px: {
get() {
return this.$store.state.px.query_px
},
set(v) {
this.$store.commit('px/update_query_px', v)
}
},
isLoading() {
// Return true if search_status is 1 (loading/in progress)
return this.$store.state.px.search_status === 1
},
btn_add_enabled() {
// if (this.$store.state.price.selected_mou.M_MouID)
return true
// return false
},
packet_name() {
if (this.$store.state.packet.selected_packet)
return this.$store.state.packet.selected_packet.T_PacketName
return ""
},
packet_type() {
if (!this.$store.state.packet.selected_packet)
return ""
if (this.$store.state.packet.selected_packet.T_PacketType == "PR")
return "Profile"
return "Panel"
},
packet_type_code() {
if (!this.$store.state.packet.selected_packet)
return ""
return this.$store.state.packet.selected_packet.T_PacketType
},
isverif() {
return this.$store.state.packet.isverif
},
state_add() {
return this.$store.state.test.state_add
},
btn_enabled() {
if (this.$store.state.packet.selected_mou &&
this.$store.state.packet.selected_packet)
return true
return false
},
total_price: {
get() {
return this.$store.state.px.estimate_total_price
},
set(v) {
this.$store.commit('px/update_estimate_total_price', v)
}
},
packet_price: {
get() {
// For Profile (PR): packet price ALWAYS = total price (summary only)
if (this.packet_type_code == "PR") {
return this.total_price || 0;
}
// For Panel (PN): just return stored packet price
// DON'T auto-update here - let the watcher handle it properly with lock checking
let price = this.$store.state.px.packet_price;
return price || 0;
},
set(v) {
// For Profile (PR): don't allow manual set, always use total price
if (this.packet_type_code == "PR") {
return; // Ignore set attempts for Profile
}
this.$store.commit('px/update_packet_price', v)
}
},
selected_packet() {
return this.$store.state.packet.selected_packet
},
selected_mou() {
return this.$store.state.packet.selected_mou
}
},
data() {
return {
edit_detail_discount_mode: false,
edit_packet_price_mode: false,
is_packet_price_locked: false, // Track if user manually edited packet price
is_data_saved: false, // Track if data has been saved
original_pxs: [],
original_packet_price: 0,
headers: [{
text: "KODE",
align: "left",
sortable: false,
value: "mr",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "NAMA",
align: "left",
sortable: false,
value: "lab",
width: "28%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "BRUTO",
align: "right",
sortable: false,
value: "name",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "Disc %",
align: "right",
sortable: false,
value: "name",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "Disc Rp",
align: "right",
sortable: false,
value: "name",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "HARGA",
align: "right",
sortable: false,
value: "name",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
},
{
text: "ACTION",
align: "center",
sortable: false,
value: "name",
width: "12%",
class: "pa-2 blue lighten-3 white--text"
}
],
pagination: {
descending: false,
page: 1,
rowsPerPage: 5,
sortBy: 'T_TestSasCode',
totalItems: this.$store.state.px.total_px
}
};
},
watch: {
// Watch pxs changes to auto-update packet price (when items are added/removed)
pxs: {
handler(newVal, oldVal) {
// Skip if not in add mode
if (!this.state_add) return;
console.log('=== Watcher PXS Triggered ===');
console.log('Locked state:', this.is_packet_price_locked);
console.log('Edit mode:', this.edit_packet_price_mode);
console.log('Packet type:', this.packet_type_code);
// If items were added/removed (length changed), reset saved state
// This ensures "Simpan Paket" button appears after adding items post-save
if (oldVal && newVal && newVal.length !== oldVal.length) {
console.log('→ Items count changed, resetting is_data_saved');
this.is_data_saved = false;
}
// Calculate total from all items
let total = 0;
for (let i in newVal) {
total += parseFloat(newVal[i].T_PacketDetailPrice) || 0;
}
console.log('Calculated total:', total);
console.log('Current packet price:', this.packet_price);
console.log('Array length:', newVal.length);
// IMPORTANT: If array is EMPTY (all items deleted), ALWAYS set packet price to 0
// regardless of lock state or packet type
if (newVal.length === 0) {
console.log('→ Array EMPTY: Force updating packet price to 0 (overriding lock)');
this.$store.commit('px/update_packet_price', 0);
}
// For Profile (PR): ALWAYS auto-update (ignore lock)
else if (this.packet_type_code == "PR") {
console.log('→ Profile: UPDATING packet price to', total);
this.$store.commit('px/update_packet_price', total);
}
// For Panel (PN): only update if NOT in edit mode AND NOT locked
else if (this.packet_type_code == "PN" && !this.edit_packet_price_mode && !this.is_packet_price_locked) {
console.log('→ Panel UNLOCKED: UPDATING packet price to', total);
this.$store.commit('px/update_packet_price', total);
} else if (this.packet_type_code == "PN" && this.is_packet_price_locked) {
console.log('→ Panel LOCKED: NOT updating (packet price stays at', this.packet_price, ')');
console.log(' With', newVal.length, 'items, price remains locked');
} else if (this.packet_type_code == "PN" && this.edit_packet_price_mode) {
console.log('→ Panel in EDIT MODE: NOT updating');
}
console.log('=== End Watcher ===');
},
deep: true
}
}
}
</script>