feat(property): add save toasts, background autosave with bottom-right status popup, and configurable autosave interval in settings
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -136,5 +136,66 @@ export const SettingService = {
|
|||||||
console.error('Error seeding database:', err);
|
console.error('Error seeding database:', err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auto-save configuration
|
||||||
|
* @returns {Promise<{enabled: boolean, intervalSeconds: number}>}
|
||||||
|
*/
|
||||||
|
getAutoSaveSetting: async () => {
|
||||||
|
const config = {
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/settings/autosave',
|
||||||
|
withCredentials: true,
|
||||||
|
headers: SettingService._getAuthHeader()
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios(config);
|
||||||
|
if (response.data?.data) {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('bm-autosave-settings', JSON.stringify(response.data.data));
|
||||||
|
}
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Error fetching autosave setting from server, using local fallback:', err);
|
||||||
|
}
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const cached = localStorage.getItem('bm-autosave-settings');
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(cached);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { enabled: true, intervalSeconds: 60 };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update auto-save configuration
|
||||||
|
* @param {{enabled: boolean, intervalSeconds: number}} data
|
||||||
|
* @returns {Promise<{enabled: boolean, intervalSeconds: number}>}
|
||||||
|
*/
|
||||||
|
updateAutoSaveSetting: async (data) => {
|
||||||
|
const config = {
|
||||||
|
method: 'put',
|
||||||
|
url: '/api/settings/autosave',
|
||||||
|
withCredentials: true,
|
||||||
|
data,
|
||||||
|
headers: SettingService._getAuthHeader()
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await axios(config);
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('bm-autosave-settings', JSON.stringify(response.data.data));
|
||||||
|
}
|
||||||
|
return response.data.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating autosave setting:', err);
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('bm-autosave-settings', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeMount, ref, watch, computed, nextTick } from 'vue';
|
import { onBeforeMount, onBeforeUnmount, ref, watch, computed, nextTick } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { CategoryService } from '@/service/CategoryService';
|
import { CategoryService } from '@/service/CategoryService';
|
||||||
import { PropertyService } from '@/service/PropertyService';
|
import { PropertyService } from '@/service/PropertyService';
|
||||||
import { PropertyStatusService } from '@/service/PropertyStatusService';
|
import { PropertyStatusService } from '@/service/PropertyStatusService';
|
||||||
|
import { SettingService } from '@/service/SettingService';
|
||||||
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
||||||
import ImageGallery from '@/views/uikit/ImageGallery.vue';
|
import ImageGallery from '@/views/uikit/ImageGallery.vue';
|
||||||
import KiwiChips from '@/views/uikit/KiwiChips.vue';
|
import KiwiChips from '@/views/uikit/KiwiChips.vue';
|
||||||
@@ -23,6 +24,7 @@ import Button from 'primevue/button';
|
|||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -40,6 +42,14 @@ const editMode = ref(false);
|
|||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const pageLoading = ref(true);
|
const pageLoading = ref(true);
|
||||||
|
|
||||||
|
// Auto-Save State
|
||||||
|
const autoSaveEnabled = ref(true);
|
||||||
|
const autoSaveIntervalSeconds = ref(60);
|
||||||
|
const lastSavedTime = ref(null);
|
||||||
|
const isAutoSaving = ref(false);
|
||||||
|
const isDirty = ref(false);
|
||||||
|
let autoSaveTimer = null;
|
||||||
|
|
||||||
// Registration Date Fields (Jalali: Year, Month, Day)
|
// Registration Date Fields (Jalali: Year, Month, Day)
|
||||||
const regYear = ref('');
|
const regYear = ref('');
|
||||||
const regMonth = ref(null);
|
const regMonth = ref(null);
|
||||||
@@ -315,6 +325,25 @@ watch(() => property.value.propertyType, (newType) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for form modifications to mark dirty for auto-save
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => property.value,
|
||||||
|
() => featuresValues.value,
|
||||||
|
() => regYear.value,
|
||||||
|
() => regMonth.value,
|
||||||
|
() => regDay.value,
|
||||||
|
() => mainImageUrlsWithData.value,
|
||||||
|
() => documentUrlsWithData.value
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
if (!pageLoading.value) {
|
||||||
|
isDirty.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const initializeFeatureDefaultValues = () => {
|
const initializeFeatureDefaultValues = () => {
|
||||||
if (features.value && Array.isArray(features.value)) {
|
if (features.value && Array.isArray(features.value)) {
|
||||||
features.value.forEach((feat) => {
|
features.value.forEach((feat) => {
|
||||||
@@ -414,16 +443,80 @@ const getStatusSeverity = (st) => {
|
|||||||
return valid.includes(st.color) ? st.color : 'secondary';
|
return valid.includes(st.color) ? st.color : 'secondary';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initAutoSaveTimer = () => {
|
||||||
|
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
||||||
|
if (autoSaveEnabled.value && autoSaveIntervalSeconds.value > 0) {
|
||||||
|
autoSaveTimer = setInterval(performAutoSave, autoSaveIntervalSeconds.value * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performAutoSave = async () => {
|
||||||
|
if (!autoSaveEnabled.value || pageLoading.value || isSaving.value || isAutoSaving.value || !isDirty.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save in editMode
|
||||||
|
if (editMode.value && property.value._id) {
|
||||||
|
isAutoSaving.value = true;
|
||||||
|
try {
|
||||||
|
const payload = { ...property.value };
|
||||||
|
payload.name = computedPropertyName.value || payload.name || 'ملک بدون عنوان';
|
||||||
|
if (regYear.value && regMonth.value && regDay.value) {
|
||||||
|
const dObj = jalaliToDate(regYear.value, regMonth.value, regDay.value);
|
||||||
|
payload.registrationDate = dObj ? dObj.toISOString() : null;
|
||||||
|
}
|
||||||
|
payload.mainImageUrls = (mainImageUrlsWithData.value || [])
|
||||||
|
.map((x) => typeof x === 'string' ? x : (x.url || x.source || ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
payload.documentUrls = (documentUrlsWithData.value || [])
|
||||||
|
.map((x) => typeof x === 'string' ? x : (x.url || x.source || ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
payload.features = serializeFeaturesPayload(featuresValues.value);
|
||||||
|
|
||||||
|
const status = await PropertyService.updateProperty(property.value._id, payload);
|
||||||
|
if (status === 'success') {
|
||||||
|
isDirty.value = false;
|
||||||
|
lastSavedTime.value = new Date().toLocaleTimeString('fa-IR');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Auto-save background exception:', e);
|
||||||
|
} finally {
|
||||||
|
isAutoSaving.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Save local draft for new property
|
||||||
|
try {
|
||||||
|
const draft = {
|
||||||
|
property: property.value,
|
||||||
|
featuresValues: featuresValues.value,
|
||||||
|
regYear: regYear.value,
|
||||||
|
regMonth: regMonth.value,
|
||||||
|
regDay: regDay.value,
|
||||||
|
savedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
localStorage.setItem('bm_property_draft', JSON.stringify(draft));
|
||||||
|
isDirty.value = false;
|
||||||
|
lastSavedTime.value = new Date().toLocaleTimeString('fa-IR');
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
pageLoading.value = true;
|
pageLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const [fetchedCategories, fetchedStatuses] = await Promise.all([
|
const [fetchedCategories, fetchedStatuses, autoSettings] = await Promise.all([
|
||||||
CategoryService.getAllCategories(),
|
CategoryService.getAllCategories(),
|
||||||
PropertyStatusService.getAllStatuses()
|
PropertyStatusService.getAllStatuses(),
|
||||||
|
SettingService.getAutoSaveSetting().catch(() => ({ enabled: true, intervalSeconds: 60 }))
|
||||||
]);
|
]);
|
||||||
categories.value = fetchedCategories || [];
|
categories.value = fetchedCategories || [];
|
||||||
statuses.value = fetchedStatuses || [];
|
statuses.value = fetchedStatuses || [];
|
||||||
|
|
||||||
|
if (autoSettings) {
|
||||||
|
autoSaveEnabled.value = autoSettings.enabled !== undefined ? autoSettings.enabled : true;
|
||||||
|
autoSaveIntervalSeconds.value = Number(autoSettings.intervalSeconds) || 60;
|
||||||
|
}
|
||||||
|
|
||||||
const propId = route.params.propertyId || route.params.belongingId;
|
const propId = route.params.propertyId || route.params.belongingId;
|
||||||
if (propId) {
|
if (propId) {
|
||||||
editMode.value = true;
|
editMode.value = true;
|
||||||
@@ -510,14 +603,26 @@ onBeforeMount(async () => {
|
|||||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات ملک از سرور', life: 4000 });
|
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات ملک از سرور', life: 4000 });
|
||||||
} finally {
|
} finally {
|
||||||
pageLoading.value = false;
|
pageLoading.value = false;
|
||||||
|
isDirty.value = false;
|
||||||
|
initAutoSaveTimer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
||||||
|
if (geoCodeDebounceTimeout) clearTimeout(geoCodeDebounceTimeout);
|
||||||
|
});
|
||||||
|
|
||||||
const saveProperty = async () => {
|
const saveProperty = async () => {
|
||||||
property.value.name = computedPropertyName.value || 'ملک جدید';
|
property.value.name = computedPropertyName.value || 'ملک جدید';
|
||||||
|
|
||||||
if (!property.value.nationalGeoCode?.trim()) {
|
if (!property.value.nationalGeoCode?.trim()) {
|
||||||
toast.add({ severity: 'warn', summary: 'هشدار', detail: 'شناسه ملی جغرافیایی الزامی است.', life: 3000 });
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'هشدار اعتبارسنجی',
|
||||||
|
detail: 'ورود شناسه ملی جغرافیایی (کد جام) الزامی است.',
|
||||||
|
life: 4000
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,6 +634,13 @@ const saveProperty = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'در حال ذخیرهسازی',
|
||||||
|
detail: editMode.value ? 'در حال بروزرسانی اطلاعات ملک...' : 'در حال ثبت ملک جدید...',
|
||||||
|
life: 2000
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
property.value.mainImageUrls = (mainImageUrlsWithData.value || [])
|
property.value.mainImageUrls = (mainImageUrlsWithData.value || [])
|
||||||
.map((x) => {
|
.map((x) => {
|
||||||
@@ -552,6 +664,8 @@ const saveProperty = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
|
isDirty.value = false;
|
||||||
|
lastSavedTime.value = new Date().toLocaleTimeString('fa-IR');
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'موفقیتآمیز',
|
summary: 'موفقیتآمیز',
|
||||||
@@ -574,7 +688,10 @@ const saveProperty = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6 dir-rtl" style="direction: rtl;">
|
<div class="space-y-6 dir-rtl relative" style="direction: rtl;">
|
||||||
|
<!-- Toast Notification Container -->
|
||||||
|
<Toast></Toast>
|
||||||
|
|
||||||
<!-- Header Bar -->
|
<!-- Header Bar -->
|
||||||
<div class="card p-4 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
<div class="card p-4 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -1026,5 +1143,42 @@ const saveProperty = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Floating Auto-Save Status Popup (Bottom-Right) -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="autoSaveEnabled && (lastSavedTime || isAutoSaving)"
|
||||||
|
class="fixed bottom-6 right-6 z-50 flex items-center gap-3 px-4 py-2.5 rounded-2xl bg-surface-0/95 dark:bg-surface-900/95 backdrop-blur-md shadow-xl border border-surface-200 dark:border-surface-700 select-none text-xs text-surface-700 dark:text-surface-200 animate-fade-in"
|
||||||
|
style="box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1);"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="isAutoSaving" class="flex items-center text-primary gap-1.5 font-semibold">
|
||||||
|
<i class="pi pi-spin pi-sync text-sm"></i>
|
||||||
|
<span>در حال ذخیره خودکار...</span>
|
||||||
|
</span>
|
||||||
|
<span v-else class="flex items-center text-emerald-600 dark:text-emerald-400 gap-1.5 font-semibold">
|
||||||
|
<i class="pi pi-check-circle text-sm"></i>
|
||||||
|
<span>آخرین ذخیره: {{ lastSavedTime }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="w-px h-3.5 bg-surface-200 dark:bg-surface-700"></span>
|
||||||
|
<span class="text-[10px] text-surface-400 font-mono" :title="`بازه زمانی ذخیره خودکار: ${autoSaveIntervalSeconds} ثانیه`">
|
||||||
|
{{ autoSaveIntervalSeconds }}s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -18,15 +18,68 @@ const seedStatus = ref({ isSeeded: false, seededAt: null });
|
|||||||
const isSeeding = ref(false);
|
const isSeeding = ref(false);
|
||||||
const seedLoading = 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 = [
|
const methodOptions = [
|
||||||
{ label: 'Typesense', value: 'typesense', icon: 'pi pi-bolt', class: 'text-amber-500' },
|
{ label: 'Typesense', value: 'typesense', icon: 'pi pi-bolt', class: 'text-amber-500' },
|
||||||
{ label: 'MongoDB', value: 'mongo', icon: 'pi pi-database', class: 'text-emerald-500' }
|
{ label: 'MongoDB', value: 'mongo', icon: 'pi pi-database', class: 'text-emerald-500' }
|
||||||
];
|
];
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadSettings(), loadSeedStatus()]);
|
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 () => {
|
const loadSeedStatus = async () => {
|
||||||
seedLoading.value = true;
|
seedLoading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -276,6 +329,63 @@ const navigateToModule = (routePath) => {
|
|||||||
</div>
|
</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 ── -->
|
<!-- ── Quick Actions Toolbar ── -->
|
||||||
<div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-4">
|
<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">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user