feat(settings): add database seeder section with one-time execution and lock mechanism
deploy-chabokan / deploy (push) Canceled after 0s

This commit is contained in:
2026-08-19 06:50:01 +03:30
parent 001ab07485
commit b81a85f672
2 changed files with 166 additions and 1 deletions
+40
View File
@@ -97,5 +97,45 @@ export const SettingService = {
console.error('Error bulk updating search settings:', err);
throw err;
}
},
/**
* Get database seeding status
* @returns {Promise<{isSeeded: boolean, seededAt: string|null, seededBy: string|null}>}
*/
getSeedStatus: async () => {
const config = {
method: 'get',
url: '/api/settings/seed-status',
withCredentials: true,
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error('Error fetching seed status:', err);
throw err;
}
},
/**
* Execute one-time database seeding
* @returns {Promise<Object>}
*/
seedDatabase: async () => {
const config = {
method: 'post',
url: '/api/settings/seed-database',
withCredentials: true,
headers: SettingService._getAuthHeader()
};
try {
const response = await axios(config);
return response.data.data;
} catch (err) {
console.error('Error seeding database:', err);
throw err;
}
}
};
+126 -1
View File
@@ -13,15 +13,72 @@ 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);
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();
await Promise.all([loadSettings(), loadSeedStatus()]);
});
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 {
@@ -151,6 +208,74 @@ const navigateToModule = (routePath) => {
</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>
<!-- ── 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">