diff --git a/src/views/pages/Property/CreateProperty.vue b/src/views/pages/Property/CreateProperty.vue index ca4ac4e..a832b8f 100644 --- a/src/views/pages/Property/CreateProperty.vue +++ b/src/views/pages/Property/CreateProperty.vue @@ -48,7 +48,15 @@ const autoSaveIntervalSeconds = ref(60); const lastSavedTime = ref(null); const isAutoSaving = ref(false); const isDirty = ref(false); -let autoSaveTimer = null; +const autoSaveFailed = ref(false); + +// Change-driven auto-save: instead of polling on a fixed interval regardless of +// activity, we debounce a save shortly after the user stops typing (cheap - only +// ever one pending timer, reset on each change) and fall back to a max-wait timer +// so long, uninterrupted editing sessions still get saved periodically. +const AUTOSAVE_DEBOUNCE_MS = 2500; +let autoSaveDebounceTimer = null; +let autoSaveMaxWaitTimer = null; // Registration Date Fields (Jalali: Year, Month, Day) const regYear = ref(''); @@ -157,6 +165,40 @@ const computedPropertyName = computed(() => { return ''; }); +// Auto-save indicator status: derived, no extra reactive bookkeeping needed +const autoSaveStatus = computed(() => { + if (isAutoSaving.value || isSaving.value) return 'saving'; + if (autoSaveFailed.value) return 'error'; + if (isDirty.value) return 'pending'; + if (lastSavedTime.value) return 'saved'; + return 'idle'; +}); + +const AUTOSAVE_STATUS_META = { + saving: { label: 'در حال ذخیره', icon: 'pi pi-spin pi-sync', dot: 'bg-primary-500', badge: 'bg-primary-50/95 dark:bg-primary-950/70 border-primary-200 dark:border-primary-800 text-primary-700 dark:text-primary-300' }, + pending: { label: 'تغییرات ذخیره‌نشده', icon: 'pi pi-circle-fill', dot: 'bg-amber-500 animate-pulse', badge: 'bg-amber-50/95 dark:bg-amber-950/70 border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300' }, + saved: { label: 'ذخیره شد', icon: 'pi pi-check-circle', dot: 'bg-emerald-500', badge: 'bg-surface-0/95 dark:bg-surface-900/95 border-surface-200 dark:border-surface-700 text-surface-600 dark:text-surface-300' }, + error: { label: 'خطا در ذخیره', icon: 'pi pi-exclamation-triangle', dot: 'bg-red-500', badge: 'bg-red-50/95 dark:bg-red-950/70 border-red-200 dark:border-red-800 text-red-600 dark:text-red-300' }, + idle: { label: 'ذخیره خودکار', icon: 'pi pi-circle', dot: 'bg-surface-400', badge: 'bg-surface-0/80 dark:bg-surface-900/80 border-surface-200 dark:border-surface-700 text-surface-400 dark:text-surface-500' } +}; + +const autoSaveMeta = computed(() => AUTOSAVE_STATUS_META[autoSaveStatus.value]); + +const autoSaveTooltip = computed(() => { + switch (autoSaveStatus.value) { + case 'saving': + return 'در حال ذخیره خودکار تغییرات...'; + case 'pending': + return 'تغییراتی ثبت شده که به‌زودی به‌صورت خودکار ذخیره می‌شود.'; + case 'error': + return 'ذخیره خودکار ناموفق بود. لطفاً به‌صورت دستی ذخیره کنید.'; + case 'saved': + return lastSavedTime.value ? `آخرین ذخیره خودکار: ${lastSavedTime.value}` : 'ذخیره شد'; + default: + return 'ذخیره خودکار فعال است'; + } +}); + // Dong options with fractional description const dongOptions = [ { value: 1, label: '۱ دانگ', fraction: '۱⁏۶' }, @@ -339,6 +381,7 @@ watch( () => { if (!pageLoading.value) { isDirty.value = true; + scheduleAutoSave(); } }, { deep: true } @@ -443,10 +486,31 @@ const getStatusSeverity = (st) => { 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 clearAutoSaveTimers = () => { + if (autoSaveDebounceTimer) { + clearTimeout(autoSaveDebounceTimer); + autoSaveDebounceTimer = null; + } + if (autoSaveMaxWaitTimer) { + clearTimeout(autoSaveMaxWaitTimer); + autoSaveMaxWaitTimer = null; + } +}; + +const triggerAutoSave = () => { + clearAutoSaveTimers(); + performAutoSave(); +}; + +const scheduleAutoSave = () => { + if (!autoSaveEnabled.value || pageLoading.value) return; + + if (autoSaveDebounceTimer) clearTimeout(autoSaveDebounceTimer); + autoSaveDebounceTimer = setTimeout(triggerAutoSave, AUTOSAVE_DEBOUNCE_MS); + + if (!autoSaveMaxWaitTimer) { + const maxWaitMs = Math.max(AUTOSAVE_DEBOUNCE_MS, (autoSaveIntervalSeconds.value || 60) * 1000); + autoSaveMaxWaitTimer = setTimeout(triggerAutoSave, maxWaitMs); } }; @@ -482,9 +546,13 @@ const performAutoSave = async () => { const status = await PropertyService.updateProperty(property.value._id, payload); if (status === 'success') { isDirty.value = false; + autoSaveFailed.value = false; lastSavedTime.value = new Date().toLocaleTimeString('fa-IR'); + } else { + autoSaveFailed.value = true; } } catch (e) { + autoSaveFailed.value = true; console.warn('Auto-save background exception:', e); } finally { isAutoSaving.value = false; @@ -502,8 +570,11 @@ const performAutoSave = async () => { }; localStorage.setItem('bm_property_draft', JSON.stringify(draft)); isDirty.value = false; + autoSaveFailed.value = false; lastSavedTime.value = new Date().toLocaleTimeString('fa-IR'); - } catch (e) {} + } catch (e) { + autoSaveFailed.value = true; + } } }; @@ -610,12 +681,11 @@ onBeforeMount(async () => { } finally { pageLoading.value = false; isDirty.value = false; - initAutoSaveTimer(); } }); onBeforeUnmount(() => { - if (autoSaveTimer) clearInterval(autoSaveTimer); + clearAutoSaveTimers(); if (geoCodeDebounceTimeout) clearTimeout(geoCodeDebounceTimeout); }); @@ -674,7 +744,9 @@ const saveProperty = async () => { if (status === 'success') { isDirty.value = false; + autoSaveFailed.value = false; lastSavedTime.value = new Date().toLocaleTimeString('fa-IR'); + clearAutoSaveTimers(); toast.add({ severity: 'success', summary: 'موفقیت‌آمیز', @@ -1153,27 +1225,16 @@ const saveProperty = async () => { - +
-
- - - در حال ذخیره خودکار... - - - - آخرین ذخیره: {{ lastSavedTime }} - -
- - - {{ autoSaveIntervalSeconds }}s - + + {{ autoSaveMeta.label }}