diff --git a/handlers/lotOccupancies-post/doSearchPastOccupants.d.ts b/handlers/lotOccupancies-post/doSearchPastOccupants.d.ts
new file mode 100644
index 00000000..9621c611
--- /dev/null
+++ b/handlers/lotOccupancies-post/doSearchPastOccupants.d.ts
@@ -0,0 +1,3 @@
+import type { RequestHandler } from "express";
+export declare const handler: RequestHandler;
+export default handler;
diff --git a/handlers/lotOccupancies-post/doSearchPastOccupants.js b/handlers/lotOccupancies-post/doSearchPastOccupants.js
new file mode 100644
index 00000000..682596e9
--- /dev/null
+++ b/handlers/lotOccupancies-post/doSearchPastOccupants.js
@@ -0,0 +1,10 @@
+import { getPastLotOccupancyOccupants } from "../../helpers/lotOccupancyDB/getPastLotOccupancyOccupants.js";
+export const handler = (request, response) => {
+ const occupants = getPastLotOccupancyOccupants(request.body, {
+ limit: Number.parseInt(request.body.limit, 10)
+ });
+ response.json({
+ occupants
+ });
+};
+export default handler;
diff --git a/handlers/lotOccupancies-post/doSearchPastOccupants.ts b/handlers/lotOccupancies-post/doSearchPastOccupants.ts
new file mode 100644
index 00000000..ea58123e
--- /dev/null
+++ b/handlers/lotOccupancies-post/doSearchPastOccupants.ts
@@ -0,0 +1,16 @@
+import type { RequestHandler } from "express";
+
+import { getPastLotOccupancyOccupants } from "../../helpers/lotOccupancyDB/getPastLotOccupancyOccupants.js";
+
+export const handler: RequestHandler = (request, response) => {
+
+ const occupants = getPastLotOccupancyOccupants(request.body, {
+ limit: Number.parseInt(request.body.limit, 10)
+ });
+
+ response.json({
+ occupants
+ });
+};
+
+export default handler;
diff --git a/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.d.ts b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.d.ts
new file mode 100644
index 00000000..be605efa
--- /dev/null
+++ b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.d.ts
@@ -0,0 +1,9 @@
+import type * as recordTypes from "../../types/recordTypes";
+interface GetPastLotOccupancyOccupantsFilters {
+ searchFilter: string;
+}
+interface GetPastLotOccupancyOccupantsOptions {
+ limit: number;
+}
+export declare const getPastLotOccupancyOccupants: (filters: GetPastLotOccupancyOccupantsFilters, options: GetPastLotOccupancyOccupantsOptions) => recordTypes.LotOccupancyOccupant[];
+export default getPastLotOccupancyOccupants;
diff --git a/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.js b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.js
new file mode 100644
index 00000000..1161a15f
--- /dev/null
+++ b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.js
@@ -0,0 +1,40 @@
+import sqlite from "better-sqlite3";
+import { lotOccupancyDB as databasePath } from "../../data/databasePaths.js";
+export const getPastLotOccupancyOccupants = (filters, options) => {
+ const database = sqlite(databasePath, {
+ readonly: true
+ });
+ let sqlWhereClause = " where o.recordDelete_timeMillis is null and l.recordDelete_timeMillis is null";
+ const sqlParameters = [];
+ if (filters.searchFilter) {
+ const searchFilterPieces = filters.searchFilter.split(" ");
+ for (const searchFilterPiece of searchFilterPieces) {
+ sqlWhereClause +=
+ " and (o.occupantName like '%' || ? || '%'" +
+ " or o.occupantAddress1 like '%' || ? || '%'" +
+ " or o.occupantAddress2 like '%' || ? || '%'" +
+ " or o.occupantCity like '%' || ? || '%')";
+ sqlParameters.push(searchFilterPiece, searchFilterPiece, searchFilterPiece, searchFilterPiece);
+ }
+ }
+ const sql = "select" +
+ " o.occupantName, o.occupantAddress1, o.occupantAddress2," +
+ " o.occupantCity, o.occupantProvince, o.occupantPostalCode," +
+ " o.occupantPhoneNumber, o.occupantEmailAddress," +
+ " count(*) as lotOccupancyIdCount," +
+ " max(o.recordUpdate_timeMillis) as recordUpdate_timeMillisMax" +
+ " from LotOccupancyOccupants o" +
+ " left join LotOccupancies l on o.lotOccupancyId = l.lotOccupancyId" +
+ sqlWhereClause +
+ " group by occupantName, occupantAddress1, occupantAddress2, occupantCity, occupantProvince, occupantPostalCode," +
+ " occupantPhoneNumber, occupantEmailAddress" +
+ " order by lotOccupancyIdCount desc, recordUpdate_timeMillisMax desc" +
+ " limit " +
+ options.limit;
+ const lotOccupancyOccupants = database
+ .prepare(sql)
+ .all(sqlParameters);
+ database.close();
+ return lotOccupancyOccupants;
+};
+export default getPastLotOccupancyOccupants;
diff --git a/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.ts b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.ts
new file mode 100644
index 00000000..762c5d32
--- /dev/null
+++ b/helpers/lotOccupancyDB/getPastLotOccupancyOccupants.ts
@@ -0,0 +1,72 @@
+import sqlite from "better-sqlite3";
+
+import { lotOccupancyDB as databasePath } from "../../data/databasePaths.js";
+
+import type * as recordTypes from "../../types/recordTypes";
+
+interface GetPastLotOccupancyOccupantsFilters {
+ searchFilter: string;
+}
+
+interface GetPastLotOccupancyOccupantsOptions {
+ limit: number;
+}
+
+export const getPastLotOccupancyOccupants = (
+ filters: GetPastLotOccupancyOccupantsFilters,
+ options: GetPastLotOccupancyOccupantsOptions
+): recordTypes.LotOccupancyOccupant[] => {
+ const database = sqlite(databasePath, {
+ readonly: true
+ });
+
+ let sqlWhereClause =
+ " where o.recordDelete_timeMillis is null and l.recordDelete_timeMillis is null";
+
+ const sqlParameters = [];
+
+ if (filters.searchFilter) {
+ const searchFilterPieces = filters.searchFilter.split(" ");
+
+ for (const searchFilterPiece of searchFilterPieces) {
+ sqlWhereClause +=
+ " and (o.occupantName like '%' || ? || '%'" +
+ " or o.occupantAddress1 like '%' || ? || '%'" +
+ " or o.occupantAddress2 like '%' || ? || '%'" +
+ " or o.occupantCity like '%' || ? || '%')";
+
+ sqlParameters.push(
+ searchFilterPiece,
+ searchFilterPiece,
+ searchFilterPiece,
+ searchFilterPiece
+ );
+ }
+ }
+
+ const sql =
+ "select" +
+ " o.occupantName, o.occupantAddress1, o.occupantAddress2," +
+ " o.occupantCity, o.occupantProvince, o.occupantPostalCode," +
+ " o.occupantPhoneNumber, o.occupantEmailAddress," +
+ " count(*) as lotOccupancyIdCount," +
+ " max(o.recordUpdate_timeMillis) as recordUpdate_timeMillisMax" +
+ " from LotOccupancyOccupants o" +
+ " left join LotOccupancies l on o.lotOccupancyId = l.lotOccupancyId" +
+ sqlWhereClause +
+ " group by occupantName, occupantAddress1, occupantAddress2, occupantCity, occupantProvince, occupantPostalCode," +
+ " occupantPhoneNumber, occupantEmailAddress" +
+ " order by lotOccupancyIdCount desc, recordUpdate_timeMillisMax desc" +
+ " limit " +
+ options.limit;
+
+ const lotOccupancyOccupants: recordTypes.LotOccupancyOccupant[] = database
+ .prepare(sql)
+ .all(sqlParameters);
+
+ database.close();
+
+ return lotOccupancyOccupants;
+};
+
+export default getPastLotOccupancyOccupants;
diff --git a/public-typescript/lotOccupancyEdit.js b/public-typescript/lotOccupancyEdit.js
index 75ef739b..76a9ea40 100644
--- a/public-typescript/lotOccupancyEdit.js
+++ b/public-typescript/lotOccupancyEdit.js
@@ -562,11 +562,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
occupantsContainer.append(tableElement);
};
document.querySelector("#button--addOccupant").addEventListener("click", () => {
- let addFormElement;
let addCloseModalFunction;
- const addOccupant = (submitEvent) => {
- submitEvent.preventDefault();
- cityssm.postJSON(urlPrefix + "/lotOccupancies/doAddLotOccupancyOccupant", addFormElement, (responseJSON) => {
+ let addFormElement;
+ let searchFormElement;
+ let searchResultsElement;
+ const addOccupant = (formOrObject) => {
+ cityssm.postJSON(urlPrefix + "/lotOccupancies/doAddLotOccupancyOccupant", formOrObject, (responseJSON) => {
if (responseJSON.success) {
lotOccupancyOccupants = responseJSON.lotOccupancyOccupants;
addCloseModalFunction();
@@ -581,25 +582,119 @@ Object.defineProperty(exports, "__esModule", { value: true });
}
});
};
+ const addOccupantFromForm = (submitEvent) => {
+ submitEvent.preventDefault();
+ addOccupant(addFormElement);
+ };
+ let pastOccupantSearchResults = [];
+ const addOccupantFromCopy = (clickEvent) => {
+ clickEvent.preventDefault();
+ const panelBlockElement = clickEvent.currentTarget;
+ const occupant = pastOccupantSearchResults[Number.parseInt(panelBlockElement.dataset.index, 10)];
+ const lotOccupantTypeId = panelBlockElement
+ .closest(".modal")
+ .querySelector("#lotOccupancyOccupantCopy--lotOccupantTypeId").value;
+ if (lotOccupantTypeId === "") {
+ bulmaJS.alert({
+ title: "No " + exports.aliases.occupant + " Type Selected",
+ message: "Select a type to apply to the newly added " +
+ exports.aliases.occupant.toLowerCase() +
+ ".",
+ contextualColorName: "warning"
+ });
+ }
+ else {
+ occupant.lotOccupantTypeId = Number.parseInt(lotOccupantTypeId, 10);
+ occupant.lotOccupancyId = Number.parseInt(lotOccupancyId, 10);
+ addOccupant(occupant);
+ }
+ };
+ const searchOccupants = (event) => {
+ event.preventDefault();
+ if (searchFormElement.querySelector("#lotOccupancyOccupantCopy--searchFilter").value === "") {
+ searchResultsElement.innerHTML =
+ '
' +
+ '
Enter a partial name or address in the search field above.
' +
+ "
";
+ return;
+ }
+ searchResultsElement.innerHTML =
+ '' +
+ '
' +
+ "Searching..." +
+ "
";
+ cityssm.postJSON(urlPrefix + "/lotOccupancies/doSearchPastOccupants", searchFormElement, (responseJSON) => {
+ pastOccupantSearchResults = responseJSON.occupants;
+ const panelElement = document.createElement("div");
+ panelElement.className = "panel";
+ for (const [index, occupant] of pastOccupantSearchResults.entries()) {
+ const panelBlockElement = document.createElement("a");
+ panelBlockElement.className = "panel-block is-block";
+ panelBlockElement.dataset.index = index.toString();
+ panelBlockElement.innerHTML =
+ "" +
+ cityssm.escapeHTML(occupant.occupantName) +
+ "" +
+ "
" +
+ '' +
+ ('
' +
+ cityssm.escapeHTML(occupant.occupantAddress1) +
+ "
" +
+ (occupant.occupantAddress2
+ ? cityssm.escapeHTML(occupant.occupantAddress2) + "
"
+ : "") +
+ cityssm.escapeHTML(occupant.occupantCity) +
+ ", " +
+ cityssm.escapeHTML(occupant.occupantProvince) +
+ "
" +
+ cityssm.escapeHTML(occupant.occupantPostalCode) +
+ "
") +
+ ('
' +
+ (occupant.occupantPhoneNumber
+ ? cityssm.escapeHTML(occupant.occupantPhoneNumber) +
+ "
"
+ : "") +
+ cityssm.escapeHTML(occupant.occupantEmailAddress) +
+ "
" +
+ "
") +
+ "
";
+ panelBlockElement.addEventListener("click", addOccupantFromCopy);
+ panelElement.append(panelBlockElement);
+ }
+ searchResultsElement.innerHTML = "";
+ searchResultsElement.append(panelElement);
+ });
+ };
cityssm.openHtmlModal("lotOccupancy-addOccupant", {
onshow: (modalElement) => {
los.populateAliases(modalElement);
modalElement.querySelector("#lotOccupancyOccupantAdd--lotOccupancyId").value = lotOccupancyId;
const lotOccupantTypeSelectElement = modalElement.querySelector("#lotOccupancyOccupantAdd--lotOccupantTypeId");
+ const lotOccupantTypeCopySelectElement = modalElement.querySelector("#lotOccupancyOccupantCopy--lotOccupantTypeId");
for (const lotOccupantType of exports.lotOccupantTypes) {
const optionElement = document.createElement("option");
optionElement.value = lotOccupantType.lotOccupantTypeId.toString();
optionElement.textContent = lotOccupantType.lotOccupantType;
lotOccupantTypeSelectElement.append(optionElement);
+ lotOccupantTypeCopySelectElement.append(optionElement.cloneNode(true));
}
modalElement.querySelector("#lotOccupancyOccupantAdd--occupantCity").value = exports.occupantCityDefault;
modalElement.querySelector("#lotOccupancyOccupantAdd--occupantProvince").value = exports.occupantProvinceDefault;
},
onshown: (modalElement, closeModalFunction) => {
bulmaJS.toggleHtmlClipped();
+ bulmaJS.init(modalElement);
modalElement.querySelector("#lotOccupancyOccupantAdd--lotOccupantTypeId").focus();
- addFormElement = modalElement.querySelector("form");
- addFormElement.addEventListener("submit", addOccupant);
+ addFormElement = modalElement.querySelector("#form--lotOccupancyOccupantAdd");
+ addFormElement.addEventListener("submit", addOccupantFromForm);
+ searchResultsElement = modalElement.querySelector("#lotOccupancyOccupantCopy--searchResults");
+ searchFormElement = modalElement.querySelector("#form--lotOccupancyOccupantCopy");
+ searchFormElement.addEventListener("submit", (formEvent) => {
+ formEvent.preventDefault();
+ });
+ modalElement
+ .querySelector("#lotOccupancyOccupantCopy--searchFilter")
+ .addEventListener("change", searchOccupants);
addCloseModalFunction = closeModalFunction;
},
onremoved: () => {
diff --git a/public-typescript/lotOccupancyEdit.ts b/public-typescript/lotOccupancyEdit.ts
index 8527efa5..f9060fe5 100644
--- a/public-typescript/lotOccupancyEdit.ts
+++ b/public-typescript/lotOccupancyEdit.ts
@@ -845,15 +845,19 @@ declare const bulmaJS: BulmaJS;
};
document.querySelector("#button--addOccupant").addEventListener("click", () => {
- let addFormElement: HTMLFormElement;
let addCloseModalFunction: () => void;
- const addOccupant = (submitEvent: SubmitEvent) => {
- submitEvent.preventDefault();
+ let addFormElement: HTMLFormElement;
+ let searchFormElement: HTMLFormElement;
+ let searchResultsElement: HTMLElement;
+
+ const addOccupant = (
+ formOrObject: HTMLFormElement | recordTypes.LotOccupancyOccupant
+ ) => {
cityssm.postJSON(
urlPrefix + "/lotOccupancies/doAddLotOccupancyOccupant",
- addFormElement,
+ formOrObject,
(responseJSON: {
success: boolean;
errorMessage?: string;
@@ -874,6 +878,122 @@ declare const bulmaJS: BulmaJS;
);
};
+ const addOccupantFromForm = (submitEvent: SubmitEvent) => {
+ submitEvent.preventDefault();
+ addOccupant(addFormElement);
+ };
+
+ let pastOccupantSearchResults: recordTypes.LotOccupancyOccupant[] = [];
+
+ const addOccupantFromCopy = (clickEvent: MouseEvent) => {
+ clickEvent.preventDefault();
+
+ const panelBlockElement = clickEvent.currentTarget as HTMLElement;
+
+ const occupant =
+ pastOccupantSearchResults[Number.parseInt(panelBlockElement.dataset.index, 10)];
+
+ const lotOccupantTypeId = (
+ panelBlockElement
+ .closest(".modal")
+ .querySelector(
+ "#lotOccupancyOccupantCopy--lotOccupantTypeId"
+ ) as HTMLSelectElement
+ ).value;
+
+ if (lotOccupantTypeId === "") {
+ bulmaJS.alert({
+ title: "No " + exports.aliases.occupant + " Type Selected",
+ message:
+ "Select a type to apply to the newly added " +
+ exports.aliases.occupant.toLowerCase() +
+ ".",
+ contextualColorName: "warning"
+ });
+ } else {
+ occupant.lotOccupantTypeId = Number.parseInt(lotOccupantTypeId, 10);
+ occupant.lotOccupancyId = Number.parseInt(lotOccupancyId, 10);
+ addOccupant(occupant);
+ }
+ };
+
+ const searchOccupants = (event: Event) => {
+ event.preventDefault();
+
+ if (
+ (
+ searchFormElement.querySelector(
+ "#lotOccupancyOccupantCopy--searchFilter"
+ ) as HTMLInputElement
+ ).value === ""
+ ) {
+ searchResultsElement.innerHTML =
+ '' +
+ '
Enter a partial name or address in the search field above.
' +
+ "
";
+
+ return;
+ }
+
+ searchResultsElement.innerHTML =
+ '' +
+ '
' +
+ "Searching..." +
+ "
";
+
+ cityssm.postJSON(
+ urlPrefix + "/lotOccupancies/doSearchPastOccupants",
+ searchFormElement,
+ (responseJSON: { occupants: recordTypes.LotOccupancyOccupant[] }) => {
+ pastOccupantSearchResults = responseJSON.occupants;
+
+ const panelElement = document.createElement("div");
+ panelElement.className = "panel";
+
+ for (const [index, occupant] of pastOccupantSearchResults.entries()) {
+ const panelBlockElement = document.createElement("a");
+ panelBlockElement.className = "panel-block is-block";
+ panelBlockElement.dataset.index = index.toString();
+
+ panelBlockElement.innerHTML =
+ "" +
+ cityssm.escapeHTML(occupant.occupantName) +
+ "" +
+ "
" +
+ '' +
+ ('
' +
+ cityssm.escapeHTML(occupant.occupantAddress1) +
+ "
" +
+ (occupant.occupantAddress2
+ ? cityssm.escapeHTML(occupant.occupantAddress2) + "
"
+ : "") +
+ cityssm.escapeHTML(occupant.occupantCity) +
+ ", " +
+ cityssm.escapeHTML(occupant.occupantProvince) +
+ "
" +
+ cityssm.escapeHTML(occupant.occupantPostalCode) +
+ "
") +
+ ('
' +
+ (occupant.occupantPhoneNumber
+ ? cityssm.escapeHTML(occupant.occupantPhoneNumber) +
+ "
"
+ : "") +
+ cityssm.escapeHTML(occupant.occupantEmailAddress) +
+ "
" +
+ "
") +
+ "
";
+
+ panelBlockElement.addEventListener("click", addOccupantFromCopy);
+
+ panelElement.append(panelBlockElement);
+ }
+
+ searchResultsElement.innerHTML = "";
+ searchResultsElement.append(panelElement);
+ }
+ );
+ };
+
cityssm.openHtmlModal("lotOccupancy-addOccupant", {
onshow: (modalElement) => {
los.populateAliases(modalElement);
@@ -888,11 +1008,18 @@ declare const bulmaJS: BulmaJS;
"#lotOccupancyOccupantAdd--lotOccupantTypeId"
) as HTMLSelectElement;
+ const lotOccupantTypeCopySelectElement = modalElement.querySelector(
+ "#lotOccupancyOccupantCopy--lotOccupantTypeId"
+ ) as HTMLSelectElement;
+
for (const lotOccupantType of exports.lotOccupantTypes as recordTypes.LotOccupantType[]) {
const optionElement = document.createElement("option");
optionElement.value = lotOccupantType.lotOccupantTypeId.toString();
optionElement.textContent = lotOccupantType.lotOccupantType;
+
lotOccupantTypeSelectElement.append(optionElement);
+
+ lotOccupantTypeCopySelectElement.append(optionElement.cloneNode(true));
}
(
@@ -900,6 +1027,7 @@ declare const bulmaJS: BulmaJS;
"#lotOccupancyOccupantAdd--occupantCity"
) as HTMLInputElement
).value = exports.occupantCityDefault;
+
(
modalElement.querySelector(
"#lotOccupancyOccupantAdd--occupantProvince"
@@ -908,6 +1036,7 @@ declare const bulmaJS: BulmaJS;
},
onshown: (modalElement, closeModalFunction) => {
bulmaJS.toggleHtmlClipped();
+ bulmaJS.init(modalElement);
(
modalElement.querySelector(
@@ -915,8 +1044,23 @@ declare const bulmaJS: BulmaJS;
) as HTMLInputElement
).focus();
- addFormElement = modalElement.querySelector("form");
- addFormElement.addEventListener("submit", addOccupant);
+ addFormElement = modalElement.querySelector("#form--lotOccupancyOccupantAdd");
+ addFormElement.addEventListener("submit", addOccupantFromForm);
+
+ searchResultsElement = modalElement.querySelector(
+ "#lotOccupancyOccupantCopy--searchResults"
+ );
+
+ searchFormElement = modalElement.querySelector(
+ "#form--lotOccupancyOccupantCopy"
+ );
+ searchFormElement.addEventListener("submit", (formEvent) => {
+ formEvent.preventDefault();
+ });
+
+ modalElement
+ .querySelector("#lotOccupancyOccupantCopy--searchFilter")
+ .addEventListener("change", searchOccupants);
addCloseModalFunction = closeModalFunction;
},
diff --git a/public/html/lotOccupancy-addOccupant.html b/public/html/lotOccupancy-addOccupant.html
index 59e40fb1..78bea57d 100644
--- a/public/html/lotOccupancy-addOccupant.html
+++ b/public/html/lotOccupancy-addOccupant.html
@@ -8,88 +8,135 @@