diff --git a/app/common/enums.js b/app/common/enums.js
index efdbe28..942fb47 100644
--- a/app/common/enums.js
+++ b/app/common/enums.js
@@ -174,10 +174,24 @@ const CRAWLER_AD_TYPE = {
ONLY_REQUEST: 4
};
+const EMAIL_FREQUENCY = {
+ ASAP: {
+ id: 1,
+ stringId: "ASAP",
+ title: "Odmah"
+ },
+ DAILY: {
+ id: 2,
+ stringId: "DAILY",
+ title: "Jednom dnevno"
+ }
+};
+
module.exports = {
AD_TYPE,
AD_CATEGORY,
AD_STATUS,
AD_AGENCY,
- CRAWLER_AD_TYPE
+ CRAWLER_AD_TYPE,
+ EMAIL_FREQUENCY
};
diff --git a/app/controllers/queryReview.js b/app/controllers/queryReview.js
index 2b08d82..da2f42a 100644
--- a/app/controllers/queryReview.js
+++ b/app/controllers/queryReview.js
@@ -3,9 +3,9 @@ const { isValidEmail } = require("../helpers/email");
const {
notifyForNewSearchRequest
} = require("../services/notificationService");
-const { AD_CATEGORY, AD_TYPE } = require("../common/enums");
+const { AD_CATEGORY, AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums");
-const getQueryReviewData = searchRequest => {
+const getQueryReviewTableData = searchRequest => {
const {
id,
adType,
@@ -87,15 +87,26 @@ const getQueryReview = async (req, res) => {
const title = "Da li je ovo to što ste tražili ?";
const nextStep = req.query.nextStep;
const error = req.query.error;
- const queryReviewData = getQueryReviewData(searchRequest);
+ const queryReviewTableData = getQueryReviewTableData(searchRequest);
const email = searchRequest.email;
+ let selectedEmailFrequency;
+ switch (searchRequest.emailFrequency) {
+ case EMAIL_FREQUENCY.ASAP.stringId:
+ selectedEmailFrequency = EMAIL_FREQUENCY.ASAP.id;
+ break;
+ case EMAIL_FREQUENCY.DAILY.stringId:
+ selectedEmailFrequency = EMAIL_FREQUENCY.DAILY.id;
+ break;
+ }
res.render("queryReview", {
nextStep,
- queryReviewData,
+ queryReviewTableData,
title,
email,
- error
+ selectedEmailFrequency,
+ error,
+ EMAIL_FREQUENCY
});
};
@@ -107,17 +118,26 @@ const postQueryReview = async (req, res) => {
}
const nextStep = req.query.nextStep || "/ponovo";
+ const emailFrequency =
+ parseInt(req.body.emailFrequency) || EMAIL_FREQUENCY.ASAP.id;
const emailInput = req.body.email;
const emailConfirmInput = req.body.confirmEmail;
const title = "Da li je ovo to što ste tražili ?";
- const queryReviewData = getQueryReviewData(searchRequest);
+ const queryReviewTableData = getQueryReviewTableData(searchRequest);
+
+ let emailFrequencyStringId = EMAIL_FREQUENCY.ASAP.stringId;
+ if (emailFrequency === EMAIL_FREQUENCY.DAILY.id) {
+ emailFrequencyStringId = EMAIL_FREQUENCY.DAILY.stringId;
+ }
+
+ searchRequest.emailFrequency = emailFrequencyStringId;
if (emailInput !== emailConfirmInput) {
const error = "Greška ! Unešeni emailovi nisu isti";
res.render("queryReview", {
error,
title,
- queryReviewData,
+ queryReviewTableData,
email: ""
});
return;
@@ -128,7 +148,7 @@ const postQueryReview = async (req, res) => {
res.render("queryReview", {
error,
title,
- queryReviewData,
+ queryReviewTableData,
email: ""
});
return;
@@ -147,7 +167,7 @@ const postQueryReview = async (req, res) => {
res.render("queryReview", {
error,
title,
- queryReviewData,
+ queryReviewTableData,
email: ""
});
return;
@@ -164,7 +184,7 @@ const postQueryReview = async (req, res) => {
res.render("queryReview", {
error,
title,
- queryReviewData,
+ queryReviewTableData,
email: ""
});
return;
diff --git a/app/helpers/db/searchRequestMatch.js b/app/helpers/db/searchRequestMatch.js
index d7cb215..11d5cde 100644
--- a/app/helpers/db/searchRequestMatch.js
+++ b/app/helpers/db/searchRequestMatch.js
@@ -24,6 +24,23 @@ const findRealEstatesForSearchRequest = async searchRequestId => {
return matchingRealEstates;
};
+const findNotNotifiedMatches = async () => {
+ const query = {
+ notified: false
+ };
+
+ const searchRequestsModel = { model: db.SearchRequest, as: "searchRequests" };
+ const realEstateModel = { model: db.RealEstate, as: "realEstates" };
+ const include = [searchRequestsModel, realEstateModel];
+
+ const matchingRecords = await db.SearchRequestMatch.findAll({
+ where: query,
+ include
+ });
+
+ return matchingRecords;
+};
+
const addMatches = async matchingRecords => {
return await db.SearchRequestMatch.bulkCreate(matchingRecords, {
ignoreDuplicates: true
@@ -32,5 +49,6 @@ const addMatches = async matchingRecords => {
module.exports = {
findRealEstatesForSearchRequest,
- addMatches
+ addMatches,
+ findNotNotifiedMatches
};
diff --git a/app/helpers/emailContentGenerator.js b/app/helpers/emailContentGenerator.js
index 549883e..861bfb2 100644
--- a/app/helpers/emailContentGenerator.js
+++ b/app/helpers/emailContentGenerator.js
@@ -20,7 +20,11 @@ const generateRealEstateLinks = realEstates => {
return realEstateLinks;
};
-const generateNotificationEmail = (realEstates, searchRequestId) => {
+const generateNotificationEmail = (
+ realEstates,
+ searchRequestId,
+ dailyNotification = false
+) => {
const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL;
const realEstatesToShow = truncateList
? realEstates.slice(0, MAX_REAL_ESTATES_IN_EMAIL)
@@ -30,9 +34,20 @@ const generateNotificationEmail = (realEstates, searchRequestId) => {
const realEstateLinks = generateRealEstateLinks(realEstatesToShow);
const moreRealEstates = `
Kompletan spisak nekretnina možete pogledati na
listi nekretnina`;
const emailFooter = generateEmailFooter(searchRequestId);
+ const asapMessageBody =
+ realEstates.length > 1
+ ? "Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi"
+ : "Pronašli smo nekretninu koja odgovara Vašoj pretrazi";
+
+ const dailyMessageBody =
+ realEstates.length > 1
+ ? "U posljednja 24h objavljene su sljedeće nekretnine koje odgovaraju uslovima Vaše pretrage"
+ : "U posljednja 24h objavljena je sljedeća nekretnina koja odgovara uslovima Vaše pretrage";
+
+ const messageBody = dailyNotification ? dailyMessageBody : asapMessageBody;
return `
Zdravo
-
Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi
+
${messageBody}
${realEstateLinks}
diff --git a/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js b/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js
new file mode 100644
index 0000000..3d39291
--- /dev/null
+++ b/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js
@@ -0,0 +1,15 @@
+"use strict";
+const { EMAIL_FREQUENCY } = require("../common/enums");
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("SearchRequests", "emailFrequency", {
+ type: Sequelize.TEXT,
+ defaultValue: EMAIL_FREQUENCY.ASAP.stringId
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("SearchRequests", "emailFrequency");
+ }
+};
diff --git a/app/models/searchRequest.js b/app/models/searchRequest.js
index 71ab26e..68483ed 100644
--- a/app/models/searchRequest.js
+++ b/app/models/searchRequest.js
@@ -1,6 +1,6 @@
"use strict";
-const { AD_TYPE } = require("../common/enums");
+const { AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums");
module.exports = (sequelize, DataTypes) => {
const SearchRequest = sequelize.define("SearchRequest", {
@@ -61,6 +61,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false
+ },
+ emailFrequency: {
+ type: DataTypes.TEXT,
+ defaultValue: EMAIL_FREQUENCY.ASAP.stringId,
+ allowNull: false
}
});
diff --git a/app/models/searchRequestMatch.js b/app/models/searchRequestMatch.js
index 19f367d..0e2e392 100644
--- a/app/models/searchRequestMatch.js
+++ b/app/models/searchRequestMatch.js
@@ -44,6 +44,12 @@ module.exports = (sequelize, DataTypes) => {
);
SearchRequestMatch.associate = models => {
+ SearchRequestMatch.hasMany(models.SearchRequest, {
+ foreignKey: "id",
+ sourceKey: "searchRequestId",
+ targetKey: "id",
+ as: "searchRequests"
+ });
SearchRequestMatch.hasMany(models.RealEstate, {
foreignKey: "id",
as: "realEstates"
diff --git a/app/npmScripts/npmDailyNotify.js b/app/npmScripts/npmDailyNotify.js
new file mode 100644
index 0000000..8eb6d33
--- /dev/null
+++ b/app/npmScripts/npmDailyNotify.js
@@ -0,0 +1,8 @@
+"use strict";
+const {
+ notifyRequestsWithDailyOption
+} = require("../services/notificationService");
+
+(async () => {
+ await notifyRequestsWithDailyOption();
+})();
diff --git a/app/public/segment.css b/app/public/segment.css
index fdd0372..313de5d 100644
--- a/app/public/segment.css
+++ b/app/public/segment.css
@@ -1,14 +1,18 @@
-.ui-segment {
+.segmented {
color: #02adba;
border: 1px solid #02adba;
border-radius: 4px;
display: inline-block;
}
-.ui-segment span.option.active {
+.segmented label {
+ color: #02adba;
+}
+.segmented input:checked + .label {
background-color: #02adba;
color: white;
}
-.ui-segment span.option {
+[type="radio"]:not(:checked) + span,
+[type="radio"]:checked + span {
padding-left: 30px;
padding-right: 30px;
height: 35px;
@@ -21,9 +25,14 @@
border-right: 1px solid #02adba;
}
-.ui-segment span.option:last-child {
+.segmented :last-child .label {
border-right: none;
}
-.segment-select {
+.segmented input {
+ display: none;
+}
+
+span.label:before,
+span.label:after {
display: none;
}
diff --git a/app/services/notificationService.js b/app/services/notificationService.js
index 7a6c2d5..194d3f6 100644
--- a/app/services/notificationService.js
+++ b/app/services/notificationService.js
@@ -8,6 +8,7 @@ const {
generateNewSearchRequestEmail,
generateEmailSubject
} = require("../helpers/emailContentGenerator");
+const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch");
const { sendEmail } = require("../services/emailService");
const notifyForNewRealEstates = async newRealEstates => {
@@ -29,34 +30,87 @@ const notifyForNewSearchRequest = async searchRequest => {
await sendEmail(email, "Kivi - novi zahtjev za pretragu", emailContent);
};
-const notifyMatches = async matches => {
+const notifyMatches = async (matches, dailyNotification = false) => {
const searchRequestsToNotify = Object.keys(matches);
const asyncSendEmailActions = [];
for (const id of searchRequestsToNotify) {
- const { searchRequest } = matches[id];
- const { email } = searchRequest;
- const allMatchingRealEstates = matches[id].realEstates || [];
- if (allMatchingRealEstates.length > 0) {
- const emailContent = generateNotificationEmail(
- allMatchingRealEstates,
- id
- );
- const emailSubject = generateEmailSubject(
- allMatchingRealEstates.length,
- allMatchingRealEstates[0].title
- );
+ const { searchRequest, notifyNow } = matches[id];
+ if (notifyNow) {
+ const { email } = searchRequest;
+ const allMatchingRealEstates = matches[id].realEstates || [];
+ if (allMatchingRealEstates.length > 0) {
+ const emailContent = generateNotificationEmail(
+ allMatchingRealEstates,
+ id,
+ dailyNotification
+ );
+ const emailSubject = generateEmailSubject(
+ allMatchingRealEstates.length,
+ allMatchingRealEstates[0].title
+ );
- const sendEmailPromise = sendEmail(email, emailSubject, emailContent);
- asyncSendEmailActions.push(sendEmailPromise);
- sendEmailPromise.catch(err => console.log("[Email Sending Failed]", err));
+ const sendEmailPromise = sendEmail(email, emailSubject, emailContent);
+ asyncSendEmailActions.push(sendEmailPromise);
+ sendEmailPromise.catch(err =>
+ console.log("[Email Sending Failed]", err)
+ );
+ }
}
}
await Promise.all(asyncSendEmailActions);
};
+const notifyRequestsWithDailyOption = async () => {
+ const notNotifiedSearchRequestMatches = await findNotNotifiedMatches();
+
+ const matches = {};
+
+ for (const searchRequestMatch of notNotifiedSearchRequestMatches) {
+ const { searchRequests, realEstates } = searchRequestMatch;
+
+ if (!Array.isArray(searchRequests) || searchRequests.length !== 1) {
+ // Something is wrong with this match
+ // (search request not found for specified search request id)
+ // OR
+ // there are multiple search requests with the same ID (this should never be the case !
+ // TODO: Maybe if association is defined better, this will be automatically only one object instead of array
+ continue;
+ }
+
+ if (!Array.isArray(realEstates) || realEstates.length !== 1) {
+ // Something is wrong with this match
+ // (real estate not found for specified real estate id)
+ // OR
+ // there are multiple real estates with the same ID (this should never be the case !
+ // TODO: Maybe if association is defined better, this will be automatically only one object instead of array
+ continue;
+ }
+
+ const searchRequest = searchRequests[0];
+ const realEstate = realEstates[0];
+ const searchRequestId = searchRequest.id;
+
+ if (!matches[searchRequestId]) {
+ matches[searchRequestId] = {
+ searchRequest,
+ realEstates: [],
+ notifyNow: true
+ };
+ }
+
+ matches[searchRequestId].realEstates.push(realEstate);
+
+ searchRequestMatch.notified = true;
+ searchRequestMatch.save();
+ }
+
+ await notifyMatches(matches, true);
+};
+
module.exports = {
notifyForNewRealEstates,
- notifyForNewSearchRequest
+ notifyForNewSearchRequest,
+ notifyRequestsWithDailyOption
};
diff --git a/app/services/searchMatchService.js b/app/services/searchMatchService.js
index 4c83504..f39d21d 100644
--- a/app/services/searchMatchService.js
+++ b/app/services/searchMatchService.js
@@ -6,6 +6,7 @@ const {
const { findRealEstatesForSearchRequest } = require("../helpers/db/realEstate");
const { addMatches } = require("../helpers/db/searchRequestMatch");
const { MAX_REAL_ESTATES_IN_FIRST_EMAIL } = require("../config/appConfig");
+const { EMAIL_FREQUENCY } = require("../common/enums");
const matchRealEstates = async realEstates => {
if (Array.isArray(realEstates)) {
@@ -18,18 +19,19 @@ const matchRealEstates = async realEstates => {
searchRequestsPromise.then(searchRequests => {
for (const searchRequest of searchRequests) {
- const { id } = searchRequest;
+ const { id, emailFrequency } = searchRequest;
if (!matches[id]) {
matches[id] = {
searchRequest,
- realEstates: []
+ realEstates: [],
+ notifyNow: emailFrequency === EMAIL_FREQUENCY.ASAP.stringId
};
}
matches[id].realEstates.push(realEstate);
matchingRecords.push({
searchRequestId: searchRequest.id,
realEstateId: realEstate.id,
- notified: false
+ notified: emailFrequency === EMAIL_FREQUENCY.ASAP.stringId
});
}
});
@@ -62,7 +64,7 @@ const matchSearchRequest = async searchRequest => {
matchingRecords.push({
searchRequestId,
realEstateId: realEstate.id,
- notified: false
+ notified: true
});
}
diff --git a/app/views/queryReview.ejs b/app/views/queryReview.ejs
index 115b067..7e0f6e4 100644
--- a/app/views/queryReview.ejs
+++ b/app/views/queryReview.ejs
@@ -2,7 +2,7 @@