From 1bbb7980c0a1b9b547d1caca80026d461f456f9e Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Wed, 19 Jun 2024 23:22:31 +0530 Subject: [PATCH 1/6] Add `NotificationPreference` for weekly reminder - Fix https://github.com/saeloun/miru-web/issues/1863 - By default, preference is set to `false` - After this PR got merge, need to run one-off script to generate NotificationPreference records --- app/models/company.rb | 1 + app/models/notification_preference.rb | 32 +++++++++++++++++++ app/models/user.rb | 1 + ...kly_reminder_for_missed_entries_service.rb | 6 +++- ...7135248_create_notification_preferences.rb | 14 ++++++++ db/schema.rb | 15 ++++++++- 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 app/models/notification_preference.rb create mode 100644 db/migrate/20240617135248_create_notification_preferences.rb diff --git a/app/models/company.rb b/app/models/company.rb index 4f8b528854..bc0c6ec251 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -45,6 +45,7 @@ class Company < ApplicationRecord has_many :holidays, dependent: :destroy has_many :holiday_infos, through: :holidays, dependent: :destroy has_many :carryovers + has_many :notification_preferences resourcify diff --git a/app/models/notification_preference.rb b/app/models/notification_preference.rb new file mode 100644 index 0000000000..a6d21b2c4c --- /dev/null +++ b/app/models/notification_preference.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: notification_preferences +# +# id :bigint not null, primary key +# notification_enabled :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# company_id :bigint not null +# user_id :bigint not null +# +# Indexes +# +# index_notification_preferences_on_company_id (company_id) +# index_notification_preferences_on_user_id (user_id) +# index_notification_preferences_on_user_id_and_company_id (user_id,company_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (company_id => companies.id) +# fk_rails_... (user_id => users.id) +# +class NotificationPreference < ApplicationRecord + belongs_to :user + belongs_to :company + + validates :user_id, presence: true + validates :company_id, presence: true + validates :notification_enabled, inclusion: { in: [true, false] } +end diff --git a/app/models/user.rb b/app/models/user.rb index f2b6b854fc..4253bb2010 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -75,6 +75,7 @@ def initialize(msg = "Spam User Login") has_many :custom_leave_users has_many :custom_leaves, through: :custom_leave_users, source: :custom_leave has_many :carryovers + has_many :notification_preferences rolify strict: true diff --git a/app/services/weekly_reminder_for_missed_entries_service.rb b/app/services/weekly_reminder_for_missed_entries_service.rb index e7fb3906c2..af99fbaf82 100644 --- a/app/services/weekly_reminder_for_missed_entries_service.rb +++ b/app/services/weekly_reminder_for_missed_entries_service.rb @@ -8,7 +8,11 @@ def process Company.find_each do |company| company.users.kept.find_each do |user| - check_entries_and_send_mail(user, company) + notification_preference = NotificationPreference.find_by(user_id: user.id, company_id: company.id) + + if notification_preference.present? && notification_preference.notification_enabled + check_entries_and_send_mail(user, company) + end end end end diff --git a/db/migrate/20240617135248_create_notification_preferences.rb b/db/migrate/20240617135248_create_notification_preferences.rb new file mode 100644 index 0000000000..756b7b8b9b --- /dev/null +++ b/db/migrate/20240617135248_create_notification_preferences.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateNotificationPreferences < ActiveRecord::Migration[7.1] + def change + create_table :notification_preferences do |t| + t.references :user, null: false, foreign_key: true + t.references :company, null: false, foreign_key: true + t.boolean :notification_enabled, default: false, null: false + t.index [:user_id, :company_id], unique: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4e7bd6c553..a5ee144365 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_05_16_054849) do +ActiveRecord::Schema[7.1].define(version: 2024_06_17_135248) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -378,6 +378,17 @@ t.index ["year", "company_id"], name: "index_leaves_on_year_and_company_id", unique: true end + create_table "notification_preferences", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "company_id", null: false + t.boolean "notification_enabled", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["company_id"], name: "index_notification_preferences_on_company_id" + t.index ["user_id", "company_id"], name: "index_notification_preferences_on_user_id_and_company_id", unique: true + t.index ["user_id"], name: "index_notification_preferences_on_user_id" + end + create_table "payments", force: :cascade do |t| t.bigint "invoice_id", null: false t.date "transaction_date", null: false @@ -599,6 +610,8 @@ add_foreign_key "invoices", "companies" add_foreign_key "leave_types", "leaves", column: "leave_id" add_foreign_key "leaves", "companies" + add_foreign_key "notification_preferences", "companies" + add_foreign_key "notification_preferences", "users" add_foreign_key "payments", "invoices" add_foreign_key "payments_providers", "companies" add_foreign_key "previous_employments", "users" From 81386916daee1a16913902b481fff34fafa9d48c Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Wed, 26 Jun 2024 23:32:09 +0530 Subject: [PATCH 2/6] Add notification settings in Team and Profile pages --- .../notification_preferences_controller.rb | 30 +++++++++ app/javascript/src/apis/preferences.ts | 11 ++++ .../src/components/Profile/Layout/routes.tsx | 10 +++ .../NotificationPreferences/index.tsx | 64 +++++++++++++++++++ .../notification_preference_policy.rb | 26 ++++++++ config/routes/internal_api.rb | 1 + 6 files changed, 142 insertions(+) create mode 100644 app/controllers/internal_api/v1/team_members/notification_preferences_controller.rb create mode 100644 app/javascript/src/apis/preferences.ts create mode 100644 app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx create mode 100644 app/policies/team_members/notification_preference_policy.rb diff --git a/app/controllers/internal_api/v1/team_members/notification_preferences_controller.rb b/app/controllers/internal_api/v1/team_members/notification_preferences_controller.rb new file mode 100644 index 0000000000..f1a733f3b4 --- /dev/null +++ b/app/controllers/internal_api/v1/team_members/notification_preferences_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class InternalApi::V1::TeamMembers::NotificationPreferencesController < InternalApi::V1::ApplicationController + def show + authorize notification_preference, policy_class: TeamMembers::NotificationPreferencePolicy + render json: { notification_enabled: notification_preference.notification_enabled }, status: :ok + end + + def update + authorize notification_preference, policy_class: TeamMembers::NotificationPreferencePolicy + + notification_preference.update!(notification_preference_params) + render json: { + notification_enabled: notification_preference.notification_enabled, + notice: "Preference updated successfully" + }, status: :ok + end + + private + + def notification_preference + @notification_preference ||= NotificationPreference.find_by( + user_id: params[:team_id], + company_id: current_company.id) + end + + def notification_preference_params + params.require(:notification_preference).permit(:notification_enabled) + end +end diff --git a/app/javascript/src/apis/preferences.ts b/app/javascript/src/apis/preferences.ts new file mode 100644 index 0000000000..8e81c1ec18 --- /dev/null +++ b/app/javascript/src/apis/preferences.ts @@ -0,0 +1,11 @@ +import axios from "./api"; + +const get = async userId => + axios.get(`team/${userId}/notification_preferences`); + +const updatePreference = async (userId, payload) => + axios.patch(`team/${userId}/notification_preferences`, payload); + +const preferencesApi = { get, updatePreference }; + +export default preferencesApi; diff --git a/app/javascript/src/components/Profile/Layout/routes.tsx b/app/javascript/src/components/Profile/Layout/routes.tsx index 8a80c076c5..66396209d1 100644 --- a/app/javascript/src/components/Profile/Layout/routes.tsx +++ b/app/javascript/src/components/Profile/Layout/routes.tsx @@ -19,6 +19,7 @@ import AllocatedDevicesDetails from "components/Profile/Personal/Devices"; import AllocatedDevicesEdit from "components/Profile/Personal/Devices/Edit"; import EmploymentDetails from "components/Profile/Personal/Employment"; import EmploymentDetailsEdit from "components/Profile/Personal/Employment/Edit"; +import NotificationPreferences from "components/Profile/Personal/NotificationPreferences"; import UserDetailsView from "components/Profile/Personal/User"; import UserDetailsEdit from "components/Profile/Personal/User/Edit"; import { Roles } from "constants/index"; @@ -80,6 +81,15 @@ export const SETTINGS = [ category: "personal", isTab: false, }, + { + label: "NOTIFICATION SETTINGS", + path: "notifications", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + Component: NotificationPreferences, + category: "personal", + isTab: true, + }, // Uncomment when Integrating with API // { // label: "COMPENSATION", diff --git a/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx b/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx new file mode 100644 index 0000000000..afa5dc7b9e --- /dev/null +++ b/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx @@ -0,0 +1,64 @@ +/* eslint-disable no-unused-vars */ +import React, { Fragment, useEffect, useState } from "react"; + +import { useParams } from "react-router-dom"; + +import preferencesApi from "apis/preferences"; +import CustomCheckbox from "common/CustomCheckbox"; +import Loader from "common/Loader/index"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; +import { useUserContext } from "context/UserContext"; + +const NotificationPreferences = () => { + const { user } = useUserContext(); + const { memberId } = useParams(); + const { isCalledFromSettings } = useProfileContext(); + const currentUserId = isCalledFromSettings ? user.id : memberId; + + const [isLoading, setIsLoading] = useState(false); + const [isSelected, setIsSelected] = useState(false); + + const getPreferences = async () => { + const res = await preferencesApi.get(currentUserId); + setIsSelected(res.data.notification_enabled); + setIsLoading(false); + }; + + const updatePreferences = async () => { + setIsLoading(true); + const res = await preferencesApi.updatePreference(currentUserId, { + notification_enabled: !isSelected, + }); + setIsSelected(res.data.notification_enabled); + setIsLoading(false); + }; + + useEffect(() => { + setIsLoading(true); + getPreferences(); + }, []); + + return ( + + + {isLoading ? ( + + ) : ( + e.stopPropagation()} + id={currentUserId} + isChecked={isSelected} + labelClassName="ml-4" + text="Weekly Email Reminder" + wrapperClassName="py-3 px-5 flex items-center hover:bg-miru-gray-100" + /> + )} + + ); +}; + +export default NotificationPreferences; diff --git a/app/policies/team_members/notification_preference_policy.rb b/app/policies/team_members/notification_preference_policy.rb new file mode 100644 index 0000000000..6a74e2ed02 --- /dev/null +++ b/app/policies/team_members/notification_preference_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class TeamMembers::NotificationPreferencePolicy < ApplicationPolicy + def show? + return false unless record.present? + + authorize_current_user + end + + def update? + return false unless record.present? + + authorize_current_user + end + + private + + def authorize_current_user + unless user.current_workspace_id == record.company_id + @error_message_key = :different_workspace + return false + end + + has_owner_or_admin_role? || record_belongs_to_user? + end +end diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index cb714372d7..cfe05168a7 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -112,6 +112,7 @@ resource :details, only: [:show, :update], controller: "team_members/details" resource :avatar, only: [:update, :destroy], controller: "team_members/avatar" collection { put "update_team_members" } + resource :notification_preferences, only: [:show, :update], controller: "team_members/notification_preferences" end resources :invitations, only: [:create, :update, :destroy] do From 5f9febc1d2ef5d0850fe9e1b60a0208e8f928749 Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Thu, 27 Jun 2024 11:50:35 +0530 Subject: [PATCH 3/6] Update mobile notification screen - Fix styling for desktop and mobile screens - Use ReminderIcon(Bell Ringing Icon) for notification - Use CustomToggle instead of Checkbox for enable/disable notification --- .../src/components/Profile/Layout/routes.tsx | 3 +- .../NotificationPreferences/index.tsx | 38 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/javascript/src/components/Profile/Layout/routes.tsx b/app/javascript/src/components/Profile/Layout/routes.tsx index 66396209d1..b6f26fc5e7 100644 --- a/app/javascript/src/components/Profile/Layout/routes.tsx +++ b/app/javascript/src/components/Profile/Layout/routes.tsx @@ -8,6 +8,7 @@ import { CalendarIcon, CakeIcon, ClientsIcon, + ReminderIcon, } from "miruIcons"; import OrgDetails from "components/Profile/Organization/Details"; @@ -84,7 +85,7 @@ export const SETTINGS = [ { label: "NOTIFICATION SETTINGS", path: "notifications", - icon: , + icon: , authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], Component: NotificationPreferences, category: "personal", diff --git a/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx b/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx index afa5dc7b9e..7faeb1e511 100644 --- a/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx +++ b/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx @@ -4,14 +4,15 @@ import React, { Fragment, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import preferencesApi from "apis/preferences"; -import CustomCheckbox from "common/CustomCheckbox"; +import CustomToggle from "common/CustomToggle"; import Loader from "common/Loader/index"; +import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; import DetailsHeader from "components/Profile/Common/DetailsHeader"; import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; const NotificationPreferences = () => { - const { user } = useUserContext(); + const { user, isDesktop } = useUserContext(); const { memberId } = useParams(); const { isCalledFromSettings } = useProfileContext(); const currentUserId = isCalledFromSettings ? user.id : memberId; @@ -30,7 +31,6 @@ const NotificationPreferences = () => { const res = await preferencesApi.updatePreference(currentUserId, { notification_enabled: !isSelected, }); - setIsSelected(res.data.notification_enabled); setIsLoading(false); }; @@ -41,21 +41,29 @@ const NotificationPreferences = () => { return ( - + {isDesktop ? ( + + ) : ( + + )} {isLoading ? ( ) : ( - e.stopPropagation()} - id={currentUserId} - isChecked={isSelected} - labelClassName="ml-4" - text="Weekly Email Reminder" - wrapperClassName="py-3 px-5 flex items-center hover:bg-miru-gray-100" - /> +
+ Weekly Email Reminder + +
)}
); From a18ff97268c190246e2730d2c573a2bfed72a404 Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Fri, 28 Jun 2024 16:19:00 +0530 Subject: [PATCH 4/6] Create `NotificationPreference` after owner sign up or team member accept the invitaton --- app/services/create_company_service.rb | 7 +++++++ app/services/create_invited_user_service.rb | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/app/services/create_company_service.rb b/app/services/create_company_service.rb index 71dafbd3ac..9046ae2d54 100644 --- a/app/services/create_company_service.rb +++ b/app/services/create_company_service.rb @@ -13,6 +13,7 @@ def initialize(current_user, params: nil, company: nil) def process company.save! add_current_user_to_company + create_notification_preference company end @@ -24,4 +25,10 @@ def add_current_user_to_company current_user.add_role(:owner, company) current_user.save! end + + def create_notification_preference + NotificationPreference.find_or_create_by( + user_id: current_user.id, + company_id: company.id) + end end diff --git a/app/services/create_invited_user_service.rb b/app/services/create_invited_user_service.rb index f01b05a71f..17bef93364 100644 --- a/app/services/create_invited_user_service.rb +++ b/app/services/create_invited_user_service.rb @@ -34,6 +34,7 @@ def process find_or_create_user! add_role_to_invited_user create_client_member + create_notification_preference end rescue StandardError => e service_failed(e.message) @@ -107,6 +108,12 @@ def create_client_member invitation.company.client_members.create!(client: invitation.client, user:) end + def create_notification_preference + NotificationPreference.find_or_create_by( + user_id: user.id, + company_id: invitation.company.id) + end + def create_reset_password_token @reset_password_token = user.create_reset_password_token end From 5aa121cfbdd74afc3ac2a6fd42788bb3690f8300 Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Mon, 8 Jul 2024 14:16:03 +0530 Subject: [PATCH 5/6] Fix review pointers --- app/models/company.rb | 2 +- app/models/user.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/company.rb b/app/models/company.rb index bc0c6ec251..07eb91e3f6 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -45,7 +45,7 @@ class Company < ApplicationRecord has_many :holidays, dependent: :destroy has_many :holiday_infos, through: :holidays, dependent: :destroy has_many :carryovers - has_many :notification_preferences + has_many :notification_preferences, dependent: :destroy resourcify diff --git a/app/models/user.rb b/app/models/user.rb index 4253bb2010..299e2d7771 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -75,7 +75,7 @@ def initialize(msg = "Spam User Login") has_many :custom_leave_users has_many :custom_leaves, through: :custom_leave_users, source: :custom_leave has_many :carryovers - has_many :notification_preferences + has_many :notification_preferences, dependent: :destroy rolify strict: true From 3d81906cada2c0b6a0c1b8976f8d4e43be78d68b Mon Sep 17 00:00:00 2001 From: Nishant Samel Date: Wed, 10 Jul 2024 10:47:38 +0530 Subject: [PATCH 6/6] Fix review comments --- app/models/notification_preference.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/notification_preference.rb b/app/models/notification_preference.rb index a6d21b2c4c..6027ca309b 100644 --- a/app/models/notification_preference.rb +++ b/app/models/notification_preference.rb @@ -26,7 +26,5 @@ class NotificationPreference < ApplicationRecord belongs_to :user belongs_to :company - validates :user_id, presence: true - validates :company_id, presence: true validates :notification_enabled, inclusion: { in: [true, false] } end