This commit is contained in:
Viet An
2026-06-30 15:12:04 +07:00
parent 8b3da71895
commit e9e4fbe24f
18 changed files with 149 additions and 158 deletions

View File

@@ -30,7 +30,7 @@
{{ v.name }} {{ v.name }}
</label> </label>
</div> </div>
<div class="has-background-white-100 mt-3 p-3 rounded-md"> <div class="has-background-grey-100 mt-3 p-3 rounded-md">
<div <div
class="tags mb-0" class="tags mb-0"
v-if="choice === 'function'" v-if="choice === 'function'"

View File

@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
<div v-if="type === 'days'"> <div v-if="type === 'days'">
<div class="fixed-grid has-7-cols mb-1 fs-12 font-medium has-background-white-100 has-text-grey rounded-full"> <div class="fixed-grid has-7-cols mb-1 fs-12 font-medium has-background-grey-100 has-text-grey rounded-full">
<div class="grid is-gap-0"> <div class="grid is-gap-0">
<p <p
v-for="(m, h) in dateOfWeek" v-for="(m, h) in dateOfWeek"

View File

@@ -19,16 +19,16 @@ const { $dayjs } = useNuxtApp();
<p class="fs-12 has-text-grey">{{ invItem.category }}</p> <p class="fs-12 has-text-grey">{{ invItem.category }}</p>
</div> </div>
</td> </td>
<td class="is-family-monospace fs-12">{{ invItem.sku }}</td> <!-- <td class="is-family-monospace fs-12">{{ invItem.sku }}</td>
<td>{{ invItem.storage }}</td> <td>{{ invItem.storage }}</td>
<td class="is-family-monospace fs-12">{{ invItem.storage__position }}</td> <td class="is-family-monospace fs-12">{{ invItem.storage__position }}</td> -->
<td class="has-text-right">{{ invItem.stock }}</td> <td class="has-text-right">{{ invItem.stock }}</td>
<td class="has-text-right">{{ invItem.preorder }}</td> <td class="has-text-right">{{ invItem.preorder }}</td>
<td :class="['has-text-right', invItem.status === 'OK' ? 'has-text-success-30' : 'has-text-warning-30']"> <td :class="['has-text-right', invItem.status === 'OK' ? 'has-text-success-30' : 'has-text-warning-30']">
{{ invItem.available }} {{ invItem.available }}
</td> </td>
<td class="is-family-monospace fs-12">{{ invItem.batch }}</td> <!-- <td class="is-family-monospace fs-12">{{ invItem.batch }}</td>
<td>{{ $dayjs(invItem.expired).format("L") }}</td> <td>{{ $dayjs(invItem.expired).format("L") }}</td> -->
<td> <td>
<span <span
:class="[ :class="[

View File

@@ -527,14 +527,14 @@ const selectedInvItem = ref(null);
<thead> <thead>
<tr> <tr>
<th class="font-semibold">Sản phẩm</th> <th class="font-semibold">Sản phẩm</th>
<th class="font-semibold">SKU</th> <!-- <th class="font-semibold">SKU</th>
<th class="font-semibold">Kho</th> <th class="font-semibold">Kho</th>
<th class="font-semibold">Vị trí</th> <th class="font-semibold">Vị trí</th> -->
<th class="font-semibold has-text-right">Tồn</th> <th class="font-semibold has-text-right">Tồn</th>
<th class="font-semibold has-text-right">Đặt trước</th> <th class="font-semibold has-text-right">Đặt trước</th>
<th class="font-semibold has-text-right">Khả dụng</th> <th class="font-semibold has-text-right">Khả dụng</th>
<th class="font-semibold">Batch</th> <!-- <th class="font-semibold">Batch</th>
<th class="font-semibold">Hạn sử dụng</th> <th class="font-semibold">Hạn sử dụng</th> -->
<th class="font-semibold">Trạng thái</th> <th class="font-semibold">Trạng thái</th>
</tr> </tr>
</thead> </thead>

View File

@@ -89,7 +89,7 @@ const emit = defineEmits("unselect");
</div> </div>
<div class="mt-6"> <div class="mt-6">
<p class="fs-15 font-semibold mb-4">Vị trí lưu kho</p> <p class="fs-15 font-semibold mb-4">Vị trí lưu kho</p>
<div class="p-4 rounded-md has-background-white-100"> <div class="p-4 rounded-md has-background-grey-100">
<div class="is-flex is-gap-1"> <div class="is-flex is-gap-1">
<Icon <Icon
name="material-symbols:location-on-outline-rounded" name="material-symbols:location-on-outline-rounded"
@@ -106,11 +106,11 @@ const emit = defineEmits("unselect");
</div> </div>
<div class="mt-6"> <div class="mt-6">
<p class="fs-15 font-semibold mb-4">Thông tin hàng</p> <p class="fs-15 font-semibold mb-4">Thông tin hàng</p>
<div class="p-4 rounded-md has-background-white-100"> <div class="p-4 rounded-md has-background-grey-100">
<p class="fs-13 has-text-grey"> </p> <p class="fs-13 has-text-grey"> </p>
<p class="mt-1 is-family-monospace">{{ invItem.batch }}</p> <p class="mt-1 is-family-monospace">{{ invItem.batch }}</p>
</div> </div>
<div class="p-4 mt-4 rounded-md has-background-white-100"> <div class="p-4 mt-4 rounded-md has-background-grey-100">
<div class="is-flex is-gap-1 is-align-items-center"> <div class="is-flex is-gap-1 is-align-items-center">
<Icon <Icon
name="material-symbols:calendar-today-outline-rounded" name="material-symbols:calendar-today-outline-rounded"
@@ -127,7 +127,7 @@ const emit = defineEmits("unselect");
<div <div
v-for="move in invItem.moveHistory" v-for="move in invItem.moveHistory"
:key="move.id" :key="move.id"
class="p-4 mb-4 rounded-md has-background-white-100 has-text-grey fs-12" class="p-4 mb-4 rounded-md has-background-grey-100 has-text-grey fs-12"
> >
<div class="is-flex is-justify-content-space-between mb-1"> <div class="is-flex is-justify-content-space-between mb-1">
<div class="is-flex is-gap-1 is-align-items-center"> <div class="is-flex is-gap-1 is-align-items-center">

View File

@@ -34,7 +34,7 @@ const emit = defineEmits(["selectOrder", "unselect"]);
</p> </p>
</div> </div>
<hr class="m-0 has-background-grey-85" /> <hr class="m-0 has-background-grey-85" />
<div class="has-background-white-100 p-4"> <div class="has-background-grey-100 p-4">
<OrderKanbanCard <OrderKanbanCard
v-if="orders.filter((o) => o.status === status.id).length > 0" v-if="orders.filter((o) => o.status === status.id).length > 0"
v-for="order in orders.filter((o) => o.status === status.id)" v-for="order in orders.filter((o) => o.status === status.id)"

View File

@@ -5,7 +5,7 @@ const props = defineProps({
invalid: Boolean, invalid: Boolean,
}); });
const { $deleteapi, $formatNum, $snackbar } = useNuxtApp(); const { $deleteapi, $formatNum, $snackbar } = useNuxtApp();
const { getCarts } = inject("pos"); const posStore = usePosStore();
const showConfirmModal = ref(); const showConfirmModal = ref();
const isDeleting = ref(false); const isDeleting = ref(false);
@@ -26,7 +26,7 @@ async function removeFromCart() {
await $deleteapi("Cart_Item", props.cartItem.id); await $deleteapi("Cart_Item", props.cartItem.id);
isDeleting.value = false; isDeleting.value = false;
$snackbar("Đã xoá sản phẩm khỏi giỏ hàng", "Success"); $snackbar("Đã xoá sản phẩm khỏi giỏ hàng", "Success");
getCarts(); posStore.getCarts();
} }
</script> </script>
@@ -69,7 +69,7 @@ async function removeFromCart() {
</p> </p>
</div> </div>
<button <button
v-if="deleteable" v-if="deleteable || invalid"
@click="openConfirmModal" @click="openConfirmModal"
:class="['button is-danger is-light', isDeleting && 'is-loading']" :class="['button is-danger is-light', isDeleting && 'is-loading']"
> >

View File

@@ -1,5 +1,9 @@
<script setup> <script setup>
const { carts, activeCartId, activeCartItems, isChangingCus, getCarts } = inject("pos"); import useActiveCart from "~/components/pos/composables/useActiveCart";
const posStore = usePosStore();
const { carts, activeCartId, isChangingCus } = storeToRefs(posStore);
const { activeCartItems } = useActiveCart();
const { $insertapi, $deleteapi } = useNuxtApp(); const { $insertapi, $deleteapi } = useNuxtApp();
const isAddingCart = ref(false); const isAddingCart = ref(false);
@@ -9,7 +13,7 @@ async function addCart() {
const newCart = await $insertapi("Cart", { notify: false }); const newCart = await $insertapi("Cart", { notify: false });
activeCartId.value = newCart.id; activeCartId.value = newCart.id;
isAddingCart.value = false; isAddingCart.value = false;
getCarts(); posStore.getCarts();
} }
const isDeletingCart = ref(); const isDeletingCart = ref();
@@ -36,7 +40,7 @@ async function removeCart(cartId) {
} }
} }
} }
getCarts(); posStore.getCarts();
} }
</script> </script>

