fix(property): fix edit property loading, map event recursion, and category sync
deploy-chabokan / deploy (push) Canceled after 0s

This commit is contained in:
2026-08-19 07:13:32 +03:30
parent 3723f801e7
commit 15dc77a76b
2 changed files with 234 additions and 273 deletions
+160 -216
View File
@@ -22,6 +22,7 @@ import ToggleSwitch from 'primevue/toggleswitch';
import Button from 'primevue/button';
import Tag from 'primevue/tag';
import SelectButton from 'primevue/selectbutton';
import ProgressSpinner from 'primevue/progressspinner';
const toast = useToast();
const route = useRoute();
@@ -37,6 +38,7 @@ const mainImageUrlsWithData = ref([]);
const documentUrlsWithData = ref([]);
const editMode = ref(false);
const isSaving = ref(false);
const pageLoading = ref(true);
// Registration Date Fields (Jalali: Year, Month, Day)
const regYear = ref('');
@@ -130,8 +132,8 @@ const mapMarker = ref([]);
const easting = ref('');
const northing = ref('');
const zone = ref('');
const lat = ref();
const long = ref();
const lat = ref(null);
const long = ref(null);
// Auto-generated property name: ownerName + شماره اصلی/شماره فرعی
const computedPropertyName = computed(() => {
@@ -162,6 +164,11 @@ const setCoordinatesAndUTM = (latitude, longitude, utmE = null, utmN = null, utm
if (latitude !== null && longitude !== null && !isNaN(latitude) && !isNaN(longitude)) {
const numLat = Number(latitude);
const numLon = Number(longitude);
if (lat.value === numLat && long.value === numLon && mapMarker.value?.[0] === numLat && mapMarker.value?.[1] === numLon) {
return;
}
lat.value = numLat;
long.value = numLon;
mapMarker.value = [numLat, numLon];
@@ -182,12 +189,6 @@ const setCoordinatesAndUTM = (latitude, longitude, utmE = null, utmN = null, utm
zone.value = String(utm.zoneNumber);
}
}
nextTick(() => {
if (mapRef.value && typeof mapRef.value.setCenterAndMarker === 'function') {
mapRef.value.setCenterAndMarker(numLat, numLon);
}
});
}
};
@@ -249,20 +250,17 @@ let geoCodeDebounceTimeout = null;
* - First 6 digits → Easting (e.g. 538136)
* - Next 7 digits → Northing (e.g. 3950135)
* - Last 2 digits → UTM Zone (e.g. 39 for most of Iran)
* If the code doesn't match the 15-digit pattern, falls back to zone 39.
*/
const parseNationalGeoCode = (code) => {
if (!code) return null;
const digits = toEnglishDigits(String(code)).replace(/\D/g, ''); // strip non-digits
const digits = toEnglishDigits(String(code)).replace(/\D/g, '');
if (digits.length === 15) {
// Standard JAM: 6 easting + 7 northing + 2 zone
const e = parseInt(digits.slice(0, 6), 10);
const n = parseInt(digits.slice(6, 13), 10);
const z = parseInt(digits.slice(13, 15), 10);
return { easting: e, northing: n, zone: z };
}
if (digits.length === 13) {
// Some codes omit the zone (older format) — assume zone 39 for Iran
const e = parseInt(digits.slice(0, 6), 10);
const n = parseInt(digits.slice(6, 13), 10);
return { easting: e, northing: n, zone: 39 };
@@ -272,7 +270,6 @@ const parseNationalGeoCode = (code) => {
const applyNationalGeoCode = (val) => {
if (!val) return;
// 1. Try parsing as a JAM UTM code (15-digit or 13-digit)
const parsed = parseNationalGeoCode(val);
if (parsed) {
try {
@@ -285,7 +282,6 @@ const applyNationalGeoCode = (val) => {
console.error('UTM conversion failed:', err);
}
}
// 2. Fallback: treat as lat,lon text or geocode search via map
if (mapRef.value && typeof mapRef.value.searchLocation === 'function') {
mapRef.value.searchLocation(val);
}
@@ -297,36 +293,35 @@ const handleNationalGeoCodeChange = () => {
const digits = toEnglishDigits(val).replace(/\D/g, '');
// Fire immediately once a full 15-digit JAM code is complete — no waiting
if (digits.length === 15) {
if (geoCodeDebounceTimeout) clearTimeout(geoCodeDebounceTimeout);
applyNationalGeoCode(val);
return;
}
// For partial / non-standard input, debounce to avoid firing on every keystroke
if (geoCodeDebounceTimeout) clearTimeout(geoCodeDebounceTimeout);
geoCodeDebounceTimeout = setTimeout(() => applyNationalGeoCode(val), 600);
};
// Auto sync category when propertyType changes
watch(() => property.value.propertyType, (newType) => {
if (pageLoading.value || editMode.value) return;
if (!categories.value || categories.value.length === 0) return;
let targetCatName = newType === 'Land' ? 'Lot' : 'Building';
let matchedCategory = categories.value.find(c => c.englishName === targetCatName || c.nativeName === (newType === 'Land' ? 'زمین' : 'ساختمان'));
if (matchedCategory) {
if (matchedCategory && !property.value.category) {
property.value.category = matchedCategory._id;
handleSelectCategory(matchedCategory._id);
}
});
const initializeFeatureDefaultValues = () => {
featuresValues.value = {};
if (features.value && Array.isArray(features.value)) {
features.value.forEach((feat) => {
const definition = feat.definition || feat;
if (definition && definition._id) {
if (definition.type === 'boolean') {
if (featuresValues.value[definition._id] === undefined) {
if (definition.type === 'boolean' || definition.featureType === 'boolean') {
featuresValues.value[definition._id] = false;
} else if (definition.type === 'multiselect') {
featuresValues.value[definition._id] = [];
@@ -334,6 +329,7 @@ const initializeFeatureDefaultValues = () => {
featuresValues.value[definition._id] = null;
}
}
}
});
}
};
@@ -403,44 +399,23 @@ const fillImagesArray = (urlsArray) => {
};
const handleSelectCategory = (catId) => {
selectedCategory.value = categories.value.find((c) => c._id === catId);
selectedCategory.value = categories.value.find((c) => c._id === catId) || null;
if (selectedCategory.value) {
features.value = selectedCategory.value.features || [];
initializeFeatureDefaultValues();
} else {
features.value = [];
featuresValues.value = {};
}
};
const onMainImageUploaded = (res) => {
if (res && res.url) {
mainImageUrlsWithData.value.push({
name: res.originalName || 'image',
url: res.url,
progress: 100
});
}
};
const onDocumentUploaded = (res) => {
if (res && res.url) {
documentUrlsWithData.value.push({
name: res.originalName || 'document',
url: res.url,
progress: 100
});
}
};
const getStatusSeverity = (st) => {
if (!st) return 'secondary';
// Use the color field from the API; fall back to secondary
const valid = ['success', 'info', 'warn', 'danger', 'secondary', 'contrast'];
return valid.includes(st.color) ? st.color : 'secondary';
};
onBeforeMount(async () => {
pageLoading.value = true;
try {
const [fetchedCategories, fetchedStatuses] = await Promise.all([
CategoryService.getAllCategories(),
@@ -454,8 +429,29 @@ onBeforeMount(async () => {
editMode.value = true;
const data = await PropertyService.getProperty(propId);
if (data) {
property.value = { ...property.value, ...data };
const catId = typeof data.category === 'object' ? data.category?._id : data.category;
const statusId = typeof data.status === 'object' ? data.status?._id : data.status;
property.value = {
...property.value,
...data,
category: catId || null,
status: statusId || null,
dong: Number(data.dong) || 6,
area: data.area !== undefined && data.area !== null ? Number(data.area) : null
};
if (catId) {
selectedCategory.value = categories.value.find((x) => x._id === catId);
if (selectedCategory.value) {
features.value = selectedCategory.value.features || [];
}
}
if (data.features && Array.isArray(data.features)) {
featuresValues.value = extractFeaturesValues(data.features);
}
initializeFeatureDefaultValues();
// 1. Parse registrationDate into Jalali Year, Month, Day
if (data.registrationDate) {
@@ -499,14 +495,8 @@ onBeforeMount(async () => {
mainImageUrlsWithData.value = fillImagesArray(mainImages);
documentUrlsWithData.value = fillImagesArray(docs);
if (data.category) {
const catId = typeof data.category === 'object' ? data.category._id : data.category;
selectedCategory.value = categories.value.find((x) => x._id === catId);
if (selectedCategory.value) {
features.value = selectedCategory.value.features || [];
}
}
} else {
toast.add({ severity: 'error', summary: 'خطا', detail: 'ملک مورد نظر یافت نشد.', life: 4000 });
}
} else {
let matchedCat = categories.value.find(c => c.englishName === 'Lot' || c.nativeName === 'زمین');
@@ -517,11 +507,13 @@ onBeforeMount(async () => {
}
} catch (err) {
console.error('Initialization error:', err);
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات ملک از سرور', life: 4000 });
} finally {
pageLoading.value = false;
}
});
const saveProperty = async () => {
// Auto-set name from ownerName + mainNumber/subNumber
property.value.name = computedPropertyName.value || 'ملک جدید';
if (!property.value.nationalGeoCode?.trim()) {
@@ -529,7 +521,6 @@ const saveProperty = async () => {
return;
}
// Convert Jalali date to standard Date object
if (regYear.value && regMonth.value && regDay.value) {
const dObj = jalaliToDate(regYear.value, regMonth.value, regDay.value);
property.value.registrationDate = dObj ? dObj.toISOString() : null;
@@ -611,14 +602,21 @@ const saveProperty = async () => {
severity="primary"
rounded
:loading="isSaving"
:disabled="pageLoading"
@click="saveProperty"
class="px-5 font-semibold"
/>
</div>
</div>
<!-- Page Loading State -->
<div v-if="pageLoading" class="card p-12 flex flex-col items-center justify-center gap-4 bg-surface-0 dark:bg-surface-900 rounded-2xl border border-surface-200 dark:border-surface-800">
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="4" />
<span class="text-base font-medium text-surface-600 dark:text-surface-300">در حال دریافت و بارگذاری اطلاعات ملک از پایگاه داده...</span>
</div>
<!-- Main Layout Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<div v-else class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<!-- Main Content Area -->
<div class="lg:col-span-8 space-y-6 w-full">
<!-- 1. General Identification & Registry Card (شناسه و اداره ثبت) -->
@@ -642,101 +640,96 @@ const saveProperty = async () => {
/>
</div>
<!-- شناسه ملی جغرافیایی (جام) -->
<!-- 2. شناسه ملی جغرافیایی (JAM) -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">
شناسه ملی جغرافیایی (جام) <span class="text-red-500">*</span>
<div class="flex items-center justify-between mb-1.5">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200">
شناسه ملی جغرافیایی (کد جام) <span class="text-red-500">*</span>
</label>
<div class="flex gap-2">
<span class="text-xs text-primary font-medium">الزامی</span>
</div>
<div class="relative">
<InputText
v-model="property.nationalGeoCode"
placeholder="کد ۱۵ رقمی جام (UTM)"
class="w-full !rounded-xl"
fluid
@input="handleNationalGeoCodeChange"
@change="handleNationalGeoCodeChange"
@keyup.enter="handleNationalGeoCodeChange"
/>
<Button
icon="pi pi-map-marker"
severity="secondary"
outlined
rounded
title="یافتن نقطه روی نقشه"
@click="handleNationalGeoCodeChange"
placeholder="مثلاً: ۵۳۸۱۳۶۳۹۵۰۱۳۵۳۹ (۱۵ رقم)"
class="w-full !rounded-xl pl-9"
fluid
/>
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-surface-400">
<i class="pi pi-map-marker text-sm"></i>
</span>
</div>
<small class="text-xs text-surface-500 mt-1 block">
شامل ۶ رقم طول + ۷ رقم عرض + ۲ رقم زون UTM
</small>
</div>
<!-- 2. شماره سریال -->
<!-- 3. شماره سریال سند -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره سریال سند</label>
<InputText v-model="property.serialNumber" placeholder="مثلاً: الف/۹۹ ۱۲۳۴۵۶" class="w-full !rounded-xl" fluid />
</div>
<!-- 3. اداره کل استان -->
<!-- 4. واحد ثبتی استان -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">اداره کل استان</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">واحد ثبتی استان</label>
<InputText v-model="property.provinceRegistry" placeholder="مثلاً: خوزستان" class="w-full !rounded-xl" fluid />
</div>
<!-- 4. اداره ثبت اسناد و املاک -->
<!-- 5. حوزه ثبتی محل -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">اداره ثبت اسناد و املاک</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">حوزه ثبتی محل</label>
<InputText v-model="property.localRegistryOffice" placeholder="مثلاً: کرج - ناحیه ۱" class="w-full !rounded-xl" fluid />
</div>
<!-- 5. تاریخ ثبت سند -->
<!-- 6. تاریخ ثبت سند (شمسی) -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">
تاریخ ثبت سند
</label>
<div class="grid grid-cols-12 gap-2 items-center">
<!-- روز -->
<div class="col-span-3">
<div class="grid grid-cols-3 gap-2">
<InputText
v-model="regDay"
placeholder="روز"
class="w-full !rounded-xl text-center"
placeholder="روز (۰۱)"
maxlength="2"
fluid
class="!rounded-xl text-center text-xs"
/>
</div>
<!-- ماه -->
<div class="col-span-5">
<Dropdown
v-model="regMonth"
:options="persianMonths"
optionLabel="label"
optionValue="value"
placeholder="انتخاب ماه"
class="w-full !rounded-xl"
showClear
fluid
optionLabel="name"
optionValue="id"
placeholder="ماه"
class="!rounded-xl text-xs"
/>
</div>
<!-- سال -->
<div class="col-span-4">
<InputText
v-model="regYear"
placeholder="سال (مثلاً ۱۴۰۲)"
class="w-full !rounded-xl text-center"
placeholder="سال (۱۴۰۲)"
maxlength="4"
fluid
class="!rounded-xl text-center text-xs"
/>
</div>
</div>
</div>
</div>
<!-- Auto Name Preview -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">
نام ملک
<span class="text-xs text-surface-400 font-normal mr-1">(خودکار تولید می‌شود)</span>
<!-- 2. Basic Info & Category Card -->
<div class="card p-4 sm:p-6 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl space-y-5">
<div class="flex items-center gap-2 text-primary font-bold text-lg border-b border-surface-100 dark:border-surface-800 pb-3">
<i class="pi pi-tag text-xl"></i>
<span>اطلاعات پایه و دسته‌بندی</span>
</div>
<div class="space-y-4">
<!-- Auto-generated Display Name Preview -->
<div class="p-3 bg-surface-50 dark:bg-surface-800/50 rounded-xl border border-surface-200 dark:border-surface-700">
<label class="block text-xs font-semibold text-surface-500 dark:text-surface-400 mb-1">
نام ثبت‌شده ملک (تولید خودکار بر اساس مالک و شماره ثبتی)
</label>
<div class="flex items-center gap-2 px-3 py-2.5 rounded-xl bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 min-h-[42px]">
<i class="pi pi-tag text-primary text-sm"></i>
<span v-if="computedPropertyName" class="text-sm font-semibold text-surface-800 dark:text-surface-100">
<div class="flex items-center gap-2">
<i class="pi pi-building text-primary text-sm"></i>
<span v-if="computedPropertyName" class="text-sm font-bold text-surface-900 dark:text-surface-100 font-mono">
{{ computedPropertyName }}
</span>
<span v-else class="text-sm text-surface-400 italic">
@@ -746,6 +739,7 @@ const saveProperty = async () => {
</div>
<!-- Category Selector & Status -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">دسته‌بندی اصلی</label>
<Dropdown
@@ -781,212 +775,168 @@ const saveProperty = async () => {
</Dropdown>
</div>
</div>
<!-- Description & Tags -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">توضیحات و یادداشت</label>
<Textarea v-model="property.description" placeholder="شرح کامل، موقعیت جغرافیایی، وضعیت حقوقی یا نکات خاص..." rows="3" class="w-full !rounded-xl" fluid />
</div>
<!-- 2. Owner Specifications Card (مشخصات مالک) -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">برچسب‌ها (Tags)</label>
<KiwiChips v-model="property.tags" placeholder="تایپ کنید و Enter بزنید..." />
</div>
</div>
</div>
<!-- 3. Section 1: مشخصات مالک -->
<div class="card p-4 sm:p-6 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl space-y-5">
<div class="flex items-center gap-2 text-primary font-bold text-lg border-b border-surface-100 dark:border-surface-800 pb-3">
<i class="pi pi-user text-xl"></i>
<span>مشخصات مالک</span>
<span>۱. مشخصات مالک</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- 1. نام و نام خانوادگی مالک -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نام و نام خانوادگی مالک</label>
<InputText v-model="property.ownerName" placeholder="نام و نام خانوادگی" class="w-full !rounded-xl" fluid />
</div>
<!-- 2. نام پدر مالک -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نام پدر مالک</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نام پدر</label>
<InputText v-model="property.fatherName" placeholder="نام پدر" class="w-full !rounded-xl" fluid />
</div>
<!-- 3. شماره شناسنامه مالک -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره شناسنامه مالک</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره شناسنامه</label>
<InputText v-model="property.idCardNumber" placeholder="شماره شناسنامه" class="w-full !rounded-xl" fluid />
</div>
<!-- 4. شماره ملی مالک یا شناسه ملی شرکت -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره ملی مالک یا شناسه ملی شرکت</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">کد ملی / شناسه ملی حقوقی</label>
<InputText v-model="property.nationalId" placeholder="شماره ملی (۱۰ رقم) / شناسه شرکت (۱۱ رقم)" class="w-full !rounded-xl" fluid />
</div>
<!-- 5. محل صدور شناسنامه مالک یا محل ثبت شرکت -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">محل صدور شناسنامه / محل ثبت شرکت</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">محل صدور / محل ثبت</label>
<InputText v-model="property.issuePlace" placeholder="محل صدور یا ثبت" class="w-full !rounded-xl" fluid />
</div>
<!-- 6. تاریخ تولد مالک یا تاریخ ثبت شرکت -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">تاریخ تولد مالک / تاریخ ثبت شرکت</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">تاریخ تولد / تاسیس</label>
<InputText v-model="property.birthDate" placeholder="مثلاً: ۱۳۶۵/۰۵/۱۲" class="w-full !rounded-xl" fluid />
</div>
<!-- 7. تابعیت -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">تابعیت</label>
<InputText v-model="property.nationality" placeholder="مثلاً: ایران" class="w-full !rounded-xl" fluid />
</div>
<!-- 8. میزان مالکیت (دانگ) -->
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-2">
میزان مالکیت (دانگ)
<span class="text-xs text-surface-400 font-normal mr-2">
(هر ملک از ۶ دانگ تشکیل شده)
</span>
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">
میزان سهم مالکیت (دانگ)
</label>
<div class="flex gap-2 flex-wrap items-center">
<button
v-for="opt in dongOptions"
:key="opt.value"
type="button"
@click="property.dong = opt.value"
class="flex flex-col items-center justify-center px-3 py-1.5 rounded-xl border transition-all cursor-pointer min-w-[64px] text-xs"
:class="property.dong === opt.value
? 'bg-primary text-primary-contrast border-primary shadow-md shadow-primary/25 font-bold scale-[1.02]'
: 'bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-surface-200 border-surface-200 dark:border-surface-700 hover:border-primary/50 hover:bg-surface-50 dark:hover:bg-surface-800'"
<Dropdown
v-model="property.dong"
:options="dongOptions"
optionLabel="label"
optionValue="value"
placeholder="انتخاب دانگ"
class="w-full !rounded-xl"
fluid
>
<span class="text-xs font-bold leading-tight">{{ opt.label }}</span>
<span class="text-[10px] opacity-70 leading-tight mt-0.5">{{ opt.fraction }}</span>
</button>
<template #option="slotProps">
<div class="flex items-center justify-between w-full">
<span class="font-semibold">{{ slotProps.option.label }}</span>
<span class="text-xs text-surface-400">({{ slotProps.option.fraction }})</span>
</div>
</template>
</Dropdown>
</div>
<!-- 9. مستندات مالکیت -->
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مستندات مالکیت</label>
<div class="sm:col-span-2 lg:col-span-3">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مستند مالکیت و سابقه انتقالات</label>
<Textarea v-model="property.ownershipDocs" placeholder="شرح مستندات، شماره بنچاق، مبایعه‌نامه و انتقال..." rows="2" class="w-full !rounded-xl" fluid />
</div>
</div>
</div>
<!-- 3. Property Specifications Card (مشخصات ملک) -->
<!-- 4. Section 2: مشخصات ملک -->
<div class="card p-4 sm:p-6 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl space-y-5">
<div class="flex items-center gap-2 text-primary font-bold text-lg border-b border-surface-100 dark:border-surface-800 pb-3">
<i class="pi pi-building text-xl"></i>
<span>مشخصات ملک</span>
<i class="pi pi-home text-xl"></i>
<span>۲. مشخصات ملک و حدود ثبتی</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- 1. شماره فرعی -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره فرعی</label>
<InputText v-model="property.subNumber" placeholder="شماره فرعی ثبت" class="w-full !rounded-xl" fluid />
</div>
<!-- 2. شماره اصلی -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره اصلی</label>
<InputText v-model="property.mainNumber" placeholder="شماره اصلی ثبت" class="w-full !rounded-xl" fluid />
</div>
<!-- 3. مفروز و مجزی از -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مفروز و مجزی از</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مفروز و مجزی شده از</label>
<InputText v-model="property.separatedFrom" placeholder="شماره پلاک یا بخش قبلی" class="w-full !rounded-xl" fluid />
</div>
<!-- 4. قطعه -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">قطعه</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره قطعه</label>
<InputText v-model="property.plotNumber" placeholder="شماره قطعه" class="w-full !rounded-xl" fluid />
</div>
<!-- 5. بخش ثبتی -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">بخش ثبتی</label>
<InputText v-model="property.registrationSection" placeholder="مثلاً: بخش ۱ اهواز" class="w-full !rounded-xl" fluid />
</div>
<!-- 6. ناحیه -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">ناحیه</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">ناحیه ثبتی</label>
<InputText v-model="property.district" placeholder="مثلاً: ناحیه ۲" class="w-full !rounded-xl" fluid />
</div>
<!-- 7. مساحت به متر مربع -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مساحت به متر مربع</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">مساحت عرصه (متر مربع)</label>
<InputNumber v-model="property.area" placeholder="مثلاً: ۳۵۰.۵" class="w-full !rounded-xl" fluid />
</div>
<!-- 8. بلوک -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">بلوک</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">شماره بلوک</label>
<InputText v-model="property.blockNumber" placeholder="شماره بلوک" class="w-full !rounded-xl" fluid />
</div>
<!-- 9. سمت -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">سمت</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">سمت ملک</label>
<Dropdown v-model="property.sideDirection" :options="sideDirectionOptions" placeholder="انتخاب سمت" class="w-full !rounded-xl" fluid showClear />
</div>
<!-- 10. طبقه -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">طبقه</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">طبقه / موقعیت استقرار</label>
<InputText v-model="property.floor" placeholder="مثلاً: طبقه اول / همکف" class="w-full !rounded-xl" fluid />
</div>
<!-- 11. نوع ملک -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">
نوع ملک <span class="text-red-500">*</span>
</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نوع ملک</label>
<SelectButton v-model="property.propertyType" :options="propertyTypes" optionLabel="label" optionValue="value" class="w-full" />
</div>
<!-- 12. وضعیت خاص -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">وضعیت خاص</label>
<Dropdown v-model="property.specialStatus" :options="specialStatusOptions" placeholder="انتخاب وضعیت خاص" class="w-full !rounded-xl" fluid />
</div>
<!-- 13. کاربری -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">کاربری</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">کاربری ثبتی</label>
<Dropdown v-model="property.zoningType" :options="zoningOptions" placeholder="انتخاب کاربری" class="w-full !rounded-xl" fluid />
</div>
<!-- 14. نوع سند -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نوع سند</label>
<Dropdown v-model="property.documentType" :options="documentTypeOptions" placeholder="انتخاب نوع سند" class="w-full !rounded-xl" fluid />
</div>
<!-- 15. نشانی ملک -->
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نشانی ملک</label>
<div class="sm:col-span-2 lg:col-span-3">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">نشانی پستی دقیق ملک</label>
<Textarea v-model="property.address" placeholder="نشانی کامل و دقیق ملک..." rows="2" class="w-full !rounded-xl" fluid />
</div>
<!-- 16. کدپستی -->
<div>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">کدپستی ۱۰ رقمی</label>
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">کد پستی ۱۰ رقمی</label>
<InputText v-model="property.postalCode" placeholder="کدپستی ملک" class="w-full !rounded-xl" fluid />
</div>
<!-- 17. حدود مفروزات -->
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">حدود مفروزات</label>
<div class="sm:col-span-2 lg:col-span-3">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">حدود و مجاورین ثبتی</label>
<Textarea v-model="property.propertyBoundaries" placeholder="شرح تفصیلی حدود مفروزات و مجاورین (شمال، جنوب، شرق، غرب)..." rows="3" class="w-full !rounded-xl" fluid />
</div>
<!-- 18. محدودیت ها و نقل انتقالات -->
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">محدودیت‌ها و نقل و انتقالات</label>
<div class="sm:col-span-2 lg:col-span-3">
<label class="block text-sm font-medium text-surface-700 dark:text-surface-200 mb-1.5">حقوق ارتفاقی و محدودیت‌ها</label>
<Textarea v-model="property.restrictions" placeholder="شرح حقوق ارتفاقی، بازداشت، رهن و نقل و انتقالات ثبت‌شده..." rows="3" class="w-full !rounded-xl" fluid />
</div>
</div>
</div>
<!-- 5. Category Dynamic Features Card (if present) -->
<!-- 5. Dynamic Features Card -->
<div v-if="features && features.length > 0" class="card p-4 sm:p-6 bg-surface-0 dark:bg-surface-900 shadow-sm border border-surface-200 dark:border-surface-800 rounded-2xl space-y-5">
<div class="flex items-center gap-2 text-primary font-bold text-lg border-b border-surface-100 dark:border-surface-800 pb-3">
<i class="pi pi-cog text-xl"></i>
@@ -1078,9 +1028,3 @@ const saveProperty = async () => {
</div>
</div>
</template>
<style scoped>
.dir-rtl {
direction: rtl;
}
</style>
+23 -6
View File
@@ -133,7 +133,7 @@
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { ref, watch, nextTick, onBeforeUnmount } from 'vue';
import 'leaflet/dist/leaflet.css';
import { LMap, LTileLayer, LMarker, LPopup, LControlLayers, LControlScale } from '@vue-leaflet/vue-leaflet';
import axios from 'axios';
@@ -152,7 +152,7 @@ const emit = defineEmits(['update:modelValue', 'marker-updated', 'cleared']);
const zoom = ref(12);
const center = ref([31.3183, 48.6706]);
const attribution = ref('© OpenStreetMap contributors');
const marker = ref([...props.modelValue]);
const marker = ref([...(props.modelValue || [])]);
const map = ref(null);
const mapContainer = ref(null);
@@ -165,7 +165,12 @@ const isFullscreen = ref(false);
// ── Core: set marker + flyTo ───────────────────────────────────────────────
const setMarker = (latVal, lonVal) => {
if (latVal !== null && lonVal !== null && !isNaN(latVal) && !isNaN(lonVal)) {
const newCoords = [parseFloat(latVal), parseFloat(lonVal)];
const numLat = parseFloat(latVal);
const numLon = parseFloat(lonVal);
if (marker.value && marker.value.length === 2 && marker.value[0] === numLat && marker.value[1] === numLon) {
return;
}
const newCoords = [numLat, numLon];
marker.value = newCoords;
center.value = newCoords;
zoom.value = 16;
@@ -200,10 +205,19 @@ const toggleFullscreen = () => {
}
};
document.addEventListener('fullscreenchange', () => {
const onFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement;
// Invalidate map size so Leaflet redraws correctly after fullscreen
nextTick(() => map.value?.leafletObject?.invalidateSize());
};
if (typeof document !== 'undefined') {
document.addEventListener('fullscreenchange', onFullscreenChange);
}
onBeforeUnmount(() => {
if (typeof document !== 'undefined') {
document.removeEventListener('fullscreenchange', onFullscreenChange);
}
});
// ── Locate Me ─────────────────────────────────────────────────────────────
@@ -293,8 +307,11 @@ const onMapReady = () => {
watch(
() => props.modelValue,
(val) => {
if (Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
if (Array.isArray(val) && val.length === 2 && val[0] !== undefined && val[1] !== undefined && !isNaN(val[0]) && !isNaN(val[1])) {
const newCoords = [parseFloat(val[0]), parseFloat(val[1])];
if (marker.value && marker.value.length === 2 && marker.value[0] === newCoords[0] && marker.value[1] === newCoords[1]) {
return;
}
marker.value = newCoords;
center.value = newCoords;
zoom.value = 16;