1184 lines
55 KiB
Vue
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> |