Files
mbmanager-dashboard/src/views/pages/Settings/SearchSettings.vue
T
2026-08-30 11:25:31 +03:30

454 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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([]);
// Seeder State
const seedStatus = ref({ isSeeded: false, seededAt: null });
const isSeeding = ref(false);
const seedLoading = ref(false);
// Auto-Save Settings State
const autoSaveSettings = ref({ enabled: true, intervalSeconds: 60 });
const autoSaveLoading = ref(false);
const autoSaveSaving = ref(false);
const autoSaveIntervalOptions = [
{ label: '۳۰ ثانیه (سریع)', value: 30 },
{ label: '۱ دقیقه (پیش‌فرض)', value: 60 },
{ label: '۲ دقیقه', value: 120 },
{ label: '۳ دقیقه', value: 180 },
{ label: '۵ دقیقه', value: 300 },
{ label: '۱۰ دقیقه', value: 600 }
];
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 Promise.all([loadSettings(), loadSeedStatus(), loadAutoSaveSettings()]);
});
const loadAutoSaveSettings = async () => {
autoSaveLoading.value = true;
try {
const data = await SettingService.getAutoSaveSetting();
if (data) {
autoSaveSettings.value = {
enabled: data.enabled !== undefined ? data.enabled : true,
intervalSeconds: Number(data.intervalSeconds) || 60
};
}
} catch (e) {
console.warn('Failed to load autosave settings:', e);
} finally {
autoSaveLoading.value = false;
}
};
const saveAutoSaveSettings = async () => {
autoSaveSaving.value = true;
try {
await SettingService.updateAutoSaveSetting(autoSaveSettings.value);
toast.add({
severity: 'success',
summary: 'موفقیت‌آمیز',
detail: 'تنظیمات ذخیره خودکار با موفقیت ذخیره شد.',
life: 3000
});
} catch (e) {
toast.add({
severity: 'error',
summary: 'خطا',
detail: 'خطا در ذخیره تنظیمات ذخیره خودکار',
life: 3000
});
} finally {
autoSaveSaving.value = false;
}
};
const loadSeedStatus = async () => {
seedLoading.value = true;
try {
const data = await SettingService.getSeedStatus();
seedStatus.value = data || { isSeeded: false, seededAt: null };
} catch (e) {
console.warn('Failed to load seed status:', e);
} finally {
seedLoading.value = false;
}
};
const handleSeedDatabase = () => {
if (seedStatus.value.isSeeded) return;
confirm.require({
message:
'توجه: این عملیات داده‌های اولیه سیستم (کاربران پیش‌فرض، نقش‌ها، دسته‌بندی‌ها، ویژگی‌های تکمیلی و وضعیت‌های املاک) را در پایگاه داده ایجاد می‌کند. این عملیات فقط یک‌بار قابل اجراست و پس از انجام دکمه برای همیشه قفل خواهد شد. آیا ادامه می‌دهید؟',
header: 'تایید مقداردهی اولیه پایگاه داده (Seed Database)',
icon: 'pi pi-exclamation-triangle',
rejectProps: { label: 'انصراف', severity: 'secondary', outlined: true },
acceptProps: { label: 'تایید و شروع مقداردهی', severity: 'primary' },
accept: async () => {
isSeeding.value = true;
try {
const res = await SettingService.seedDatabase();
seedStatus.value = {
isSeeded: true,
seededAt: res.seededAt || new Date()
};
toast.add({
severity: 'success',
summary: 'موفقیت‌آمیز',
detail: res.message || 'پایگاه داده با موفقیت مقداردهی شد و قفل گردید.',
life: 5000
});
await loadSettings();
} catch (err) {
const errMsg = err?.response?.data?.message || err.message || 'خطا در مقداردهی پایگاه داده';
toast.add({
severity: 'error',
summary: 'خطا در مقداردهی',
detail: errMsg,
life: 4000
});
await loadSeedStatus();
} finally {
isSeeding.value = false;
}
}
});
};
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>
<!-- ── Database Seeder Section (One-Time Execution) ── -->
<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 lg:flex-row lg:items-center justify-between gap-6">
<div class="flex items-start gap-4">
<div
class="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 transition-colors"
:class="seedStatus.isSeeded ? 'bg-surface-100 text-surface-500 dark:bg-surface-800 dark:text-surface-400' : 'bg-indigo-100 text-indigo-600 dark:bg-indigo-950 dark:text-indigo-400 shadow-sm'"
>
<i :class="seedStatus.isSeeded ? 'pi pi-lock' : 'pi pi-database'"></i>
</div>
<div>
<div class="flex items-center gap-2.5 flex-wrap">
<h3 class="text-xl font-bold m-0 text-surface-900 dark:text-surface-0">مقداردهی اولیه پایگاه داده (Seed Database)</h3>
<Tag v-if="seedStatus.isSeeded" value="مقداردهی شده و قفل است" severity="success" icon="pi pi-lock" rounded class="px-2.5 text-xs font-bold" />
<Tag v-else value="آماده مقداردهی (یک‌بار مصرف)" severity="warn" icon="pi pi-exclamation-circle" rounded class="px-2.5 text-xs font-bold" />
</div>
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1.5 text-sm leading-relaxed max-w-2xl">
ایجاد خودکار کاربران اولیه، نقش‌ها و سطوح دسترسی، دسته‌بندی‌های املاک، ویژگی‌های تکمیلی و وضعیت‌های املاک در دیتابیس. این عملیات صرفاً یک‌بار قابل اجراست و پس از انجام دکمه برای همیشه قفل می‌شود.
</p>
<div v-if="seedStatus.isSeeded && seedStatus.seededAt" class="mt-2 text-xs text-surface-400 flex items-center gap-1.5 font-mono">
<i class="pi pi-calendar text-xs"></i>
<span>تاریخ اجرا: {{ new Date(seedStatus.seededAt).toLocaleString('fa-IR') }}</span>
</div>
</div>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<Button v-if="!seedStatus.isSeeded" label="اجرای مقداردهی اولیه داده‌ها" icon="pi pi-database" severity="primary" rounded :loading="isSeeding" @click="handleSeedDatabase" class="px-5 font-semibold" />
<Button v-else label="پایگاه داده مقداردهی شده است" icon="pi pi-lock" severity="secondary" rounded disabled class="px-5 font-semibold opacity-70 cursor-not-allowed" />
</div>
</div>
</div>
<!-- ── Auto-Save Settings Card ── -->
<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 lg:flex-row lg:items-center justify-between gap-6">
<div class="flex items-start gap-4">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 bg-emerald-100 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400 shadow-sm">
<i class="pi pi-save"></i>
</div>
<div>
<div class="flex items-center gap-2.5 flex-wrap">
<h3 class="text-xl font-bold m-0 text-surface-900 dark:text-surface-0">تنظیمات ذخیره خودکار (Auto-Save)</h3>
<Tag :value="autoSaveSettings.enabled ? 'فعال' : 'غیرفعال'" :severity="autoSaveSettings.enabled ? 'success' : 'secondary'" rounded class="px-2.5 text-xs font-bold" />
</div>
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1.5 text-sm leading-relaxed max-w-2xl">
پیکربندی ذخیره پس‌زمینه خودکار فرم‌ها و املاک. در صورت فعال بودن، تغییرات شما بدون نیاز به فشردن دستی دکمه ذخیره، در بازه‌های مشخص ذخیره شده و اعلان آن در گوشه صفحه نمایش داده می‌شود.
</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-4 flex-shrink-0">
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-surface-600 dark:text-surface-300">فعال بودن:</span>
<ToggleSwitch v-model="autoSaveSettings.enabled" />
</div>
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-surface-600 dark:text-surface-300">بازه زمانی:</span>
<Dropdown v-model="autoSaveSettings.intervalSeconds" :options="autoSaveIntervalOptions" optionLabel="label" optionValue="value" :disabled="!autoSaveSettings.enabled" class="!rounded-xl text-xs w-44" />
</div>
<Button label="ذخیره تنظیمات" icon="pi pi-check" severity="primary" rounded size="small" :loading="autoSaveSaving" @click="saveAutoSaveSettings" class="px-4 font-semibold" />
</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>