ADD: Search and Index system
FIX: Minor Bugs FIX: Bugs in CreateBelonging Page
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
name: deploy-chabokan
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
- name: update-chabokan
|
||||||
|
env:
|
||||||
|
CHABOKAN_TOKEN: ${{ secrets.CHABOKAN_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npm install -g @chabokan.net/cli
|
||||||
|
chabok login -t $CHABOKAN_TOKEN
|
||||||
|
chabok deploy -s bm-vue
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 6rem 2rem 0 2rem;
|
|
||||||
transition: margin-right var(--layout-section-transition-duration);
|
transition: margin-right var(--layout-section-transition-duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
height: calc(100vh - 8rem);
|
height: calc(100vh - 8rem);
|
||||||
z-index: 999;
|
z-index: 1001;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
top: 6rem;
|
top: 6rem;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
&.layout-overlay {
|
&.layout-overlay {
|
||||||
.layout-main-container {
|
.layout-main-container {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-sidebar {
|
.layout-sidebar {
|
||||||
@@ -52,7 +51,6 @@
|
|||||||
|
|
||||||
.layout-main-container {
|
.layout-main-container {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,11 +69,10 @@
|
|||||||
.layout-wrapper {
|
.layout-wrapper {
|
||||||
.layout-main-container {
|
.layout-main-container {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-sidebar {
|
.layout-sidebar {
|
||||||
transform: translateX(-100%);
|
transform: translateX(350px);
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -99,7 +96,7 @@
|
|||||||
|
|
||||||
&.layout-mobile-active {
|
&.layout-mobile-active {
|
||||||
.layout-sidebar {
|
.layout-sidebar {
|
||||||
transform: translateX(0);
|
transform: translateX(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-mask {
|
.layout-mask {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<!-- LoadingCheck.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="inline-flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="transform transition-transform duration-500"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
class="text-blue-500"
|
||||||
|
:style="circleStyle"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 12l3 3l5-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
class="text-green-500"
|
||||||
|
:style="checkStyle"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isComplete: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dashOffset = ref(0)
|
||||||
|
let animationFrame = null
|
||||||
|
|
||||||
|
const circleStyle = computed(() => ({
|
||||||
|
strokeDasharray: '60 60',
|
||||||
|
strokeDashoffset: props.isComplete ? '60' : String(dashOffset.value),
|
||||||
|
transition: props.isComplete ? 'all 0.2s ease-in-out' : 'none',
|
||||||
|
transformOrigin: 'center',
|
||||||
|
transform: `rotate(${props.isComplete ? -90 : 0}deg)`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const checkStyle = computed(() => ({
|
||||||
|
opacity: props.isComplete ? 1 : 0,
|
||||||
|
strokeDasharray: 20,
|
||||||
|
strokeDashoffset: props.isComplete ? 0 : 20,
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
transitionDelay: '0.2s'
|
||||||
|
}))
|
||||||
|
|
||||||
|
let startTime = null
|
||||||
|
const animate = (timestamp) => {
|
||||||
|
if (!startTime) startTime = timestamp
|
||||||
|
dashOffset.value = (timestamp - startTime) / 10
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isComplete,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue && animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
animationFrame = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.isComplete) {
|
||||||
|
animationFrame = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ProductService } from '@/service/ProductService';
|
import { ProductService } from '@/service/ProductService';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { BelongingService } from '@/service/BelongingService';
|
import { BelongingService } from '@/service/BelongingService';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const products = ref(null);
|
const products = ref(null);
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ onMounted(() => {
|
|||||||
BelongingService.getAllBelongings().then((data) => (products.value = data));
|
BelongingService.getAllBelongings().then((data) => (products.value = data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
function editBelonging(belongingId) {
|
function editBelonging(belongingId) {
|
||||||
router.push('/belonging/edit-belonging/' + belongingId);
|
router.push('/belonging/edit-belonging/' + belongingId);
|
||||||
}
|
}
|
||||||
@@ -34,7 +36,7 @@ function editBelonging(belongingId) {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column style="min-width: 15%" header="نمایش">
|
<Column style="min-width: 15%" header="نمایش">
|
||||||
<template #body>
|
<template #body="slotProps">
|
||||||
<Button icon="pi pi-search" type="button" class="p-button-text" @click="editBelonging(slotProps.data._id)"></Button>
|
<Button icon="pi pi-search" type="button" class="p-button-text" @click="editBelonging(slotProps.data._id)"></Button>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
@@ -53,14 +53,13 @@ function isOutsideClicked(event) {
|
|||||||
|
|
||||||
return !(sidebarEl.isSameNode(event.target) || sidebarEl.contains(event.target) || topbarEl.isSameNode(event.target) || topbarEl.contains(event.target));
|
return !(sidebarEl.isSameNode(event.target) || sidebarEl.contains(event.target) || topbarEl.isSameNode(event.target) || topbarEl.contains(event.target));
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout-wrapper" :class="containerClass">
|
<div class="layout-wrapper" :class="containerClass">
|
||||||
<app-topbar></app-topbar>
|
<app-topbar></app-topbar>
|
||||||
<app-sidebar></app-sidebar>
|
<app-sidebar></app-sidebar>
|
||||||
<div class="layout-main-container">
|
<div class="layout-main-container pt-24 lg:px-10 px-3">
|
||||||
<div class="layout-main">
|
<div class="layout-main">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,33 @@ export const BelongingService = {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
searchBelongings: async (query) => {
|
||||||
|
const config = {
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/belongings/admin/search',
|
||||||
|
withCredentials: true,
|
||||||
|
params: query
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios(config);
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indexBelongings: async () => {
|
||||||
|
const config = {
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/belongings/admin/index-all',
|
||||||
|
withCredentials: true
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios(config);
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
createBelonging: async (data) => {
|
createBelonging: async (data) => {
|
||||||
const config = {
|
const config = {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ const formatDate = (dateString) => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<TransactionDialog v-model:visible="showTransactionDialog" :belongingId="belonging._id" @saved="onTransactionSaved" />
|
<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">
|
<div class="mb-4" v-if="editMode === true">
|
||||||
<Button @click="openTransactionDialog">افزایش یا کاهش موجودی</Button>
|
<Button @click="openTransactionDialog">افزایش یا کاهش موجودی</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,8 +299,8 @@ const formatDate = (dateString) => {
|
|||||||
<span class="text-primary">{{ belonging.name }}</span>
|
<span class="text-primary">{{ belonging.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex my-6">
|
<div class="lg:flex my-6">
|
||||||
<div class="w-3/4 ml-3">
|
<div class="lg:w-3/4 w-full ml-3">
|
||||||
<div class="">
|
<div class="">
|
||||||
<label class="text-lg"> نام دارایی </label>
|
<label class="text-lg"> نام دارایی </label>
|
||||||
<InputText v-model="belonging.name" class="w-full my-2" placeholder="مثلا: 100 عدد کولر گازی" />
|
<InputText v-model="belonging.name" class="w-full my-2" placeholder="مثلا: 100 عدد کولر گازی" />
|
||||||
@@ -349,7 +349,7 @@ const formatDate = (dateString) => {
|
|||||||
</p>
|
</p>
|
||||||
<div class="mx-6 py-4">
|
<div class="mx-6 py-4">
|
||||||
<label> تراکنش ها </label>
|
<label> تراکنش ها </label>
|
||||||
<div class="card">
|
<div class="card lg:p-5 px-3">
|
||||||
<DataTable
|
<DataTable
|
||||||
:value="transactions"
|
:value="transactions"
|
||||||
paginator
|
paginator
|
||||||
@@ -362,11 +362,10 @@ const formatDate = (dateString) => {
|
|||||||
:globalFilterFields="['type', 'status', 'value', 'date', 'description']"
|
:globalFilterFields="['type', 'status', 'value', 'date', 'description']"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-content-between">
|
<div class="lg:flex lg:justify-content-between">
|
||||||
<h3 class="m-0">تراکنشها</h3>
|
|
||||||
<span class="p-input-icon-left">
|
<span class="p-input-icon-left">
|
||||||
<i class="pi pi-search" />
|
<!-- <i class="pi pi-search" />-->
|
||||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
<InputText v-model="filters['global'].value" placeholder="جستجو..." class="w-full" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -416,7 +415,7 @@ const formatDate = (dateString) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4">
|
<div class="lg:w-1/4 w-full">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-lg"> دسته بندی </label>
|
<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" />
|
<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 { ProductService } from '@/service/ProductService';
|
||||||
import { FilterMatchMode } from '@primevue/core/api';
|
import { FilterMatchMode } from '@primevue/core/api';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { BelongingService } from '@/service/BelongingService';
|
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(() => {
|
onMounted(() => {
|
||||||
BelongingService.getAllBelongings().then((data) => (belongings.value = data));
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -23,12 +27,6 @@ const filters = ref({
|
|||||||
});
|
});
|
||||||
const submitted = ref(false);
|
const submitted = ref(false);
|
||||||
|
|
||||||
function openNew() {
|
|
||||||
product.value = {};
|
|
||||||
submitted.value = false;
|
|
||||||
productDialog.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDialog() {
|
function hideDialog() {
|
||||||
productDialog.value = false;
|
productDialog.value = false;
|
||||||
submitted.value = false;
|
submitted.value = false;
|
||||||
@@ -40,69 +38,115 @@ function editBelonging(belongingId) {
|
|||||||
router.push('/belonging/edit-belonging/' + belongingId);
|
router.push('/belonging/edit-belonging/' + belongingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDeleteProduct(prod) {
|
const route = useRoute();
|
||||||
product.value = prod;
|
const searchPhrase = ref(route.query.query);
|
||||||
deleteProductDialog.value = true;
|
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() {
|
let searchTimeout;
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function findIndexById(id) {
|
// Update your search watcher
|
||||||
let index = -1;
|
watch(searchPhrase, (newPhrase, oldPhrase) => {
|
||||||
for (let i = 0; i < products.value.length; i++) {
|
if (newPhrase !== oldPhrase) {
|
||||||
if (products.value[i].id === id) {
|
// Debounce search to avoid too many requests
|
||||||
index = i;
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
break;
|
searchTimeout = setTimeout(() => {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
query: newPhrase || undefined,
|
||||||
|
page: 1 // Reset to first page on new search
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return index;
|
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 = '';
|
||||||
|
};
|
||||||
|
|
||||||
function createId() {
|
const removeFromQuery = (queryToRemove) => {
|
||||||
let id = '';
|
const newQuery = { ...route.query };
|
||||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
for (var i = 0; i < 5; i++) {
|
|
||||||
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportCSV() {
|
delete newQuery[queryToRemove];
|
||||||
dt.value.exportCSV();
|
|
||||||
|
router.replace({ query: newQuery }).catch((err) => {
|
||||||
|
if (err.name !== 'NavigationDuplicated') {
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function confirmDeleteSelected() {
|
const onPageChange = (event) => {
|
||||||
deleteProductsDialog.value = true;
|
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
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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) {
|
const loadData = () => {
|
||||||
switch (status) {
|
BelongingService.searchBelongings(route.query)
|
||||||
case 'INSTOCK':
|
.then((response) => {
|
||||||
return 'success';
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
case 'LOWSTOCK':
|
const loading = ref(false);
|
||||||
return 'warn';
|
const indexingFinished = ref(false);
|
||||||
|
const indexingMessage = ref('در حال ایندکس...');
|
||||||
|
const animationKey = ref(0);
|
||||||
|
|
||||||
case 'OUTOFSTOCK':
|
// Function to handle indexing
|
||||||
return 'danger';
|
const indexBelongings = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
indexingFinished.value = false;
|
||||||
|
|
||||||
default:
|
try {
|
||||||
return null;
|
// 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);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -110,8 +154,18 @@ function getStatusLabel(status) {
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<Toolbar class="mb-6">
|
<Toolbar class="mb-6">
|
||||||
<template #start>
|
<template #start>
|
||||||
<Button label="ساخت دارایی جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="openNew" />
|
<Button label="ساخت دارایی جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="router.push('/belonging/create-belonging')" />
|
||||||
<Button label="حذف دارایی" icon="pi pi-trash" severity="secondary" @click="confirmDeleteSelected" :disabled="!selectedBelongings || !selectedBelongings.length" />
|
<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>
|
||||||
|
|
||||||
<template #end> </template>
|
<template #end> </template>
|
||||||
@@ -123,35 +177,39 @@ function getStatusLabel(status) {
|
|||||||
:value="belongings"
|
:value="belongings"
|
||||||
dataKey="id"
|
dataKey="id"
|
||||||
:paginator="true"
|
:paginator="true"
|
||||||
:rows="20"
|
:rows="per_page"
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
|
:total-records="totalCount"
|
||||||
|
:current-page="page - 1"
|
||||||
|
@page="onPageChange($event)"
|
||||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
:rowsPerPageOptions="[20, 40, 80]"
|
:rowsPerPageOptions="[20, 40, 80]"
|
||||||
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دارایی"
|
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دارایی"
|
||||||
>
|
>
|
||||||
<template #header>
|
<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>
|
<h4 class="m-0">مدیریت اموال</h4>
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon>
|
<InputIcon>
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" />
|
||||||
</InputIcon>
|
</InputIcon>
|
||||||
<InputText v-model="filters['global'].value" placeholder="جستجو..." />
|
<InputText v-model="searchPhrase" placeholder="جستجو..." />
|
||||||
</IconField>
|
</IconField>
|
||||||
</div>
|
</div>
|
||||||
|
<Chip v-if="searchPhrase" class="mx-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
||||||
<Column header="تصویر">
|
<Column header="تصویر">
|
||||||
<template #body="slotProps">
|
<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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="belongingCode" header="کد" style="min-width: 4rem"></Column>
|
<Column field="belongingCode" header="کد" style="min-width: 4rem"></Column>
|
||||||
<Column field="name" header="نام" sortable style="min-width: 16rem"></Column>
|
<Column field="name" header="نام" sortable style="min-width: 16rem"></Column>
|
||||||
<Column field="category" header="دسته بندی" sortable style="min-width: 6rem">
|
<Column field="category" header="دسته بندی" sortable style="min-width: 6rem">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
{{ slotProps.data.category?.nativeName }}
|
{{ slotProps.data.category?.nativeName || slotProps.data.nativeCategory }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column :exportable="false" style="min-width: 12rem">
|
<Column :exportable="false" style="min-width: 12rem">
|
||||||
@@ -163,63 +221,6 @@ function getStatusLabel(status) {
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</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">
|
<Dialog v-model:visible="deleteProductDialog" :style="{ width: '450px' }" header="Confirm" :modal="true">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<i class="pi pi-exclamation-triangle !text-3xl" />
|
<i class="pi pi-exclamation-triangle !text-3xl" />
|
||||||
|
|||||||
Reference in New Issue
Block a user