View File

@@ -2,11 +2,11 @@
import AddIMEIForm from "~/components/imports/AddIMEIForm.vue"; import AddIMEIForm from "~/components/imports/AddIMEIForm.vue";
import ImportData from "~/components/parameter/ImportData.vue"; import ImportData from "~/components/parameter/ImportData.vue";
import { remove } from "es-toolkit"; import { remove } from "es-toolkit";
import useActiveCart from "~/components/pos/composables/useActiveCart";
const props = defineProps({ const props = defineProps({
variant: Object, variant: Object,
}); });
const store = useStore();
const { $getdata, $insertapi, $snackbar } = useNuxtApp(); const { $getdata, $insertapi, $snackbar } = useNuxtApp();
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
@@ -22,7 +22,8 @@ function toggleSelected(imeiRec) {
} }
} }
const { activeCart, activeCartItems, getCarts } = inject("pos"); const posStore = usePosStore();
const { activeCart, activeCartItems } = useActiveCart();
const isAdding = ref(false); const isAdding = ref(false);
async function addToCart() { async function addToCart() {
@@ -41,7 +42,7 @@ async function addToCart() {
}); });
$snackbar(`Đã thêm ${newCartItems.length} sản phẩm vào giỏ hàng`, "Success"); $snackbar(`Đã thêm ${newCartItems.length} sản phẩm vào giỏ hàng`, "Success");
getCarts(); posStore.getCarts();
emit("close"); emit("close");
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -1,21 +1,16 @@
<script setup> <script setup>
import CartItem from "~/components/pos/CartItem.vue"; import CartItem from "~/components/pos/CartItem.vue";
import useActiveCart from "~/components/pos/composables/useActiveCart";
import useOrderInfo from "~/components/pos/composables/useOrderInfo";
import usePlaceOrder from "~/components/pos/composables/usePlaceOrder"; import usePlaceOrder from "~/components/pos/composables/usePlaceOrder";
const id = "confirmOrderModal"; const id = "confirmOrderModal";
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
const { activeCart, activeCartItems, invalidCartItems, orderInfo, fullAddress, getCarts, subtotal, total } = const posStore = usePosStore();
inject("pos"); const { orderInfo, invalidCartItems } = storeToRefs(posStore);
const { activeCart, activeCartItems, subtotal } = useActiveCart();
const { fullAddress, total } = useOrderInfo();
const { isPending, showVietQRModal, placeOrder, testReturnPage } = usePlaceOrder({ const { isPending, showVietQRModal, placeOrder, testReturnPage } = usePlaceOrder({
activeCart,
activeCartItems,
invalidCartItems,
orderInfo,
fullAddress,
subtotal,
total,
getCarts,
onSuccess: () => emit("close"), onSuccess: () => emit("close"),
}); });
</script> </script>
@@ -144,6 +139,7 @@ const { isPending, showVietQRModal, placeOrder, testReturnPage } = usePlaceOrder
<button <button
@click="placeOrder" @click="placeOrder"
:class="['button is-success', isPending && 'is-loading']" :class="['button is-success', isPending && 'is-loading']"
:disabled="invalidCartItems.length > 0 || activeCartItems.length === 0"
> >
Đặt hàng Đặt hàng
</button> </button>

