felicity-lims/webapp/views/inventory/InvListing.tsx

384 lines
18 KiB
TypeScript
Raw Normal View History

2024-05-29 21:25:53 +08:00
import { computed, watch, defineComponent, reactive, ref, h, defineAsyncComponent } from 'vue';
import { useInventoryStore } from '@/stores';
import { IStockLot, IStockProduct } from '@/models/inventory';
import { useApiUtil } from '@/composables';
import { ADD_STOCK_ADJUSTMENT } from '@/graphql/operations/inventory.mutations';
import { GET_ALL_STOCK_LOTS } from '@/graphql/operations/inventory.queries';
import { parseDate } from '@/utils/helpers';
2023-11-10 14:05:15 +08:00
const DataTable = defineAsyncComponent(
() => import('@/components/ui/datatable/FelDataTable.vue')
2023-11-10 14:05:15 +08:00
)
const Drawer = defineAsyncComponent(
() => import( '@/components/ui/FelDrawer.vue')
2023-11-10 14:05:15 +08:00
)
const Modal = defineAsyncComponent(
() => import('@/components/ui/FelModal.vue')
2023-11-10 14:05:15 +08:00
)
2024-05-29 21:25:53 +08:00
const StockReceiveForm = defineAsyncComponent(
() => import('./StockReceiveForm.vue')
2023-11-10 14:05:15 +08:00
)
2024-06-24 00:27:04 +08:00
const ProductDetail = defineAsyncComponent(
() => import('./ProductDetail')
)
2023-11-10 14:05:15 +08:00
const InventoryListing = defineComponent({
name: 'stock-listing',
setup(props, ctx) {
2024-05-29 21:25:53 +08:00
const { withClientMutation, withClientQuery } = useApiUtil();
2023-11-10 14:05:15 +08:00
const inventoryStore = useInventoryStore();
inventoryStore.fetchProducts({
first: 50,
after: '',
text: '',
sortBy: ['-uid'],
});
const choiceProduct = reactive({
product: {} as IStockProduct,
quantity: 0,
2024-06-01 05:37:11 +08:00
stockLotUid: "",
2023-11-10 14:05:15 +08:00
type: "",
remarks: ""
});
const openAddProduct = ref(false);
2024-05-29 21:25:53 +08:00
const stockLots = ref([]);
const fetchLots = (productUid: string) => {
withClientQuery(GET_ALL_STOCK_LOTS, { productUid }, 'stockLots').then(result => {
stockLots.value = result;
})
}
watch(() => choiceProduct.product?.uid, (itemUid, _) => (itemUid && fetchLots(itemUid)))
2023-11-10 14:05:15 +08:00
const openAdjustProduct = ref(false);
2024-06-24 00:27:04 +08:00
const openProductDetail = ref(false);
const productDetailItem = ref({} as IStockProduct);
2024-05-29 21:25:53 +08:00
2023-11-10 14:05:15 +08:00
const tableColumns = ref([
{
name: 'UID',
value: 'uid',
sortable: true,
sortBy: 'asc',
defaultSort: true,
showInToggler: false,
hidden: true,
},
{
name: 'Name',
2024-05-29 21:25:53 +08:00
value: 'stockItem.name',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
2024-05-29 21:25:53 +08:00
name: 'Type',
value: 'name',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
2024-05-29 21:25:53 +08:00
name: 'Category',
value: 'stockItem.category.name',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
2024-05-29 21:25:53 +08:00
name: 'Hazard',
value: 'stockItem.hazard.name',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
2024-05-29 21:25:53 +08:00
name: 'Quantity',
value: 'quantity',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
2024-05-29 21:25:53 +08:00
name: 'Description',
value: 'description',
2023-11-10 14:05:15 +08:00
sortable: false,
sortBy: 'asc',
hidden: false,
},
{
name: 'Actions',
value: '',
sortable: false,
sortBy: 'asc',
hidden: false,
customRender: function (product, _) {
return h(
'div',
{
2024-06-24 00:27:04 +08:00
class: 'flex justify-start align-items-center gap-x-4',
2023-11-10 14:05:15 +08:00
},
[
h(
'button',
{
type: 'button',
2024-06-01 05:37:11 +08:00
class: 'bg-sky-800 text-white py-1 px-2 rounded-sm leading-none disabled:bg-gray-500',
2023-11-10 14:05:15 +08:00
innerHTML: '+ Basket',
2024-06-01 05:37:11 +08:00
disabled: product.quantity < 1,
2023-11-10 14:05:15 +08:00
onClick: () => {
choiceProduct.product = product;
choiceProduct.quantity = 0;
openAddProduct.value = true;
},
},
[]
),
h(
'button',
{
type: 'button',
2024-06-01 05:37:11 +08:00
class: 'bg-sky-800 text-white py-1 px-2 rounded-sm leading-none disabled:bg-gray-500',
2023-11-10 14:05:15 +08:00
innerHTML: '+/- Adjust',
2024-06-01 05:37:11 +08:00
disabled: product.quantity < 1,
2023-11-10 14:05:15 +08:00
onClick: () => {
choiceProduct.product = product;
choiceProduct.quantity = 0;
openAdjustProduct.value = true;
},
},
[]
),
2024-06-24 00:27:04 +08:00
h(
'button',
{
type: 'button',
class: 'bg-sky-800 text-white py-1 px-2 rounded-sm leading-none',
innerHTML: 'View Detail',
onClick: () => {
openProductDetail.value = true;
productDetailItem.value = product;
},
},
[]
),
2023-11-10 14:05:15 +08:00
]
);
},
},
]);
let productParams = reactive({
first: 50,
before: '',
text: '',
sortBy: ['-uid'],
filterAction: false,
});
return {
tableColumns,
inventoryStore,
openDrawer: ref(false),
openAddProduct,
choiceProduct,
openAdjustProduct,
2024-05-29 21:25:53 +08:00
stockLots,
2024-06-24 00:27:04 +08:00
openProductDetail,
productDetailItem,
2023-11-10 14:05:15 +08:00
filterProducts: (opts: any) => {
productParams.first = 50;
productParams.before = '';
productParams.text = opts.filterText;
productParams.filterAction = true;
inventoryStore.fetchProducts(productParams);
},
showMoreProducts: (opts: any) => {
productParams.first = opts.fetchCount;
productParams.before = inventoryStore.productsPaging?.pageInfo?.endCursor ?? '';
productParams.text = opts.filterText;
productParams.filterAction = false;
inventoryStore.fetchProducts(productParams);
},
countNone: computed(() => inventoryStore.products?.length + ' of ' + inventoryStore.productsPaging.totalCount + ' products'),
validateMinMax: event => {
2024-05-29 21:25:53 +08:00
// const value = Math.max(0, Math.min(choiceProduct.product.remaining ?? 0, Number(event.target.value)));
// choiceProduct.quantity = value;
2023-11-10 14:05:15 +08:00
},
adjustStock: () => {
withClientMutation(
ADD_STOCK_ADJUSTMENT,
{
payload: {
productUid: choiceProduct.product.uid,
2024-06-01 05:37:11 +08:00
stockLotUid: choiceProduct.stockLotUid,
2023-11-10 14:05:15 +08:00
adjustmentType: choiceProduct.type,
adjust: choiceProduct.quantity,
remarks: choiceProduct.remarks
}
},
'createStockAjustment'
).then(result => {
});
},
};
},
render() {
return (
<>
<div>
<button
onClick={() => (this.openDrawer = true)}
class="px-4 my-2 p-1 text-sm border-sky-800 border text-dark-700 transition-colors duration-150 rounded-sm focus:outline-none hover:bg-sky-800 hover:text-gray-100"
>
2024-05-29 21:25:53 +08:00
Receive Stock
2023-11-10 14:05:15 +08:00
</button>
</div>
<DataTable
columns={this.tableColumns}
data={this.inventoryStore.products}
toggleColumns={false}
loading={false}
paginable={true}
pageMeta={{
fetchCount: 10,
hasNextPage: false,
countNone: this.countNone,
}}
searchable={true}
filterable={false}
selectable={false}
onOnSearch={x => this.filterProducts(x)}
onOnPaginate={x => this.showMoreProducts(x)}
></DataTable>
{/* Drawer */}
<Drawer show={this.openDrawer} onClose={() => (this.openDrawer = false)}>
{{
2024-05-29 21:25:53 +08:00
header: () => 'Receive Stock',
body: () => [<StockReceiveForm onClose={() => (this.openDrawer = false)} />],
2023-11-10 14:05:15 +08:00
}}
</Drawer>
2024-06-24 00:27:04 +08:00
{/* Drawer */}
<Drawer show={this.openProductDetail} onClose={() => (this.openProductDetail = false)}>
{{
header: () => 'Product Details',
body: () => [<ProductDetail product={this.productDetailItem} onClose={() => (this.openProductDetail = false)} />],
}}
</Drawer>
2023-11-10 14:05:15 +08:00
{this.openAddProduct && (
<Modal onClose={() => (this.openAddProduct = false)} contentWidth="w-1/4">
{{
2024-05-29 21:25:53 +08:00
header: () => <h3>{this.choiceProduct.product?.stockItem?.name} ({this.choiceProduct.product.name})</h3>,
2023-11-10 14:05:15 +08:00
body: () => {
return (
<form action="post" class="p-1">
2024-05-29 21:25:53 +08:00
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700 text-nowrap">Product Lot</span>
2024-06-01 05:37:11 +08:00
<select class="col-span-3 form-select block w-full mt-1" v-model={this.choiceProduct.stockLotUid}>
2024-05-29 21:25:53 +08:00
<option></option>
2024-06-24 00:27:04 +08:00
{this.stockLots?.map((lot: IStockLot) => (<option key={lot.uid} value={lot.uid}>
{lot.lotNumber} ({lot.quantity}) [{parseDate(lot.expiryDate, false)}]
</option>))}
2024-05-29 21:25:53 +08:00
</select>
</label>
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700 text-nowrap">Quantiy</span>
2023-11-10 14:05:15 +08:00
<input
2024-05-29 21:25:53 +08:00
class="col-span-3 form-input mt-1 block w-full"
2023-11-10 14:05:15 +08:00
type="number"
onChange={this.validateMinMax}
v-model={this.choiceProduct.quantity}
placeholder="Name ..."
/>
</label>
<hr />
<button
type="button"
onClick={() => {
this.inventoryStore.addToBasket(
this.choiceProduct.product.uid,
2024-06-01 05:37:11 +08:00
this.choiceProduct.stockLotUid,
2023-11-10 14:05:15 +08:00
this.choiceProduct.quantity
);
this.openAddProduct = false;
}}
2024-05-29 21:25:53 +08:00
class="-mb-4 border border-sky-800 bg-sky-800 text-white rounded-sm px-4 py-2 m-2 transition-colors duration-500 ease select-none hover:bg-sky-800 focus:outline-none focus:shadow-outline disabled:bg-gray-500"
2024-06-01 05:37:11 +08:00
disabled={!this.choiceProduct.stockLotUid}>
2023-11-10 14:05:15 +08:00
Add to basket
</button>
</form>
);
},
}}
</Modal>
)}
{this.openAdjustProduct && (
<Modal onClose={() => (this.openAdjustProduct = false)} contentWidth="w-1/4">
{{
2024-05-29 21:25:53 +08:00
header: () => <h3>{this.choiceProduct.product?.stockItem?.name} ({this.choiceProduct.product.name})</h3>,
2023-11-10 14:05:15 +08:00
body: () => {
return (
<form action="post" class="p-1">
2024-05-29 21:25:53 +08:00
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700 text-nowrap">Product Lot</span>
2024-06-01 05:37:11 +08:00
<select class="col-span-3 form-select block w-full mt-1" v-model={this.choiceProduct.stockLotUid}>
2024-05-29 21:25:53 +08:00
<option></option>
{this.stockLots?.map((lot: IStockLot) => (<option key={lot.uid} value={lot.uid}>{lot.lotNumber}</option>))}
</select>
</label>
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700">Adjustmet</span>
<select
class="col-span-3 form-select block w-full mt-1"
v-model={this.choiceProduct.type}
>
2023-11-10 14:05:15 +08:00
<option value="lost">Lost</option>
<option value="theft">Theft</option>
<option value="transfer-out">Transfer Out</option>
</select>
</label>
2024-05-29 21:25:53 +08:00
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700">Quantiy</span>
2023-11-10 14:05:15 +08:00
<input
2024-05-29 21:25:53 +08:00
class="col-span-3 form-input mt-1 block w-full"
2023-11-10 14:05:15 +08:00
type="number"
onChange={this.validateMinMax}
v-model={this.choiceProduct.quantity}
placeholder="Name ..."
/>
</label>
2024-05-29 21:25:53 +08:00
<label class="grid grid-cols-4 items-center gap-4 mb-4">
<span class="col-span-1 text-gray-700">Remarks</span>
2023-11-10 14:05:15 +08:00
<textarea
2024-05-29 21:25:53 +08:00
class="col-span-3 form-input mt-1 block w-full"
2023-11-10 14:05:15 +08:00
rows="3"
v-model={this.choiceProduct.remarks}
placeholder="Remarks ..."
></textarea>
</label>
<hr />
<button
type="button"
onClick={() => {
this.adjustStock()
this.openAdjustProduct = false;
}}
2024-05-29 21:25:53 +08:00
class="-mb-4 border border-sky-800 bg-sky-800 text-white rounded-sm px-4 py-2 m-2 transition-colors duration-500 ease select-none hover:bg-sky-800 focus:outline-none focus:shadow-outline disabled:bg-gray-500"
2024-06-01 05:37:11 +08:00
disabled={!this.choiceProduct.stockLotUid}>
2023-11-10 14:05:15 +08:00
Adjust
</button>
</form>
);
},
}}
</Modal>
)}
</>
);
},
});
export { InventoryListing };
export default InventoryListing