mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
Merge pull request #958 from AbleKSaju/feat-pos-loyalty-program
feat: Loyalty program functionality in POS
This commit is contained in:
commit
20a4a8c727
@ -8,7 +8,7 @@ export type ItemSerialNumbers = { [item: string]: string };
|
||||
|
||||
export type DiscountType = "percent" | "amount";
|
||||
|
||||
export type ModalName = 'ShiftOpen' | 'ShiftClose' | 'Payment'
|
||||
export type ModalName = 'ShiftOpen' | 'ShiftClose' | 'Payment' | 'LoyaltyProgram'
|
||||
|
||||
export interface POSItem {
|
||||
image?:string,
|
||||
|
166
src/pages/POS/LoyaltyprogramModal.vue
Normal file
166
src/pages/POS/LoyaltyprogramModal.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<Modal class="h-96 w-96" :set-close-listener="false">
|
||||
<p class="text-center py-4">Redeem Loyalty Points</p>
|
||||
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<div class="flex gap-2 p-3 justify-end pt-10">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="20px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="20px"
|
||||
fill="#F19E39"
|
||||
>
|
||||
<path
|
||||
d="M480-60q-117 0-217.22-65.23T108.33-287.77v133.92H58.08v-220h220v50.26H146.05q44.87 89.95 137.44 151.64 92.56 61.69 196.51 61.69 74.95 0 141.19-27.89 66.25-27.9 116.04-76.35 49.8-48.45 79.81-114.31 30.01-65.86 32.32-141.19h50.25q-1.92 85.23-35.65 159.85-33.73 74.61-90.46 130.38Q716.77-124 641.19-92 565.61-60 480-60Zm-24.92-144.31v-51.54q-42.85-10.64-72.5-35.38-29.66-24.74-48.58-68.21l43.44-16.41q14.3 37.85 43.39 58.77 29.09 20.93 65.58 20.93 37.8 0 66.31-18.94 28.51-18.94 28.51-56.14 0-33.21-23.55-53.89t-89.22-46.62q-62.36-24.06-90.82-51.52-28.46-27.46-28.46-73.61 0-40.18 28.54-71.05 28.54-30.87 79.36-38.49v-48.87h47.43v48.87q34.51 3.03 62.12 22.91 27.6 19.88 43.11 51.81l-41.53 18.36q-13.7-24.54-34.75-38.53-21.05-13.99-51.46-13.99-39.21 0-62.42 19.18-23.22 19.18-23.22 49.8 0 31.28 22.18 48.95 22.18 17.66 84.43 41.1 70.52 27.49 97.72 58.31 27.21 30.82 27.21 77.28 0 26.97-10.17 47.92-10.17 20.95-27.12 35.48-16.94 14.52-39.7 23.06t-48.4 10.92v49.54h-47.43ZM60.39-490q2.69-87.15 36.8-161.96 34.12-74.81 91.23-130.19 57.12-55.39 132.12-86.62Q395.54-900 480-900q115.85 0 217.22 65.43 101.37 65.42 154.45 163.88v-135.46h50.25v220h-220v-50.26h132.03q-43.72-88.15-136.14-150.74-92.42-62.59-197.81-62.59-73.41 0-139.59 27.51t-116.23 75.58q-50.05 48.06-80.64 113.92-30.59 65.86-32.9 142.73H60.39Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p>{{ loyaltyPoints }}</p>
|
||||
</div>
|
||||
|
||||
<Data
|
||||
v-if="sinvDoc.fieldMap"
|
||||
class="flex-shrink-0 px-10 pb-10"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="sinvDoc.loyaltyPoints"
|
||||
:df="sinvDoc.fieldMap.loyaltyPoints"
|
||||
@change="updateLoyaltyPoints"
|
||||
/>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto mb-2 px-10">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-green-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="validationError"
|
||||
@click="setLoyaltyPoints()"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Save` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto px-10">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-red-500"
|
||||
style="padding: 1.35rem"
|
||||
@click="$emit('toggleModal', 'LoyaltyProgram')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Data from 'src/components/Controls/Data.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { t } from 'fyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LoyaltyProgramModal',
|
||||
components: {
|
||||
Modal,
|
||||
Button,
|
||||
Data,
|
||||
},
|
||||
props: {
|
||||
loyaltyPoints: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
loyaltyProgram: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['setLoyaltyPoints', 'toggleModal'],
|
||||
setup() {
|
||||
return {
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
validationError: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async updateLoyaltyPoints(newValue: number) {
|
||||
try {
|
||||
if (this.loyaltyPoints >= newValue) {
|
||||
this.sinvDoc.loyaltyPoints = newValue;
|
||||
} else {
|
||||
throw new Error(
|
||||
`${this.sinvDoc.party as string} only has ${
|
||||
this.loyaltyPoints
|
||||
} points`
|
||||
);
|
||||
}
|
||||
|
||||
const loyaltyProgramDoc = await this.fyo.db.getAll(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
{
|
||||
fields: ['conversionFactor'],
|
||||
filters: { name: this.loyaltyProgram },
|
||||
}
|
||||
);
|
||||
|
||||
const loyaltyPoint =
|
||||
newValue * ((loyaltyProgramDoc[0]?.conversionFactor as number) || 0);
|
||||
|
||||
if (this.sinvDoc.baseGrandTotal?.lt(loyaltyPoint)) {
|
||||
throw new Error(t`no need ${newValue} points to purchase this item`);
|
||||
}
|
||||
this.validationError = false;
|
||||
} catch (error) {
|
||||
this.validationError = true;
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
setLoyaltyPoints() {
|
||||
try {
|
||||
if (!this.sinvDoc.loyaltyPoints || this.sinvDoc.loyaltyPoints < 0) {
|
||||
throw new Error(t`Points must be greater than 0`);
|
||||
}
|
||||
|
||||
this.$emit('setLoyaltyPoints', this.sinvDoc.loyaltyPoints);
|
||||
this.$emit('toggleModal', 'LoyaltyProgram');
|
||||
|
||||
this.validationError = false;
|
||||
} catch (error) {
|
||||
this.validationError = true;
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -22,6 +22,14 @@
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<LoyaltyProgramModal
|
||||
:open-modal="openLoyaltyProgramModal"
|
||||
:loyalty-points="loyaltyPoints"
|
||||
:loyalty-program="loyaltyProgram"
|
||||
@set-loyalty-points="setLoyaltyPoints"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<PaymentModal
|
||||
:open-modal="openPaymentModal"
|
||||
@create-transaction="createTransaction"
|
||||
@ -92,14 +100,92 @@
|
||||
:item-qty-map="itemQtyMap"
|
||||
@add-item="addItem"
|
||||
/>
|
||||
<div
|
||||
class="bg-gray-100 p-2 fixed bottom-0 mb-7 rounded-md"
|
||||
@click="toggleView"
|
||||
>
|
||||
<FeatherIcon
|
||||
:name="tableView ? 'grid' : 'list'"
|
||||
class="w-6 h-6 text-black"
|
||||
/>
|
||||
|
||||
<div class="flex fixed bottom-0 p-1 mb-7 gap-x-3">
|
||||
<div class="relative group">
|
||||
<div class="bg-gray-100 p-1 rounded-md" @click="toggleView">
|
||||
<FeatherIcon
|
||||
:name="tableView ? 'grid' : 'list'"
|
||||
class="w-6 h-6 text-black"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="
|
||||
absolute
|
||||
bottom-full
|
||||
left-1/2
|
||||
transform
|
||||
-translate-x-1/2
|
||||
mb-2
|
||||
bg-gray-100
|
||||
dark:bg-gray-850 dark:text-white
|
||||
text-black text-xs
|
||||
rounded-md
|
||||
p-2
|
||||
w-20
|
||||
text-center
|
||||
opacity-0
|
||||
group-hover:opacity-100
|
||||
transition-opacity
|
||||
duration-300
|
||||
"
|
||||
>
|
||||
{{ tableView ? 'Grid View' : 'List View' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="p-1 rounded-md bg-gray-100"
|
||||
:class="{
|
||||
hidden: !fyo.singles.AccountingSettings?.enableLoyaltyProgram,
|
||||
'bg-gray-100': loyaltyPoints,
|
||||
'dark:bg-gray-600 cursor-not-allowed':
|
||||
!loyaltyPoints || !sinvDoc.party || !sinvDoc.items?.length,
|
||||
}"
|
||||
@click="
|
||||
loyaltyPoints && sinvDoc.party && sinvDoc.items?.length
|
||||
? toggleModal('LoyaltyProgram', true)
|
||||
: null
|
||||
"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="26px"
|
||||
fill="#000"
|
||||
>
|
||||
<path
|
||||
d="M100-180v-600h760v600H100Zm50.26-50.26h659.48v-499.48H150.26v499.48Zm0 0v-499.48 499.48Zm181.64-56.77h50.25v-42.56h48.67q14.37 0 23.6-10.38 9.22-10.38 9.22-24.25v-106.93q0-14.71-9.22-24.88-9.23-10.17-23.6-10.17H298.77v-73.95h164.87v-50.26h-81.49v-42.56H331.9v42.56h-48.41q-14.63 0-24.8 10.38-10.18 10.38-10.18 25v106.27q0 14.62 10.18 23.71 10.17 9.1 24.8 9.1h129.9v76.1H248.51v50.26h83.39v42.56Zm312.97-27.94L705.9-376H583.85l61.02 61.03ZM583.85-574H705.9l-61.03-61.03L583.85-574Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="
|
||||
absolute
|
||||
bottom-full
|
||||
left-1/2
|
||||
transform
|
||||
-translate-x-1/2
|
||||
mb-2
|
||||
bg-gray-100
|
||||
dark:bg-gray-850 dark:text-white
|
||||
text-black text-xs
|
||||
rounded-md
|
||||
p-2
|
||||
w-28
|
||||
text-center
|
||||
opacity-0
|
||||
group-hover:opacity-100
|
||||
transition-opacity
|
||||
duration-300
|
||||
"
|
||||
>
|
||||
Loyalty Program
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -125,7 +211,7 @@
|
||||
:border="true"
|
||||
:value="sinvDoc.party"
|
||||
:df="sinvDoc.fieldMap.party"
|
||||
@change="(value:string) => (sinvDoc.party = value)"
|
||||
@change="(value:string) => setCustomer(value)"
|
||||
/>
|
||||
|
||||
<SelectedItemTable />
|
||||
@ -270,7 +356,8 @@ import {
|
||||
validateSinv,
|
||||
} from 'src/utils/pos';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
import { getPricingRule } from 'models/helpers';
|
||||
import { getAddedLPWithGrandTotal, getPricingRule } from 'models/helpers';
|
||||
import LoyaltyProgramModal from './LoyaltyprogramModal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'POS',
|
||||
@ -285,6 +372,7 @@ export default defineComponent({
|
||||
OpenPOSShiftModal,
|
||||
PageHeader,
|
||||
PaymentModal,
|
||||
LoyaltyProgramModal,
|
||||
SelectedItemTable,
|
||||
Barcode,
|
||||
},
|
||||
@ -311,6 +399,7 @@ export default defineComponent({
|
||||
|
||||
isItemsSeeded: false,
|
||||
openPaymentModal: false,
|
||||
openLoyaltyProgramModal: false,
|
||||
openShiftCloseModal: false,
|
||||
openShiftOpenModal: false,
|
||||
|
||||
@ -322,6 +411,10 @@ export default defineComponent({
|
||||
|
||||
totalQuantity: 0,
|
||||
|
||||
loyaltyPoints: 0,
|
||||
appliedLoyaltyPoints: 0,
|
||||
loyaltyProgram: '' as string,
|
||||
|
||||
defaultCustomer: undefined as string | undefined,
|
||||
itemSearchTerm: '',
|
||||
transferRefNo: undefined as string | undefined,
|
||||
@ -385,6 +478,22 @@ export default defineComponent({
|
||||
toggleSidebar(true);
|
||||
},
|
||||
methods: {
|
||||
async setCustomer(value: string) {
|
||||
if (!value) {
|
||||
this.sinvDoc.party = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.sinvDoc.party = value;
|
||||
|
||||
const party = await this.fyo.db.getAll(ModelNameEnum.Party, {
|
||||
fields: ['loyaltyProgram', 'loyaltyPoints'],
|
||||
filters: { name: value },
|
||||
});
|
||||
|
||||
this.loyaltyProgram = party[0]?.loyaltyProgram as string;
|
||||
this.loyaltyPoints = party[0].loyaltyPoints as number;
|
||||
},
|
||||
async setItems() {
|
||||
const items = (await fyo.db.getAll(ModelNameEnum.Item, {
|
||||
fields: [],
|
||||
@ -447,6 +556,23 @@ export default defineComponent({
|
||||
setTotalTaxedAmount() {
|
||||
this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice);
|
||||
},
|
||||
async setLoyaltyPoints(value: number) {
|
||||
this.appliedLoyaltyPoints = value;
|
||||
|
||||
this.sinvDoc.redeemLoyaltyPoints = true;
|
||||
|
||||
const totalLotaltyAmount = await getAddedLPWithGrandTotal(
|
||||
this.fyo,
|
||||
this.loyaltyProgram,
|
||||
value
|
||||
);
|
||||
|
||||
const total = totalLotaltyAmount
|
||||
.sub(this.sinvDoc.baseGrandTotal as Money)
|
||||
.abs();
|
||||
|
||||
this.sinvDoc.grandTotal = total;
|
||||
},
|
||||
setTransferAmount(amount: Money = fyo.pesa(0)) {
|
||||
this.transferAmount = amount;
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user