View File

@@ -3,32 +3,17 @@ import Address from "~/components/pos/Address.vue";
import CartItem from "~/components/pos/CartItem.vue"; import CartItem from "~/components/pos/CartItem.vue";
import CartTabs from "~/components/pos/CartTabs.vue"; import CartTabs from "~/components/pos/CartTabs.vue";
import useActiveCart from "~/components/pos/composables/useActiveCart"; import useActiveCart from "~/components/pos/composables/useActiveCart";
import useCartData from "~/components/pos/composables/useCartData";
import useCustomer from "~/components/pos/composables/useCustomer";
import useOrderInfo from "~/components/pos/composables/useOrderInfo"; import useOrderInfo from "~/components/pos/composables/useOrderInfo";
import SearchBox from "~/components/SearchBox.vue"; import SearchBox from "~/components/SearchBox.vue";
const store = useStore(); const store = useStore();
const { $insertapi, $formatNum } = useNuxtApp(); const { $insertapi, $formatNum } = useNuxtApp();
const { carts, cartItems, customers, getCarts, isPending } = useCartData(); const posStore = usePosStore();
const { carts, isPending, activeCartId, invalidCartItems, addresses, orderInfo } = storeToRefs(posStore);
const { activeCartId, activeCart, activeCartItems, customerIdsWithNoCart, subtotal, invalidCartItems } = useActiveCart({ const { activeCart, activeCartItems, customerIdsWithNoCart, subtotal } = useActiveCart();
carts, const { isOrderValid, total } = useOrderInfo();
cartItems,
customers,
});
const { addresses, getAddresses, isChangingCus, changeCustomer } = useCustomer({ activeCart, getCarts });
const { orderInfo, isOrderValid, fullAddress, total } = useOrderInfo({
activeCartId,
activeCart,
activeCartItems,
addresses,
getAddresses,
subtotal,
});
const showModal = ref(); const showModal = ref();
@@ -57,19 +42,6 @@ function openAddAddressModal() {
height: "auto", height: "auto",
}; };
} }
provide("pos", {
carts,
activeCartId,
activeCart,
activeCartItems,
invalidCartItems,
isChangingCus,
orderInfo,
fullAddress,
getCarts,
subtotal,
total,
});
</script> </script>
<template> <template>
@@ -170,7 +142,7 @@ provide("pos", {
placeholder: 'Khách hàng', placeholder: 'Khách hàng',
optionid: activeCart?.customer, optionid: activeCart?.customer,
onOption: (e) => { onOption: (e) => {
if (e?.id !== activeCart?.customer) changeCustomer(e?.id || null); if (e?.id !== activeCart?.customer) posStore.changeCustomer(e?.id || null);
}, },
addon: { addon: {
component: 'customer/CustomerQuickAdd', component: 'customer/CustomerQuickAdd',
@@ -250,7 +222,7 @@ provide("pos", {
:address="address" :address="address"
:selected="orderInfo.address?.id === address.id" :selected="orderInfo.address?.id === address.id"
@selectAddress="orderInfo.address = $event" @selectAddress="orderInfo.address = $event"
@submit="getAddresses" @submit="posStore.getAddresses(activeCart.customer)"
/> />
</div> </div>
<div <div
@@ -379,7 +351,7 @@ provide("pos", {
async () => { async () => {
const newCart = await $insertapi('Cart', { notify: false }); const newCart = await $insertapi('Cart', { notify: false });
activeCartId = newCart.id; activeCartId = newCart.id;
getCarts(); posStore.getCarts();
} }
" "
class="button is-primary" class="button is-primary"

View File

@@ -3,7 +3,7 @@ const props = defineProps({
invoiceId: Number, invoiceId: Number,
finalizeOrder: Function, finalizeOrder: Function,
}); });
const { activeCartItems } = inject("pos"); const { activeCartItems } = useActiveCart();
const subtotal = computed(() => { const subtotal = computed(() => {
return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0); return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0);
}); });

View File

@@ -1,21 +1,12 @@
import { without } from "es-toolkit"; import { without } from "es-toolkit";
export default function useActiveCart({ carts, cartItems, customers }) { export default function useActiveCart() {
const activeCartId = ref(); const posStore = usePosStore();
const { carts, cartItems, customers, activeCartId } = storeToRefs(posStore);
const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value)); const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value));
const activeCartItems = computed(() => cartItems.value.filter((ci) => ci.cart === activeCartId.value)); const activeCartItems = computed(() => cartItems.value.filter((ci) => ci.cart === activeCartId.value));
watch(
carts,
(newVal) => {
if (!activeCartId.value && newVal.length) {
activeCartId.value = newVal[0].id;
}
},
{ immediate: true },
);
const customerIdsWithNoCart = computed(() => { const customerIdsWithNoCart = computed(() => {
const cusIds = customers.value.map((c) => c.id); const cusIds = customers.value.map((c) => c.id);
const cusIdsWithCart = carts.value.filter((c) => c.customer).map((c) => c.customer); const cusIdsWithCart = carts.value.filter((c) => c.customer).map((c) => c.customer);
@@ -27,10 +18,5 @@ export default function useActiveCart({ carts, cartItems, customers }) {
return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0); return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0);
}); });
const invalidCartItems = ref([]); return { activeCart, activeCartItems, customerIdsWithNoCart, subtotal };
watch(activeCartId, () => {
invalidCartItems.value = [];
});
return { activeCartId, activeCart, activeCartItems, customerIdsWithNoCart, subtotal, invalidCartItems };
} }

