ADD: Search and Index system
FIX: Minor Bugs FIX: Bugs in CreateBelonging Page
This commit is contained in:
@@ -288,7 +288,7 @@ const formatDate = (dateString) => {
|
||||
</script>
|
||||
<template>
|
||||
<TransactionDialog v-model:visible="showTransactionDialog" :belongingId="belonging._id" @saved="onTransactionSaved" />
|
||||
<div className="card">
|
||||
<div className="card lg:p-5 px-3">
|
||||
<div class="mb-4" v-if="editMode === true">
|
||||
<Button @click="openTransactionDialog">افزایش یا کاهش موجودی</Button>
|
||||
</div>
|
||||
@@ -299,8 +299,8 @@ const formatDate = (dateString) => {
|
||||
<span class="text-primary">{{ belonging.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex my-6">
|
||||
<div class="w-3/4 ml-3">
|
||||
<div class="lg:flex my-6">
|
||||
<div class="lg:w-3/4 w-full ml-3">
|
||||
<div class="">
|
||||
<label class="text-lg"> نام دارایی </label>
|
||||
<InputText v-model="belonging.name" class="w-full my-2" placeholder="مثلا: 100 عدد کولر گازی" />
|
||||
@@ -349,7 +349,7 @@ const formatDate = (dateString) => {
|
||||
</p>
|
||||
<div class="mx-6 py-4">
|
||||
<label> تراکنش ها </label>
|
||||
<div class="card">
|
||||
<div class="card lg:p-5 px-3">
|
||||
<DataTable
|
||||
:value="transactions"
|
||||
paginator
|
||||
@@ -362,11 +362,10 @@ const formatDate = (dateString) => {
|
||||
:globalFilterFields="['type', 'status', 'value', 'date', 'description']"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-between">
|
||||
<h3 class="m-0">تراکنشها</h3>
|
||||
<div class="lg:flex lg:justify-content-between">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
||||
<!-- <i class="pi pi-search" />-->
|
||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -416,7 +415,7 @@ const formatDate = (dateString) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/4">
|
||||
<div class="lg:w-1/4 w-full">
|
||||
<div>
|
||||
<label class="text-lg"> دسته بندی </label>
|
||||
<Listbox @change="handleSelectCategory($event)" v-model="belonging.category" :options="categories" optionLabel="nativeName" optionValue="_id" checkmark :highlightOnSelect="false" class="w-full my-2" />
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import { ProductService } from '@/service/ProductService';
|
||||
import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { BelongingService } from '@/service/BelongingService';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import Chip from 'primevue/chip';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import IconField from 'primevue/iconfield';
|
||||
import LoadingCheck from '@/components/LoadingCheck.vue';
|
||||
|
||||
onMounted(() => {
|
||||
BelongingService.getAllBelongings().then((data) => (belongings.value = data));
|
||||
loadData();
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
@@ -23,12 +27,6 @@ const filters = ref({
|
||||
});
|
||||
const submitted = ref(false);
|
||||
|
||||
function openNew() {
|
||||
product.value = {};
|
||||
submitted.value = false;
|
||||
productDialog.value = true;
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
productDialog.value = false;
|
||||
submitted.value = false;
|
||||
@@ -40,69 +38,115 @@ function editBelonging(belongingId) {
|
||||
router.push('/belonging/edit-belonging/' + belongingId);
|
||||
}
|
||||
|
||||
function confirmDeleteProduct(prod) {
|
||||
product.value = prod;
|
||||
deleteProductDialog.value = true;
|
||||
}
|
||||
const route = useRoute();
|
||||
const searchPhrase = ref(route.query.query);
|
||||
const page = ref(Number(route.query.page) || 1);
|
||||
const per_page = ref(Number(route.query.per_page) || 20);
|
||||
const totalCount = ref(0);
|
||||
const pagesCount = ref(0);
|
||||
|
||||
function deleteProduct() {
|
||||
products.value = products.value.filter((val) => val.id !== product.value.id);
|
||||
deleteProductDialog.value = false;
|
||||
product.value = {};
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Product Deleted', life: 3000 });
|
||||
}
|
||||
let searchTimeout;
|
||||
|
||||
function findIndexById(id) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < products.value.length; i++) {
|
||||
if (products.value[i].id === id) {
|
||||
index = i;
|
||||
break;
|
||||
// Update your search watcher
|
||||
watch(searchPhrase, (newPhrase, oldPhrase) => {
|
||||
if (newPhrase !== oldPhrase) {
|
||||
// Debounce search to avoid too many requests
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
query: newPhrase || undefined,
|
||||
page: 1 // Reset to first page on new search
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
page.value = Number(route.query.page) || 1;
|
||||
per_page.value = Number(route.query.per_page) || 20;
|
||||
searchPhrase.value = route.query.query || '';
|
||||
loadData();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
const handleRemoveSearch = () => {
|
||||
removeFromQuery('searchPhrase'); // Ensure 'searchPhrase' is the correct key
|
||||
searchPhrase.value = '';
|
||||
};
|
||||
|
||||
const removeFromQuery = (queryToRemove) => {
|
||||
const newQuery = { ...route.query };
|
||||
|
||||
delete newQuery[queryToRemove];
|
||||
|
||||
router.replace({ query: newQuery }).catch((err) => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onPageChange = (event) => {
|
||||
const newPage = event.page + 1;
|
||||
const newPerPage = event.rows;
|
||||
|
||||
if (page.value !== newPage || per_page.value !== newPerPage) {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
page: newPage,
|
||||
per_page: newPerPage
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return index;
|
||||
}
|
||||
const loadData = () => {
|
||||
BelongingService.searchBelongings(route.query)
|
||||
.then((response) => {
|
||||
console.log('response', response);
|
||||
belongings.value = response.belongings || response;
|
||||
totalCount.value = response.totalCount || response.length || 0;
|
||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to load data', life: 3000 });
|
||||
console.error('Error loading data:', error);
|
||||
});
|
||||
};
|
||||
|
||||
function createId() {
|
||||
let id = '';
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
const loading = ref(false);
|
||||
const indexingFinished = ref(false);
|
||||
const indexingMessage = ref('در حال ایندکس...');
|
||||
const animationKey = ref(0);
|
||||
|
||||
// Function to handle indexing
|
||||
const indexBelongings = async () => {
|
||||
loading.value = true;
|
||||
indexingFinished.value = false;
|
||||
|
||||
try {
|
||||
// Your indexing logic here
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate API call
|
||||
await BelongingService.indexBelongings();
|
||||
// When indexing completes
|
||||
indexingFinished.value = true;
|
||||
indexingMessage.value = 'ایندکس با موفقیت انجام شد';
|
||||
} catch (error) {
|
||||
indexingMessage.value = 'خطا در ایندکس';
|
||||
} finally {
|
||||
// Reset after some time
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
animationKey.value++; // Force re-render of animation
|
||||
}, 3000);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
dt.value.exportCSV();
|
||||
}
|
||||
|
||||
function confirmDeleteSelected() {
|
||||
deleteProductsDialog.value = true;
|
||||
}
|
||||
|
||||
function deleteSelectedBelongings() {
|
||||
belonging.value = belonging.value.filter((val) => !selectedBelongings.value.includes(val));
|
||||
deleteProductsDialog.value = false;
|
||||
selectedBelongings.value = null;
|
||||
toast.add({ severity: 'success', summary: 'Successful', detail: 'Products Deleted', life: 3000 });
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
switch (status) {
|
||||
case 'INSTOCK':
|
||||
return 'success';
|
||||
|
||||
case 'LOWSTOCK':
|
||||
return 'warn';
|
||||
|
||||
case 'OUTOFSTOCK':
|
||||
return 'danger';
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -110,8 +154,18 @@ function getStatusLabel(status) {
|
||||
<div class="card">
|
||||
<Toolbar class="mb-6">
|
||||
<template #start>
|
||||
<Button label="ساخت دارایی جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="openNew" />
|
||||
<Button label="حذف دارایی" icon="pi pi-trash" severity="secondary" @click="confirmDeleteSelected" :disabled="!selectedBelongings || !selectedBelongings.length" />
|
||||
<Button label="ساخت دارایی جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="router.push('/belonging/create-belonging')" />
|
||||
<Button @click="indexBelongings" :disabled="loading" severity="secondary mx-3" class="indexing-button">
|
||||
<div class="button-content">
|
||||
<div v-if="!loading">ایندکس کردن محصولات</div>
|
||||
<div v-else class="loading-content flex">
|
||||
<LoadingCheck :key="animationKey" :is-complete="indexingFinished" />
|
||||
<Transition enter-active-class="animate__animated animate__fadeIn animate__fast" leave-active-class="animate__animated animate__fadeOut animate__fast" mode="out-in">
|
||||
<div :key="indexingMessage">{{ indexingMessage }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template #end> </template>
|
||||
@@ -123,35 +177,39 @@ function getStatusLabel(status) {
|
||||
:value="belongings"
|
||||
dataKey="id"
|
||||
:paginator="true"
|
||||
:rows="20"
|
||||
:rows="per_page"
|
||||
:filters="filters"
|
||||
:total-records="totalCount"
|
||||
:current-page="page - 1"
|
||||
@page="onPageChange($event)"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
:rowsPerPageOptions="[20, 40, 80]"
|
||||
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دارایی"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<div class="flex flex-wrap gap-2 items-center justify-start">
|
||||
<h4 class="m-0">مدیریت اموال</h4>
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
||||
<InputText v-model="searchPhrase" placeholder="جستجو..." />
|
||||
</IconField>
|
||||
</div>
|
||||
<Chip v-if="searchPhrase" class="mx-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
||||
</template>
|
||||
|
||||
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
||||
<Column header="تصویر">
|
||||
<template #body="slotProps">
|
||||
<img :src="`${slotProps.data.mainImageUrls[0]}`" :alt="slotProps.data.name" class="rounded" style="max-width: 64px" />
|
||||
<img :src="`${slotProps.data.imageUrl}`" :alt="slotProps.data.name" class="rounded" style="max-width: 64px" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="belongingCode" header="کد" style="min-width: 4rem"></Column>
|
||||
<Column field="name" header="نام" sortable style="min-width: 16rem"></Column>
|
||||
<Column field="category" header="دسته بندی" sortable style="min-width: 6rem">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.category?.nativeName }}
|
||||
{{ slotProps.data.category?.nativeName || slotProps.data.nativeCategory }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column :exportable="false" style="min-width: 12rem">
|
||||
@@ -163,63 +221,6 @@ function getStatusLabel(status) {
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="productDialog" :style="{ width: '450px' }" header="Product Details" :modal="true">
|
||||
<div class="flex flex-col gap-6">
|
||||
<img v-if="belonging.image" :src="`https://primefaces.org/cdn/primevue/images/product/${product.image}`" :alt="product.image" class="block m-auto pb-4" />
|
||||
<div>
|
||||
<label for="name" class="block font-bold mb-3">Name</label>
|
||||
<InputText id="name" v-model.trim="product.name" required="true" autofocus :invalid="submitted && !product.name" fluid />
|
||||
<small v-if="submitted && !product.name" class="text-red-500">Name is required.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block font-bold mb-3">Description</label>
|
||||
<Textarea id="description" v-model="product.description" required="true" rows="3" cols="20" fluid />
|
||||
</div>
|
||||
<div>
|
||||
<label for="inventoryStatus" class="block font-bold mb-3">Inventory Status</label>
|
||||
<Select id="inventoryStatus" v-model="product.inventoryStatus" :options="statuses" optionLabel="label" placeholder="Select a Status" fluid></Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block font-bold mb-4">Category</span>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category1" v-model="product.category" name="category" value="Accessories" />
|
||||
<label for="category1">Accessories</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category2" v-model="product.category" name="category" value="Clothing" />
|
||||
<label for="category2">Clothing</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category3" v-model="product.category" name="category" value="Electronics" />
|
||||
<label for="category3">Electronics</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-6">
|
||||
<RadioButton id="category4" v-model="product.category" name="category" value="Fitness" />
|
||||
<label for="category4">Fitness</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-6">
|
||||
<label for="price" class="block font-bold mb-3">Price</label>
|
||||
<InputNumber id="price" v-model="product.price" mode="currency" currency="USD" locale="en-US" fluid />
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<label for="quantity" class="block font-bold mb-3">Quantity</label>
|
||||
<InputNumber id="quantity" v-model="product.quantity" integeronly fluid />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancel" icon="pi pi-times" text @click="hideDialog" />
|
||||
<Button label="Save" icon="pi pi-check" @click="saveProduct" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="deleteProductDialog" :style="{ width: '450px' }" header="Confirm" :modal="true">
|
||||
<div class="flex items-center gap-4">
|
||||
<i class="pi pi-exclamation-triangle !text-3xl" />
|
||||
|
||||
Reference in New Issue
Block a user