feat: add search settings page in dashboard, sidebar menu, and MongoDB search integration
deploy-chabokan / deploy (push) Canceled after 0s

This commit is contained in:
2026-08-19 05:50:18 +03:30
parent cf7e1ee45c
commit 859a86f1a5
8 changed files with 581 additions and 20 deletions
+7
View File
@@ -37,8 +37,15 @@ const model = ref([
{ label: 'کاربران', icon: 'pi pi-fw pi-users', to: '/users/list-users' }, { label: 'کاربران', icon: 'pi pi-fw pi-users', to: '/users/list-users' },
{ label: 'نقش‌ها و دسترسی‌ها', icon: 'pi pi-fw pi-shield', to: '/roles/list-roles' } { label: 'نقش‌ها و دسترسی‌ها', icon: 'pi pi-fw pi-shield', to: '/roles/list-roles' }
] ]
},
{
label: 'تنظیمات سیستم',
items: [
{ label: 'تنظیمات جستجو', icon: 'pi pi-fw pi-sliders-h', to: '/settings/search-settings' }
]
} }
]); ]);
</script> </script>
<template> <template>
+11
View File
@@ -121,6 +121,17 @@ const router = createRouter({
name: 'list-roles', name: 'list-roles',
component: () => import('@/views/pages/Roles/ListRoles.vue') component: () => import('@/views/pages/Roles/ListRoles.vue')
}, },
// ─── Settings ────────────────────────────────────────────────
{
path: '/settings/search-settings',
name: 'search-settings',
component: () => import('@/views/pages/Settings/SearchSettings.vue')
},
{
path: '/settings',
redirect: '/settings/search-settings'
},
{ {
path: '/uikit/formlayout', path: '/uikit/formlayout',
name: 'formlayout', name: 'formlayout',
+19
View File
@@ -55,9 +55,28 @@ export const PropertyService = {
return response.data; return response.data;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
throw err;
} }
}, },
searchPropertiesMongo: async (query) => {
const config = {
method: 'get',
url: '/api/properties/admin/search-mongo',
withCredentials: true,
params: query,
headers: PropertyService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data;
} catch (err) {
console.log(err);
throw err;
}
},
indexProperties: async () => { indexProperties: async () => {
const config = { const config = {
method: 'post', method: 'post',
+101
View File
@@ -0,0 +1,101 @@
import axios from 'axios';
export const SettingService = {
_getAuthHeader: () => {
const bmToken = document.cookie
.split('; ')
.find((row) => row.startsWith('bm-token='))
?.split('=')[1];
return {
Authorization: `Bearer ${bmToken}`
};
},
/**
* Get search settings for all modules
* @returns {Promise<Array>}
*/
getSearchSettings: async () => {
const config = {
method: 'get',
url: '/api/settings/search-settings',
withCredentials: true,
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error('Error fetching search settings:', err);
throw err;
}
},
/**
* Get search setting for a specific page key
* @param {string} pageKey
* @returns {Promise<Object>}
*/
getSearchSettingByPage: async (pageKey) => {
const config = {
method: 'get',
url: `/api/settings/search-settings/${pageKey}`,
withCredentials: true,
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error(`Error fetching search setting for ${pageKey}:`, err);
throw err;
}
},
/**
* Update search method for a specific page key
* @param {string} pageKey
* @param {string} searchMethod - 'typesense' | 'mongo'
* @param {Object} extraConfig
* @returns {Promise<Object>}
*/
updateSearchSetting: async (pageKey, searchMethod, extraConfig = {}) => {
const config = {
method: 'put',
url: `/api/settings/search-settings/${pageKey}`,
withCredentials: true,
data: { searchMethod, extraConfig },
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error(`Error updating search setting for ${pageKey}:`, err);
throw err;
}
},
/**
* Bulk update search settings
* @param {Array<{pageKey: string, searchMethod: string}>} settings
* @returns {Promise<Array>}
*/
bulkUpdateSearchSettings: async (settings) => {
const config = {
method: 'put',
url: '/api/settings/search-settings',
withCredentials: true,
data: { settings },
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error('Error bulk updating search settings:', err);
throw err;
}
}
};
+19
View File
@@ -168,9 +168,28 @@ export const TransactionService = {
return response.data; return response.data;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
throw err;
} }
}, },
searchTransactionsMongo: async (query) => {
const config = {
method: 'get',
url: '/api/transactions/admin/search-mongo',
withCredentials: true,
params: query,
headers: TransactionService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data;
} catch (err) {
console.log(err);
throw err;
}
},
indexTransactions: async () => { indexTransactions: async () => {
const config = { const config = {
method: 'post', method: 'post',
+37 -2
View File
@@ -3,8 +3,10 @@ import { FilterMatchMode } from '@primevue/core/api';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { onMounted, ref, watch, computed } from 'vue'; import { onMounted, ref, watch, computed } from 'vue';
import { PropertyService } from '@/service/PropertyService'; import { PropertyService } from '@/service/PropertyService';
import { SettingService } from '@/service/SettingService';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import Chip from 'primevue/chip'; import Chip from 'primevue/chip';
import Tag from 'primevue/tag';
import InputIcon from 'primevue/inputicon'; import InputIcon from 'primevue/inputicon';
import IconField from 'primevue/iconfield'; import IconField from 'primevue/iconfield';
import SelectButton from 'primevue/selectbutton'; import SelectButton from 'primevue/selectbutton';
@@ -34,6 +36,7 @@ const selectedPropertyId = ref(null);
const mapRef = ref(null); const mapRef = ref(null);
const splitMapRef = ref(null); const splitMapRef = ref(null);
const propertyCardRefs = ref({}); const propertyCardRefs = ref({});
const searchMethod = ref(null); // 'typesense' | 'mongo'
const filters = ref({ const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS } global: { value: null, matchMode: FilterMatchMode.CONTAINS }
@@ -47,7 +50,18 @@ const pagesCount = ref(0);
const DEFAULT_IMAGE = 'https://cloud.mbmanager.ir/images/item-no-image.png'; const DEFAULT_IMAGE = 'https://cloud.mbmanager.ir/images/item-no-image.png';
onMounted(() => { const loadSearchSetting = async () => {
try {
const setting = await SettingService.getSearchSettingByPage('properties');
searchMethod.value = setting?.searchMethod || 'typesense';
} catch (e) {
console.warn('Failed to load search setting, defaulting to typesense', e);
searchMethod.value = 'typesense';
}
};
onMounted(async () => {
await loadSearchSetting();
loadData(); loadData();
}); });
@@ -112,7 +126,15 @@ const onPageChange = (event) => {
const loadData = async () => { const loadData = async () => {
try { try {
const response = await PropertyService.searchProperties(route.query); if (searchMethod.value === null) {
await loadSearchSetting();
}
let response;
if (searchMethod.value === 'mongo') {
response = await PropertyService.searchPropertiesMongo(route.query);
} else {
response = await PropertyService.searchProperties(route.query);
}
properties.value = response.properties || response.belongings || response || []; properties.value = response.properties || response.belongings || response || [];
totalCount.value = response.totalCount || (Array.isArray(response) ? response.length : 0); totalCount.value = response.totalCount || (Array.isArray(response) ? response.length : 0);
pagesCount.value = Math.ceil(totalCount.value / per_page.value); pagesCount.value = Math.ceil(totalCount.value / per_page.value);
@@ -122,6 +144,7 @@ const loadData = async () => {
} }
}; };
const loading = ref(false); const loading = ref(false);
const indexingFinished = ref(false); const indexingFinished = ref(false);
const indexingMessage = ref('در حال ایندکس...'); const indexingMessage = ref('در حال ایندکس...');
@@ -262,6 +285,18 @@ const onCardHover = (propertyId) => {
</IconField> </IconField>
<Chip v-if="searchPhrase" class="mx-1" :label="searchPhrase" removable @remove="handleRemoveSearch" /> <Chip v-if="searchPhrase" class="mx-1" :label="searchPhrase" removable @remove="handleRemoveSearch" />
<!-- Search Engine Mode Tag -->
<Tag
v-if="searchMethod"
:value="searchMethod === 'mongo' ? 'جستجو: MongoDB' : 'جستجو: Typesense'"
:severity="searchMethod === 'mongo' ? 'success' : 'info'"
:icon="searchMethod === 'mongo' ? 'pi pi-database' : 'pi pi-bolt'"
class="cursor-pointer text-xs"
v-tooltip.bottom="'کلیک برای تغییر موتور جستجو در تنظیمات'"
@click="router.push('/settings/search-settings')"
/>
<!-- View Mode Selector --> <!-- View Mode Selector -->
<SelectButton <SelectButton
v-model="viewMode" v-model="viewMode"
+334
View File
@@ -0,0 +1,334 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { SettingService } from '@/service/SettingService';
import { useRouter } from 'vue-router';
const toast = useToast();
const confirm = useConfirm();
const router = useRouter();
const loading = ref(false);
const saving = ref(false);
const settings = ref([]);
const methodOptions = [
{ label: 'Typesense', value: 'typesense', icon: 'pi pi-bolt', class: 'text-amber-500' },
{ label: 'MongoDB', value: 'mongo', icon: 'pi pi-database', class: 'text-emerald-500' }
];
onMounted(async () => {
await loadSettings();
});
const loadSettings = async () => {
loading.value = true;
try {
const data = await SettingService.getSearchSettings();
settings.value = data;
} catch (e) {
toast.add({
severity: 'error',
summary: 'خطا',
detail: 'خطا در بارگذاری تنظیمات جستجو',
life: 3000
});
} finally {
loading.value = false;
}
};
const stats = computed(() => {
const total = settings.value.length;
const mongoCount = settings.value.filter((s) => s.searchMethod === 'mongo').length;
const typesenseCount = settings.value.filter((s) => s.searchMethod === 'typesense').length;
const persistedCount = settings.value.filter((s) => s.isPersisted).length;
return { total, mongoCount, typesenseCount, persistedCount };
});
const updateMethod = async (pageKey, newMethod) => {
try {
const updated = await SettingService.updateSearchSetting(pageKey, newMethod);
const index = settings.value.findIndex((s) => s.pageKey === pageKey);
if (index !== -1) {
settings.value[index] = { ...settings.value[index], ...updated };
}
toast.add({
severity: 'success',
summary: 'بروزرسانی شد',
detail: `روش جستجوی "${updated.title}" به ${newMethod === 'mongo' ? 'MongoDB' : 'Typesense'} تغییر یافت.`,
life: 3000
});
} catch (e) {
toast.add({
severity: 'error',
summary: 'خطا',
detail: 'خطا در ذخیره‌سازی تنظیمات در پایگاه داده',
life: 3000
});
}
};
const setAllToMethod = async (method) => {
confirm.require({
message: `آیا از تغییر روش جستجوی تمامی بخش‌ها به ${method === 'mongo' ? 'MongoDB' : 'Typesense'} اطمینان دارید؟`,
header: 'تایید تغییر همگانی',
icon: 'pi pi-exclamation-triangle',
rejectProps: { label: 'انصراف', severity: 'secondary', outlined: true },
acceptProps: { label: 'تایید و اعمال', severity: method === 'mongo' ? 'success' : 'warn' },
accept: async () => {
saving.value = true;
try {
const payload = settings.value.map((s) => ({
pageKey: s.pageKey,
searchMethod: method
}));
const updatedList = await SettingService.bulkUpdateSearchSettings(payload);
settings.value = updatedList;
toast.add({
severity: 'success',
summary: 'موفقیت‌آمیز',
detail: `تمامی بخش‌ها با موفقیت به ${method === 'mongo' ? 'MongoDB' : 'Typesense'} تغییر یافتند.`,
life: 4000
});
} catch (e) {
toast.add({
severity: 'error',
summary: 'خطا',
detail: 'خطا در بروزرسانی همگانی تنظیمات',
life: 3000
});
} finally {
saving.value = false;
}
}
});
};
const navigateToModule = (routePath) => {
if (routePath) {
router.push(routePath);
}
};
</script>
<template>
<div class="search-settings-page">
<Toast />
<ConfirmDialog />
<!-- ── Header Banner ── -->
<div class="card p-6 mb-6 shadow-sm border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900 rounded-2xl">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-primary-100 dark:bg-primary-950 flex items-center justify-center text-primary-600 dark:text-primary-400">
<i class="pi pi-sliders-h text-2xl"></i>
</div>
<div>
<h2 class="text-2xl font-bold m-0 text-surface-900 dark:text-surface-0">تنظیمات موتور جستجو</h2>
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1 text-sm">
انتخاب روش جستجو (MongoDB یا Typesense) برای هر بخش از سیستم به صورت مجزا
</p>
</div>
</div>
<!-- Stats Badges -->
<div class="flex flex-wrap items-center gap-2">
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 text-xs font-semibold">
<i class="pi pi-list text-primary-500"></i>
<span>کل بخش‌ها: {{ stats.total }}</span>
</div>
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-emerald-50 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 text-xs font-semibold">
<i class="pi pi-database text-emerald-500"></i>
<span>MongoDB: {{ stats.mongoCount }}</span>
</div>
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-amber-50 dark:bg-amber-950 text-amber-700 dark:text-amber-300 text-xs font-semibold">
<i class="pi pi-bolt text-amber-500"></i>
<span>Typesense: {{ stats.typesenseCount }}</span>
</div>
</div>
</div>
</div>
<!-- ── Quick Actions Toolbar ── -->
<div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-wrap items-center gap-3">
<Button
label="تنظیم همه روی MongoDB"
icon="pi pi-database"
severity="success"
outlined
size="small"
:disabled="loading || saving"
@click="setAllToMethod('mongo')"
/>
<Button
label="تنظیم همه روی Typesense"
icon="pi pi-bolt"
severity="warn"
outlined
size="small"
:disabled="loading || saving"
@click="setAllToMethod('typesense')"
/>
</div>
<div class="flex items-center gap-2">
<Button
icon="pi pi-refresh"
severity="secondary"
text
rounded
:loading="loading"
v-tooltip.top="'بارگذاری مجدد'"
@click="loadSettings"
/>
</div>
</div>
<!-- ── Settings Cards Grid ── -->
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="i in 6" :key="i" class="card p-6">
<Skeleton width="40%" height="1.5rem" class="mb-3" />
<Skeleton width="80%" height="1rem" class="mb-4" />
<Skeleton width="100%" height="2.5rem" />
</div>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div
v-for="item in settings"
:key="item.pageKey"
class="card p-6 rounded-2xl border transition-all duration-200 hover:shadow-md flex flex-col justify-between"
:class="[
item.searchMethod === 'mongo'
? 'border-emerald-200 dark:border-emerald-900 bg-emerald-50/10'
: 'border-amber-200 dark:border-amber-900 bg-amber-50/10'
]"
>
<div>
<!-- Card Top Header -->
<div class="flex items-start justify-between gap-3 mb-3">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl flex items-center justify-center text-lg"
:class="[
item.searchMethod === 'mongo'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300'
]"
>
<i :class="item.icon || 'pi pi-file'"></i>
</div>
<div>
<h3 class="text-lg font-bold m-0 text-surface-900 dark:text-surface-0">{{ item.title }}</h3>
<span class="text-xs text-surface-500 font-mono">{{ item.pageKey }}</span>
</div>
</div>
<!-- Persistence Badge -->
<Tag
:value="item.isPersisted ? 'ذخیره در دیتابیس' : 'پیش‌فرض'"
:severity="item.isPersisted ? 'success' : 'secondary'"
class="text-xs"
/>
</div>
<!-- Description -->
<p class="text-sm text-surface-600 dark:text-surface-300 mb-4 leading-relaxed">
{{ item.description }}
</p>
<!-- Endpoints Information -->
<div class="p-3 mb-4 rounded-xl bg-surface-100 dark:bg-surface-800/80 text-xs space-y-1 font-mono">
<div class="flex items-center justify-between">
<span class="text-surface-500">اندپوینت فعال:</span>
<span class="font-bold" :class="item.searchMethod === 'mongo' ? 'text-emerald-600 dark:text-emerald-400' : 'text-amber-600 dark:text-amber-400'">
{{ item.searchMethod === 'mongo' ? item.endpointMongo : item.endpointTypesense }}
</span>
</div>
</div>
</div>
<!-- Bottom Selector & Actions -->
<div class="pt-4 border-t border-surface-200 dark:border-surface-700 flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-2">
<span class="text-xs font-semibold text-surface-600 dark:text-surface-400">موتور جستجو:</span>
<SelectButton
:modelValue="item.searchMethod"
:options="methodOptions"
optionLabel="label"
optionValue="value"
:allowEmpty="false"
size="small"
@update:modelValue="(val) => updateMethod(item.pageKey, val)"
>
<template #option="slotProps">
<div class="flex items-center gap-1.5 px-1 py-0.5 text-xs font-bold">
<i :class="slotProps.option.icon"></i>
<span>{{ slotProps.option.label }}</span>
</div>
</template>
</SelectButton>
</div>
<Button
v-if="item.routePath"
label="مشاهده صفحه"
icon="pi pi-arrow-left"
iconPos="right"
size="small"
severity="secondary"
text
@click="navigateToModule(item.routePath)"
/>
</div>
</div>
</div>
<!-- ── Information Guide ── -->
<div class="card p-6 mt-8 rounded-2xl bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700">
<h4 class="text-base font-bold mb-3 flex items-center gap-2 text-surface-800 dark:text-surface-100">
<i class="pi pi-info-circle text-primary-500"></i>
<span>راهنمای روش‌های جستجو</span>
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs leading-relaxed text-surface-600 dark:text-surface-300">
<div class="p-4 rounded-xl bg-surface-0 dark:bg-surface-900 border border-amber-200 dark:border-amber-900/50">
<div class="font-bold text-amber-700 dark:text-amber-400 flex items-center gap-2 mb-1">
<i class="pi pi-bolt"></i>
<span>موتور Typesense</span>
</div>
<p class="m-0">
موتور جستجوی ایندکس‌شده با سرعت فوق‌العاده بالا، تحمل خطای تایپی (Typo Tolerance) و سیستم Faceting پیشرفته. نیازمند فرآیند ایندکس‌سازی داده‌ها است.
</p>
</div>
<div class="p-4 rounded-xl bg-surface-0 dark:bg-surface-900 border border-emerald-200 dark:border-emerald-900/50">
<div class="font-bold text-emerald-700 dark:text-emerald-400 flex items-center gap-2 mb-1">
<i class="pi pi-database"></i>
<span>موتور MongoDB</span>
</div>
<p class="m-0">
جستجوی زنده مستقیم روی پایگاه داده MongoDB با الگوهای عبارت منظم (Regex) و فیلترهای دقیق. بدون نیاز به ایندکس جداگانه و همیشه هماهنگ با آخرین تغییرات پایگاه داده.
</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.search-settings-page {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -3,13 +3,29 @@ import { FilterMatchMode } from '@primevue/core/api';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { TransactionService } from '@/service/TransactionService'; import { TransactionService } from '@/service/TransactionService';
import { SettingService } from '@/service/SettingService';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import Chip from 'primevue/chip'; import Chip from 'primevue/chip';
import Tag from 'primevue/tag';
import InputIcon from 'primevue/inputicon'; import InputIcon from 'primevue/inputicon';
import IconField from 'primevue/iconfield'; import IconField from 'primevue/iconfield';
import LoadingCheck from '@/components/LoadingCheck.vue'; import LoadingCheck from '@/components/LoadingCheck.vue';
import Popover from 'primevue/popover'; import Popover from 'primevue/popover';
onMounted(() => {
const searchMethod = ref(null);
const loadSearchSetting = async () => {
try {
const setting = await SettingService.getSearchSettingByPage('transactions');
searchMethod.value = setting?.searchMethod || 'typesense';
} catch (e) {
console.warn('Failed to load search setting for transactions, defaulting to typesense', e);
searchMethod.value = 'typesense';
}
};
onMounted(async () => {
await loadSearchSetting();
loadData(); loadData();
}); });
@@ -74,7 +90,7 @@ watch(
{ deep: true } { deep: true }
); );
const handleRemoveSearch = () => { const handleRemoveSearch = () => {
removeFromQuery('searchPhrase'); // Ensure 'searchPhrase' is the correct key removeFromQuery('query');
searchPhrase.value = ''; searchPhrase.value = '';
}; };
@@ -105,20 +121,27 @@ const onPageChange = (event) => {
} }
}; };
const loadData = () => { const loadData = async () => {
TransactionService.searchTransactions(route.query) try {
.then((response) => { if (searchMethod.value === null) {
console.log('response', response); await loadSearchSetting();
}
let response;
if (searchMethod.value === 'mongo') {
response = await TransactionService.searchTransactionsMongo(route.query);
} else {
response = await TransactionService.searchTransactions(route.query);
}
transactions.value = response.transactions || response; transactions.value = response.transactions || response;
totalCount.value = response.totalCount || response.length || 0; totalCount.value = response.totalCount || response.length || 0;
pagesCount.value = Math.ceil(totalCount.value / per_page.value); pagesCount.value = Math.ceil(totalCount.value / per_page.value);
}) } catch (error) {
.catch((error) => {
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 }); toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
console.error('Error loading data:', error); console.error('Error loading data:', error);
}); }
}; };
const loading = ref(false); const loading = ref(false);
const indexingFinished = ref(false); const indexingFinished = ref(false);
const indexingMessage = ref('در حال ایندکس...'); const indexingMessage = ref('در حال ایندکس...');
@@ -270,15 +293,27 @@ const toggle = (event, data) => {
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']" :globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
> >
<template #header> <template #header>
<div class=""> <div class="flex flex-wrap items-center justify-between gap-3">
<h4 class="m-0">مدیریت تراکنش‌ها</h4> <div class="flex items-center gap-3">
<div class="p-input-icon-right w-1/3 mt-2 lg:mt-0"> <h4 class="m-0 font-bold">مدیریت تراکنش‌ها</h4>
<InputText v-model="searchPhrase" placeholder="جستجو..." class="w-full" /> <Tag
v-if="searchMethod"
:value="searchMethod === 'mongo' ? 'جستجو: MongoDB' : 'جستجو: Typesense'"
:severity="searchMethod === 'mongo' ? 'success' : 'info'"
:icon="searchMethod === 'mongo' ? 'pi pi-database' : 'pi pi-bolt'"
class="cursor-pointer text-xs"
v-tooltip.bottom="'کلیک برای تغییر موتور جستجو در تنظیمات'"
@click="router.push('/settings/search-settings')"
/>
</div>
<div class="p-input-icon-right w-full sm:w-72 mt-2 lg:mt-0">
<InputText v-model="searchPhrase" placeholder="جستجو در تراکنش‌ها..." class="w-full" />
</div> </div>
</div> </div>
<Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" /> <Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
</template> </template>
<Column field="_id" header="شناسه" sortable style="width: 10%"></Column> <Column field="_id" header="شناسه" sortable style="width: 10%"></Column>
<Column field="belongingName" header="ملک" sortable style="width: 15%"></Column> <Column field="belongingName" header="ملک" sortable style="width: 15%"></Column>