View File

@@ -1,22 +0,0 @@
export default function useCartData() {
const { $findapi, $getapi } = useNuxtApp();
const carts = ref([]);
const cartItems = ref([]);
const customers = ref([]);
const isPending = ref(false);
async function getCarts() {
isPending.value = true;
const apis = $findapi(["Cart", "Cart_Item", "customer"]);
const [cartsRes, cartItemsRes, customersRes] = await $getapi(apis);
isPending.value = false;
carts.value = cartsRes.data.rows || [];
cartItems.value = cartItemsRes.data.rows || [];
customers.value = customersRes.data.rows || [];
}
onMounted(getCarts);
return { carts, cartItems, customers, getCarts, isPending };
}

View File

@@ -1,24 +0,0 @@
export default function useCustomer({ activeCart, getCarts }) {
const { $getdata, $patchapi } = useNuxtApp();
const addresses = ref([]);
async function getAddresses() {
addresses.value =
(await $getdata("Customer_Address", {
filter: { customer: activeCart.value.customer },
})) || [];
}
const isChangingCus = ref(false);
async function changeCustomer(cusId) {
isChangingCus.value = true;
const updatedCart = await $patchapi("Cart", {
id: activeCart.value.id,
customer: cusId,
});
await getCarts();
isChangingCus.value = false;
}
return { addresses, getAddresses, isChangingCus, changeCustomer };
}

