// Properties page logic for unified rent/sale endpoint
const API_BASE = '/api';
// Combined area dropdown values (from both transactions and rents)
const areas = [
'abu hail', 'al athbah', 'al aweer first', 'al aweer second', 'al bada', 'al baraha',
'al barari', 'al barsha first', 'al barsha second', 'al barsha third', 'al barshaa south first',
'al barshaa south second', 'al barshaa south third', 'al buteen', 'al dhagaya', 'al eyas',
'al furjan', 'al garhoud', 'al goze first', 'al goze fourth', 'al goze industrial first',
'al goze industrial fourth', 'al goze industrial second', 'al goze industrial third', 'al goze third',
'al hamriya', 'al hebiah fifth', 'al hebiah first', 'al hebiah fourth', 'al hebiah sixth',
'al hebiah third', 'al hudaiba', 'al jadaf', 'al jafliya', 'al karama', 'al khabeesi',
'al khail heights', 'al khairan first', 'al khawaneej first', 'al khawaneej second', 'al kheeran',
'al kifaf', 'al lusaily', 'al mamzer', 'al manara', 'al mararr', 'al merkadh',
'al mizhar first', 'al mizhar fourth', 'al mizhar second', 'al mizhar third', 'al murqabat',
'al muteena', 'al nahda first', 'al nahda second', 'al qusais', 'al qusais industrial fifth',
'al qusais industrial first', 'al qusais industrial fourth', 'al qusais industrial third', 'al raffa',
'al ras', 'al rashidiya', 'al rega', 'al rowaiyah third', 'al saffa first', 'al saffa second',
'al safouh first', 'al satwa', 'al suq al kabeer', 'al thanyah fifth', 'al thanyah third',
'al ttay', 'al twar fifth', 'al twar first', 'al twar fourth', 'al twar second', 'al twar third',
'al waha', 'al waheda', 'al warqa first', 'al warqa fourth', 'al warqa second', 'al warqa third',
'al warsan second', 'al warsan third', 'al wasl', 'al yelayiss 1', 'al yelayiss 2', 'al yelayiss 5',
'al yufrah 1', 'arabian ranches i', 'arabian ranches ii', 'arabian ranches iii',
'arabian ranches polo club', 'arjan', 'barsha heights', 'bluewaters', 'bukadra', 'burj khalifa',
'business bay', 'business park', 'cherrywoods', 'city of arabia', 'city walk', 'damac hills',
'discovery gardens', 'dmcc-ez2', 'down town jabal ali', 'dubai creek harbour', 'dubai design district',
'dubai golf city', 'dubai harbour', 'dubai healthcare city - phase 1', 'dubai healthcare city - phase 2',
'dubai hills', 'dubai industrial city', 'dubai international airport', 'dubai investment park first',
'dubai investment park second', 'dubai land residence complex', 'dubai lifestyle city', 'dubai marina',
'dubai maritime city', 'dubai production city', 'dubai science park', 'dubai south', 'dubai sports city',
'dubai studio city', 'dubai water canal', 'dubai water front', 'emaar south', 'emirate living',
'eyal nasser', 'falcon city of wonders', 'ghadeer al tair', 'ghadeer barashy', 'grand hills dubai',
'grand views', 'hadaeq sheikh mohammed bin rashid', 'hessyan second', 'hor al anz', 'hor al anz east',
'horizon', 'international city ph 1', 'international city ph 2 & 3', 'island 2', 'jabal ali first',
'jabal ali industrial first', 'jabal ali industrial second', 'jabel ali hills', 'jaddaf waterfront',
'jumeira bay', 'jumeirah beach residence', 'jumeirah first', 'jumeirah golf', 'jumeirah heights',
'jumeirah islands', 'jumeirah lakes towers', 'jumeirah living', 'jumeirah park', 'jumeirah second',
'jumeirah third', 'jumeirah village circle', 'jumeirah village triangle', 'la mer', 'lehbab first',
'lehbab second', 'living legends', 'liwan', 'liwan 2', 'madinat al mataar', 'madinat dubai almelaheyah',
'madinat hind 3', 'madinat hind 4', 'madinat latifa', 'majan', 'mankhool', 'margham', 'marsa dubai',
'mbr district 1', 'mbr district 7', "me'aisem first", "me'aisem second", 'medyan race course villas',
'mena jabal ali', 'meydan avenue', 'meydan one', 'millennium', 'mina rashid', 'mira', 'mirdif',
'motor city', 'mudon', 'muhaisanah first', 'muhaisanah fourth', 'muhaisanah second', 'muhaisanah third',
'mushrif', 'nad al hamar', 'nad al sheba gardens', 'nad al shiba first', 'nad al shiba fourth',
'nad al shiba second', 'nad al shiba third', 'nad shamma', 'nadd hessa', 'naif', 'nazwah', 'oud metha',
'palm deira', 'palm jabal ali', 'palm jumeirah', 'palmarosa', 'pearl jumeira', 'polo townhouses igo',
'port saeed', 'ras al khor', 'ras al khor industrial first', 'ras al khor industrial second',
'ras al khor industrial third', 'rega al buteen', 'remraam', 'rukan', 'saih shuaib 1', 'saih shuaib 2',
'saih shuaib 3', 'saih shuaib 4', 'sama al jadaf', 'serena', 'silicon oasis', 'sobha heartland',
'sufouh gardens', 'sustainable city', 'tecom site a', 'tecom site d', 'the beach', 'the field',
'the greens', 'the lakes', 'the valley', 'the villa', 'the world', 'tilal al ghaf', 'town square',
'trade center first', 'trade center second', 'um al sheif', 'um hurair first', 'um hurair second',
'um ramool', 'um suqaim first', 'um suqaim second', 'um suqaim third', 'umm addamin', 'villanova',
'wadi al amardi', 'wadi al safa 2', 'wadi al safa 3', 'wadi al safa 4', 'wadi al safa 5',
'wadi al safa 6', 'wadi al safa 7', 'warsan first', 'warsan fourth', 'yaraah', 'zaabeel first', 'zaabeel second'
];
// Property types for rent
const rentPropertyTypes = [
{ value: 'all', label: 'All Types' },
{ value: 'unit', label: 'Unit' },
{ value: 'villa', label: 'Villa' },
{ value: 'virtual unit', label: 'Virtual Unit' },
{ value: 'land', label: 'Land' },
{ value: 'building', label: 'Building' }
];
// Property types for sale
const salePropertyTypes = [
{ value: 'all', label: 'All Types' },
{ value: 'building', label: 'Building' },
{ value: 'land', label: 'Land' },
{ value: 'unit', label: 'Unit' }
];
// Combined projects list (simplified - you may want to load dynamically)
const projects = [
'014 tower', '08 life residences', '1 residences', '10 oxford by iman', '105 residences by kamdar',
'11 hills park', '15 cascade', '15 northside', '161 jumeirah lane', '17 icon bay',
'171 garden heights', '1wood residence', '2020 marquis', '23 marina',
'29 boulevard', '310 riverside crescent', '311 boulevard by bam eskan',
'empire estates', 'empire heights', 'starz tower by danube', 'azizi feirouz i',
'candace acacia hotel apartments', 'burj al nujoom', 'azizi riviera 35'
// Add more projects as needed
];
let currentType = 'rent';
let currentPage = 1;
let lastPaging = { total: 0, page: null, page_size: 30, total_pages: 1, has_next: false, has_prev: false };
function populateAreas() {
const areaSelect = document.getElementById('area_name');
areas.forEach(area => {
const option = document.createElement('option');
option.value = area;
option.textContent = area.charAt(0).toUpperCase() + area.slice(1).replace(/\b\w/g, l => l.toUpperCase());
areaSelect.appendChild(option);
});
}
function loadProjects() {
const projectSelect = document.getElementById('project');
const sortedProjects = projects.slice().sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
sortedProjects.forEach(project => {
const option = document.createElement('option');
option.value = project;
option.textContent = project.length > 70 ? project.substring(0, 70) + '...' : project;
projectSelect.appendChild(option);
});
}
function updatePropertyTypes() {
const propertyTypeSelect = document.getElementById('property_type');
propertyTypeSelect.innerHTML = '';
const types = currentType === 'rent' ? rentPropertyTypes : salePropertyTypes;
types.forEach(type => {
const option = document.createElement('option');
option.value = type.value;
option.textContent = type.label;
propertyTypeSelect.appendChild(option);
});
}
function updateTypeDependentFields() {
const typeInput = document.getElementById('type');
typeInput.value = currentType;
// Show/hide rooms vs beds
const roomsGroup = document.getElementById('roomsGroup');
const bedsGroup = document.getElementById('bedsGroup');
if (currentType === 'rent') {
roomsGroup.style.display = 'flex';
bedsGroup.style.display = 'none';
document.getElementById('beds').value = 'all';
} else {
roomsGroup.style.display = 'none';
bedsGroup.style.display = 'flex';
document.getElementById('rooms').value = 'all';
}
updatePropertyTypes();
updateTableHeaders();
}
function updateTableHeaders() {
const tableHead = document.getElementById('tableHead');
if (currentType === 'rent') {
tableHead.innerHTML = `
| rent_id |
registration_date |
start_date |
end_date |
version_en |
area_en |
contract_amount |
annual_amount |
is_free_hold_en |
actual_area |
prop_type_en |
prop_sub_type_en |
rooms |
usage_en |
nearest_metro_en |
nearest_mall_en |
nearest_landmark_en |
parking |
total_properties |
master_project_en |
project_en |
created_at |
updated_at |
`;
} else {
tableHead.innerHTML = `
| transaction_id |
transaction_number |
instance_date |
group_en |
procedure_en |
is_offplan_en |
is_free_hold_en |
usage_en |
area_en |
prop_type_en |
prop_sb_type_en |
trans_value |
procedure_area |
actual_area |
rooms_en |
parking |
nearest_metro_en |
nearest_mall_en |
nearest_landmark_en |
total_buyer |
total_seller |
master_project_en |
project_en |
created_at |
updated_at |
`;
}
}
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function showError(message) {
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
function hideError() {
document.getElementById('error').style.display = 'none';
}
function showResults() {
document.getElementById('resultsSection').style.display = 'block';
}
function hideResults() {
document.getElementById('resultsSection').style.display = 'none';
}
function formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function formatNumber(num, decimals = 0) {
if (num === null || num === undefined) return 'N/A';
return parseFloat(num).toLocaleString('en-US', { maximumFractionDigits: decimals });
}
function formatCurrency(amount) {
if (!amount) return 'N/A';
return 'AED ' + parseFloat(amount).toLocaleString('en-US', { maximumFractionDigits: 0 });
}
async function searchProperties(pageOverride) {
const formData = new FormData(document.getElementById('filtersForm'));
const params = new URLSearchParams();
const type = formData.get('type') || currentType;
const area_name = formData.get('area_name');
const property_type = formData.get('property_type');
const size_min = formData.get('size_min');
const size_max = formData.get('size_max');
const rooms = formData.get('rooms');
const beds = formData.get('beds');
const project = formData.get('project');
const pageSize = formData.get('page_size') || '30';
const page = pageOverride != null ? pageOverride : (formData.get('page') || currentPage || 1);
params.append('type', type);
if (area_name) params.append('area_name', area_name);
if (property_type && property_type !== 'all') params.append('property_type', property_type);
if (size_min && !isNaN(parseFloat(size_min))) params.append('size_min', size_min);
if (size_max && !isNaN(parseFloat(size_max))) params.append('size_max', size_max);
if (type === 'rent' && rooms && rooms !== 'all') {
params.append('rooms', rooms);
} else if (type === 'sale' && beds && beds !== 'all') {
params.append('beds', beds);
}
if (project && project !== 'all') params.append('project', project);
if (page) params.append('page', page);
if (pageSize) params.append('page_size', pageSize);
showLoading(true);
hideError();
hideResults();
try {
const response = await fetch(`${API_BASE}/properties/recent?${params.toString()}`);
const data = await response.json();
if (data.success) {
currentPage = data.data.page || 1;
lastPaging = {
total: data.data.total ?? data.data.count,
page: data.data.page,
page_size: data.data.page_size ?? parseInt(pageSize, 10),
total_pages: data.data.total_pages ?? 1,
has_next: !!data.data.has_next,
has_prev: !!data.data.has_prev
};
displayResults(data.data);
} else {
showError(data.message || 'Failed to fetch properties');
}
} catch (error) {
showError('Network error: ' + error.message);
} finally {
showLoading(false);
}
}
function displayResults(data) {
const dataKey = currentType === 'rent' ? 'rents' : 'transactions';
const results = data[dataKey] || [];
const { count, total, page, page_size, total_pages, has_next, has_prev } = data;
const tbody = document.getElementById('propertiesBody');
const resultsCount = document.getElementById('resultsCount');
const showing = count;
const grandTotal = total ?? count;
resultsCount.textContent = `Showing ${showing} of ${grandTotal} ${currentType === 'rent' ? 'rents' : 'transactions'}`;
const pageInfo = document.getElementById('pageInfo');
if (pageInfo) {
if (page && total_pages) {
pageInfo.textContent = `Page ${page} of ${total_pages}`;
} else {
pageInfo.textContent = '';
}
}
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (prevBtn) prevBtn.disabled = !(has_prev);
if (nextBtn) nextBtn.disabled = !(has_next);
if (results.length === 0) {
const colCount = document.querySelectorAll('#propertiesTable thead th').length;
tbody.innerHTML = (
'' +
`| ` +
'' +
` No ${currentType === 'rent' ? 'rents' : 'transactions'} found matching your filters ` +
' | ' +
'
'
);
showResults();
return;
}
if (currentType === 'rent') {
tbody.innerHTML = results.map(r => (
'' +
`| ${r.rent_id ?? 'N/A'} | ` +
`${formatDate(r.registration_date)} | ` +
`${formatDate(r.start_date)} | ` +
`${formatDate(r.end_date)} | ` +
`${r.version_en ?? 'N/A'} | ` +
`${r.area_en ?? 'N/A'} | ` +
`${formatCurrency(r.contract_amount)} | ` +
`${formatCurrency(r.annual_amount)} | ` +
`${r.is_free_hold_en ?? 'N/A'} | ` +
`${formatNumber(r.actual_area)} | ` +
`${r.prop_type_en ?? 'N/A'} | ` +
`${r.prop_sub_type_en ?? 'N/A'} | ` +
`${r.rooms != null ? formatNumber(r.rooms, 1) : 'N/A'} | ` +
`${r.usage_en ?? 'N/A'} | ` +
`${r.nearest_metro_en ?? 'N/A'} | ` +
`${r.nearest_mall_en ?? 'N/A'} | ` +
`${r.nearest_landmark_en ?? 'N/A'} | ` +
`${r.parking != null ? formatNumber(r.parking, 1) : 'N/A'} | ` +
`${r.total_properties ?? '0'} | ` +
`${r.master_project_en ?? 'N/A'} | ` +
`${r.project_en ?? 'N/A'} | ` +
`${formatDate(r.created_at)} | ` +
`${formatDate(r.updated_at)} | ` +
'
'
)).join('');
} else {
tbody.innerHTML = results.map(t => (
'' +
`| ${t.transaction_id ?? 'N/A'} | ` +
`${t.transaction_number ?? 'N/A'} | ` +
`${formatDate(t.instance_date)} | ` +
`${t.group_en ?? 'N/A'} | ` +
`${t.procedure_en ?? 'N/A'} | ` +
`${t.is_offplan_en ?? 'N/A'} | ` +
`${t.is_free_hold_en ?? 'N/A'} | ` +
`${t.usage_en ?? 'N/A'} | ` +
`${t.area_en ?? 'N/A'} | ` +
`${t.prop_type_en ?? 'N/A'} | ` +
`${t.prop_sb_type_en ?? 'N/A'} | ` +
`${formatCurrency(t.trans_value)} | ` +
`${t.procedure_area != null ? formatNumber(t.procedure_area) : 'N/A'} | ` +
`${t.actual_area != null ? formatNumber(t.actual_area) : 'N/A'} | ` +
`${t.rooms_en ?? 'N/A'} | ` +
`${t.parking ?? 'N/A'} | ` +
`${t.nearest_metro_en ?? 'N/A'} | ` +
`${t.nearest_mall_en ?? 'N/A'} | ` +
`${t.nearest_landmark_en ?? 'N/A'} | ` +
`${t.total_buyer ?? '0'} | ` +
`${t.total_seller ?? '0'} | ` +
`${t.master_project_en ?? 'N/A'} | ` +
`${t.project_en ?? 'N/A'} | ` +
`${formatDate(t.created_at)} | ` +
`${formatDate(t.updated_at)} | ` +
'
'
)).join('');
}
showResults();
}
function resetFilters() {
document.getElementById('filtersForm').reset();
document.getElementById('size_min').value = '';
document.getElementById('size_max').value = '';
document.getElementById('sizeValue').textContent = 'All Sizes';
document.getElementById('type_rent').checked = true;
currentType = 'rent';
updateTypeDependentFields();
hideResults();
hideError();
}
document.addEventListener('DOMContentLoaded', () => {
populateAreas();
loadProjects();
updateTypeDependentFields();
// Type selector radio buttons
document.querySelectorAll('input[name="type"]').forEach(radio => {
radio.addEventListener('change', (e) => {
currentType = e.target.value;
updateTypeDependentFields();
hideResults();
hideError();
});
});
// Size slider setup
const sizeMinHidden = document.getElementById('size_min');
const sizeMaxHidden = document.getElementById('size_max');
const sizeMinRange = document.getElementById('size_min_range');
const sizeMaxRange = document.getElementById('size_max_range');
const sizeFill = document.getElementById('sizeFill');
const sizeValue = document.getElementById('sizeValue');
const MIN_VAL = parseFloat(sizeMinRange.min);
const MAX_VAL = parseFloat(sizeMinRange.max);
const STEP = parseFloat(sizeMinRange.step) || 100;
function updateSizeLabel() {
const min = parseFloat(sizeMinRange.value);
const max = parseFloat(sizeMaxRange.value);
const hasMin = !isNaN(min);
const hasMax = !isNaN(max);
if (!hasMin && !hasMax) {
sizeValue.textContent = 'All Sizes';
} else if (hasMin && hasMax) {
sizeValue.textContent = `${min.toLocaleString('en-US')} - ${max.toLocaleString('en-US')} sq.ft`;
} else if (hasMin) {
sizeValue.textContent = `≥ ${min.toLocaleString('en-US')} sq.ft`;
} else {
sizeValue.textContent = `≤ ${max.toLocaleString('en-US')} sq.ft`;
}
sizeMinHidden.value = (min > MIN_VAL) ? min : '';
sizeMaxHidden.value = (max < MAX_VAL) ? max : '';
const left = ((Math.max(MIN_VAL, Math.min(min, max)) - MIN_VAL) / (MAX_VAL - MIN_VAL)) * 100;
const right = ((Math.max(MIN_VAL, Math.max(min, max)) - MIN_VAL) / (MAX_VAL - MIN_VAL)) * 100;
sizeFill.style.left = `${left}%`;
sizeFill.style.width = `${Math.max(0, right - left)}%`;
}
function clampRanges() {
if (parseFloat(sizeMinRange.value) > parseFloat(sizeMaxRange.value) - STEP) {
sizeMinRange.value = (parseFloat(sizeMaxRange.value) - STEP).toString();
}
if (parseFloat(sizeMaxRange.value) < parseFloat(sizeMinRange.value) + STEP) {
sizeMaxRange.value = (parseFloat(sizeMinRange.value) + STEP).toString();
}
}
sizeMinRange.addEventListener('input', () => { clampRanges(); updateSizeLabel(); });
sizeMaxRange.addEventListener('input', () => { clampRanges(); updateSizeLabel(); });
function bringMinToFront() {
sizeMinRange.style.zIndex = '6';
sizeMaxRange.style.zIndex = '5';
}
function bringMaxToFront() {
sizeMinRange.style.zIndex = '5';
sizeMaxRange.style.zIndex = '6';
}
['mousedown','pointerdown','touchstart'].forEach(evt => {
sizeMinRange.addEventListener(evt, bringMinToFront, { passive: true });
sizeMaxRange.addEventListener(evt, bringMaxToFront, { passive: true });
});
sizeMinRange.value = MIN_VAL;
sizeMaxRange.value = MAX_VAL;
updateSizeLabel();
// Form submission
document.getElementById('filtersForm').addEventListener('submit', async (e) => {
e.preventDefault();
currentPage = 1;
const pageInput = document.getElementById('page');
if (pageInput) pageInput.value = '1';
await searchProperties(1);
});
// Reset button
const resetBtn = document.getElementById('resetBtn');
if (resetBtn) {
resetBtn.addEventListener('click', resetFilters);
}
// Pagination buttons
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (prevBtn) {
prevBtn.addEventListener('click', async () => {
if (lastPaging.has_prev && currentPage > 1) {
currentPage -= 1;
const pageInput2 = document.getElementById('page');
if (pageInput2) pageInput2.value = String(currentPage);
await searchProperties(currentPage);
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', async () => {
if (lastPaging.has_next) {
currentPage += 1;
const pageInput3 = document.getElementById('page');
if (pageInput3) pageInput3.value = String(currentPage);
await searchProperties(currentPage);
}
});
}
});