diff --git a/components/property/propertyModel.js b/components/property/propertyModel.js index 1593196..c9134d6 100644 --- a/components/property/propertyModel.js +++ b/components/property/propertyModel.js @@ -57,7 +57,7 @@ const propertySchema = new mongoose.Schema( serialNumber: String, // شماره سریال چاپی سند provinceRegistry: String, localRegistryOffice: String, - registrationDate: String, // تاریخ ثبت سند + registrationDate: Date, // تاریخ ثبت سند (MongoDB Date object) // Owner Specifications (مشخصات مالک) ownerName: String, diff --git a/components/property/propertyService.js b/components/property/propertyService.js index 2a7207b..9327563 100644 --- a/components/property/propertyService.js +++ b/components/property/propertyService.js @@ -5,6 +5,7 @@ const AppError = require('../../globalErrorHandler/AppError') const { client } = require('../../setup/typesense') const TsApiFeatures = require('../../utils/TsApiFeatures') const { moveFromTempToMain, getSignedUrl } = require('../file/fileService') +const { normalizeToDate } = require('../../utils/jalali') const getAllProperties = async () => { return Property.find({}, null, null).populate('category').populate('status') @@ -18,6 +19,10 @@ const createProperty = async (data) => { data.documentUrls = await moveFromTempToMain(data.documentUrls, 'documents') } + if (data.registrationDate !== undefined) { + data.registrationDate = normalizeToDate(data.registrationDate) + } + if (data.propertyType === 'Land' || data.propertyType === 'زمین') { return Land.create(data) } else if (data.propertyType === 'Building' || data.propertyType === 'ساختمان') { @@ -52,6 +57,10 @@ const updateProperty = async (propertyId, data) => { data.documentUrls = await moveFromTempToMain(data.documentUrls, 'documents') } + if (data.registrationDate !== undefined) { + data.registrationDate = normalizeToDate(data.registrationDate) + } + const property = await Property.findOneAndUpdate({ _id: propertyId }, data, { new: true }) if (!property) { @@ -180,6 +189,7 @@ const searchPropertiesMongo = async (inputQuery = {}) => { registrationPlate: prop.registrationPlate || '', uniqueId: prop.uniqueId || '', serialNumber: prop.serialNumber || '', + registrationDate: prop.registrationDate, coordinates: prop.location?.coordinates || [0, 0], location: prop.location, createdAt: Math.floor(new Date(prop.createdAt || Date.now()).getTime() / 1000), diff --git a/utils/jalali.js b/utils/jalali.js new file mode 100644 index 0000000..baf3986 --- /dev/null +++ b/utils/jalali.js @@ -0,0 +1,174 @@ +/** + * Jalali (Solar Hijri / Shamsi) calendar conversion utilities (Backend) + */ + +const persianMonths = [ + { value: 1, label: 'فروردین' }, + { value: 2, label: 'اردیبهشت' }, + { value: 3, label: 'خرداد' }, + { value: 4, label: 'تیر' }, + { value: 5, label: 'مرداد' }, + { value: 6, label: 'شهریور' }, + { value: 7, label: 'مهر' }, + { value: 8, label: 'آبان' }, + { value: 9, label: 'آذر' }, + { value: 10, label: 'دی' }, + { value: 11, label: 'بهمن' }, + { value: 12, label: 'اسفند' } +] + +function toEnglishDigits(str) { + if (str === null || str === undefined) return '' + return String(str) + .replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d)) + .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d)) +} + +function jalaliToGregorian(jy, jm, jd) { + jy = Number(jy) + jm = Number(jm) + jd = Number(jd) + + let gy = jy > 979 ? 1600 : 621 + jy = jy > 979 ? jy - 979 : jy + + let days = + 365 * jy + + Math.floor(jy / 33) * 8 + + Math.floor(((jy % 33) + 3) / 4) + + 78 + + jd + + (jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186) + + gy += 400 * Math.floor(days / 146097) + days %= 146097 + + if (days > 36524) { + gy += 100 * Math.floor(--days / 36524) + days %= 36524 + if (days >= 365) days++ + } + + gy += 4 * Math.floor(days / 1461) + days %= 1461 + + if (days > 365) { + gy += Math.floor((days - 1) / 365) + days = (days - 1) % 365 + } + + let gd = days + 1 + const isLeap = (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 + const sal_a = [0, 31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + let gm + for (gm = 0; gm < 13; gm++) { + const v = sal_a[gm] + if (gd <= v) break + gd -= v + } + + return { gy, gm, gd } +} + +function gregorianToJalali(gy, gm, gd) { + gy = Number(gy) + gm = Number(gm) + gd = Number(gd) + + const g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] + let jy = gy > 1600 ? 979 : 0 + gy = gy > 1600 ? gy - 1600 : gy - 621 + + const gy2 = gm > 2 ? gy + 1 : gy + let days = + 365 * gy + + Math.floor((gy2 + 3) / 4) - + Math.floor((gy2 + 99) / 100) + + Math.floor((gy2 + 399) / 400) - + 80 + + gd + + g_d_m[gm - 1] + + jy += 33 * Math.floor(days / 12053) + days %= 12053 + + jy += 4 * Math.floor(days / 1461) + days %= 1461 + + if (days > 365) { + jy += Math.floor((days - 1) / 365) + days = (days - 1) % 365 + } + + const jm = days < 186 ? 1 + Math.floor(days / 31) : 7 + Math.floor((days - 186) / 30) + const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30) + + return { jy, jm, jd } +} + +function jalaliToDate(jy, jm, jd) { + const y = parseInt(toEnglishDigits(jy), 10) + const m = parseInt(toEnglishDigits(jm), 10) + const d = parseInt(toEnglishDigits(jd), 10) + + if (isNaN(y) || isNaN(m) || isNaN(d) || y <= 0 || m < 1 || m > 12 || d < 1 || d > 31) { + return null + } + + const { gy, gm, gd } = jalaliToGregorian(y, m, d) + return new Date(Date.UTC(gy, gm - 1, gd, 12, 0, 0)) +} + +function parseDateToJalali(dateVal) { + if (!dateVal) return null + + if (dateVal instanceof Date || (typeof dateVal === 'string' && (dateVal.includes('T') || dateVal.includes('-')))) { + const d = new Date(dateVal) + if (!isNaN(d.getTime())) { + return gregorianToJalali(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()) + } + } + + if (typeof dateVal === 'string' && dateVal.includes('/')) { + const clean = toEnglishDigits(dateVal.trim()) + const parts = clean.split('/') + if (parts.length === 3) { + const jy = parseInt(parts[0], 10) + const jm = parseInt(parts[1], 10) + const jd = parseInt(parts[2], 10) + if (!isNaN(jy) && !isNaN(jm) && !isNaN(jd)) { + return { jy, jm, jd } + } + } + } + + return null +} + +function normalizeToDate(input) { + if (!input) return null + if (input instanceof Date && !isNaN(input.getTime())) return input + if (typeof input === 'string') { + const trimmed = input.trim() + if (trimmed.includes('/')) { + const parts = toEnglishDigits(trimmed).split('/') + if (parts.length === 3) { + return jalaliToDate(parts[0], parts[1], parts[2]) + } + } + const d = new Date(trimmed) + if (!isNaN(d.getTime())) return d + } + return null +} + +module.exports = { + persianMonths, + toEnglishDigits, + jalaliToGregorian, + gregorianToJalali, + jalaliToDate, + parseDateToJalali, + normalizeToDate, +}