관리-도구
편집 파일: pos.vue
<template> <div class="pos_page"> <div class="container-fluid p-0 app-admin-wrap layout-sidebar-large clearfix" id="pos"> <div v-if="isLoading" class="loading_page spinner spinner-primary mr-3"></div> <b-row v-if="!isLoading"> <!-- Card Left Panel Details Sale--> <b-col md="5"> <b-card no-body class="card-order"> <div class="main-header" style=" height: 50px; "> <div class="logo"> <router-link to="/app/dashboard"> <img :src="'/images/'+currentUser.logo" alt width="40" height="40" style=" width: 40px; height: 40px; "> </router-link> </div> <div class="mx-auto"></div> <div class="header-part-right"> <!-- Dashboard --> <router-link class="i-Back header-icon d-sm-inline-block" to="/app/dashboard" title="Dashboard" > </router-link> <!-- Full screen toggle --> <i style="color: #8b5cf6;" title="Full screen" class="i-Full-Screen header-icon d-none d-sm-inline-block" @click="handleFullScreen" ></i> <!-- Pos Settings --> <router-link v-if="currentUserPermissions && currentUserPermissions.includes('pos_settings')" class="i-Data-Settings header-icon d-sm-inline-block" to="/app/settings/pos_settings" title="Pos Settings" > </router-link> <!-- Today's Sales --> <i @click="get_today_sales()" style="color: #8b5cf6;" title="Today's Sales" v-if="currentUserPermissions && currentUserPermissions.includes('Sales_view')" class="i-Receipt header-icon d-sm-inline-block" ></i> <!-- Grid menu Dropdown --> <div class="dropdown" v-if="show_language"> <b-dropdown id="dropdown" text="Dropdown Button" class="m-md-2" toggle-class="text-decoration-none" no-caret right variant="link" > <template slot="button-content"> <i style="color: #8b5cf6!important;" class="i-Globe text-muted header-icon" role="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ></i> </template> <vue-perfect-scrollbar :settings="{ suppressScrollX: true, wheelPropagation: false }" ref="myData" class="dropdown-menu-left rtl-ps-none notification-dropdown ps scroll" > <div class="menu-icon-grid"> <a v-for="lang in languages_available" :key="lang.locale" @click="SetLocal(lang.locale)"> <img :src="`/flags/${lang.flag}`" :alt="lang.name" class="flag-icon flag-icon-squared" style="width: 20px; margin-right: 8px" /> <span class="title-lang">{{ lang.name }}</span> </a> </div> </vue-perfect-scrollbar> </b-dropdown> </div> <!-- User avatar dropdown --> <div class="dropdown"> <b-dropdown id="dropdown-1" text="Dropdown Button" class="m-md-2 user col align-self-end" toggle-class="text-decoration-none" no-caret variant="link" right > <template slot="button-content"> <img :src="'/images/avatar/'+currentUser.avatar" id="userDropdown" alt data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" > </template> <div class="dropdown-menu-left" aria-labelledby="userDropdown"> <div class="dropdown-header"> <i class="i-Lock-User mr-1"></i> <span>{{currentUser.username}}</span> </div> <router-link to="/app/profile" class="dropdown-item">{{$t('profil')}}</router-link> <router-link v-if="currentUserPermissions && currentUserPermissions.includes('setting_system')" to="/app/settings/System_settings" class="dropdown-item" >{{$t('Settings')}}</router-link> <a class="dropdown-item" href="#" @click.prevent="logoutUser">{{$t('logout')}}</a> </div> </b-dropdown> </div> </div> </div> <validation-observer ref="create_pos"> <b-form @submit.prevent="Submit_Pos"> <b-card-body> <b-row> <b-modal hide-footer id="open_scan" size="md" title="Barcode Scanner"> <qrcode-scanner :qrbox="250" :fps="10" style="width: 100%; height: calc(100vh - 56px);" @result="onScan" /> </b-modal> <!-- Customer --> <b-col lg="12" md="12" sm="12"> <validation-provider name="Customer" :rules="{ required: true}"> <b-input-group slot-scope="{ valid, errors }" class="input-customer"> <v-select :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" v-model="selectedClientId" @input="onClientSelected" :reduce="label => label.value" :placeholder="$t('Choose_Customer')" class="w-100" :options="clients.map(clients => ({label: clients.name, value: clients.id}))" /> <b-input-group-append> <b-button variant="primary" @click="New_Client()"> <span> <i class="i-Add-User"></i> </span> </b-button> </b-input-group-append> </b-input-group> </validation-provider> </b-col> <!-- warehouse --> <b-col lg="12" md="12" sm="12"> <validation-provider name="warehouse" :rules="{ required: true}"> <b-form-group slot-scope="{ valid, errors }" class="mt-2"> <v-select :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" :disabled="details.length > 0" @input="Selected_Warehouse" v-model="sale.warehouse_id" :reduce="label => label.value" :placeholder="$t('Choose_Warehouse')" :options="warehouses.map(warehouses => ({label: warehouses.name, value: warehouses.id}))" /> </b-form-group> </validation-provider> </b-col> <!-- Details Product --> <b-col md="12" class="mt-2"> <div class="pos-detail"> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th scope="col">{{$t('ProductName')}}</th> <th scope="col">{{$t('Price')}}</th> <th scope="col" class="text-center">{{$t('Qty')}}</th> <th scope="col" class="text-center">{{$t('SubTotal')}}</th> <th scope="col" class="text-center"> <i class="fa fa-trash"></i> </th> </tr> </thead> <tbody> <tr v-if="details.length <= 0"> <td colspan="5">{{$t('NodataAvailable')}}</td> </tr> <tr v-for="(detail, index) in details" :key="index"> <td> <span>{{detail.code}}</span> <br> <span class="badge badge-success">{{detail.name}}</span> <i v-if="currentUserPermissions && currentUserPermissions.includes('edit_product_sale')" @click="Modal_Updat_Detail(detail)" class="i-Edit text-success cursor-pointer"></i> </td> <td>{{currentUser.currency}} {{formatNumber(detail.Total_price, 2)}}</td> <td> <div class="quantity"> <b-input-group> <b-input-group-prepend> <span class="btn btn-primary btn-sm" @click="decrement(detail ,detail.detail_id)" >-</span> </b-input-group-prepend> <input class="form-control" @keyup="Verified_Qty(detail,detail.detail_id)" v-model.number="detail.quantity" > <b-input-group-append> <span class="btn btn-primary btn-sm" @click="increment(detail.detail_id)" >+</span> </b-input-group-append> </b-input-group> </div> </td> <td class="text-center">{{currentUser.currency}} {{detail.subtotal.toFixed(2)}}</td> <td> <a @click="delete_Product_Detail(detail.detail_id)" title="Delete" > <i class="i-Close-Window text-25 text-danger cursor-pointer"></i> </a> </td> </tr> </tbody> </table> </div> </div> </b-col> </b-row> <!-- Calcul Grand Total --> <div class="footer_panel"> <b-row> <b-col md="12"> <div class="grandtotal"> <span>{{$t("Total_Payable")}} : {{currentUser.currency}} {{GrandTotal.toFixed(2)}}</span> </div> </b-col> <!-- Discount --> <b-col lg="6" md="6" sm="12" v-if="currentUserPermissions && currentUserPermissions.includes('edit_tax_discount_shipping_sale')"> <validation-provider name="Discount" :rules="{ regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Discount')" append="%"> <b-input-group :append="currentUser.currency"> <b-form-input :state="getValidationState(validationContext)" aria-describedby="Discount-feedback" label="Discount" v-model.number="sale.discount" @keyup="keyup_Discount()" ></b-form-input> </b-input-group> <b-form-invalid-feedback id="Discount-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Points --> <b-col lg="6" md="6" sm="12" v-if="clientIsEligible && currentUserPermissions && currentUserPermissions.includes('edit_tax_discount_shipping_sale')"> <b-form-group :label="$t('Available_Points')"> <b-input-group> <b-form-input :value="selectedClientPoints" disabled class="text-right" style="background: unset;" /> <b-input-group-append> <b-button :style="pointsConverted ?'border: 1px solid #9CA3AF':''" :variant="pointsConverted ? 'secondary' : 'primary'" @click="convertPointsToDiscount" :disabled="selectedClientPoints === 0" > {{ pointsConverted ? '✅' : $t('Convert') }} </b-button> </b-input-group-append> </b-input-group> <small v-if="discount_from_points > 0" class="text-success d-block mt-1"> ✅ {{ $t('Discount') }} {{ discount_from_points }} {{ currentUser.currency }} will be applied </small> <input type="hidden" name="discount_from_points" :value="discount_from_points"> </b-form-group> </b-col> <!-- Order Tax --> <b-col lg="6" md="6" sm="12" v-if="currentUserPermissions && currentUserPermissions.includes('edit_tax_discount_shipping_sale')"> <validation-provider name="Order Tax" :rules="{ regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Tax')" append="%"> <b-input-group append="%"> <b-form-input :state="getValidationState(validationContext)" aria-describedby="OrderTax-feedback" label="Order Tax" v-model.number="sale.tax_rate" @keyup="keyup_OrderTax()" ></b-form-input> </b-input-group> <b-form-invalid-feedback id="OrderTax-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Shipping --> <b-col lg="6" md="6" sm="12" v-if="currentUserPermissions && currentUserPermissions.includes('edit_tax_discount_shipping_sale')"> <validation-provider name="Shipping" :rules="{ regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Shipping')"> <b-input-group :append="currentUser.currency"> <b-form-input :state="getValidationState(validationContext)" aria-describedby="Shipping-feedback" label="Shipping" v-model.number="sale.shipping" @keyup="keyup_Shipping()" ></b-form-input> </b-input-group> <b-form-invalid-feedback id="Shipping-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> </b-row> </div> </b-card-body> </b-form> </validation-observer> <!-- Update Detail Product --> <validation-observer ref="Update_Detail"> <b-modal hide-footer size="lg" id="form_Update_Detail" :title="detail.name"> <b-form @submit.prevent="submit_Update_Detail"> <b-row> <!-- Unit Price --> <b-col lg="6" md="6" sm="12"> <validation-provider name="Product Price" :rules="{ required: true , regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('ProductPrice') + ' ' + '*'" id="Price-input"> <b-form-input label="Product Price" v-model="detail.Unit_price" :state="getValidationState(validationContext)" aria-describedby="Price-feedback" ></b-form-input> <b-form-invalid-feedback id="Price-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Tax Method --> <b-col lg="6" md="6" sm="12"> <validation-provider name="Tax Method" :rules="{ required: true}"> <b-form-group slot-scope="{ valid, errors }" :label="$t('TaxMethod') + ' ' + '*'"> <v-select :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" v-model="detail.tax_method" :reduce="label => label.value" :placeholder="$t('Choose_Method')" :options=" [ {label: 'Exclusive', value: '1'}, {label: 'Inclusive', value: '2'} ]" ></v-select> <b-form-invalid-feedback>{{ errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Tax --> <b-col lg="6" md="6" sm="12"> <validation-provider name="Tax" :rules="{ required: true , regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Tax') + ' ' + '*'"> <b-input-group append="%"> <b-form-input label="Tax" v-model="detail.tax_percent" :state="getValidationState(validationContext)" aria-describedby="Tax-feedback" ></b-form-input> </b-input-group> <b-form-invalid-feedback id="Tax-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Discount Method --> <b-col lg="6" md="6" sm="12"> <validation-provider name="Discount Method" :rules="{ required: true}"> <b-form-group slot-scope="{ valid, errors }" :label="$t('Discount_Method') + ' ' + '*'"> <v-select v-model="detail.discount_Method" :reduce="label => label.value" :placeholder="$t('Choose_Method')" :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" :options=" [ {label: 'Percent %', value: '1'}, {label: 'Fixed', value: '2'} ]" ></v-select> <b-form-invalid-feedback>{{ errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Discount Rate --> <b-col lg="6" md="6" sm="12"> <validation-provider name="Discount Rate" :rules="{ required: true , regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Discount') + ' ' + '*'"> <b-form-input label="Discount" v-model="detail.discount" :state="getValidationState(validationContext)" aria-describedby="Discount-feedback" ></b-form-input> <b-form-invalid-feedback id="Discount-feedback" >{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Unit Sale --> <b-col lg="6" md="6" sm="12" v-if="detail.product_type != 'is_service'"> <validation-provider name="Unit Sale" :rules="{ required: true}"> <b-form-group slot-scope="{ valid, errors }" :label="$t('UnitSale') + ' ' + '*'"> <v-select :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" v-model="detail.sale_unit_id" :placeholder="$t('Choose_Unit_Sale')" :reduce="label => label.value" :options="units.map(units => ({label: units.name, value: units.id}))" /> <b-form-invalid-feedback>{{ errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Imei or serial numbers --> <b-col lg="12" md="12" sm="12" v-show="detail.is_imei"> <b-form-group :label="$t('Add_product_IMEI_Serial_number')"> <b-form-input label="Add_product_IMEI_Serial_number" v-model="detail.imei_number" :placeholder="$t('Add_product_IMEI_Serial_number')" ></b-form-input> </b-form-group> </b-col> <b-col md="12"> <b-form-group> <b-button variant="primary" type="submit">{{$t('submit')}}</b-button> </b-form-group> </b-col> </b-row> </b-form> </b-modal> </validation-observer> </b-card> </b-col> <!-- Card right Of Products --> <b-col md="7"> <b-card class="list-grid"> <b-row> <b-col md="6"> <button v-b-toggle.sidebar-category class="btn btn-outline-info mt-1 btn-block"> <i class="i-Two-Windows"></i> {{$t('ListofCategory')}} </button> </b-col> <b-col md="6"> <button v-b-toggle.sidebar-brand class="btn btn-outline-info mt-1 btn-block"> <i class="i-Library"></i> {{$t('ListofBrand')}} </button> </b-col> <!-- Product --> <b-col md="12" class="mt-2 mb-2"> <div id="autocomplete" class="autocomplete"> <div class="input-with-icon"> <img src="/assets_setup/scan.png" alt="Scan" class="scan-icon" @click="showModal"> <input :placeholder="$t('Scan_Search_Product_by_Code_Name')" @input='e => search_input = e.target.value' @keyup="search(search_input)" @focus="handleFocus" @blur="handleBlur" ref="product_autocomplete" class="autocomplete-input" /> </div> <ul class="autocomplete-result-list" v-show="focused"> <li class="autocomplete-result" v-for="product_fil in product_filter" @mousedown="SearchProduct(product_fil)">{{getResultValue(product_fil)}}</li> </ul> </div> </b-col> <div class="col-md-12 d-flex flex-row flex-wrap bd-highlight list-item mt-2"> <div @click="Check_Product_Exist(product , product.id)" v-for="product in products" class="card o-hidden bd-highlight m-1" > <div class="list-thumb d-flex"> <img alt :src="'/images/products/'+product.image"> </div> <div class="flex-grow-1 d-bock"> <div class="card-body align-self-center d-flex flex-column justify-content-between align-items-lg-center" > <div class="w-40 w-sm-100 item-title">{{product.name}}</div> <p class="text-muted text-small w-15 w-sm-100 mb-2">{{product.code}}</p> <span class="badge badge-primary w-15 w-sm-100 mb-2" >{{currentUser.currency}} {{formatNumber(product.Net_price , 2)}}</span> <p v-if="product.product_type != 'is_service'" class="m-0 text-muted text-small w-15 w-sm-100 d-none d-lg-block item-badges" > <span class="badge badge-info" >{{formatNumber(product.qte_sale , 2)}} {{product.unitSale}}</span> </p> </div> </div> </div> </div> </b-row> <b-row class="mb-3"> <b-col md="12" class="mt-4"> <b-pagination @change="Product_onPageChanged" :total-rows="product_totalRows" :per-page="product_perPage" v-model="product_currentPage" class="my-0 gull-pagination align-items-center" align="center" first-text last-text > <p class="list-arrow m-0" slot="prev-text"> <i class="i-Arrow-Left text-40"></i> </p> <p class="list-arrow m-0" slot="next-text"> <i class="i-Arrow-Right text-40"></i> </p> </b-pagination> </b-col> </b-row> </b-card> <!-- Fixed Footer Action Buttons --> <div class="pos-button-actions" style="position: sticky; bottom: 0; z-index: 10; background: white; padding: 10px 15px; border-top: 1px solid #dee2e6; display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-start;" > <b-button style="min-width: 120px;" @click="Reset_Pos()" variant="danger ripple btn-rounded" > <i class="i-Power-2"></i> {{ $t("Reset") }} </b-button> <b-button style="min-width: 120px;" @click="Submit_Pos()" variant="success ripple btn-rounded" > <i class="i-Checkout"></i> {{ $t("payNow") }} </b-button> <b-button style="min-width: 120px;" @click="Submit_Draft()" :disabled="DraftProcessing" variant="primary ripple btn-rounded" > <i class="i-Sand-watch"></i> {{ $t("Draft") }} </b-button> <b-button style="min-width: 140px;" @click="Show_Draft_Sales()" variant="light ripple btn-rounded" > <i class="i-Alarm-Clock"></i> {{ $t("Recent_Drafts") }} </b-button> </div> </b-col> <!-- Today Sales --> <b-modal hide-footer size="md" scrollable id="modal_today_sales" :title="'Today\'s Sales'+ ' ( '+ today_sales.today +' )'"> <b-row class="mb-3"> <b-col md="6" class="text-left"> <h6>{{$t('Total_Sales_Amount')}}:</h6> </b-col> <b-col md="6" class="text-right"> <h6> {{currentUser.currency}} {{ formatNumber(today_sales.total_sales_amount,2) }}</h6> </b-col> </b-row> <b-row class="mb-3"> <b-col md="6" class="text-left"> <h6>{{$t('Total_Amount_Paid')}}:</h6> </b-col> <b-col md="6" class="text-right"> <h6> {{currentUser.currency}} {{ formatNumber(today_sales.total_amount_paid,2) }}</h6> </b-col> </b-row> <b-row class="mb-3"> <b-col md="6" class="text-left"> <h6>{{$t('Total_Cash')}}:</h6> </b-col> <b-col md="6" class="text-right"> <h6> {{currentUser.currency}} {{ formatNumber(today_sales.total_cash,2) }}</h6> </b-col> </b-row> <b-row class="mb-3"> <b-col md="6" class="text-left"> <h6>{{$t('Total_Credit_Card')}}:</h6> </b-col> <b-col md="6" class="text-right"> <h6> {{currentUser.currency}} {{ formatNumber(today_sales.total_credit_card,2) }}</h6> </b-col> </b-row> <b-row class="mb-3"> <b-col md="6" class="text-left"> <h6>{{$t('Total_Cheque')}}:</h6> </b-col> <b-col md="6" class="text-right"> <h6> {{currentUser.currency}} {{ formatNumber(today_sales.total_cheque,2) }}</h6> </b-col> </b-row> </b-modal> <!-- Sidebar Brand --> <b-sidebar id="sidebar-brand" :title="$t('ListofBrand')" bg-variant="white" right shadow> <div class="px-3 py-2"> <b-row> <b-col md="12" class="mb-3"> <b-form-input v-model="search_brand" :placeholder="$t('Search_Brands')" class="w-100" ></b-form-input> </b-col> </b-row> <b-row> <div class="col-md-12 d-flex flex-row flex-wrap bd-highlight list-item mt-2"> <div @click="GetAllBrands()" :class="{ 'brand-Active' : brand_id == ''}" class="card o-hidden bd-highlight m-1" > <div class="list-thumb d-flex"> <img alt :src="'/images/no-image.png'"> </div> <div class="flex-grow-1 d-bock"> <div class="card-body align-self-center d-flex flex-column justify-content-between align-items-lg-center" > <div class="item-title">{{$t('All_Brand')}}</div> </div> </div> </div> <div class="card o-hidden bd-highlight m-1" v-for="brand in filteredBrands" :key="brand.id" @click="Products_by_Brands(brand.id)" :class="{ 'brand-Active' : brand.id === brand_id}" > <img alt :src="'/images/brands/'+brand.image"> <div class="flex-grow-1 d-bock"> <div class="card-body align-self-center d-flex flex-column justify-content-between align-items-lg-center" > <div class="item-title">{{brand.name}}</div> </div> </div> </div> </div> </b-row> <b-row> <b-col md="12" class="mt-4"> <b-pagination @change="BrandonPageChanged" :total-rows="brand_totalRows" :per-page="brand_perPage" v-model="brand_currentPage" class="my-0 gull-pagination align-items-center" align="center" first-text last-text > <p class="list-arrow m-0" slot="prev-text"> <i class="i-Arrow-Left text-40"></i> </p> <p class="list-arrow m-0" slot="next-text"> <i class="i-Arrow-Right text-40"></i> </p> </b-pagination> </b-col> </b-row> </div> </b-sidebar> <!-- Sidebar category --> <b-sidebar id="sidebar-category" :title="$t('ListofCategory')" bg-variant="white" right shadow > <div class="px-3 py-2"> <b-row> <b-col md="12" class="mb-3"> <b-form-input v-model="search_category" :placeholder="$t('Search_Categories')" class="w-100" ></b-form-input> </b-col> </b-row> <b-row> <div class="col-md-12 flex-row flex-wrap bd-highlight list-item mt-2"> <div @click="getAllCategory()" :class="{ 'brand-Active' : category_id == ''}" class="card bd-highlight m-1" > <div class="flex-grow-1 d-bock" style=" cursor: pointer; "> <div class="card-body align-self-center flex-column justify-content-between align-items-lg-center" > <div class="item-title">{{$t('All_Category')}}</div> </div> </div> </div> <div class="card bd-highlight m-1" v-for="category in filteredCategories" :key="category.id" @click="Products_by_Category(category.id)" :class="{ 'brand-Active' : category.id === category_id}" > <div class="flex-grow-1 d-bock" style=" cursor: pointer; "> <div class="card-body align-self-center flex-column justify-content-between align-items-lg-center" > <div class="item-title">{{category.name}}</div> </div> </div> </div> </div> </b-row> <b-row> <b-col md="12" class="mt-4"> <b-pagination @change="Category_onPageChanged" :total-rows="category_totalRows" :per-page="category_perPage" v-model="category_currentPage" class="my-0 gull-pagination align-items-center" align="center" first-text last-text > <p class="list-arrow m-0" slot="prev-text"> <i class="i-Arrow-Left text-40"></i> </p> <p class="list-arrow m-0" slot="next-text"> <i class="i-Arrow-Right text-40"></i> </p> </b-pagination> </b-col> </b-row> </div> </b-sidebar> <!-- Modal Show Invoice POS--> <b-modal hide-footer size="sm" scrollable id="Show_invoice" :title="$t('Invoice_POS')"> <div id="invoice-POS"> <div style="max-width:400px;margin:0px auto"> <div class="info"> <div class="invoice_logo text-center mb-2"> <img :src="'/images/'+invoice_pos.setting.logo" alt width="60" height="60"> </div> <p> <span>{{$t('date')}} : {{invoice_pos.sale.date}} <br></span> <span>{{$t('Seller')}} : {{invoice_pos.sale.seller_name}} <br></span> <span v-show="pos_settings.show_address">{{$t('Adress')}} : {{invoice_pos.setting.CompanyAdress}} <br></span> <span v-show="pos_settings.show_email">{{$t('Email')}} : {{invoice_pos.setting.email}} <br></span> <span v-show="pos_settings.show_phone">{{$t('Phone')}} : {{invoice_pos.setting.CompanyPhone}} <br></span> <span v-show="pos_settings.show_customer">{{$t('Customer')}} : {{invoice_pos.sale.client_name}} <br></span> <span v-show="pos_settings.show_Warehouse">{{$t('warehouse')}} : {{invoice_pos.sale.warehouse_name}} <br></span> </p> </div> <table class="table_data"> <tbody> <tr v-for="detail_invoice in invoice_pos.details"> <td colspan="3"> {{detail_invoice.name}} <br v-show="detail_invoice.is_imei && detail_invoice.imei_number !==null"> <span v-show="detail_invoice.is_imei && detail_invoice.imei_number !==null ">{{$t('IMEI_SN')}} : {{detail_invoice.imei_number}}</span> <br> <span>{{formatNumber(detail_invoice.quantity,2)}} {{detail_invoice.unit_sale}} x {{formatNumber(detail_invoice.total/detail_invoice.quantity,2)}}</span> </td> <td style="text-align:right;vertical-align:bottom" >{{formatNumber(detail_invoice.total,2)}}</td> </tr> <tr style="margin-top:10px" v-show="pos_settings.show_discount"> <td colspan="3" class="total">{{$t('OrderTax')}}</td> <td style="text-align:right;" class="total">{{invoice_pos.symbol}} {{formatNumber(invoice_pos.sale.taxe ,2)}} ({{formatNumber(invoice_pos.sale.tax_rate,2)}} %)</td> </tr> <tr style="margin-top:10px" v-show="pos_settings.show_discount"> <td colspan="3" class="total">{{$t('Discount')}}</td> <td style="text-align:right;" class="total">{{invoice_pos.symbol}} {{formatNumber(invoice_pos.sale.discount ,2)}}</td> </tr> <tr style="margin-top:10px" v-show="pos_settings.show_discount"> <td colspan="3" class="total">{{$t('Shipping')}}</td> <td style="text-align:right;" class="total">{{invoice_pos.symbol}} {{formatNumber(invoice_pos.sale.shipping ,2)}}</td> </tr> <tr style="margin-top:10px"> <td colspan="3" class="total">{{$t('Total')}}</td> <td style="text-align:right;" class="total" >{{invoice_pos.symbol}} {{formatNumber(invoice_pos.sale.GrandTotal ,2)}}</td> </tr> <tr v-show="invoice_pos.sale.paid_amount < invoice_pos.sale.GrandTotal"> <td colspan="3" class="total">{{$t('Paid')}}</td> <td style="text-align:right;" class="total" >{{invoice_pos.symbol}} {{formatNumber(invoice_pos.sale.paid_amount ,2)}}</td> </tr> <tr v-show="invoice_pos.sale.paid_amount < invoice_pos.sale.GrandTotal"> <td colspan="3" class="total">{{$t('Due')}}</td> <td style="text-align:right;" class="total" >{{invoice_pos.symbol}} {{parseFloat(invoice_pos.sale.GrandTotal - invoice_pos.sale.paid_amount).toFixed(2)}}</td> </tr> </tbody> </table> <table class="change mt-3" style=" font-size: 10px;" v-show="invoice_pos.sale.paid_amount > 0" > <thead> <tr style="background: #eee; "> <th style="text-align: left;" colspan="1">{{$t('PayeBy')}}:</th> <th style="text-align: center;" colspan="2">{{$t('Amount')}}:</th> <th style="text-align: right;" colspan="1">{{$t('Change')}}:</th> </tr> </thead> <tbody> <tr v-for="payment_pos in payments"> <td style="text-align: left;" colspan="1">{{payment_pos.payment_method?payment_pos.payment_method.name:'---'}}</td> <td style="text-align: center;" colspan="2" >{{formatNumber(payment_pos.montant ,2)}}</td> <td style="text-align: right;" colspan="1" >{{formatNumber(payment_pos.change ,2)}}</td> </tr> </tbody> </table> <div id="legalcopy" class="ml-2"> <p class="legal" v-show="pos_settings.show_note"> <strong>{{pos_settings.note_customer}}</strong> </p> <div id="bar" v-show="pos_settings.show_barcode"> <barcode class="barcode" :format="barcodeFormat" :value="invoice_pos.sale.Ref" textmargin="0" fontoptions="bold" fontSize="15" height="25" width="1" ></barcode> </div> </div> </div> </div> <button @click="print_pos()" class="btn btn-outline-primary"> <i class="i-Billing"></i> {{$t('print')}} </button> </b-modal> <!-- Modal Add Payment--> <validation-observer ref="Add_payment"> <b-modal id="Add_Payment" size="lg" hide-footer centered dialog-class="modal-custom-width"> <!-- HEADER --> <template #modal-header> <div class="d-flex w-100 justify-content-between align-items-center"> <!-- Left: Modal Title --> <h5 class="modal-title mb-0">{{ $t('Payment') }}</h5> <!-- Center: Grand Total --> <span class="h5 text-success mb-0"> {{ currentUser.currency }} {{ GrandTotal.toFixed(2) }} </span> <!-- Right: Close Icon --> <b-button variant="link" size="sm" class="close m-0 p-0" @click="$bvModal.hide('Add_Payment')" aria-label="Close" > <span aria-hidden="true">×</span> </b-button> </div> </template> <b-form @submit.prevent="Submit_Payment"> <!-- CLIENT NAME --> <h1 class="text-center mt-3 mb-3">{{ client_name }}</h1> <b-row> <!-- SUMMARY CARD --> <b-col md="4"> <b-card class="mb-3"> <b-list-group flush> <!-- Total Paying with icon --> <b-list-group-item class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <i class="i-Money-2 text-primary mr-2" style="font-size:1.2rem;"></i> <span>{{ $t('TotalPaying') }}</span> </div> <strong>{{ currentUser.currency }} {{ totalPaid }}</strong> </b-list-group-item> <!-- Balance with icon --> <b-list-group-item class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <i class="i-Money-Bag text-warning mr-2" style="font-size:1.2rem;"></i> <span>{{ $t('Balance') }}</span> </div> <strong>{{ currentUser.currency }} {{ balance }}</strong> </b-list-group-item> <!-- Change Return with icon --> <b-list-group-item class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <i class="i-Arrow-Back3 text-success mr-2" style="font-size:1.2rem;"></i> <span>{{ $t('ChangeReturn') }}</span> </div> <strong>{{ currentUser.currency }} {{ changeReturn }}</strong> </b-list-group-item> </b-list-group> </b-card> </b-col> <!-- SPLIT-PAYMENT FORM --> <b-col md="8"> <b-card class="mb-3" v-for="(p, idx) in paymentLines" :key="idx"> <!-- Header with remove icon --> <template #header> <div class="d-flex justify-content-between align-items-center"> <span>{{ $t('Payment') }} #{{ idx + 1 }}</span> <i v-if="idx > 0" class="i-Remove text-danger" style="cursor: pointer; font-size: 1.3rem;" @click="removePaymentLine(idx)" ></i> </div> </template> <b-row class="align-items-end mb-2"> <!-- Paying Amount --> <b-col md="12"> <validation-provider name="Paying Amount" :rules="{ required: true , regex: /^\d*\.?\d*$/}" v-slot="validationContext" > <b-form-group :label="$t('Paying_Amount') + ' *'"> <b-form-input label="Paying_Amount" :placeholder="$t('Paying_Amount')" v-model.number="p.amount" :state="getValidationState(validationContext)" aria-describedby="Paying_Amount-feedback" ></b-form-input> <b-form-invalid-feedback id="Paying_Amount-feedback"> {{ validationContext.errors[0] }} </b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Payment choice --> <b-col md="12"> <validation-provider name="Payment choice" :rules="{ required: true}"> <b-form-group slot-scope="{ valid, errors }" :label="$t('Paymentchoice') + ' ' + '*'"> <v-select :class="{'is-invalid': !!errors.length}" :state="errors[0] ? false : (valid ? true : null)" v-model="p.payment_method_id" @input="Selected_PaymentMethod" :reduce="label => label.value" :placeholder="$t('PleaseSelect')" :options="payment_methods.map(payment_methods => ({label: payment_methods.name, value: payment_methods.id}))" ></v-select> <b-form-invalid-feedback>{{ errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> </b-row> </b-card> <!-- Add another payment --> <b-row class="mb-4"> <b-col md="12"> <b-button block variant="outline-primary" @click="addPaymentLine"> + {{ $t('AddAnotherPaymentOption') }} </b-button> </b-col> </b-row> <b-row class="mb-4"> <b-col md="12"> <b-card v-show="anyCreditCardUsed"> <!-- Show spinner while submitting --> <div v-if="submit_showing_credit_card"> <div class="spinner sm spinner-primary mt-3"></div> </div> <!-- Saved card block --> <div v-if="displaySavedPaymentMethods && !submit_showing_credit_card"> <div class="mt-3"> <span class="mr-3">Saved Credit Card Info For This Client</span> <b-button variant="outline-info" @click="show_new_credit_card()"> <i class="i-Two-Windows"></i> New Credit Card </b-button> </div> <table class="table table-hover mt-3"> <thead> <tr> <th>Last 4 digits</th> <th>Type</th> <th>Exp</th> <th>Action</th> </tr> </thead> <tbody> <tr v-for="card in savedPaymentMethods" :key="card.card_id" :class="{ 'bg-selected-card': isSelectedCard(card) }" > <td>**** {{ card.last4 }}</td> <td>{{ card.type }}</td> <td>{{ card.exp }}</td> <td> <b-button variant="outline-primary" @click="selectCard(card)" v-if="!isSelectedCard(card) && card_id !== card.card_id" > <i class="i-Drag-Up"></i> Use This </b-button> <i v-else class="i-Yes" style="font-size: 20px;" ></i> </td> </tr> </tbody> </table> </div> <!-- New card form --> <div v-if="displayFormNewCard && !submit_showing_credit_card"> <form id="payment-form"> <label for="card-element" class="leading-7 text-sm text-gray-600"> {{ $t('Credit_Card_Info') }} <b-button variant="outline-info" @click="show_saved_credit_card()" v-if="savedPaymentMethods.length" > <i class="i-Two-Windows"></i> Use Saved Credit Card </b-button> </label> <div id="card-element"></div> <div id="card-errors" class="is-invalid" role="alert"></div> </form> </div> </b-card> </b-col> </b-row> <!-- Popular Tendered --> <div class="mb-3"> <small class="text-muted">{{ $t('PopularTendered') }}</small> <div class="d-flex flex-wrap mt-2"> <b-button v-for="amt in [GrandTotal,15,20,50,100,200]" :key="amt" variant="outline-secondary" size="sm" class="mr-2 mb-2" @click="setQuickAmount(amt)" > {{ currentUser.currency }} {{ amt.toFixed(2) }} </b-button> </div> </div> <!-- Keypad --> <div class="keypad mb-3" style="display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem" > <b-button variant="light" @click="appendDigit('1')">1</b-button> <b-button variant="light" @click="appendDigit('2')">2</b-button> <b-button variant="light" @click="appendDigit('3')">3</b-button> <b-button variant="light" @click="appendDigit('4')">4</b-button> <b-button variant="light" @click="appendDigit('5')">5</b-button> <b-button variant="light" @click="appendDigit('6')">6</b-button> <b-button variant="light" @click="appendDigit('7')">7</b-button> <b-button variant="light" @click="appendDigit('8')">8</b-button> <b-button variant="light" @click="appendDigit('9')">9</b-button> <b-button variant="light" @click="clearInput">{{ $t('clear') }}</b-button> <b-button variant="light" @click="appendDigit('0')">0</b-button> <b-button variant="light" @click="backspace"> <i class="i-Backspace"></i> </b-button> </div> <!-- Global Payment Note & Sale Note --> <b-row class="mb-4"> <b-col md="6"> <b-form-group :label="$t('Payment_note')"> <b-form-textarea v-model="globalPaymentNote" rows="3" /> </b-form-group> </b-col> <b-col md="6"> <b-form-group :label="$t('sale_note')"> <b-form-textarea v-model="sale.notes" rows="3" /> </b-form-group> </b-col> <b-col md="6"> <b-form-group :label="$t('Account')"> <v-select :options="accounts.map(a => ({ label: a.account_name, value: a.id }))" v-model="selectedAccount" :reduce="o => o.value" :placeholder="$t('Choose_Account')" /> </b-form-group> </b-col> <b-col md="6"> </b-col> <b-col md="6" class="mt-3"> <b-form-checkbox v-model="sendEmail" name="sendEmail" > {{ $t('Send_Email') }} </b-form-checkbox> </b-col> <b-col md="6" class="mt-3"> <b-form-checkbox v-model="sendSMS" name="sendSMS" > {{ $t('Send_SMS') }} </b-form-checkbox> </b-col> </b-row> <!-- PAY BUTTON --> <b-button variant="success" block size="lg" @click="Submit_Payment" :disabled="paymentProcessing" > {{ $t('Pay') }} </b-button> <div v-once class="typo__p" v-if="paymentProcessing"> <div class="spinner sm spinner-primary mt-3"></div> </div> </b-col> </b-row> </b-form> </b-modal> </validation-observer> <validation-observer ref="Create_Customer"> <b-modal hide-footer size="lg" id="New_Customer" :title="$t('Add')"> <b-form @submit.prevent="Submit_Customer"> <b-row> <!-- Customer Name --> <b-col md="6" sm="12"> <validation-provider name="Name Customer" :rules="{ required: true}" v-slot="validationContext" > <b-form-group :label="$t('CustomerName') + ' ' + '*'"> <b-form-input :state="getValidationState(validationContext)" aria-describedby="name-feedback" label="name" v-model="client.name" :placeholder="$t('CustomerName')" ></b-form-input> <b-form-invalid-feedback id="name-feedback">{{ validationContext.errors[0] }}</b-form-invalid-feedback> </b-form-group> </validation-provider> </b-col> <!-- Customer Email --> <b-col md="6" sm="12"> <b-form-group :label="$t('Email')"> <b-form-input label="email" v-model="client.email" :placeholder="$t('Email')" ></b-form-input> </b-form-group> </b-col> <!-- Customer Phone --> <b-col md="6" sm="12"> <b-form-group :label="$t('Phone')"> <b-form-input label="Phone" v-model="client.phone" :placeholder="$t('Phone')" ></b-form-input> </b-form-group> </b-col> <!-- Customer Country --> <b-col md="6" sm="12"> <b-form-group :label="$t('Country')"> <b-form-input label="Country" v-model="client.country" :placeholder="$t('Country')" ></b-form-input> </b-form-group> </b-col> <!-- Customer City --> <b-col md="6" sm="12"> <b-form-group :label="$t('City')"> <b-form-input label="City" v-model="client.city" :placeholder="$t('City')" ></b-form-input> </b-form-group> </b-col> <!-- Customer Tax Number --> <b-col md="6" sm="12"> <b-form-group :label="$t('Tax_Number')"> <b-form-input label="Tax Number" v-model="client.tax_number" :placeholder="$t('Tax_Number')" ></b-form-input> </b-form-group> </b-col> <!-- Customer Adress --> <b-col md="12" sm="12"> <b-form-group :label="$t('Adress')"> <textarea label="Adress" class="form-control" rows="4" v-model="client.adresse" :placeholder="$t('Adress')" ></textarea> </b-form-group> </b-col> <b-col md="6" sm="12" class="mt-4 mb-4"> <label class="checkbox checkbox-primary mb-3"><input type="checkbox" v-model="client.is_royalty_eligible"><h5>is royalty eligible </h5><span class="checkmark"></span></label> </b-col> <b-col md="12" class="mt-3"> <b-button variant="primary" type="submit"><i class="i-Yes me-2 font-weight-bold"></i> {{$t('submit')}}</b-button> </b-col> </b-row> </b-form> </b-modal> </validation-observer> <b-modal hide-footer size="lg" id="show_draft_sales" title="Draft Sales" > <vue-good-table mode="remote" :columns="columns_draft_sales" :totalRows="totalRows_draft_sales" :rows="draft_sales" @on-page-change="onPageChange" @on-per-page-change="onPerPageChange" :pagination-options="{ enabled: true, mode: 'records', nextLabel: 'next', prevLabel: 'prev', }" styleClass="tableOne table-hover vgt-table" > <template slot="table-row" slot-scope="props"> <span v-if="props.column.field == 'actions'"> <router-link v-b-tooltip.hover title="Edit" :to="{ name:'pos_draft', params: { id: props.row.id } }" > <i class="i-Edit text-25 text-success"></i> </router-link> <a @click="Remove_Draft_Sale(props.row.id)" v-b-tooltip.hover title="Delete" class="cursor-pointer" > <i class="i-Close-Window text-25 text-danger"></i> </a> </span> </template> </vue-good-table> </b-modal> <!-- <div class="pos-button-actions" style="display: flex;margin-top: 10px;bottom: 0px;margin-left: 29px;width: 100%; flex-wrap: wrap; "> <b-button style="width: auto;margin-bottom: 8px;" @click="Reset_Pos()" variant="danger ripple btn-rounded mr-1" > <i class="i-Power-2"></i> {{ $t("Reset") }} </b-button> <b-button style="width: auto;margin-bottom: 8px;" @click="Submit_Pos()" variant="success ripple mr-1 btn-rounded"> <i class="i-Checkout"></i> {{ $t("payNow") }} </b-button> <b-button style="width: auto;margin-bottom: 8px;" @click="Submit_Draft()" :disabled="DraftProcessing" variant="primary ripple mr-1 btn-rounded"> <i class="i-Sand-watch"></i> Draft </b-button> <b-button style="width: auto;margin-bottom: 8px;" @click="Show_Draft_Sales()" variant="light ripple mr-1 btn-rounded"> <i class="i-Alarm-Clock"></i> Recent Drafts </b-button> </div> --> </b-row> </div> </div> </template> <script> import NProgress from "nprogress"; import { mapActions, mapGetters } from "vuex"; import vueEasyPrint from "vue-easy-print"; import VueBarcode from "vue-barcode"; import Util from "../../../utils"; import { loadStripe } from "@stripe/stripe-js"; export default { components: { vueEasyPrint, barcode: VueBarcode, }, metaInfo: { title: "POS" }, data() { return { sendEmail: false, sendSMS: false, stripe: {}, stripe_key: "", cardElement: {}, paymentProcessing: false, DraftProcessing: false, savedPaymentMethods: [], hasSavedPaymentMethod: false, useSavedPaymentMethod: false, selectedCard:null, card_id:'', is_new_credit_card: false, submit_showing_credit_card: false, totalRows_draft_sales: "", draft_sales:[], limit: "10", serverParams: { sort: { field: "id", type: "desc" }, page: 1, perPage: 10 }, client_name:'', paymentLines: [ { // only the first line shows Received Amount amount: 0, payment_method_id: '', } ], globalPaymentNote: '', selectedAccount: null, payment_methods:[], search_category: '', search_brand: '', focused: false, timer:null, search_input:'', product_filter:[], isLoading: true, load_product: true, GrandTotal: 0, total: 0, Ref: "", clients: [], units: [], warehouses: [], payments: [], products: [], products_pos: [], details: [], detail: {}, categories: [], brands: [], accounts: [], pos_settings:{}, product_currentPage: 1, paginated_Products: "", product_perPage: '', product_totalRows: "", paginated_Brands: "", brand_currentPage: 1, brand_perPage: 3, paginated_Category: "", category_currentPage: 1, category_perPage: 3, barcodeFormat: "CODE128", today_sales:{ total_sales_amount: "", total_amount_paid: "", today: "", total_cash: "", total_credit_card: "", total_cheque: "", }, invoice_pos: { sale: { Ref: "", client_name: "", discount: "", taxe: "", date: "", tax_rate: "", shipping: "", GrandTotal: "", paid_amount: "" }, details: [], setting: { logo: "", CompanyName: "", CompanyAdress: "", email: "", CompanyPhone: "" } }, selectedClientPoints: 0, showPointsSection: false, discount_from_points: 0, used_points: 0, clientIsEligible: false, pointsConverted: false, point_to_amount_rate: 0, sale: { warehouse_id: "", client_id: "", tax_rate: 0, shipping: 0, discount: 0, TaxNet: 0, notes:'', }, client: { id: "", name: "", code: "", email: "", phone: "", country: "", tax_number: "", city: "", adresse: "", is_royalty_eligible: "", }, category_id: "", brand_id: "", languages_available:[], product: { id: "", code: "", product_type: "", current: "", quantity: "", check_qty: "", discount: "", DiscountNet: "", discount_Method: "", sale_unit_id: "", fix_stock: "", fix_price: "", name: "", unitSale: "", Net_price: "", Unit_price: "", Total_price: "", subtotal: "", product_id: "", detail_id: "", taxe: "", tax_percent: "", tax_method: "", product_variant_id: "", is_imei: "", imei_number:"", }, sound: "/audio/Beep.wav", audio: new Audio("/audio/Beep.wav") }; }, computed: { ...mapGetters(["currentUser", "currentUserPermissions","show_language"]), anyCreditCardUsed() { return this.paymentLines.some(p => p.payment_method_id === '1' || p.payment_method_id === 1); }, // Sum of all entered payment lines totalPaid() { return this.paymentLines.reduce((sum, p) => sum + Number(p.amount || 0), 0).toFixed(2); }, // What’s still due (never negative) balance() { const b = this.GrandTotal - this.totalPaid; return (b > 0 ? b : 0).toFixed(2); }, // How much to return if over-paid changeReturn() { const c = this.totalPaid - this.GrandTotal; return (c > 0 ? c : 0).toFixed(2); }, brand_totalRows() { return this.brands.length; }, category_totalRows() { return this.categories.length; }, filteredCategories() { if (this.search_category.trim() === '') { return this.paginated_Category; } return this.paginated_Category.filter(category => category.name.toLowerCase().includes(this.search_category.toLowerCase()) ); }, filteredBrands() { if (this.search_brand.trim() === '') { return this.paginated_Brands; } return this.paginated_Brands.filter(brand => brand.name.toLowerCase().includes(this.search_brand.toLowerCase()) ); }, displaySavedPaymentMethods() { if(this.hasSavedPaymentMethod){ return true; }else{ return false; } }, displayFormNewCard() { if(this.useSavedPaymentMethod){ return false; }else{ return true; } }, isSelectedCard() { return card => this.selectedCard === card; }, columns_draft_sales() { return [ { label: this.$t("date"), field: "date", tdClass: "text-left", thClass: "text-left", sortable: false }, { label: this.$t("Reference"), field: "Ref", tdClass: "text-left", thClass: "text-left", sortable: false }, { label: this.$t("Customer"), field: "client_name", tdClass: "text-left", thClass: "text-left", sortable: false }, { label: this.$t("warehouse"), field: "warehouse_name", tdClass: "text-left", thClass: "text-left", sortable: false }, { label: this.$t("Total"), field: "GrandTotal", tdClass: "text-left", thClass: "text-left", sortable: false }, { label: this.$t("Action"), field: "actions", html: true, tdClass: "text-right", thClass: "text-right", sortable: false } ]; } }, mounted() { this.changeSidebarProperties(); this.paginate_products(this.product_perPage, 0); }, methods: { ...mapActions(["changeSidebarProperties", "changeThemeMode", "logout"]), // ...mapGetters(["currentUser"]), logoutUser() { this.$store.dispatch("logout"); }, handleFocus() { this.focused = true }, handleBlur() { this.focused = false }, showModal() { this.$bvModal.show('open_scan'); }, onScan (decodedText, decodedResult) { const code = decodedText; this.search_input = code; this.search(); this.$bvModal.hide('open_scan'); }, addPaymentLine() { this.paymentLines.push({ amount: 0, payment_method_id: '', }) }, removePaymentLine(idx) { if (this.paymentLines.length > 1) { this.paymentLines.splice(idx, 1) } }, setQuickAmount(val) { // assign to current active line (e.g. last) const line = this.paymentLines[this.paymentLines.length - 1]; line.amount = val; }, appendDigit(d) { // append to the last line’s amount let line = this.paymentLines[this.paymentLines.length - 1]; let s = String(line.amount || ''); if (s === '0') s = d; else s += d; line.amount = parseFloat(s); }, clearInput() { this.paymentLines[this.paymentLines.length - 1].amount = 0; }, backspace() { let line = this.paymentLines[this.paymentLines.length - 1]; let s = String(line.amount || ''); s = s.slice(0, -1) || '0'; line.amount = parseFloat(s); }, async Selected_PaymentMethod(value) { if (value == '1' || value == 1) { this.savedPaymentMethods = []; this.submit_showing_credit_card = true; this.selectedCard = null this.card_id = ''; // Check if the customer has saved payment methods await axios.get(`/retrieve-customer?customerId=${this.selectedClientId}`) .then(response => { // If the customer has saved payment methods, display them this.savedPaymentMethods = response.data.data; this.card_id = response.data.customer_default_source; this.hasSavedPaymentMethod = true; this.useSavedPaymentMethod = true; this.is_new_credit_card = false; this.submit_showing_credit_card = false; }) .catch(error => { // If the customer does not have saved payment methods, show the card element for them to enter their payment information this.hasSavedPaymentMethod = false; this.useSavedPaymentMethod = false; this.is_new_credit_card = true; this.card_id = ''; setTimeout(() => { this.loadStripe_payment(); }, 1000); this.submit_showing_credit_card = false; }); }else{ this.hasSavedPaymentMethod = false; this.useSavedPaymentMethod = false; this.is_new_credit_card = false; } }, show_saved_credit_card() { this.hasSavedPaymentMethod = true; this.useSavedPaymentMethod = true; this.is_new_credit_card = false; this.Selected_PaymentMethod(1); }, show_new_credit_card() { this.selectedCard = null; this.card_id = ''; this.useSavedPaymentMethod = false; this.hasSavedPaymentMethod = false; this.is_new_credit_card = true; setTimeout(() => { this.loadStripe_payment(); }, 500); }, selectCard(card) { this.selectedCard = card; this.card_id = card.card_id; }, async loadStripe_payment() { this.stripe = await loadStripe(`${this.stripe_key}`); const elements = this.stripe.elements(); this.cardElement = elements.create("card", { classes: { base: "bg-gray-100 rounded border border-gray-300 focus:border-indigo-500 text-base outline-none text-gray-700 p-3 leading-8 transition-colors duration-200 ease-in-out" } }); this.cardElement.mount("#card-element"); }, SetLocal(locale) { this.$i18n.locale = locale; this.$store.dispatch("language/setLanguage", locale); Fire.$emit("ChangeLanguage"); window.location.reload(); }, handleFullScreen() { Util.toggleFullScreen(); }, logoutUser() { this.logout(); }, // ------------------------ Paginate Products --------------------\\ Product_paginatePerPage() { this.paginate_products(this.product_perPage, 0); }, paginate_products(pageSize, pageNumber) { let itemsToParse = this.products; this.paginated_Products = itemsToParse.slice( pageNumber * pageSize, (pageNumber + 1) * pageSize ); }, Product_onPageChanged(page) { this.paginate_products(this.product_perPage, page - 1); this.getProducts(page); }, // ------------------------ Paginate Brands --------------------\\ BrandpaginatePerPage() { this.paginate_Brands(this.brand_perPage, 0); }, paginate_Brands(pageSize, pageNumber) { let itemsToParse = this.brands; this.paginated_Brands = itemsToParse.slice( pageNumber * pageSize, (pageNumber + 1) * pageSize ); }, BrandonPageChanged(page) { this.paginate_Brands(this.brand_perPage, page - 1); }, // ------------------------ Paginate Categories --------------------\\ Category_paginatePerPage() { this.paginate_Category(this.category_perPage, 0); }, paginate_Category(pageSize, pageNumber) { let itemsToParse = this.categories; this.paginated_Category = itemsToParse.slice( pageNumber * pageSize, (pageNumber + 1) * pageSize ); }, Category_onPageChanged(page) { this.paginate_Category(this.category_perPage, page - 1); }, //--- Submit Validate Create Sale Submit_Pos() { // Start the progress bar. NProgress.start(); NProgress.set(0.1); this.$refs.create_pos.validate().then(success => { if (!success) { NProgress.done(); if (this.selectedClientId == "" || this.selectedClientId === null) { this.makeToast( "danger", this.$t("Choose_Customer"), this.$t("Failed") ); } else if ( this.sale.warehouse_id == "" || this.sale.warehouse_id === null ) { this.makeToast( "danger", this.$t("Choose_Warehouse"), this.$t("Failed") ); } else { this.makeToast( "danger", this.$t("Please_fill_the_form_correctly"), this.$t("Failed") ); } } else { if (this.verifiedForm()) { Fire.$emit("pay_now"); } else { NProgress.done(); } } }); }, //--- Submit Validate Draft Submit_Draft() { // Start the progress bar. NProgress.start(); NProgress.set(0.1); this.$refs.create_pos.validate().then(success => { if (!success) { NProgress.done(); if (this.selectedClientId == "" || this.selectedClientId === null) { this.makeToast( "danger", this.$t("Choose_Customer"), this.$t("Failed") ); } else if ( this.sale.warehouse_id == "" || this.sale.warehouse_id === null ) { this.makeToast( "danger", this.$t("Choose_Warehouse"), this.$t("Failed") ); } else { this.makeToast( "danger", this.$t("Please_fill_the_form_correctly"), this.$t("Failed") ); } } else { if (this.verifiedForm()) { this.Create_Draft(); } else { NProgress.done(); } } }); }, //---------------------------------- Create Draft ------------------------------\\ Create_Draft(){ NProgress.start(); NProgress.set(0.1); this.DraftProcessing = true; axios .post("pos/create_draft", { client_id: this.selectedClientId, warehouse_id: this.sale.warehouse_id, tax_rate: this.sale.tax_rate?this.sale.tax_rate:0, TaxNet: this.sale.TaxNet?this.sale.TaxNet:0, discount: this.sale.discount?this.sale.discount:0, shipping: this.sale.shipping?this.sale.shipping:0, notes: this.sale.notes, details: this.details, GrandTotal: this.GrandTotal, }) .then(response => { if (response.data.success === true) { // Complete the animation of theprogress bar. this.makeToast( "success", this.$t("Draft_Created_successfully"), this.$t("Success") ); NProgress.done(); this.DraftProcessing = false; this.Reset_Pos(); } }) .catch(error => { // Complete the animation of theprogress bar. NProgress.done(); this.DraftProcessing = false; this.makeToast("danger", this.$t("InvalidData"), this.$t("Failed")); }); }, Show_Draft_Sales(){ this.get_Draft_Sales(1); setTimeout(() => { this.$bvModal.show("show_draft_sales"); }, 1000); }, get_Draft_Sales(page){ NProgress.start(); NProgress.set(0.1); axios .get("get_draft_sales?page=" + page + "&limit=" + this.limit ) .then(response => { this.draft_sales = response.data.draft_sales; this.totalRows_draft_sales = response.data.totalRows; NProgress.done(); }) .catch(response => { NProgress.done(); }); }, //----------------------------------- Remove draft_sales ------------------------------\\ Remove_Draft_Sale(id) { this.$swal({ title: this.$t("Delete_Title"), text: this.$t("Delete_Text"), type: "warning", showCancelButton: true, confirmButtonColor: "#3085d6", cancelButtonColor: "#d33", cancelButtonText: this.$t("Delete_cancelButtonText"), confirmButtonText: this.$t("Delete_confirmButtonText") }).then(result => { if (result.value) { // Start the progress bar. NProgress.start(); NProgress.set(0.1); axios .delete("remove_draft_sale/" + id) .then(() => { this.$swal( this.$t("Delete_Deleted"), this.$t("Deleted_in_successfully"), "success" ); Fire.$emit("event_delete_draft_sale"); }) .catch(() => { // Complete the animation of theprogress bar. setTimeout(() => NProgress.done(), 500); this.$swal( this.$t("Delete_Failed"), this.$t("Delete_Therewassomethingwronge"), "warning" ); }); } }); }, //---- update Params Table updateParams(newProps) { this.serverParams = Object.assign({}, this.serverParams, newProps); }, //---- Event Page Change onPageChange({ currentPage }) { if (this.serverParams.page !== currentPage) { this.updateParams({ page: currentPage }); this.get_Draft_Sales(currentPage); } }, //---- Event Per Page Change onPerPageChange({ currentPerPage }) { if (this.limit !== currentPerPage) { this.limit = currentPerPage; this.updateParams({ page: 1, perPage: currentPerPage }); this.get_Draft_Sales(1); } }, //---Submit Validation Update Detail submit_Update_Detail() { this.$refs.Update_Detail.validate().then(success => { if (!success) { return; } else { this.Update_Detail(); } }); }, //------ Validate Form Submit_Payment Submit_Payment() { // Start the progress bar NProgress.start(); NProgress.set(0.1); this.$refs.Add_payment.validate().then(success => { if (!success) { NProgress.done(); this.makeToast( "danger", this.$t("Please_fill_the_form_correctly"), this.$t("Failed") ); return; } const total = parseFloat(this.totalPaid); const due = parseFloat(this.GrandTotal.toFixed(2)); const multi = this.paymentLines.length > 1; // 2) Multi-payment over-pay guard if (multi && total > due) { NProgress.done(); this.makeToast( "warning", this.$t("TotalPaidExceedsGrandTotalForMultiPayment"), this.$t("Warning") ); return; } // All checks passed: submit to POS this.CreatePOS(); }); }, //------------- Submit Validation Create & Edit Customer Submit_Customer() { // Start the progress bar. NProgress.start(); NProgress.set(0.1); this.$refs.Create_Customer.validate().then(success => { if (!success) { NProgress.done(); this.makeToast( "danger", this.$t("Please_fill_the_form_correctly"), this.$t("Failed") ); } else { this.Create_Client(); } }); }, //---------------------------------------- Create new Customer -------------------------------\\ Create_Client() { axios .post("clients", { name: this.client.name, email: this.client.email, phone: this.client.phone, tax_number: this.client.tax_number, country: this.client.country, city: this.client.city, adresse: this.client.adresse, is_royalty_eligible: this.client.is_royalty_eligible }) .then(response => { NProgress.done(); // Extract the newly created client from response const newClient = response.data; // Add the new client to the clients list this.clients.push({ id: newClient.id, name: newClient.name, }); // Automatically select the new client this.selectedClientId = newClient.id; this.client_name = newClient.name; this.onClientSelected(newClient.id); this.makeToast( "success", this.$t("Successfully_Created"), this.$t("Success") ); this.Get_Client_Without_Paginate(); this.$bvModal.hide("New_Customer"); }) .catch(error => { NProgress.done(); this.makeToast("danger", this.$t("InvalidData"), this.$t("Failed")); }); }, //------------------------------ New Model (create Customer) -------------------------------\\ New_Client() { this.reset_Form_client(); this.$bvModal.show("New_Customer"); }, //-------------------------------- reset Form -------------------------------\\ reset_Form_client() { this.client = { id: "", name: "", email: "", phone: "", tax_number: "", country: "", city: "", adresse: "", is_royalty_eligible: "" }; }, //------------------------------------ Get Clients Without Paginate -------------------------\\ Get_Client_Without_Paginate() { axios .get("get_clients_without_paginate") .then(({ data }) => (this.clients = data)); }, //---Validate State Fields getValidationState({ dirty, validated, valid = null }) { return dirty || validated ? valid : null; }, //------ Toast makeToast(variant, msg, title) { this.$root.$bvToast.toast(msg, { title: title, variant: variant, solid: true }); }, //---------------------- Event Select Warehouse ------------------------------\\ Selected_Warehouse(value) { this.search_input= ''; this.product_filter = []; this.Get_Products_By_Warehouse(value); this.getProducts(1); }, async onClientSelected(selectedClientId) { this.client_name = ''; this.selectedClientPoints = 0; this.discount_from_points = 0; this.used_points = 0; this.clientIsEligible = false; this.pointsConverted = false; // 👈 Reset conversion state this.sale.discount = 0; // 👈 Reset applied discount const client = this.clients.find(c => c.id === selectedClientId); if (client) { this.client_name = client.name; this.selectedClientId = selectedClientId; try { const response = await axios.get(`/get_points_client/${selectedClientId}`); const data = response.data; if (data.is_royalty_eligible) { this.selectedClientPoints = data.points; this.clientIsEligible = true; } else { this.selectedClientPoints = 0; this.clientIsEligible = false; } } catch (error) { console.error('Error fetching client points:', error); } } // ✅ Recalculate totals after client change this.CalculTotal(); }, convertPointsToDiscount() { if (this.pointsConverted) { // Reset conversion this.discount_from_points = 0; this.used_points = 0; this.sale.discount = 0; this.pointsConverted = false; } else { // Calculate discount based on conversion rate const discount = this.selectedClientPoints * this.point_to_amount_rate; this.discount_from_points = discount; this.sale.discount = discount; this.used_points = this.selectedClientPoints; this.pointsConverted = true; } this.CalculTotal(); // Recalculate grand total }, //------------------------------------ get_today_sales -------------------------\\ get_today_sales() { // Start the progress bar. NProgress.start(); NProgress.set(0.1); axios .get("get_today_sales") .then(response => { this.today_sales = response.data; setTimeout(() => { this.$bvModal.show("modal_today_sales"); NProgress.done(); }, 1000); }) .catch(error => { }); }, //------------------------------------ Get Products By Warehouse -------------------------\\ Get_Products_By_Warehouse(id) { // Start the progress bar. NProgress.start(); NProgress.set(0.1); axios .get("get_Products_by_warehouse/" + id + "?stock=" + 1 + "&is_sale=" + 1 + "&product_service=" + 1 + "&product_combo=" + 1) .then(response => { this.products_pos = response.data; NProgress.done(); }) .catch(error => { }); }, //----------------------------------------- Add Detail of Sale -------------------------\\ add_product(code) { this.audio.play(); if (this.details.some(detail => detail.code === code)) { this.increment_qty_scanner(code); } else { if (this.details.length > 0) { this.order_detail_id(); } else if (this.details.length === 0) { this.product.detail_id = 1; } this.details.push(this.product); setTimeout(() => { this.load_product = true; }, 300); if(this.product.is_imei){ this.Modal_Updat_Detail(this.product); } } }, //-------------------------------- order detail id -------------------------\\ order_detail_id() { this.product.detail_id = 0; var len = this.details.length; this.product.detail_id = this.details[len - 1].detail_id + 1; }, //---------------------- get_units ------------------------------\\ get_units(value) { axios .get("get_units?id=" + value) .then(({ data }) => (this.units = data)); }, //------ Show Modal Update Detail Product Modal_Updat_Detail(detail) { this.detail = {}; this.get_units(detail.product_id); this.detail.detail_id = detail.detail_id; this.detail.sale_unit_id = detail.sale_unit_id; this.detail.name = detail.name; this.detail.product_type = detail.product_type; this.detail.Unit_price = detail.Unit_price; this.detail.fix_price = detail.fix_price; this.detail.fix_stock = detail.fix_stock; this.detail.current = detail.current; this.detail.tax_method = detail.tax_method; this.detail.discount_Method = detail.discount_Method; this.detail.discount = detail.discount; this.detail.quantity = detail.quantity; this.detail.tax_percent = detail.tax_percent; this.detail.is_imei = detail.is_imei; this.detail.imei_number = detail.imei_number; setTimeout(() => { this.$bvModal.show("form_Update_Detail"); }, 1000); }, //------ Submit Update Detail Product Update_Detail() { for (var i = 0; i < this.details.length; i++) { if (this.details[i].detail_id === this.detail.detail_id) { // this.convert_unit(); for (var k = 0; k < this.units.length; k++) { if (this.units[k].id == this.detail.sale_unit_id) { if (this.units[k].operator == "/") { this.details[i].current = this.detail.fix_stock * this.units[k].operator_value; this.details[i].unitSale = this.units[k].ShortName; } else { this.details[i].current = this.detail.fix_stock / this.units[k].operator_value; this.details[i].unitSale = this.units[k].ShortName; } } } if (this.details[i].current < this.details[i].quantity) { this.details[i].quantity = this.details[i].current; } else { this.details[i].quantity = 1; } this.details[i].Unit_price = this.detail.Unit_price; this.details[i].tax_percent = this.detail.tax_percent; this.details[i].tax_method = this.detail.tax_method; this.details[i].discount_Method = this.detail.discount_Method; this.details[i].discount = this.detail.discount; this.details[i].sale_unit_id = this.detail.sale_unit_id; this.details[i].imei_number = this.detail.imei_number; this.details[i].product_type = this.detail.product_type; if (this.details[i].discount_Method == "2") { //Fixed this.details[i].DiscountNet = this.details[i].discount; } else { //Percentage % this.details[i].DiscountNet = parseFloat( (this.details[i].Unit_price * this.details[i].discount) / 100 ); } if (this.details[i].tax_method == "1") { //Exclusive this.details[i].Net_price = parseFloat( this.details[i].Unit_price - this.details[i].DiscountNet ); this.details[i].taxe = parseFloat( (this.details[i].tax_percent * (this.details[i].Unit_price - this.details[i].DiscountNet)) / 100 ); this.details[i].Total_price = parseFloat( this.details[i].Net_price + this.details[i].taxe ); } else { //Inclusive this.details[i].taxe = parseFloat( (this.details[i].Unit_price - this.details[i].DiscountNet) * (this.details[i].tax_percent / 100) ); this.details[i].Net_price = parseFloat( this.details[i].Unit_price - this.details[i].taxe - this.details[i].DiscountNet ); this.details[i].Total_price = parseFloat( this.details[i].Net_price + this.details[i].taxe ); } this.$forceUpdate(); } } this.CalculTotal(); setTimeout(() => { this.$bvModal.hide("form_Update_Detail"); }, 1000); }, //-- check Qty of details order if Null or zero verifiedForm() { if (this.details.length <= 0) { this.makeToast( "warning", this.$t("AddProductToList"), this.$t("Warning") ); return false; } else { var count = 0; for (var i = 0; i < this.details.length; i++) { if ( this.details[i].quantity == "" || this.details[i].quantity === 0 || this.details[i].quantity > this.details[i].current ) { count += 1; if(this.details[i].quantity > this.details[i].current){ this.makeToast("warning", this.$t("LowStock"), this.$t("Warning")); return false; } } } if (count > 0) { this.makeToast("warning", this.$t("AddQuantity"), this.$t("Warning")); return false; } else { return true; } } }, //------------------------------ Print -------------------------\\ print_pos() { var divContents = document.getElementById("invoice-POS").innerHTML; var a = window.open("", "", "height=500, width=500"); a.document.write( '<link rel="stylesheet" href="/css/pos_print.css"><html>' ); a.document.write("<body >"); a.document.write(divContents); a.document.write("</body></html>"); a.document.close(); setTimeout(() => { a.print(); }, 1000); }, //-------------------------------- Invoice POS ------------------------------\\ Invoice_POS(id) { // Start the progress bar. NProgress.start(); NProgress.set(0.1); axios .get("sales_print_invoice/" + id) .then(response => { this.invoice_pos = response.data; this.payments = response.data.payments; this.pos_settings = response.data.pos_settings; setTimeout(() => { // Complete the animation of the progress bar. NProgress.done(); this.$bvModal.show("Show_invoice"); }, 500); if(response.data.pos_settings.is_printable){ setTimeout(() => this.print_pos(), 1000); } }) .catch(() => { // Complete the animation of the progress bar. setTimeout(() => NProgress.done(), 500); }); }, //----------------------------------Process Payment ------------------------------\\ async processPayment() { this.paymentProcessing = true; const { token, error } = await this.stripe.createToken(this.cardElement); if (error) { this.paymentProcessing = false; NProgress.done(); this.makeToast("danger", this.$t("InvalidData"), this.$t("Failed")); } else { axios .post("pos/create_pos", { client_id: this.selectedClientId, warehouse_id: this.sale.warehouse_id, tax_rate: this.sale.tax_rate?this.sale.tax_rate:0, TaxNet: this.sale.TaxNet?this.sale.TaxNet:0, discount: this.sale.discount?this.sale.discount:0, shipping: this.sale.shipping?this.sale.shipping:0, details: this.details, GrandTotal: this.GrandTotal, notes: this.sale.notes, // ✅ NEW: Multi-payment array payments: this.paymentLines, send_email: this.sendEmail, send_sms: this.sendSMS, // Optional global account_id (used once for all) account_id: this.selectedAccount, payment_note: this.globalPaymentNote || '', token: token.id, is_new_credit_card: this.is_new_credit_card, selectedCard: this.selectedCard, card_id: this.card_id, discount_from_points: this.discount_from_points, used_points: this.used_points, }) .then(response => { this.paymentProcessing = false; if (response.data.success === true) { // Complete the animation of theprogress bar. NProgress.done(); this.Invoice_POS(response.data.id); this.$bvModal.hide("Add_Payment"); this.Reset_Pos(); } }) .catch(error => { this.paymentProcessing = false; // Complete the animation of theprogress bar. NProgress.done(); this.makeToast("danger", this.$t("InvalidData"), this.$t("Failed")); }); } }, //----------------------------------Create POS ------------------------------\\ CreatePOS() { NProgress.start(); NProgress.set(0.1); if (this.paymentLines.length > 1 && this.totalPaid > this.GrandTotal) { this.makeToast( "warning", this.$t("TotalPaidExceedsGrandTotalForMultiPayment"), this.$t("Warning") ); NProgress.done(); return; } // Check if any payment is credit card AND marked as new card const anyNewCard = this.paymentLines.some( p => (p.payment_method_id === '1' || p.payment_method_id === 1) && this.is_new_credit_card ); if (anyNewCard) { if (this.stripe_key !== '') { this.processPayment(); // continue to Stripe token generation } else { this.makeToast( 'danger', this.$t('credit_card_account_not_available'), this.$t('Failed') ); NProgress.done(); } } else { this.paymentProcessing = true; axios .post("pos/create_pos", { client_id: this.selectedClientId, warehouse_id: this.sale.warehouse_id, tax_rate: this.sale.tax_rate?this.sale.tax_rate:0, TaxNet: this.sale.TaxNet?this.sale.TaxNet:0, discount: this.sale.discount?this.sale.discount:0, shipping: this.sale.shipping?this.sale.shipping:0, notes: this.sale.notes, details: this.details, GrandTotal: this.GrandTotal, // ✅ NEW: Multi-payment array payments: this.paymentLines, send_email: this.sendEmail, send_sms: this.sendSMS, // Optional global account_id (used once for all) account_id: this.selectedAccount, payment_note: this.globalPaymentNote || '', is_new_credit_card: this.is_new_credit_card, selectedCard: this.selectedCard, card_id: this.card_id, discount_from_points: this.discount_from_points, used_points: this.used_points, }) .then(response => { if (response.data.success === true) { // Complete the animation of theprogress bar. NProgress.done(); this.paymentProcessing = false; this.Invoice_POS(response.data.id); this.$bvModal.hide("Add_Payment"); this.Reset_Pos(); } }) .catch(error => { // Complete the animation of theprogress bar. NProgress.done(); this.paymentProcessing = false; this.makeToast("danger", this.$t("InvalidData"), this.$t("Failed")); }); } }, //------------------------------Formetted Numbers -------------------------\\ formatNumber(number, dec) { const value = (typeof number === "string" ? number : number.toString() ).split("."); if (dec <= 0) return value[0]; let formated = value[1] || ""; if (formated.length > dec) return `${value[0]}.${formated.substr(0, dec)}`; while (formated.length < dec) formated += "0"; return `${value[0]}.${formated}`; }, //---------------------------------Get Product Details ------------------------\\ Get_Product_Details(product_id, variant_id) { axios.get("/show_product_data/" + product_id +"/"+ variant_id).then(response => { this.product.discount = response.data.discount; this.product.DiscountNet = response.data.DiscountNet; this.product.discount_Method = response.data.discount_method; this.product.product_id = response.data.id; this.product.product_type = response.data.product_type; this.product.name = response.data.name; this.product.Net_price = response.data.Net_price; this.product.Total_price = response.data.Total_price; this.product.Unit_price = response.data.Unit_price; this.product.taxe = response.data.tax_price; this.product.tax_method = response.data.tax_method; this.product.tax_percent = response.data.tax_percent; this.product.unitSale = response.data.unitSale; this.product.product_variant_id = variant_id; this.product.code = response.data.code; this.product.fix_price = response.data.fix_price; this.product.sale_unit_id = response.data.sale_unit_id; this.product.is_imei = response.data.is_imei; this.product.imei_number = ''; this.add_product(response.data.code); this.CalculTotal(); // Complete the animation of theprogress bar. NProgress.done(); }); }, //----------- Calcul Total CalculTotal() { this.total = 0; for (var i = 0; i < this.details.length; i++) { var tax = this.details[i].taxe * this.details[i].quantity; this.details[i].subtotal = parseFloat( this.details[i].quantity * this.details[i].Net_price + tax ); this.total = parseFloat(this.total + this.details[i].subtotal); } const total_without_discount = parseFloat( this.total - this.sale.discount ); this.sale.TaxNet = parseFloat( (total_without_discount * this.sale.tax_rate) / 100 ); this.GrandTotal = parseFloat( total_without_discount + this.sale.TaxNet + this.sale.shipping ); var grand_total = this.GrandTotal.toFixed(2); this.GrandTotal = parseFloat(grand_total); }, //-------Verified QTY Verified_Qty(detail, id) { for (var i = 0; i < this.details.length; i++) { if (this.details[i].detail_id === id) { if (isNaN(detail.quantity)) { this.details[i].quantity = detail.current; } if (detail.quantity > detail.current) { this.makeToast("warning", this.$t("LowStock"), this.$t("Warning")); this.details[i].quantity = detail.current; } else { this.details[i].quantity = detail.quantity; } } } this.$forceUpdate(); this.CalculTotal(); }, //----------------------------------- Increment QTY with barcode scanner ------------------------------\\ increment_qty_scanner(code) { for (var i = 0; i < this.details.length; i++) { if (this.details[i].code === code) { if (this.details[i].quantity + 1 > this.details[i].current) { this.makeToast("warning", this.$t("LowStock"), this.$t("Warning")); } else { this.details[i].quantity++; } } } this.CalculTotal(); this.$forceUpdate(); NProgress.done(); setTimeout(() => { this.load_product = true; }, 300); }, //----------------------------------- Increment QTY ------------------------------\\ increment(id) { for (var i = 0; i < this.details.length; i++) { if (this.details[i].detail_id == id) { if (this.details[i].quantity + 1 > this.details[i].current) { this.makeToast("warning", this.$t("LowStock"), this.$t("Warning")); } else { this.details[i].quantity++; } } } this.CalculTotal(); this.$forceUpdate(); }, //----------------------------------- decrement QTY ------------------------------\\ decrement(detail, id) { for (var i = 0; i < this.details.length; i++) { if (this.details[i].detail_id == id) { if (detail.quantity - 1 > detail.current || detail.quantity - 1 < 1) { this.makeToast("warning", this.$t("LowStock"), this.$t("Warning")); } else { this.details[i].quantity--; } } } this.CalculTotal(); this.$forceUpdate(); }, //---------- keyup OrderTax keyup_OrderTax() { if (isNaN(this.sale.tax_rate)) { this.sale.tax_rate = 0; } else if(this.sale.tax_rate == ''){ this.sale.tax_rate = 0; this.CalculTotal(); }else { this.CalculTotal(); } }, //---------- keyup Discount keyup_Discount() { if (isNaN(this.sale.discount)) { this.sale.discount = 0; } else if(this.sale.discount == ''){ this.sale.discount = 0; this.CalculTotal(); }else { this.CalculTotal(); } }, //---------- keyup Shipping keyup_Shipping() { if (isNaN(this.sale.shipping)) { this.sale.shipping = 0; } else if(this.sale.shipping == ''){ this.sale.shipping = 0; this.CalculTotal(); }else { this.CalculTotal(); } }, //---------- keyup paid Amount // Verified_paidAmount() { // if (isNaN(this.payment.amount)) { // this.payment.amount = 0; // } else { // if (this.payment.amount > this.payment.received_amount) { // this.makeToast( // "warning", // this.$t("Paying_amount_is_greater_than_Received_amount"), // this.$t("Warning") // ); // this.payment.amount = 0; // } // else if (this.payment.amount > this.GrandTotal) { // this.makeToast( // "warning", // this.$t("Paying_amount_is_greater_than_Grand_Total"), // this.$t("Warning") // ); // this.payment.amount = 0; // } // } // }, //---------- keyup Received Amount // Verified_Received_Amount() { // if (isNaN(this.payment.received_amount)) { // this.payment.received_amount = 0; // } // }, //-----------------------------------Delete Detail Product ------------------------------\\ delete_Product_Detail(id) { for (var i = 0; i < this.details.length; i++) { if (id === this.details[i].detail_id) { this.details.splice(i, 1); this.CalculTotal(); } } }, //----------Reset Pos async Reset_Pos() { this.details = []; this.product = {}; // Reset multi-payment lines this.paymentLines = [ { amount: 0, payment_method_id: '2', } ]; this.selectedAccount = null; this.globalPaymentNote = ''; this.savedPaymentMethods= [], this.hasSavedPaymentMethod= false, this.useSavedPaymentMethod= false, this.selectedCard=null, this.card_id='', this.is_new_credit_card= false, this.submit_showing_credit_card= false, this.sale.tax_rate = 0; this.sale.TaxNet = 0; this.sale.shipping = 0; this.sale.discount = 0; this.sale.notes = ''; this.GrandTotal = 0; this.total = 0; this.category_id = ""; this.brand_id = ""; this.selectedClientPoints = 0; this.used_points = 0; this.discount_from_points = 0; this.clientIsEligible = false; this.pointsConverted = false; // 👈 Reset conversion state const client = this.clients.find(client => client.id === 1); if (client) { this.client_name = client.name; this.selectedClientId = 1; try { const response = await axios.get(`/get_points_client/${this.selectedClientId}`); const data = response.data; if (data.is_royalty_eligible) { this.selectedClientPoints = data.points; this.clientIsEligible = true; } else { this.selectedClientPoints = 0; this.clientIsEligible = false; } } catch (error) { } } this.getProducts(1); }, //------------------------- get Result Value Search Product getResultValue(result) { return result.code + " " + "(" + result.name + ")"; }, //------------------------- Submit Search Product SearchProduct(result) { if(this.load_product){ this.load_product = false; this.product = {}; if(result.product_type == 'is_service'){ this.product.quantity = 1; this.product.code = result.code; }else{ this.product.code = result.code; this.product.current = result.qte_sale; this.product.fix_stock = result.qte; if (result.qte_sale < 1) { this.product.quantity = result.qte_sale; } else { this.product.quantity = 1; } } this.product.product_variant_id = result.product_variant_id; this.Get_Product_Details(result.id, result.product_variant_id); this.search_input= ''; this.$refs.product_autocomplete.value = ""; this.product_filter = []; }else{ this.makeToast( "warning", this.$t("Please_wait_until_the_product_is_loaded"), this.$t("Warning") ); } }, // Search Products search(){ if (this.timer) { clearTimeout(this.timer); this.timer = null; } if (this.search_input.length < 2) { return this.product_filter= []; } if (this.sale.warehouse_id != "" && this.sale.warehouse_id != null) { this.timer = setTimeout(() => { let barcode = this.search_input.trim(); let weight = null; // Check if the barcode is from a weighing scale (13 digits) if (barcode.length === 13 && !isNaN(barcode)) { // Find the product by product code let product = this.products_pos.find(prod => prod.code === barcode); if (product) { this.Check_Product_Exist(product, product.id, weight); return; }else{ let productCode = barcode.substring(0, 7); // First 7 digits → Product Code let weight = parseFloat(barcode.substring(7, 12)) / 1000; // Convert weight (grams to kg) let product = this.products_pos.find(prod => prod.code === productCode); if (product) { product.quantity = weight; // Assign weight to product this.Check_Product_Exist(product, product.id, weight); return; } } this.makeToast("danger", "Invalid product code scanned", this.$t("Error")); this.search_input= ''; this.$refs.product_autocomplete.value = ""; this.product_filter = []; } // else{ // // No product found - Display Error Alert // this.makeToast("danger", "Invalid product code scanned", this.$t("Error")); // this.search_input= ''; // this.$refs.product_autocomplete.value = ""; // this.product_filter = []; // } // Regular product search (for non-weighing scale barcodes) const product_filter = this.products_pos.filter(product => product.code === this.search_input || product.barcode.includes(this.search_input)); if(product_filter.length === 1){ this.Check_Product_Exist(product_filter[0], product_filter[0].id, weight = null); }else { this.product_filter= this.products_pos.filter(product => { return ( product.name.toLowerCase().includes(this.search_input.toLowerCase()) || product.code.toLowerCase().includes(this.search_input.toLowerCase()) || product.barcode.toLowerCase().includes(this.search_input.toLowerCase()) ); }); } }, 800); } else { this.makeToast( "warning", this.$t("SelectWarehouse"), this.$t("Warning") ); } }, //---------------------------------- Check if Product Exist in Order List ---------------------\\ Check_Product_Exist(product, id, weight = null) { if(this.load_product){ this.load_product = false; NProgress.start(); NProgress.set(0.1); this.product = {}; if( product.product_type == 'is_service'){ this.product.quantity = 1; }else{ this.product.current = product.qte_sale; this.product.fix_stock = product.qte; // Check if it's a weighing scale product if (weight !== null) { this.product.quantity = weight; // Assign extracted weight } else { this.product.quantity = product.qte_sale < 1 ? product.qte_sale : 1; } } this.Get_Product_Details(id, product.product_variant_id); NProgress.done(); this.search_input= ''; this.$refs.product_autocomplete.value = ""; this.product_filter = []; }else{ this.makeToast( "warning", this.$t("Please_wait_until_the_product_is_loaded"), this.$t("Warning") ); } }, //--- Get Products by Category Products_by_Category(id) { this.category_id = id; this.getProducts(1); }, //--- Get Products by Brand Products_by_Brands(id) { this.brand_id = id; this.getProducts(1); }, //--- Get All Category getAllCategory() { this.category_id = ""; this.search_category = ''; this.getProducts(1); }, //--- Get All Brands GetAllBrands() { this.brand_id = ""; this.search_brand = ''; this.getProducts(1); }, //------------------------------- Get Products with Filters ------------------------------\\ getProducts(page = 1) { // Start the progress bar. NProgress.start(); NProgress.set(0.1); axios .get( "pos/get_products_pos?page=" + page + "&category_id=" + this.category_id + "&brand_id=" + this.brand_id + "&warehouse_id=" + this.sale.warehouse_id + "&stock=" + 1 + "&product_service=" + 1 + "&product_combo=" + 1 ) .then(response => { this.products = response.data.products; this.product_totalRows = response.data.totalRows; this.Product_paginatePerPage(); // Complete the animation of theprogress bar. NProgress.done(); }) .catch(response => { // Complete the animation of theprogress bar. NProgress.done(); }); }, //---------------------------------------Get Elements ------------------------------\\ GetElementsPos() { axios .get("pos/data_create_pos") .then(response => { this.clients = response.data.clients; this.accounts = response.data.accounts; this.warehouses = response.data.warehouses; this.categories = response.data.categories; this.brands = response.data.brands; this.payment_methods = response.data.payment_methods; this.sale.warehouse_id = response.data.defaultWarehouse; this.selectedClientId = response.data.defaultClient; this.client_name = response.data.default_client_name; this.clientIsEligible = response.data.default_client_eligible === true || response.data.default_client_eligible === 1; this.selectedClientPoints = this.clientIsEligible ? parseFloat(response.data.default_client_points) : 0; this.point_to_amount_rate = response.data.point_to_amount_rate; this.product_perPage = response.data.products_per_page; this.languages_available = response.data.languages_available; this.getProducts(); if (response.data.defaultWarehouse != "") { this.Get_Products_By_Warehouse(response.data.defaultWarehouse); } this.paginate_Brands(this.brand_perPage, 0); this.paginate_Category(this.category_perPage, 0); this.stripe_key = response.data.stripe_key; this.isLoading = false; }) .catch(response => { this.isLoading = false; }); } }, //-------------------- Created Function -----\\ created() { this.GetElementsPos(); this.addPaymentLine(); Fire.$on("pay_now", () => { setTimeout(() => { this.paymentLines = [{ amount: parseFloat(this.GrandTotal.toFixed(2)), payment_method_id: 2, }]; this.globalPaymentNote = ''; this.selectedAccount= null; this.$bvModal.show("Add_Payment"); // Complete the animation of theprogress bar. NProgress.done(); }, 500); }); Fire.$on("event_delete_draft_sale", () => { this.get_Draft_Sales(this.serverParams.page); // Complete the animation of theprogress bar. setTimeout(() => NProgress.done(), 500); }); } }; </script> <style> .total { font-weight: bold; font-size: 14px; } .bg-selected-card{ background-color: #dcdfe6; } .input-with-icon { display: flex; align-items: center; } .scan-icon { width: 50px; /* Adjust size as needed */ height: 50px; margin-right: 8px; /* Adjust spacing as needed */ cursor: pointer; } .modal-custom-width { max-width: 1000px !important; } .menu-icon-grid a { cursor: pointer; display: flex; align-items: center; } /* Media query*/ @media screen and (min-width: 1350px){ .pos-button-actions { position: fixed; } } </style>