View File

@@ -1,12 +1,9 @@
export default function useOrderInfo({ activeCartId, activeCart, activeCartItems, addresses, getAddresses, subtotal }) { import useActiveCart from "~/components/pos/composables/useActiveCart";
const orderInfo = ref({
address: null, export default function useOrderInfo() {
receiver_name: null, const posStore = usePosStore();
receiver_phone: null, const { activeCartId, addresses, orderInfo } = storeToRefs(posStore);
shipping_fee: null, const { activeCart, activeCartItems, subtotal } = useActiveCart();
deliveryMethod: null,
paymentMethod: null,
});
const total = computed(() => subtotal.value + orderInfo.value.shipping_fee); const total = computed(() => subtotal.value + orderInfo.value.shipping_fee);
@@ -19,7 +16,7 @@ export default function useOrderInfo({ activeCartId, activeCart, activeCartItems
watch(activeCart, async (newVal, oldVal) => { watch(activeCart, async (newVal, oldVal) => {
// set order info // set order info
if (newVal?.customer) { if (newVal?.customer) {
await getAddresses(); await posStore.getAddresses(newVal.customer);
if (!oldVal || !oldVal.customer || oldVal.customer !== newVal.customer) { if (!oldVal || !oldVal.customer || oldVal.customer !== newVal.customer) {
const defaultAddress = addresses.value.find((add) => add.is_default); const defaultAddress = addresses.value.find((add) => add.is_default);
orderInfo.value.address = defaultAddress; orderInfo.value.address = defaultAddress;
@@ -59,5 +56,5 @@ export default function useOrderInfo({ activeCartId, activeCart, activeCartItems
return `${orderInfo.value.address.address_detail}, ${orderInfo.value.address.ward}, ${orderInfo.value.address.district}, ${orderInfo.value.address.city}`; return `${orderInfo.value.address.address_detail}, ${orderInfo.value.address.ward}, ${orderInfo.value.address.district}, ${orderInfo.value.address.city}`;
}); });
return { orderInfo, isOrderValid, fullAddress, total }; return { isOrderValid, fullAddress, total };
} }

View File

@@ -1,15 +1,14 @@
export default function usePlaceOrder({ import useActiveCart from "~/components/pos/composables/useActiveCart";
activeCart, import useOrderInfo from "~/components/pos/composables/useOrderInfo";
activeCartItems,
invalidCartItems, export default function usePlaceOrder({ onSuccess }) {
orderInfo,
fullAddress,
subtotal,
total,
getCarts,
onSuccess,
}) {
const { $dayjs, $id, $getdata, $insertapi, $patchapi, $deleteapi, $snackbar } = useNuxtApp(); const { $dayjs, $id, $getdata, $insertapi, $patchapi, $deleteapi, $snackbar } = useNuxtApp();
const posStore = usePosStore();
const { invalidCartItems, orderInfo } = storeToRefs(posStore);
const { activeCart, activeCartItems, subtotal } = useActiveCart();
const { fullAddress, total } = useOrderInfo();
const isPending = ref(false); const isPending = ref(false);
const paidPaymentStatus = ref(); const paidPaymentStatus = ref();
const invoice = ref(); const invoice = ref();
@@ -206,7 +205,7 @@ export default function usePlaceOrder({
customer: null, customer: null,
}), }),
]); ]);
getCarts(); posStore.getCarts();
} }
async function placeOrder() { async function placeOrder() {

82
app/stores/usePosStore.js Normal file
View File

@@ -0,0 +1,82 @@
import { defineStore } from "pinia";
export const usePosStore = defineStore("pos", () => {
const { $findapi, $getapi, $getdata, $patchapi } = useNuxtApp();
const carts = ref([]);
const cartItems = ref([]);
const customers = ref([]);
const isPending = ref(false);
const activeCartId = ref();
const invalidCartItems = ref([]);
const isChangingCus = ref(false);
const addresses = ref([]);
const orderInfo = ref({
address: null,
receiver_name: null,
receiver_phone: null,
shipping_fee: null,
deliveryMethod: null,
paymentMethod: null,
});
async function getCarts() {
isPending.value = true;
const apis = $findapi(["Cart", "Cart_Item", "customer"]);
const [cartsRes, cartItemsRes, customersRes] = await $getapi(apis);
isPending.value = false;
carts.value = cartsRes.data.rows || [];
cartItems.value = cartItemsRes.data.rows || [];
customers.value = customersRes.data.rows || [];
}
onMounted(getCarts);
async function changeCustomer(cusId) {
isChangingCus.value = true;
const updatedCart = await $patchapi("Cart", {
id: activeCartId.value,
customer: cusId,
});
await getCarts();
isChangingCus.value = false;
}
async function getAddresses(activeCartCustomerId) {
addresses.value =
(await $getdata("Customer_Address", {
filter: { customer: activeCartCustomerId },
})) || [];
}
watch(
carts,
(newVal) => {
if (!activeCartId.value && newVal.length) {
activeCartId.value = newVal[0].id;
}
},
{ immediate: true },
);
watch(activeCartId, () => {
invalidCartItems.value = [];
});
return {
carts,
cartItems,
customers,
isPending,
getCarts,
getAddresses,
changeCustomer,
activeCartId,
invalidCartItems,
isChangingCus,
addresses,
orderInfo,
};
});