From 3271165a37796bc9727e065d9ecb376c7cbfebdc Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:03:35 -0500 Subject: [PATCH 01/62] Create pull_request_template.md --- .github/pull_request_template.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..8e950a23 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Describe your changes + +Briefly describe the changes you made and their purpose. + +## Issue number + +Mention the issue number(s) this PR addresses (e.g., #123). + +## Please ensure all items are checked off before requesting a review: + +- [ ] I deployed the code locally. +- [ ] I have performed a self-review of my code. +- [ ] I have included the issue # in the PR. +- [ ] I have labelled the PR correctly. +- [ ] The issue I am working on is assigned to me. +- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). +- [ ] I made sure font sizes, color choices etc are all referenced from the theme. +- [ ] My PR is granular and targeted to one specific feature. +- [ ] I took a screenshot or a video and attached to this PR if there is a UI change. From 052f22382b271ec9e2455b453a05016a41bb316f Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:14:41 -0500 Subject: [PATCH 02/62] Add installation document to readme.md --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/README.md b/README.md index a7bf2ff9..f507733a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,84 @@ This is a work-in-progress application. The source code is available under GNU A - [Node.js](https://nodejs.org/en) - [PostgreSQL](https://postgresql.org) + +## Installation + +1. Make sure Docker is installed to your machine where the server will run. +2. Make sure git is installed to your machine Git. +3. Make sure nginx is installed. + +4. Clone GitHub Repository + +``cd ~ +git clone https://github.com/bluewave-labs/bluewave-onboarding.git +cd bluewave-onboarding`` + +5. Configure Nginx + +Open the Nginx configuration file: + +``sudo nano /etc/nginx/sites-available/onboarding-demo`` + +Add the following configuration. Change YOUR_DOMAIN_NAME with your domain name: + +``server { + listen 80; + server_name YOUR_DOMAIN_NAME; + return 301 https://$host$request_uri; +} +server { + listen 443 ssl; + server_name YOUR_DOMAIN_NAME; + + ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN_NAME/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN_NAME/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + + location / { + proxy_pass http://localhost:4173; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/ { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}`` + +6. Create a symbolic link to enable the configuration: + +``sudo ln -s /etc/nginx/sites-available/onboarding-demo /etc/nginx/sites-enabled/`` + +7. Install Certbot and its Nginx plugin: + +``sudo apt install certbot python3-certbot-nginx`` + +8. Obtain SSL Certificate. Run Certbot to obtain a certificate for your domain: + +``sudo certbot --nginx`` + +9. Verify the Nginx configuration: + +``sudo nginx -t`` + +10. Restart Nginx to apply the changes: + +``sudo systemctl restart nginx`` + +11. Start the project + +``cd ~/bluewave-onboarding +docker compose up -d`` + ## Contributing Here's how you can contribute to the Onboarding product. From 59fa49325703bfe37978a8d466579be13ea24002 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:31:22 -0500 Subject: [PATCH 03/62] fix backticks --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f507733a..07af1441 100644 --- a/README.md +++ b/README.md @@ -50,21 +50,20 @@ Open the Nginx configuration file: Add the following configuration. Change YOUR_DOMAIN_NAME with your domain name: -``server { +```server { listen 80; server_name YOUR_DOMAIN_NAME; - return 301 https://$host$request_uri; -} + return 301 https://$host$request_uri; + } + server { listen 443 ssl; server_name YOUR_DOMAIN_NAME; - ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN_NAME/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN_NAME/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - location / { proxy_pass http://localhost:4173; proxy_set_header Host $host; @@ -80,7 +79,9 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } -}`` +} +``` + 6. Create a symbolic link to enable the configuration: From 5861c70c92559706ab25a703aa487acd3e6a248f Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:42:17 -0500 Subject: [PATCH 04/62] Fixes for installation scripts --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07af1441..2906a297 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ This is a work-in-progress application. The source code is available under GNU A 4. Clone GitHub Repository -``cd ~ +``` +cd ~ git clone https://github.com/bluewave-labs/bluewave-onboarding.git -cd bluewave-onboarding`` +cd bluewave-onboarding +``` 5. Configure Nginx From b00bcb3076da404d908148902481884d86e87a29 Mon Sep 17 00:00:00 2001 From: tunckiral Date: Wed, 18 Dec 2024 10:55:28 -0500 Subject: [PATCH 05/62] adding agent code --- backend/src/controllers/guide.controller.js | 2 +- .../src/scenes/settings/CodeTab/CodeTab.jsx | 5 +- jsAgent/links.js | 32 +++++++++--- jsAgent/main.js | 2 +- jsAgent/popupRender.js | 52 ------------------- 5 files changed, 30 insertions(+), 63 deletions(-) delete mode 100644 jsAgent/popupRender.js diff --git a/backend/src/controllers/guide.controller.js b/backend/src/controllers/guide.controller.js index e1f432cf..965347e9 100644 --- a/backend/src/controllers/guide.controller.js +++ b/backend/src/controllers/guide.controller.js @@ -2,7 +2,7 @@ const bannerService = require("../service/banner.service"); const guidelogService = require("../service/guidelog.service"); const helperLinkService = require("../service/helperLink.service"); const popupService = require("../service/popup.service"); - +const { internalServerError } = require("../utils/errors.helper"); class GuideController { async getGuidesByUrl(req, res) { try { diff --git a/frontend/src/scenes/settings/CodeTab/CodeTab.jsx b/frontend/src/scenes/settings/CodeTab/CodeTab.jsx index aad20339..d453ef4a 100644 --- a/frontend/src/scenes/settings/CodeTab/CodeTab.jsx +++ b/frontend/src/scenes/settings/CodeTab/CodeTab.jsx @@ -64,15 +64,14 @@ const CodeTab = () => { {` `} diff --git a/jsAgent/links.js b/jsAgent/links.js index ffcd3320..c07fd810 100644 --- a/jsAgent/links.js +++ b/jsAgent/links.js @@ -2,7 +2,10 @@ console.log('link.js is loaded'); const linksDefaultOptions = { "url": "https://www.google.com", "order": 1, - "target": true + "target": true, + headerBackgroundColor: "#F8F9F8", + iconColor: "#7F56D9", + linkFontColor: "#344054" }; @@ -16,14 +19,31 @@ bw.links={ }, putIcon:function(){ let temp_html = ` - + `; document.body.insertAdjacentHTML('beforeend', temp_html); }, putPlaceHolder: function(){ - let temp_html = ``; - document.getElementById('bw-links').insertAdjacentHTML('beforebegin', temp_html); + const options = window.bwonboarddata.helperLink[0]; + let option = { + ...linksDefaultOptions, + ...options + }; + + const temp_html = ``; + + document.getElementById('bw-link-icon').insertAdjacentHTML('beforebegin', temp_html); + }, + + generateHeader: function(bgColor){ + const temp_header_html =`
`; }, bindClick : function(){ bw.util.bindLive("#bw-links", "click", function(){ @@ -31,7 +51,7 @@ bw.links={ }) }, show : function(){ - document.getElementById('bw-links-placeholder').style.display = 'block'; + document.getElementById('bw-links-placeholder').style.display = 'flex'; }, hide : function(){ document.getElementById('bw-links-placeholder').style.display = 'none'; diff --git a/jsAgent/main.js b/jsAgent/main.js index fbc192d6..b5b9944a 100644 --- a/jsAgent/main.js +++ b/jsAgent/main.js @@ -168,7 +168,7 @@ bw.init = (cb) => { if (onBoardConfig.banner?.length > 0) { bw.util.loadScriptAsync(BW_BANNER_JS_URL); } - if (onBoardConfig.link?.length > 0) { + if (onBoardConfig.helperLink?.length > 0) { bw.util.loadScriptAsync(BW_LINKS_JS_URL); } } catch (error) { diff --git a/jsAgent/popupRender.js b/jsAgent/popupRender.js deleted file mode 100644 index 6c8cb366..00000000 --- a/jsAgent/popupRender.js +++ /dev/null @@ -1,52 +0,0 @@ -const showPopup = (popupData) => { - if (!popupData || popupData.length === 0) { - console.warn('No popup data available'); - return; - } - - popupData.forEach(popup => { - // Create popup container - const popupContainer = document.createElement('div'); - Object.assign(popupContainer.style, { - position: 'fixed', - bottom: '20px', - right: '20px', - backgroundColor: popup.headerBg || '#FFFFFF', - padding: '20px', - border: '1px solid #000', - zIndex: 1000, - }); - - // Create header - const header = document.createElement('h2'); - header.textContent = popup.headerText; - header.style.color = popup.headerTextColor || '#000'; - popupContainer.appendChild(header); - - // Create content - const content = document.createElement('div'); - content.innerHTML = popup.contentHtml; - Object.assign(content.style, { - color: popup.fontColor || '#000', - fontSize: popup.font || '14px', - }); - popupContainer.appendChild(content); - - // Create action button - const actionButton = document.createElement('button'); - actionButton.textContent = popup.actionButtonText || 'Close'; - Object.assign(actionButton.style, { - backgroundColor: popup.actionButtonColor || '#CCC', - }); - actionButton.addEventListener('click', () => { - document.body.removeChild(popupContainer); // Remove popup when button is clicked - }); - popupContainer.appendChild(actionButton); - - // Append the popup to the document body - document.body.appendChild(popupContainer); - }); - }; - - export default showPopup; - \ No newline at end of file From bdd5d6f459601dc22a1fcea3c3d1c45260482783 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:04:34 -0800 Subject: [PATCH 06/62] feat: add statistics model and migration --- .../20241219015147-create-statistics.js | 87 +++++++++++++++++++ backend/src/models/Statistics.js | 85 ++++++++++++++++++ backend/src/models/index.js | 1 + 3 files changed, 173 insertions(+) create mode 100644 backend/migrations/20241219015147-create-statistics.js create mode 100644 backend/src/models/Statistics.js diff --git a/backend/migrations/20241219015147-create-statistics.js b/backend/migrations/20241219015147-create-statistics.js new file mode 100644 index 00000000..f4a8bba7 --- /dev/null +++ b/backend/migrations/20241219015147-create-statistics.js @@ -0,0 +1,87 @@ +"use strict"; + +const { GuideType } = require("../src/utils/guidelog.helper"); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + "Statistics", + { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + guideType: { + type: Sequelize.INTEGER, + allowNull: false, + validate: { + isIn: Object.values(GuideType), + }, + }, + views: { + type: Sequelize.NUMBER, + allowNull: false, + defaultValue: 0, + validate: { + isInt: true, + isPositive: true, + }, + }, + change: { + type: Sequelize.NUMBER, + allowNull: false, + defaultValue: 0, + validate: { + isFloat: true, + }, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + }, + }, + { + transaction, + } + ); + await queryInterface.addIndex("Statistics", ["guideType"], { + name: "idx_statistics_guideType", + transaction, + }); + await queryInterface.addIndex("Statistics", ["createdAt"], { + name: "idx_statistics_createdAt", + transaction, + }); + await queryInterface.addIndex("Statistics", ["updatedAt"], { + name: "idx_statistics_updatedAt", + transaction, + }); + await queryInterface.addIndex("Statistics", ["views"], { + name: "idx_statistics_views", + transaction, + }); + await queryInterface.addIndex("Statistics", ["change"], { + name: "idx_statistics_change", + transaction, + }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("Statistics"); + }, +}; diff --git a/backend/src/models/Statistics.js b/backend/src/models/Statistics.js new file mode 100644 index 00000000..1fec2dbf --- /dev/null +++ b/backend/src/models/Statistics.js @@ -0,0 +1,85 @@ +"use strict"; +const { Model } = require("sequelize"); +const { GuideType } = require("../utils/guidelog.helper"); +module.exports = (sequelize, DataTypes) => { + class Statistics extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + } + Statistics.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + guideType: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: Object.keys(GuideType), + }, + }, + views: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + isInt: true, + isPositive: true, + }, + }, + change: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + isFloat: true, + }, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: sequelize.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: sequelize.NOW, + }, + }, + { + sequelize, + modelName: "Statistics", + indexes: [ + { + name: "idx_statistics_createdAt", + fields: ["createdAt"], + }, + { + name: "idx_statistics_updatedAt", + fields: ["updatedAt"], + }, + { + name: "idx_statistics_guideType", + fields: ["guideType"], + }, + { + name: "idx_statistics_views", + fields: ["views"], + }, + { + name: "idx_statistics_change", + fields: ["change"], + }, + ], + } + ); + return Statistics; +}; diff --git a/backend/src/models/index.js b/backend/src/models/index.js index f23d7312..3e45732b 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -33,6 +33,7 @@ db.Hint = require("./Hint.js")(sequelize, Sequelize.DataTypes); db.Tour = require("./Tour.js")(sequelize, Sequelize.DataTypes); db.Link = require("./Link.js")(sequelize, Sequelize.DataTypes); db.HelperLink = require("./HelperLink.js")(sequelize, Sequelize.DataTypes); +db.Statistics = require("./Statistics.js")(sequelize, Sequelize.DataTypes); // Define associations here db.User.hasMany(db.Popup, { foreignKey: "createdBy", as: "popups" }); From 077f7d55f0f66cc6177fbd92b4ad4f15eb511e4e Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:04:48 -0800 Subject: [PATCH 07/62] feat: add statistics service --- backend/src/service/statistics.service.js | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 backend/src/service/statistics.service.js diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js new file mode 100644 index 00000000..941bfbc9 --- /dev/null +++ b/backend/src/service/statistics.service.js @@ -0,0 +1,59 @@ +const db = require("../models"); +const { GuideType } = require("../utils/guidelog.helper"); +const GuideLog = db.GuideLog; +const Statistics = db.Statistics; + +class StatisticsService { + async generateStatistics({ userId }) { + const views = await Promise.all( + Object.entries(GuideLog).map(async ([guideName, guideType]) => { + await GuideLog.findAll({ + where: { + guideType, + userId, + }, + }); + const thisMonth = new Date(); + thisMonth.setDate(thisMonth.getDate() - 30); + const lastMonth = new Date(); + lastMonth.setDate(lastMonth.getDate() - 60); + const thisMonthViews = views.filter( + (view) => view.showingTime >= thisMonth + ); + const lastMonthViews = views.filter( + (view) => + view.showingTime >= lastMonth && view.showingTime < thisMonth + ); + const percentageDifference = Math.round( + ((thisMonthViews.length - lastMonthViews.length) / + lastMonthViews.length) * + 100 + ); + const result = { + views: thisMonthViews.length, + change: percentageDifference, + guideType: guideName, + }; + const newData = await Statistics.create(result); + return newData; + }) + ); + return views; + } + + async getStatisticsByUserId({ userId }) { + return await Statistics.findAll({ + where: { userId }, + order: [["createdAt", "DESC"]], + }); + } + + async getStatisticsByGuideType({ guideType, userId }) { + return await Statistics.findAll({ + where: { guideType, userId }, + order: [["createdAt", "DESC"]], + }); + } +} + +module.exports = new StatisticsService(); From 2deabd6d18da2cc7d58371a6a1d38fa258788eaa Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:15:12 -0800 Subject: [PATCH 08/62] feat: update logic to use realtime data --- .../20241219015147-create-statistics.js | 87 --------------- .../20241219181037-add-index-guidelog.js | 28 +++++ backend/src/models/GuideLog.js | 104 +++++++++--------- backend/src/models/Statistics.js | 85 -------------- backend/src/models/index.js | 1 - backend/src/service/statistics.service.js | 22 +--- 6 files changed, 86 insertions(+), 241 deletions(-) delete mode 100644 backend/migrations/20241219015147-create-statistics.js create mode 100644 backend/migrations/20241219181037-add-index-guidelog.js delete mode 100644 backend/src/models/Statistics.js diff --git a/backend/migrations/20241219015147-create-statistics.js b/backend/migrations/20241219015147-create-statistics.js deleted file mode 100644 index f4a8bba7..00000000 --- a/backend/migrations/20241219015147-create-statistics.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; - -const { GuideType } = require("../src/utils/guidelog.helper"); - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - const transaction = await queryInterface.sequelize.transaction(); - try { - await queryInterface.createTable( - "Statistics", - { - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: Sequelize.INTEGER, - }, - guideType: { - type: Sequelize.INTEGER, - allowNull: false, - validate: { - isIn: Object.values(GuideType), - }, - }, - views: { - type: Sequelize.NUMBER, - allowNull: false, - defaultValue: 0, - validate: { - isInt: true, - isPositive: true, - }, - }, - change: { - type: Sequelize.NUMBER, - allowNull: false, - defaultValue: 0, - validate: { - isFloat: true, - }, - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - }, - }, - { - transaction, - } - ); - await queryInterface.addIndex("Statistics", ["guideType"], { - name: "idx_statistics_guideType", - transaction, - }); - await queryInterface.addIndex("Statistics", ["createdAt"], { - name: "idx_statistics_createdAt", - transaction, - }); - await queryInterface.addIndex("Statistics", ["updatedAt"], { - name: "idx_statistics_updatedAt", - transaction, - }); - await queryInterface.addIndex("Statistics", ["views"], { - name: "idx_statistics_views", - transaction, - }); - await queryInterface.addIndex("Statistics", ["change"], { - name: "idx_statistics_change", - transaction, - }); - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - }, - async down(queryInterface, Sequelize) { - await queryInterface.dropTable("Statistics"); - }, -}; diff --git a/backend/migrations/20241219181037-add-index-guidelog.js b/backend/migrations/20241219181037-add-index-guidelog.js new file mode 100644 index 00000000..c4b45874 --- /dev/null +++ b/backend/migrations/20241219181037-add-index-guidelog.js @@ -0,0 +1,28 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + /** + * Add altering commands here. + * + * Example: + * await queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + await queryInterface.addIndex("guide_logs", ["showingTime"], { + name: "idx_guide_logs_showingTime", + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeIndex("guide_logs", ["showingTime"], { + name: "idx_guide_logs_showingTime", + }); + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + }, +}; diff --git a/backend/src/models/GuideLog.js b/backend/src/models/GuideLog.js index c007fddc..d4841ef6 100644 --- a/backend/src/models/GuideLog.js +++ b/backend/src/models/GuideLog.js @@ -1,58 +1,64 @@ -const { GuideType } = require('../utils/guidelog.helper'); +const { GuideType } = require("../utils/guidelog.helper"); module.exports = (sequelize, DataTypes) => { - const GuideLog = sequelize.define('GuideLog', { - guideType: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - isIn: { - args: [ - Object.values(GuideType), - ], - msg: 'guideType must be a valid value.', - }, - }, + const GuideLog = sequelize.define( + "GuideLog", + { + guideType: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + isIn: { + args: [Object.values(GuideType)], + msg: "guideType must be a valid value.", + }, }, - guideId: { - type: DataTypes.INTEGER, - allowNull: false, + }, + guideId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + userId: { + type: DataTypes.STRING, + allowNull: false, + }, + showingTime: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + completed: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + }, + { + timestamps: false, + tableName: "guide_logs", + indexes: [ + { + name: "idx_guide_logs_userId", + fields: ["userId"], }, - userId: { - type: DataTypes.STRING, - allowNull: false, + { + name: "idx_guide_logs_guideId", + fields: ["guideId"], }, - showingTime: { - type: DataTypes.DATE, - defaultValue: DataTypes.NOW, + { + name: "idx_guide_logs_guideType", + fields: ["guideType"], }, - completed: { - type: DataTypes.BOOLEAN, - defaultValue: false, + { + name: "idx_guide_logs_userId_guideId_guideType", + fields: ["userId", "guideId", "guideType"], + unique: false, }, - }, { - timestamps: false, - tableName: 'guide_logs', - indexes: [ - { - name: 'idx_guide_logs_userId', - fields: ['userId'], - }, - { - name: 'idx_guide_logs_guideId', - fields: ['guideId'], - }, - { - name: 'idx_guide_logs_guideType', - fields: ['guideType'], - }, - { - name: 'idx_guide_logs_userId_guideId_guideType', - fields: ['userId', 'guideId', 'guideType'], - unique: false, - }, - ], - }); + { + name: "idx_guide_logs_showingTime", + fields: ["showingTime"], + }, + ], + } + ); - return GuideLog; + return GuideLog; }; diff --git a/backend/src/models/Statistics.js b/backend/src/models/Statistics.js deleted file mode 100644 index 1fec2dbf..00000000 --- a/backend/src/models/Statistics.js +++ /dev/null @@ -1,85 +0,0 @@ -"use strict"; -const { Model } = require("sequelize"); -const { GuideType } = require("../utils/guidelog.helper"); -module.exports = (sequelize, DataTypes) => { - class Statistics extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ - static associate(models) { - // define association here - } - } - Statistics.init( - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - }, - guideType: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isIn: Object.keys(GuideType), - }, - }, - views: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - isInt: true, - isPositive: true, - }, - }, - change: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - isFloat: true, - }, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: sequelize.NOW, - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: sequelize.NOW, - }, - }, - { - sequelize, - modelName: "Statistics", - indexes: [ - { - name: "idx_statistics_createdAt", - fields: ["createdAt"], - }, - { - name: "idx_statistics_updatedAt", - fields: ["updatedAt"], - }, - { - name: "idx_statistics_guideType", - fields: ["guideType"], - }, - { - name: "idx_statistics_views", - fields: ["views"], - }, - { - name: "idx_statistics_change", - fields: ["change"], - }, - ], - } - ); - return Statistics; -}; diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 3e45732b..f23d7312 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -33,7 +33,6 @@ db.Hint = require("./Hint.js")(sequelize, Sequelize.DataTypes); db.Tour = require("./Tour.js")(sequelize, Sequelize.DataTypes); db.Link = require("./Link.js")(sequelize, Sequelize.DataTypes); db.HelperLink = require("./HelperLink.js")(sequelize, Sequelize.DataTypes); -db.Statistics = require("./Statistics.js")(sequelize, Sequelize.DataTypes); // Define associations here db.User.hasMany(db.Popup, { foreignKey: "createdBy", as: "popups" }); diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index 941bfbc9..f582efdf 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -1,12 +1,11 @@ const db = require("../models"); const { GuideType } = require("../utils/guidelog.helper"); const GuideLog = db.GuideLog; -const Statistics = db.Statistics; class StatisticsService { async generateStatistics({ userId }) { const views = await Promise.all( - Object.entries(GuideLog).map(async ([guideName, guideType]) => { + Object.entries(GuideType).map(async ([guideName, guideType]) => { await GuideLog.findAll({ where: { guideType, @@ -32,28 +31,13 @@ class StatisticsService { const result = { views: thisMonthViews.length, change: percentageDifference, - guideType: guideName, + guideType: guideName.toLowerCase(), }; - const newData = await Statistics.create(result); - return newData; + return result; }) ); return views; } - - async getStatisticsByUserId({ userId }) { - return await Statistics.findAll({ - where: { userId }, - order: [["createdAt", "DESC"]], - }); - } - - async getStatisticsByGuideType({ guideType, userId }) { - return await Statistics.findAll({ - where: { guideType, userId }, - order: [["createdAt", "DESC"]], - }); - } } module.exports = new StatisticsService(); From d71f0a6d8ffc5c0cf9dd4e8c190a8aeba6fca6f5 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:17:23 -0800 Subject: [PATCH 09/62] feat: add statistics controller --- backend/src/controllers/statistics.controller.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/src/controllers/statistics.controller.js diff --git a/backend/src/controllers/statistics.controller.js b/backend/src/controllers/statistics.controller.js new file mode 100644 index 00000000..fe269d86 --- /dev/null +++ b/backend/src/controllers/statistics.controller.js @@ -0,0 +1,15 @@ +const statisticsService = require("../service/statistics.service"); + +class StatisticsController { + async getStatistics(req, res) { + try { + const userId = req.user.id; + const statistics = await statisticsService.generateStatistics({ userId }); + res.status(200).json(statistics); + } catch { + res.status(500).json({ message: "Internal server error" }); + } + } +} + +module.exports = new StatisticsController(); From c9f2a166e1024bbf97e6edb4c263c16810f0a1f0 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:19:29 -0800 Subject: [PATCH 10/62] feat: add statistics route --- backend/src/routes/statistics.routes.js | 8 +++ backend/src/server.js | 74 +++++++++++++------------ 2 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 backend/src/routes/statistics.routes.js diff --git a/backend/src/routes/statistics.routes.js b/backend/src/routes/statistics.routes.js new file mode 100644 index 00000000..ffd1105d --- /dev/null +++ b/backend/src/routes/statistics.routes.js @@ -0,0 +1,8 @@ +const express = require("express"); +const statisticsController = require("../controllers/statistics.controller"); + +const router = express.Router(); + +router.get("/", statisticsController.getStatistics); + +module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js index 5a8d530b..da72bd4a 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,27 +1,28 @@ -const express = require('express'); -const cors = require('cors'); -const helmet = require('helmet'); -const dotenv = require('dotenv'); -const bodyParser = require('body-parser'); -const jsonErrorMiddleware = require('./middleware/jsonError.middleware'); -const fileSizeValidator = require('./middleware/fileSizeValidator.middleware'); -const { MAX_FILE_SIZE } = require('./utils/constants.helper'); +const express = require("express"); +const cors = require("cors"); +const helmet = require("helmet"); +const dotenv = require("dotenv"); +const bodyParser = require("body-parser"); +const jsonErrorMiddleware = require("./middleware/jsonError.middleware"); +const fileSizeValidator = require("./middleware/fileSizeValidator.middleware"); +const { MAX_FILE_SIZE } = require("./utils/constants.helper"); // Load environment variables from .env file dotenv.config(); -const authRoutes = require('./routes/auth.routes'); -const userRoutes = require('./routes/user.routes'); -const mocks = require('./routes/mocks.routes'); -const popup = require('./routes/popup.routes'); -const guide_log = require('./routes/guidelog.routes'); -const banner = require('./routes/banner.routes'); -const teamRoutes = require('./routes/team.routes'); -const hint = require('./routes/hint.routes'); -const tourRoutes = require('./routes/tour.routes'); -const linkRoutes = require('./routes/link.routes'); -const helperLinkRoutes = require('./routes/helperLink.routes'); -const guideRoutes = require('./routes/guide.routes'); +const authRoutes = require("./routes/auth.routes"); +const userRoutes = require("./routes/user.routes"); +const mocks = require("./routes/mocks.routes"); +const popup = require("./routes/popup.routes"); +const guide_log = require("./routes/guidelog.routes"); +const banner = require("./routes/banner.routes"); +const teamRoutes = require("./routes/team.routes"); +const hint = require("./routes/hint.routes"); +const tourRoutes = require("./routes/tour.routes"); +const linkRoutes = require("./routes/link.routes"); +const helperLinkRoutes = require("./routes/helperLink.routes"); +const guideRoutes = require("./routes/guide.routes"); +const statisticsRoutes = require("./routes/statistics.routes"); const app = express(); @@ -35,30 +36,31 @@ const { sequelize } = require("./models"); sequelize .authenticate() - .then(() => console.log('Database connected...')) - .catch((err) => console.log('Error: ' + err)); + .then(() => console.log("Database connected...")) + .catch((err) => console.log("Error: " + err)); sequelize .sync({ force: false }) .then(() => console.log("Models synced with the database...")) .catch((err) => console.log("Error syncing models: " + err)); -app.use('/api/auth', authRoutes); -app.use('/api/users', userRoutes); -app.use('/api/mock/', mocks); -app.use('/api/popup', popup); -app.use('/api/guide_log', guide_log); -app.use('/api/banner', banner); -app.use('/api/team', teamRoutes); -app.use('/api/guide', guideRoutes); -app.use('/api/hint', hint); -app.use('/api/tour', tourRoutes); -app.use('/api/link', linkRoutes); -app.use('/api/helper-link', helperLinkRoutes); +app.use("/api/auth", authRoutes); +app.use("/api/users", userRoutes); +app.use("/api/mock/", mocks); +app.use("/api/popup", popup); +app.use("/api/guide_log", guide_log); +app.use("/api/banner", banner); +app.use("/api/team", teamRoutes); +app.use("/api/guide", guideRoutes); +app.use("/api/hint", hint); +app.use("/api/tour", tourRoutes); +app.use("/api/link", linkRoutes); +app.use("/api/helper-link", helperLinkRoutes); +app.use("/api/statistics", statisticsRoutes); app.use((err, req, res, next) => { console.error(err.stack); - res.status(500).json({ message: 'Internal Server Error' }); + res.status(500).json({ message: "Internal Server Error" }); }); -module.exports = app; \ No newline at end of file +module.exports = app; From b470793458f8638a36062d51905b8a78b0936a5e Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:35:16 -0800 Subject: [PATCH 11/62] test: add statistics service test --- backend/src/service/statistics.service.js | 24 ++++++----- backend/src/test/mocks/guidelog.mock.js | 27 +++++++++--- .../src/test/unit/services/statistics.test.js | 42 +++++++++++++++++++ 3 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 backend/src/test/unit/services/statistics.test.js diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index f582efdf..fc4d570a 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -6,7 +6,7 @@ class StatisticsService { async generateStatistics({ userId }) { const views = await Promise.all( Object.entries(GuideType).map(async ([guideName, guideType]) => { - await GuideLog.findAll({ + const logs = await GuideLog.findAll({ where: { guideType, userId, @@ -16,18 +16,20 @@ class StatisticsService { thisMonth.setDate(thisMonth.getDate() - 30); const lastMonth = new Date(); lastMonth.setDate(lastMonth.getDate() - 60); - const thisMonthViews = views.filter( - (view) => view.showingTime >= thisMonth + const thisMonthViews = logs.filter( + (log) => log.showingTime >= thisMonth && log.guideType === guideType ); - const lastMonthViews = views.filter( - (view) => - view.showingTime >= lastMonth && view.showingTime < thisMonth - ); - const percentageDifference = Math.round( - ((thisMonthViews.length - lastMonthViews.length) / - lastMonthViews.length) * - 100 + const lastMonthViews = logs.filter( + (log) => log.showingTime >= lastMonth && log.showingTime < thisMonth && log.guideType === guideType ); + const percentageDifference = + lastMonthViews.length === 0 + ? 100 + : Math.round( + ((thisMonthViews.length - lastMonthViews.length) / + lastMonthViews.length) * + 100 + ); const result = { views: thisMonthViews.length, change: percentageDifference, diff --git a/backend/src/test/mocks/guidelog.mock.js b/backend/src/test/mocks/guidelog.mock.js index 2de5ac73..43d561d0 100644 --- a/backend/src/test/mocks/guidelog.mock.js +++ b/backend/src/test/mocks/guidelog.mock.js @@ -1,16 +1,18 @@ +const { GuideType } = require("../../utils/guidelog.helper"); + class GuideLogBuilder { - constructor(id) { + constructor(id, guideType) { this.guideLog = { - guideType: 3, + guideType, guideId: id, - userId: '1', + userId: "1", showingTime: new Date("2024-11-29T00:00:00.000Z"), completed: false, }; } - static guideLog(id = 1) { - return new GuideLogBuilder(id); + static guideLog(id = 1, guideType = 1) { + return new GuideLogBuilder(id, guideType); } invalidGuideType() { @@ -44,7 +46,20 @@ class GuideLogBuilder { } const guideLogList = Array.from({ length: 10 }, (_, i) => { - return GuideLogBuilder.guideLog(i + 1).build(); + const values = Object.values(GuideType); + let index = 0; + if (i % 2 === 0 && i % 3 === 0) { + index = 1; + } else if (i % 3 === 0) { + index = 2; + } else if (i % 2 === 0 && i % 5 === 0) { + index = 3; + } else if (i % 2 === 0) { + index = 4; + } else if (i % 5 === 0) { + index = 5; + } + return GuideLogBuilder.guideLog(i + 1, values[index]).build(); }).map((guideLog, i) => (i % 2 === 0 ? guideLog : { ...guideLog, userId: 2 })); module.exports = { GuideLogBuilder, guideLogList }; diff --git a/backend/src/test/unit/services/statistics.test.js b/backend/src/test/unit/services/statistics.test.js new file mode 100644 index 00000000..4f169ce7 --- /dev/null +++ b/backend/src/test/unit/services/statistics.test.js @@ -0,0 +1,42 @@ +const { describe, it, beforeEach, afterEach } = require("mocha"); +const sinon = require("sinon"); +const { expect } = require("chai"); +const db = require("../../../models/index.js"); +const mocks = require("../../mocks/guidelog.mock.js"); +const statisticsService = require("../../../service/statistics.service.js"); + +const GuideLog = db.GuideLog; + +describe.only("Test statistics service", () => { + const GuideLogMock = {}; + let commit; + let rollback; + beforeEach(() => { + commit = sinon.spy(); + rollback = sinon.spy(); + sinon.stub(db.sequelize, "transaction").callsFake(async () => ({ + commit, + rollback, + })); + }); + afterEach(sinon.restore); + it("should return statistics", async () => { + const guideLogs = mocks.guideLogList; + GuideLogMock.findAll = sinon.stub(GuideLog, "findAll").resolves(guideLogs); + const statistics = await statisticsService.generateStatistics({ + userId: 1, + }); + const expected = [ + { views: 2, change: 100, guideType: 'popup' }, + { views: 2, change: 100, guideType: 'hint' }, + { views: 2, change: 100, guideType: 'banner' }, + { views: 0, change: 100, guideType: 'link' }, + { views: 3, change: 100, guideType: 'tour' }, + { views: 1, change: 100, guideType: 'checklist' } + ] + expect(GuideLogMock.findAll.called).to.equal(true); + expect(statistics).to.be.an("array"); + expect(statistics).to.have.lengthOf(6); + expect(statistics).to.deep.equal(expected); + }); +}); From 33f14ba5994a40ab15c39e9c41e16909ae591880 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 10:41:00 -0800 Subject: [PATCH 12/62] feat: add statistics controller test --- .../test/unit/controllers/statistics.test.js | 40 +++++++++++++++++++ .../src/test/unit/services/statistics.test.js | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/unit/controllers/statistics.test.js diff --git a/backend/src/test/unit/controllers/statistics.test.js b/backend/src/test/unit/controllers/statistics.test.js new file mode 100644 index 00000000..5ade7b84 --- /dev/null +++ b/backend/src/test/unit/controllers/statistics.test.js @@ -0,0 +1,40 @@ +const { describe, it, afterEach } = require("mocha"); +const { expect } = require("chai"); +const sinon = require("sinon"); +const service = require("../../../service/statistics.service.js"); +const { guideLogList } = require("../../mocks/guidelog.mock.js"); +const controller = require("../../../controllers/statistics.controller.js"); + +describe("Test Statistics controller", () => { + const serviceMock = {}; + const req = {}; + const res = {}; + beforeEach(() => { + res.status = sinon.stub().returns(res); + res.json = sinon.stub().returns(res); + }); + afterEach(sinon.restore); + describe("getStatistics", () => { + req.user = { id: 1 }; + it("should return status 200 and the statistics to all guides", async () => { + serviceMock.generateStatistics = sinon + .stub(service, "generateStatistics") + .resolves(guideLogList); + await controller.getStatistics(req, res); + const status = res.status.firstCall.args[0]; + const json = res.json.firstCall.args[0]; + expect(status).to.be.equal(200); + expect(json).to.be.equal(guideLogList); + }); + it("should return status 500 if something goes wrong", async () => { + serviceMock.generateStatistics = sinon + .stub(service, "generateStatistics") + .rejects(); + await controller.getStatistics(req, res); + const status = res.status.firstCall.args[0]; + const json = res.json.firstCall.args[0]; + expect(status).to.be.equal(500); + expect(json).to.be.deep.equal({ message: 'Internal server error' }); + }); + }); +}); diff --git a/backend/src/test/unit/services/statistics.test.js b/backend/src/test/unit/services/statistics.test.js index 4f169ce7..234f3981 100644 --- a/backend/src/test/unit/services/statistics.test.js +++ b/backend/src/test/unit/services/statistics.test.js @@ -7,7 +7,7 @@ const statisticsService = require("../../../service/statistics.service.js"); const GuideLog = db.GuideLog; -describe.only("Test statistics service", () => { +describe("Test statistics service", () => { const GuideLogMock = {}; let commit; let rollback; From 62d1a70def6d8b59c1010ad97e37eddae46636fb Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 12:06:37 -0800 Subject: [PATCH 13/62] feat: add statistics request function to front --- backend/src/service/statistics.service.js | 2 +- .../src/test/unit/services/statistics.test.js | 14 ++++++------- frontend/src/services/statisticsService.js | 20 +++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 frontend/src/services/statisticsService.js diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index fc4d570a..e1e19ff0 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -24,7 +24,7 @@ class StatisticsService { ); const percentageDifference = lastMonthViews.length === 0 - ? 100 + ? 0 : Math.round( ((thisMonthViews.length - lastMonthViews.length) / lastMonthViews.length) * diff --git a/backend/src/test/unit/services/statistics.test.js b/backend/src/test/unit/services/statistics.test.js index 234f3981..655856f5 100644 --- a/backend/src/test/unit/services/statistics.test.js +++ b/backend/src/test/unit/services/statistics.test.js @@ -27,13 +27,13 @@ describe("Test statistics service", () => { userId: 1, }); const expected = [ - { views: 2, change: 100, guideType: 'popup' }, - { views: 2, change: 100, guideType: 'hint' }, - { views: 2, change: 100, guideType: 'banner' }, - { views: 0, change: 100, guideType: 'link' }, - { views: 3, change: 100, guideType: 'tour' }, - { views: 1, change: 100, guideType: 'checklist' } - ] + { views: 2, change: 0, guideType: "popup" }, + { views: 2, change: 0, guideType: "hint" }, + { views: 2, change: 0, guideType: "banner" }, + { views: 0, change: 0, guideType: "link" }, + { views: 3, change: 0, guideType: "tour" }, + { views: 1, change: 0, guideType: "checklist" }, + ]; expect(GuideLogMock.findAll.called).to.equal(true); expect(statistics).to.be.an("array"); expect(statistics).to.have.lengthOf(6); diff --git a/frontend/src/services/statisticsService.js b/frontend/src/services/statisticsService.js new file mode 100644 index 00000000..d29c8c0a --- /dev/null +++ b/frontend/src/services/statisticsService.js @@ -0,0 +1,20 @@ +import { emitToastError } from "../utils/guideHelper"; +import { apiClient } from "./apiClient"; + +const getStatistics = async () => { + try { + const response = await apiClient.get(`/api/statistics`); + return response.data; + } catch (error) { + console.error("Get Statistics error:", error.response); + if (error.response.data.errors[0].msg) { + emitToastError(error); + } else { + emitToastError({ + response: { data: { errors: [{ msg: error.message }] } }, + }); + } + } +}; + +export { getStatistics }; From 261ea2bd3f5c9a08d5bbffad7d03cdc1004856a7 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:15:06 -0800 Subject: [PATCH 14/62] fix: fix info passed to service --- backend/src/controllers/statistics.controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/statistics.controller.js b/backend/src/controllers/statistics.controller.js index fe269d86..4b10b1bf 100644 --- a/backend/src/controllers/statistics.controller.js +++ b/backend/src/controllers/statistics.controller.js @@ -4,9 +4,10 @@ class StatisticsController { async getStatistics(req, res) { try { const userId = req.user.id; - const statistics = await statisticsService.generateStatistics({ userId }); + const statistics = await statisticsService.generateStatistics({ userId: String(userId) }); res.status(200).json(statistics); - } catch { + } catch (e) { + console.log(e); res.status(500).json({ message: "Internal server error" }); } } From 416de1f1f10914f52bb1863acaf7dcd2db8fbb15 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:15:23 -0800 Subject: [PATCH 15/62] fix: add auth middleware --- backend/src/routes/statistics.routes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/statistics.routes.js b/backend/src/routes/statistics.routes.js index ffd1105d..7022c551 100644 --- a/backend/src/routes/statistics.routes.js +++ b/backend/src/routes/statistics.routes.js @@ -1,8 +1,9 @@ const express = require("express"); const statisticsController = require("../controllers/statistics.controller"); +const authenticateJWT = require("../middleware/auth.middleware"); const router = express.Router(); - +router.use(authenticateJWT) router.get("/", statisticsController.getStatistics); module.exports = router; From 702efefeeaed2923ed3292d655ea84fb34a2df99 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:15:42 -0800 Subject: [PATCH 16/62] fix: fix info passed to model --- backend/src/service/statistics.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index e1e19ff0..57bb826c 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -8,7 +8,7 @@ class StatisticsService { Object.entries(GuideType).map(async ([guideName, guideType]) => { const logs = await GuideLog.findAll({ where: { - guideType, + guideType: Number(guideType), userId, }, }); @@ -38,7 +38,7 @@ class StatisticsService { return result; }) ); - return views; + return views.sort((a, b) => b.views - a.views); } } From 9fef355607ac1bc44b9c4dd48c6692a39cc72ca5 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:16:20 -0800 Subject: [PATCH 17/62] fix: fix error treatment on front service --- frontend/src/services/statisticsService.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/services/statisticsService.js b/frontend/src/services/statisticsService.js index d29c8c0a..fbcf6b10 100644 --- a/frontend/src/services/statisticsService.js +++ b/frontend/src/services/statisticsService.js @@ -3,11 +3,10 @@ import { apiClient } from "./apiClient"; const getStatistics = async () => { try { - const response = await apiClient.get(`/api/statistics`); + const response = await apiClient.get(`/statistics/`); return response.data; } catch (error) { - console.error("Get Statistics error:", error.response); - if (error.response.data.errors[0].msg) { + if (error.response.data.errors?.[0].msg) { emitToastError(error); } else { emitToastError({ From 273114e94deb50c13f04eae5087c217fa52885ad Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:17:03 -0800 Subject: [PATCH 18/62] feat: add statistics route to dashboard --- frontend/src/scenes/dashboard/Dashboard.jsx | 70 ++++++++++++++------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/frontend/src/scenes/dashboard/Dashboard.jsx b/frontend/src/scenes/dashboard/Dashboard.jsx index 0be3ac50..69f538ca 100644 --- a/frontend/src/scenes/dashboard/Dashboard.jsx +++ b/frontend/src/scenes/dashboard/Dashboard.jsx @@ -1,18 +1,48 @@ -import React from "react"; -import DateDisplay from "./HomePageComponents/DateDisplay/DateDisplay"; -import UserTitle from "./HomePageComponents/UserTitle/UserTitle"; +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { getStatistics } from "../../services/statisticsService"; import styles from "./Dashboard.module.scss"; -import StatisticCardList from "./HomePageComponents/StatisticCardsList/StatisticCardsList"; import CreateActivityButtonList from "./HomePageComponents/CreateActivityButtonList/CreateActivityButtonList"; -import { useNavigate } from "react-router-dom"; +import DateDisplay from "./HomePageComponents/DateDisplay/DateDisplay"; +import StatisticCardList from "./HomePageComponents/StatisticCardsList/StatisticCardsList"; +import UserTitle from "./HomePageComponents/UserTitle/UserTitle"; + +const mapMetricName = (guideType) => { + switch (guideType) { + case "popup": + return "Popups views"; + case "hint": + return "Hints views"; + case "banner": + return "Banners views"; + case "link": + return "Links views"; + case "tour": + return "Tours views"; + case "checklist": + return "Checklists views"; + default: + return "Unknown"; + } +}; const Dashboard = ({ name }) => { const navigate = useNavigate(); - const metrics = [ - { metricName: "Popup views", metricValue: 5000, changeRate: 5 }, - { metricName: "Hint views", metricValue: 2000, changeRate: -20 }, - { metricName: "Banner Views", metricValue: 3000, changeRate: 15 }, - ]; + const [metrics, setMetrics] = useState([]); + + useEffect(() => { + getStatistics().then((data) => { + setMetrics( + data + .map((metric) => ({ + metricName: mapMetricName(metric.guideType), + metricValue: metric.views, + changeRate: metric.change, + })) + .filter((_, i) => i < 3) + ); + }); + }, []); const buttons = [ { @@ -29,19 +59,15 @@ const Dashboard = ({ name }) => { }, ]; return ( - - -
-
- - -
-
- Start with a popular onboarding process -
- - +
+
+ +
+
Start with a popular onboarding process
+ + +
); }; From d6c2ac03e094409556f0fb8adf15c9fea596930e Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 13:17:45 -0800 Subject: [PATCH 19/62] feat: update card to use N/A if views are 0 --- .../StatisticCards/StatisticCards.jsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.jsx b/frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.jsx index f847f696..11db1d79 100644 --- a/frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.jsx +++ b/frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.jsx @@ -1,20 +1,29 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './StatisticCards.module.scss'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import PropTypes from "prop-types"; +import React from "react"; +import styles from "./StatisticCards.module.scss"; const StatisticCard = ({ metricName, metricValue = 0, changeRate = 0 }) => { + const getChangeRate = () => { + if (changeRate === 0) return "N/A"; + return Math.abs(changeRate) + "%"; + }; + + const getRateColor = () => { + if (changeRate === 0) return "inherit"; + return changeRate >= 0 ? "green" : "red"; + }; return (
{metricName}
{metricValue}
- - = 0 ? 'green' : 'red'}} className={styles.change}> - {changeRate >= 0 ? : } - {Math.abs(changeRate)}% + + {changeRate !== 0 && + (changeRate >= 0 ? : )} + {getChangeRate()}  vs last month
From be5b35c2be301dfc8bd602a4fcf5d72fbadd1453 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 14:22:37 -0800 Subject: [PATCH 20/62] refactor: add coderabbit suggestions --- .../src/controllers/statistics.controller.js | 13 +++++-- backend/src/service/statistics.service.js | 37 ++++++++++++------- .../src/test/unit/services/statistics.test.js | 14 +------ frontend/src/scenes/dashboard/Dashboard.jsx | 11 ++++-- frontend/src/services/statisticsService.js | 13 +++---- 5 files changed, 49 insertions(+), 39 deletions(-) diff --git a/backend/src/controllers/statistics.controller.js b/backend/src/controllers/statistics.controller.js index 4b10b1bf..1696694f 100644 --- a/backend/src/controllers/statistics.controller.js +++ b/backend/src/controllers/statistics.controller.js @@ -1,14 +1,21 @@ const statisticsService = require("../service/statistics.service"); +const { internalServerError } = require("../utils/errors.helper"); class StatisticsController { async getStatistics(req, res) { try { const userId = req.user.id; - const statistics = await statisticsService.generateStatistics({ userId: String(userId) }); + const statistics = await statisticsService.generateStatistics({ + userId: userId.toString(), + }); res.status(200).json(statistics); } catch (e) { - console.log(e); - res.status(500).json({ message: "Internal server error" }); + console.log(e) + const { statusCode, payload } = internalServerError( + "GET_STATISTICS_ERROR", + e.message + ); + res.status(statusCode).json(payload); } } } diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index 57bb826c..e39dc024 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -4,34 +4,43 @@ const GuideLog = db.GuideLog; class StatisticsService { async generateStatistics({ userId }) { + const now = new Date(); + const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const twoMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 2, 1); const views = await Promise.all( Object.entries(GuideType).map(async ([guideName, guideType]) => { const logs = await GuideLog.findAll({ where: { guideType: Number(guideType), userId, + showingTime: { + [db.Sequelize.Op.gte]: twoMonthsAgo, + }, }, }); - const thisMonth = new Date(); - thisMonth.setDate(thisMonth.getDate() - 30); - const lastMonth = new Date(); - lastMonth.setDate(lastMonth.getDate() - 60); - const thisMonthViews = logs.filter( - (log) => log.showingTime >= thisMonth && log.guideType === guideType - ); - const lastMonthViews = logs.filter( - (log) => log.showingTime >= lastMonth && log.showingTime < thisMonth && log.guideType === guideType + const { thisMonthViews, lastMonthViews } = logs.reduce( + (acc, log) => { + if (log.guideType !== guideType) { + return acc; + } + if (log.showingTime >= thisMonth) { + acc.thisMonthViews += 1; + } else if (log.showingTime >= lastMonth) { + acc.lastMonthViews += 1; + } + return acc; + }, + { thisMonthViews: 0, lastMonthViews: 0 } ); const percentageDifference = - lastMonthViews.length === 0 + lastMonthViews === 0 ? 0 : Math.round( - ((thisMonthViews.length - lastMonthViews.length) / - lastMonthViews.length) * - 100 + ((thisMonthViews - lastMonthViews) / lastMonthViews) * 100 ); const result = { - views: thisMonthViews.length, + views: thisMonthViews, change: percentageDifference, guideType: guideName.toLowerCase(), }; diff --git a/backend/src/test/unit/services/statistics.test.js b/backend/src/test/unit/services/statistics.test.js index 655856f5..2518f55f 100644 --- a/backend/src/test/unit/services/statistics.test.js +++ b/backend/src/test/unit/services/statistics.test.js @@ -9,16 +9,6 @@ const GuideLog = db.GuideLog; describe("Test statistics service", () => { const GuideLogMock = {}; - let commit; - let rollback; - beforeEach(() => { - commit = sinon.spy(); - rollback = sinon.spy(); - sinon.stub(db.sequelize, "transaction").callsFake(async () => ({ - commit, - rollback, - })); - }); afterEach(sinon.restore); it("should return statistics", async () => { const guideLogs = mocks.guideLogList; @@ -27,12 +17,12 @@ describe("Test statistics service", () => { userId: 1, }); const expected = [ + { views: 3, change: 0, guideType: "tour" }, { views: 2, change: 0, guideType: "popup" }, { views: 2, change: 0, guideType: "hint" }, { views: 2, change: 0, guideType: "banner" }, - { views: 0, change: 0, guideType: "link" }, - { views: 3, change: 0, guideType: "tour" }, { views: 1, change: 0, guideType: "checklist" }, + { views: 0, change: 0, guideType: "link" }, ]; expect(GuideLogMock.findAll.called).to.equal(true); expect(statistics).to.be.an("array"); diff --git a/frontend/src/scenes/dashboard/Dashboard.jsx b/frontend/src/scenes/dashboard/Dashboard.jsx index 69f538ca..259e9386 100644 --- a/frontend/src/scenes/dashboard/Dashboard.jsx +++ b/frontend/src/scenes/dashboard/Dashboard.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import LoadingArea from "../../components/LoadingPage/LoadingArea"; import { getStatistics } from "../../services/statisticsService"; import styles from "./Dashboard.module.scss"; import CreateActivityButtonList from "./HomePageComponents/CreateActivityButtonList/CreateActivityButtonList"; @@ -26,21 +27,25 @@ const mapMetricName = (guideType) => { } }; +const MAX_METRICS_DISPLAYED = 3; + const Dashboard = ({ name }) => { const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); const [metrics, setMetrics] = useState([]); useEffect(() => { getStatistics().then((data) => { setMetrics( data - .map((metric) => ({ + ?.map((metric) => ({ metricName: mapMetricName(metric.guideType), metricValue: metric.views, changeRate: metric.change, })) - .filter((_, i) => i < 3) + ?.filter((_, i) => i < MAX_METRICS_DISPLAYED) ); + setIsLoading(false); }); }, []); @@ -66,7 +71,7 @@ const Dashboard = ({ name }) => {
Start with a popular onboarding process
- + {isLoading ? : }
); }; diff --git a/frontend/src/services/statisticsService.js b/frontend/src/services/statisticsService.js index fbcf6b10..4fc5c0e6 100644 --- a/frontend/src/services/statisticsService.js +++ b/frontend/src/services/statisticsService.js @@ -6,13 +6,12 @@ const getStatistics = async () => { const response = await apiClient.get(`/statistics/`); return response.data; } catch (error) { - if (error.response.data.errors?.[0].msg) { - emitToastError(error); - } else { - emitToastError({ - response: { data: { errors: [{ msg: error.message }] } }, - }); - } + const errorMessage = + error.response?.data?.errors?.[0]?.msg || error.message; + emitToastError({ + response: { data: { errors: [{ msg: errorMessage }] } }, + }); + return null; } }; From b737ceebf49cb48fd87c50186d51c45dd3155ee0 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 14:29:42 -0800 Subject: [PATCH 21/62] refactor: remove promise all from service --- backend/src/service/statistics.service.js | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/service/statistics.service.js b/backend/src/service/statistics.service.js index e39dc024..d008e6d4 100644 --- a/backend/src/service/statistics.service.js +++ b/backend/src/service/statistics.service.js @@ -4,12 +4,13 @@ const GuideLog = db.GuideLog; class StatisticsService { async generateStatistics({ userId }) { - const now = new Date(); - const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const twoMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 2, 1); - const views = await Promise.all( - Object.entries(GuideType).map(async ([guideName, guideType]) => { + try { + const now = new Date(); + const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const twoMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 2, 1); + const views = []; + for (const [guideName, guideType] of Object.entries(GuideType)) { const logs = await GuideLog.findAll({ where: { guideType: Number(guideType), @@ -44,10 +45,13 @@ class StatisticsService { change: percentageDifference, guideType: guideName.toLowerCase(), }; - return result; - }) - ); - return views.sort((a, b) => b.views - a.views); + views.push(result); + } + return views.sort((a, b) => b.views - a.views); + } catch (error) { + console.log(error); + throw new Error(`Failed to generate statistics: ${error.message}`); + } } } From 87b7d801d2c19d0c47214c273fe39a621e71bf02 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 14:35:23 -0800 Subject: [PATCH 22/62] refactor: change test verification on service --- backend/src/test/unit/services/statistics.test.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/test/unit/services/statistics.test.js b/backend/src/test/unit/services/statistics.test.js index 2518f55f..8ca765aa 100644 --- a/backend/src/test/unit/services/statistics.test.js +++ b/backend/src/test/unit/services/statistics.test.js @@ -16,17 +16,14 @@ describe("Test statistics service", () => { const statistics = await statisticsService.generateStatistics({ userId: 1, }); - const expected = [ - { views: 3, change: 0, guideType: "tour" }, - { views: 2, change: 0, guideType: "popup" }, - { views: 2, change: 0, guideType: "hint" }, - { views: 2, change: 0, guideType: "banner" }, - { views: 1, change: 0, guideType: "checklist" }, - { views: 0, change: 0, guideType: "link" }, - ]; + expect(GuideLogMock.findAll.called).to.equal(true); expect(statistics).to.be.an("array"); expect(statistics).to.have.lengthOf(6); - expect(statistics).to.deep.equal(expected); + statistics.forEach((statistic) => { + expect(statistic).to.have.property("guideType"); + expect(statistic).to.have.property("views"); + expect(statistic).to.have.property("change"); + }); }); }); From 7bbc3f129024d4ac279a8357ad65be39970efa94 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Thu, 19 Dec 2024 14:47:16 -0800 Subject: [PATCH 23/62] test: fix controller test --- backend/src/test/unit/controllers/statistics.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/test/unit/controllers/statistics.test.js b/backend/src/test/unit/controllers/statistics.test.js index 5ade7b84..74ffc6e1 100644 --- a/backend/src/test/unit/controllers/statistics.test.js +++ b/backend/src/test/unit/controllers/statistics.test.js @@ -29,12 +29,15 @@ describe("Test Statistics controller", () => { it("should return status 500 if something goes wrong", async () => { serviceMock.generateStatistics = sinon .stub(service, "generateStatistics") - .rejects(); + .rejects(`Failed to generate statistics:`); await controller.getStatistics(req, res); const status = res.status.firstCall.args[0]; const json = res.json.firstCall.args[0]; expect(status).to.be.equal(500); - expect(json).to.be.deep.equal({ message: 'Internal server error' }); + expect(json).to.be.deep.equal({ + error: "Internal Server Error", + errorCode: "GET_STATISTICS_ERROR", + }); }); }); }); From f2507a861f045ed11b1344a3fc841aa2395e0ee4 Mon Sep 17 00:00:00 2001 From: Debora Serra Date: Fri, 20 Dec 2024 07:18:01 -0800 Subject: [PATCH 24/62] test: add e2e test to statistics --- backend/src/test/e2e/statistics.test.mjs | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 backend/src/test/e2e/statistics.test.mjs diff --git a/backend/src/test/e2e/statistics.test.mjs b/backend/src/test/e2e/statistics.test.mjs new file mode 100644 index 00000000..3b968692 --- /dev/null +++ b/backend/src/test/e2e/statistics.test.mjs @@ -0,0 +1,90 @@ +import { expect } from "chai"; +import { after, afterEach, before, beforeEach, describe } from "mocha"; +import waitOn from "wait-on"; +import db from "../../models/index.js"; +import app from "../../server.js"; +import mocks from "../mocks/guidelog.mock.js"; +import { UserBuilder, validList } from "../mocks/user.mock.js"; +import chai from "./index.mjs"; + +const user = UserBuilder.user; +const dbReadyOptions = { + resources: ["tcp:localhost:5432"], + delay: 1000, + timeout: 30000, + interval: 1000, +}; + +const guideLogList = mocks.guideLogList; + +const setupTestDatabase = () => { + before(async () => { + db.sequelize.connectionManager.initPools(); + }); + + after(async () => { + try { + const conn = await db.sequelize.connectionManager.getConnection(); + db.sequelize.connectionManager.releaseConnection(conn); + } catch (error) { + console.error("Failed to release database connection:", error); + throw error; + } + }); +}; + +const populateGuideLogs = async (token) => { + for (const guideLog of guideLogList) { + await chai.request + .execute(app) + .post("/api/guide_log/add_guide_log") + .set("Authorization", `Bearer ${token}`) + .send(guideLog); + } +} + +describe("E2e tests statistics", () => { + describe("GET /api/statistics", () => { + setupTestDatabase(); + let token; + + beforeEach(async () => { + process.env.NODE_ENV = "test"; + try { + await waitOn(dbReadyOptions); + } catch (err) { + console.error("Database not ready in time:", err); + throw err; + } + const login = await chai.request + .execute(app) + .post("/api/auth/register") + .send(user().build()); + token = login.body.token; + }); + afterEach(async () => { + await db.sequelize.sync({ force: true, match: /_test$/ }); + }); + it("should return 401 if token is not passed", async () => { + const response = await chai.request.execute(app).get("/api/statistics"); + expect(response).to.have.status(401); + expect(response.body).to.be.deep.equal({ error: "No token provided" }); + }) + it("should return 200 if token is passed", async () => { + populateGuideLogs(token); + const response = await chai.request + .execute(app) + .get("/api/statistics") + .set("Authorization", `Bearer ${token}`); + expect(response).to.have.status(200); + expect(response.body).not.to.deep.equal(guideLogList); + expect(response.body).to.be.an("array"); + expect(response.body).to.have.lengthOf(6); + response.body.forEach((statistic) => { + expect(statistic).to.have.property("views"); + expect(statistic).to.have.property("change"); + expect(statistic).to.have.property("guideType"); + }) + }) + }); +}); From abd2ed48465c095b0cb3288f3e81c28ba7f12a83 Mon Sep 17 00:00:00 2001 From: tunckiral Date: Fri, 20 Dec 2024 16:27:48 -0500 Subject: [PATCH 25/62] adding footer --- jsAgent/links.js | 73 +++++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/jsAgent/links.js b/jsAgent/links.js index c07fd810..dbdc3c3d 100644 --- a/jsAgent/links.js +++ b/jsAgent/links.js @@ -13,51 +13,74 @@ const linksDefaultOptions = { bw.links={ init:function(){ - bw.links.putIcon(); - bw.links.putPlaceHolder(); + bw.links.putHtml(); + bw.links.putHeader(); + // bw.links.putPlaceHolder(); bw.links.bindClick(); }, - putIcon:function(){ - let temp_html = ` - `; - document.body.insertAdjacentHTML('beforeend', temp_html); - }, - putPlaceHolder: function(){ + putHtml:function(){ const options = window.bwonboarddata.helperLink[0]; let option = { ...linksDefaultOptions, ...options }; - - const temp_html = `