Script to get the current available / taken flats for HDB BTO Selection
// ==UserScript==
// @name HDB Flat Availability
// @namespace http://tampermonkey.net/
// @version 0.8
// @description Script to get the current available / taken flats for HDB BTO Selection
// @author You
// @match https://homes.hdb.gov.sg/home/bto/details/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=undefined.
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Wait for 2 Seconds for Ajax queries to complete first before running script
setTimeout(() => {
main();
}, 4000);
function main() {
let curr_date = new Date();
let data = { blocks: {}, summary: {}, meta:{
created_time:`${curr_date.toLocaleString()}}`,
created_timestamp: `${curr_date.getTime()}`,
} };
let blocks = [];
var taken_units = [];
// Create a text area on screen
let container_div = document.evaluate(
"/html/body/app-root/div[2]/app-bto-details/section/div/div[5]/div/div[1]/div",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
const text_area = document.createElement("textarea"); // For JSON message
const text_area2 = document.createElement("textarea"); // For normal non telegram forammted message
const text_area3 = document.createElement("textarea"); // tele msg fomatted
const text_area4 = document.createElement("textarea"); // tele msg fomatted
const text_area5 = document.createElement("textarea"); // Taken units
// edit placeholder for text area
text_area.placeholder = "JSON Output";
text_area2.placeholder = "Formatted Messgage";
text_area3.placeholder = "Telegram Formatted Message";
text_area4.placeholder = "Paste Previous Data";
text_area5.placeholder = "Taken Units";
const start_btn = document.createElement("button");
const status = document.createElement("p");
// Get the first option in select
let room_type = document.getElementById("choose-room-type").options[1].text;
// Get the name of the project
let project = document
.getElementsByClassName("col-12 col-sm-8 col-md-8 mb-5 mb-md-0")[0]
.getElementsByTagName("h4")[0].innerHTML;
// Add function to button
start_btn.innerHTML = "Start";
start_btn.onclick = function () {
runner();
};
container_div.appendChild(text_area4);
container_div.appendChild(text_area);
container_div.appendChild(start_btn);
container_div.appendChild(text_area2);
container_div.appendChild(text_area3);
container_div.appendChild(text_area5);
container_div.appendChild(status);
function formatTeleMonospace(msg) {
let newMsg = "";
var lines = msg.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].length != 0) {
newMsg += `\`\`\`${lines[i]}\`\`\`\n`;
} else {
newMsg += "\n";
}
}
return newMsg;
}
function formatMsg(json) {
let msg = "";
msg = `${project}\n${room_type}\n`;
msg += `Generated on ${curr_date.toLocaleString()}\n\n`;
for (const [block, blk_details] of Object.entries(json.blocks)) {
let floors = blk_details["unit_info"];
let quota = blk_details["quota"];
let summary = blk_details["summary"];
msg += `\n${block}\n`;
let seen_units = {};
// Parsing floors and units data
for (const [floor, units] of Object.entries(floors)) {
msg += floor;
for (const [unit, unit_details] of Object.entries(units)) {
if (unit_details.avail) {
msg += " 🟢 ";
} else {
msg += " 🔴 ";
}
if (unit in seen_units) {
seen_units[unit] += 1;
} else {
seen_units[unit] = 1;
}
}
msg += "\n";
}
// Adding the Units at the bottom of the message
msg += " ";
for (let unit of Object.keys(seen_units)) {
msg += ` ${unit} `;
}
// Adding the quoata details
msg += "\n";
for (let [race, qty] of Object.entries(quota)) {
msg += ` ${race}:${qty}`;
}
msg += "\n";
// Adding Block Summary
msg += ` Total:${summary.total} Taken:${summary.taken} Avail:${summary.avail}\n`;
}
// Add Project Summary
msg += `\nTotal:${json.summary.total} Taken:${json.summary.taken} Avail:${json.summary.avail}`;
// Add Taken Units
if (taken_units.length != 0){
msg += `\n\nTaken Units:\n${taken_units.join("\n")}`;
}
console.debug(msg);
return msg;
}
function runner() {
console.debug("Running Script");
let prev_data = null;
if(text_area4.value.length != 0){
prev_data = JSON.parse(text_area4.value);
}
// Get the list of different blocks
var select_block = document.querySelector('[aria-label="Block"]');
for (let i = 0; i < select_block.options.length; i++) {
var option = select_block.options[i];
if (option.innerHTML != "Choose Block No.") {
let block_info = option.innerHTML;
blocks.push(block_info);
data["blocks"][block_info] = {};
}
}
// Get the grid element
let project_total = 0;
let project_taken = 0;
for (let k = 0; k < blocks.length; k++) {
let block_total = 0;
let block_taken = 0;
console.debug("Inside " + blocks[k]);
const grid = document.getElementById("available-grid");
let floors = grid.getElementsByClassName("row level");
select_block.value = k;
// Trigger new select button change
var event = new Event("change");
select_block.dispatchEvent(event);
let curr_block = blocks[k];
// get quota
let quota_elems = document
.getElementById("available-sidebar")
.getElementsByClassName("col-12 col-md-auto");
let quota_obj = {};
for (let quota of quota_elems) {
let quota_split = quota.innerHTML.trim().split(": ");
quota_obj[quota_split[0]] = quota_split[1];
}
data["blocks"][curr_block]["quota"] = quota_obj;
data["blocks"][curr_block]["unit_info"] = {};
// Iterate through each row in the grid
for (let i = 0; i < floors.length; i++) {
let curr_floor = floors[i];
let labels_curr_floor = curr_floor.getElementsByTagName("label");
let curr_floor_text = labels_curr_floor[0].innerHTML;
if (
data["blocks"][curr_block]["unit_info"][curr_floor_text] ==
undefined
) {
data["blocks"][curr_block]["unit_info"][curr_floor_text] = {};
}
for (let j = 1; j < labels_curr_floor.length; j++) {
let curr_unit_label = labels_curr_floor[j]; // should have length of 0 = unit , 2 = sqm, 4 = price
let curr_unit_nodes = curr_unit_label.childNodes; // should have length of 0 = unit , 2 = sqm, 4 = price
let curr_p1 = curr_unit_nodes[0].data;
let curr_p2 = curr_unit_nodes[2].data;
let curr_p3 = curr_unit_nodes[4].data;
let curr_p4 =
curr_unit_label.parentElement.hasAttribute("disabled"); // to check if a unit is selected and unavailable
block_total += 1;
if (curr_p4) {
block_taken += 1;
if(prev_data && prev_data.blocks[curr_block].unit_info[curr_floor_text][curr_p1].avail){
// predict ethnic type
let ethnic_type = "NA";
let prev_quota = prev_data.blocks[curr_block].quota;
for (const race in prev_quota){
if(quota_obj[race] !== prev_quota[race]){
ethnic_type = race
prev_quota[race] = (parseInt(prev_quota[race]) + 1).toString();
}
}
taken_units.push(`${curr_block} ${curr_floor_text} ${curr_p1} ${ethnic_type}`);
}
}
data["blocks"][curr_block]["unit_info"][curr_floor_text][curr_p1] =
{
sqm: curr_p2,
price: curr_p3,
avail: !curr_p4,
};
}
}
data["blocks"][curr_block]["summary"] = {
total: block_total,
taken: block_taken,
avail: block_total - block_taken,
};
project_total += block_total;
project_taken += block_taken;
}
data.summary.total = project_total;
data.summary.taken = project_taken;
data.summary.avail = project_total - project_taken;
text_area.value = JSON.stringify(data);
let msg = formatMsg(data);
text_area2.value = msg;
text_area3.value = formatTeleMonospace(msg);
text_area5.value = taken_units.join("\n");
console.debug("Script Finished");
}
}
})();