diff --git a/.env.example b/.env.example index 1c277032d0..a451afcbd5 100644 --- a/.env.example +++ b/.env.example @@ -27,13 +27,6 @@ GOOGLE_OAUTH_CLIENT_SECRET='google-oauth-client-secret' # Application Monitoring NEW_RELIC_LICENSE_KEY='replace with newrelic licence key' -# Redis url -REDIS_URL='redis://127.0.0.1:6379/12' - -# Sidekiq Configuration -SIDEKIQ_USERNAME: -SIDEKIQ_PASSWORD: - # Stripe STRIPE_PUBLISHABLE_KEY="stripe_publishable_key" STRIPE_SECRET_KEY="stripe_secret_key" @@ -61,4 +54,8 @@ SEED_DATA_FROM_CSV=true VIRTUAL_VERIFIED_ADMIN_EMAILS=[] -CI=true \ No newline at end of file +CI=true + +# SolidQueue Configuration +SOLID_QUEUE_USERNAME: +SOLID_QUEUE_PASSWORD: \ No newline at end of file diff --git a/Gemfile b/Gemfile index d583ce6432..84ad5007df 100644 --- a/Gemfile +++ b/Gemfile @@ -114,11 +114,9 @@ gem "active_interaction" # For stripe payments gem "stripe" -# Background job processing adapter -gem "sidekiq", "~> 7.2" - -# job scheduler extension for Sidekiq -gem "sidekiq-scheduler" +# Background job processing adapter and dashboard +gem "mission_control-jobs" +gem "solid_queue", "~> 0.3" # searchkick for elasticsearch gem "elasticsearch", "< 7.14" # select one @@ -209,7 +207,6 @@ group :test, :ci do # Strategies for cleaning databases in Ruby. gem "database_cleaner", "~> 2.0" gem "hash_dot" - gem "rspec-sidekiq", git: "https://github.com/wspurgin/rspec-sidekiq", branch: "main" gem "rspec-buildkite" gem "rspec-retry" diff --git a/Gemfile.lock b/Gemfile.lock index 3fec248c4b..26f2fe9ff3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,17 +11,6 @@ GIT rubocop smart_properties -GIT - remote: https://github.com/wspurgin/rspec-sidekiq - revision: 0e710d7698c2fcea77245c7d458bb00b0fc50dae - branch: main - specs: - rspec-sidekiq (4.1.0) - rspec-core (~> 3.0) - rspec-expectations (~> 3.0) - rspec-mocks (~> 3.0) - sidekiq (>= 5, < 8) - GEM remote: https://rubygems.org/ specs: @@ -222,7 +211,7 @@ GEM faraday (~> 1) multi_json erubi (1.12.0) - et-orbi (1.2.7) + et-orbi (1.2.11) tzinfo execjs (2.8.1) factory_bot (6.2.1) @@ -257,8 +246,8 @@ GEM faraday-retry (1.0.3) ffi (1.15.5) foreman (0.87.2) - fugit (1.8.1) - et-orbi (~> 1, >= 1.2.7) + fugit (1.11.0) + et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) gems (1.2.0) globalid (1.1.0) @@ -305,6 +294,10 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) io-console (0.7.2) irb (1.12.0) rdoc @@ -354,6 +347,11 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) minitest (5.23.1) + mission_control-jobs (0.2.1) + importmap-rails + rails (~> 7.1) + stimulus-rails + turbo-rails money (6.16.0) i18n (>= 0.6.4, <= 2) msgpack (1.6.0) @@ -483,8 +481,6 @@ GEM railties (>= 3.2) tilt redis (4.8.0) - redis-client (0.22.1) - connection_pool regexp_parser (2.9.0) reline (0.5.3) io-console (~> 0.5) @@ -552,8 +548,6 @@ GEM ruby_audit (2.2.0) bundler-audit (~> 0.9.0) rubyzip (2.3.2) - rufus-scheduler (3.8.2) - fugit (~> 1.1, >= 1.1.6) safely_block (0.4.0) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -589,15 +583,6 @@ GEM activesupport (>= 3) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.2.4) - concurrent-ruby (< 2) - connection_pool (>= 2.3.0) - rack (>= 2.2.4) - redis-client (>= 0.19.0) - sidekiq-scheduler (5.0.1) - rufus-scheduler (~> 3.2) - sidekiq (>= 4, < 8) - tilt (>= 1.4.0) signet (0.17.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -613,6 +598,12 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) + solid_queue (0.3.3) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) spring (4.1.1) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -621,6 +612,8 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + stimulus-rails (1.3.3) + railties (>= 6.0.0) stringio (3.1.0) stripe (8.2.0) strong_migrations (1.4.2) @@ -630,6 +623,10 @@ GEM tilt (2.0.11) timeout (0.4.1) trailblazer-option (0.1.2) + turbo-rails (2.0.5) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) @@ -698,6 +695,7 @@ DEPENDENCIES jbuilder (~> 2.11) letter_opener letter_opener_web + mission_control-jobs money newrelic_rpm (~> 9.8.0) nokogiri (>= 1.16.2) @@ -720,7 +718,6 @@ DEPENDENCIES rspec-buildkite rspec-rails (~> 6.1) rspec-retry - rspec-sidekiq! rubocop rubocop-performance rubocop-rails @@ -735,9 +732,8 @@ DEPENDENCIES shakapacker (= 6.0.0) shoulda-callback-matchers (~> 1.1.1) shoulda-matchers (~> 5.1) - sidekiq (~> 7.2) - sidekiq-scheduler simplecov + solid_queue (~> 0.3) spring stripe strong_migrations diff --git a/Procfile b/Procfile index 9e34c4fd47..c066c20815 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-production} -worker: bundle exec sidekiq -e production -C config/sidekiq.yml +worker: bundle exec rake solid_queue:start \ No newline at end of file diff --git a/Procfile.dev b/Procfile.dev index ea1ced25b2..528c28b238 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,3 +1,3 @@ web: bin/rails s -b 0.0.0.0 -p 3000 webpacker: bin/webpacker-dev-server -sidekiq: bundle exec sidekiq -e development -C config/sidekiq.yml +solidqueue: bundle exec rake solid_queue:start diff --git a/README.md b/README.md index 0ceb2f81df..8f69094458 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ organizations to help them streamline their workflow. ![GitHub release (latest by date)](https://img.shields.io/github/v/release/saeloun/miru-web) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/saeloun/miru-web) [![GitHub license](https://img.shields.io/github/license/saeloun/miru-web)](https://github.com/saeloun/miru-web) -[![Twitter Follow](https://img.shields.io/twitter/follow/GetMiru?style=social)](https://twitter.com/getmiru) +[![Twitter Follow](https://img.shields.io/twitter/follow/GetMiru?style=social)](https://x.com/getmiru) Miru Monthly Timetracking page @@ -64,7 +64,7 @@ The documentation covers everything from installation and setup to advanced usag - Subscribe to our latest [blog articles](https://blog.miru.so) and tutorials. - [Discussions](https://github.com/saeloun/miru-web/discussions): Post your questions regarding Miru Web -- [**Twitter**](https://twitter.com/getmiru) +- [**Twitter**](https://x.com/getmiru) ## Contributing diff --git a/app/controllers/internal_api/v1/profile_controller.rb b/app/controllers/internal_api/v1/profile_controller.rb index 2fb1acec20..4e1b1cf8a3 100644 --- a/app/controllers/internal_api/v1/profile_controller.rb +++ b/app/controllers/internal_api/v1/profile_controller.rb @@ -1,17 +1,6 @@ # frozen_string_literal: true class InternalApi::V1::ProfileController < InternalApi::V1::ApplicationController - def show - authorize :show, policy_class: ProfilePolicy - render :show, locals: { user: current_user }, status: :ok - end - - def remove_avatar - authorize :remove_avatar, policy_class: ProfilePolicy - current_user.avatar.destroy - render json: { notice: "Avatar deleted successfully" }, status: :ok - end - def update authorize :update, policy_class: ProfilePolicy service = UpdateProfileSettingsService.new(current_user, user_params).process 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/controllers/mission_control_controller.rb b/app/controllers/mission_control_controller.rb new file mode 100644 index 0000000000..99465b3b85 --- /dev/null +++ b/app/controllers/mission_control_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class MissionControlController < ApplicationController + before_action :authenticate!, if: :restricted_env? + skip_after_action :verify_authorized + + private + + def authenticate! + authenticate_or_request_with_http_basic do |username, password| + username == ENV.fetch("SOLID_QUEUE_USERNAME") && password == ENV.fetch("SOLID_QUEUE_PASSWORD") + end + end + + def restricted_env? + Rails.env.production? + 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/apis/profile.ts b/app/javascript/src/apis/profile.ts index 9cddc740ec..9686dd9c9a 100644 --- a/app/javascript/src/apis/profile.ts +++ b/app/javascript/src/apis/profile.ts @@ -2,30 +2,10 @@ import axios from "./api"; const path = "/profile"; -const index = () => axios.get(path); - -const getAddress = id => axios.get(`/users/${id}/addresses`); - const update = payload => axios.put(`${path}`, payload); -const upadteAvatar = (payload, config) => axios.put(`${path}`, payload, config); - -const createAddress = (userId, payload) => - axios.post(`/users/${userId}/addresses`, payload); - -const updateAddress = (userId, addressId, payload) => - axios.put(`/users/${userId}/addresses/${addressId}`, payload); - -const removeAvatar = () => axios.delete(`${path}/remove_avatar`); - const profileApi = { - index, update, - removeAvatar, - getAddress, - updateAddress, - createAddress, - upadteAvatar, }; export default profileApi; diff --git a/app/javascript/src/apis/team.ts b/app/javascript/src/apis/team.ts index 4e47068feb..d8d369b219 100644 --- a/app/javascript/src/apis/team.ts +++ b/app/javascript/src/apis/team.ts @@ -16,8 +16,8 @@ const updateTeamMember = (id, payload) => axios.put(`${path}/${id}`, payload); const destroyTeamMemberAvatar = id => axios.delete(`${path}/${id}/avatar`); -const updateTeamMemberAvatar = (id, payload) => - axios.put(`${path}/${id}/avatar`, payload); +const updateTeamMemberAvatar = (id, payload, config) => + axios.put(`${path}/${id}/avatar`, payload, config); const updateTeamMembers = payload => axios.put(`${path}/update_team_members`, payload); diff --git a/app/javascript/src/components/InvoiceEmail/index.tsx b/app/javascript/src/components/InvoiceEmail/index.tsx index 4566a17a5b..09b01a965c 100644 --- a/app/javascript/src/components/InvoiceEmail/index.tsx +++ b/app/javascript/src/components/InvoiceEmail/index.tsx @@ -101,11 +101,7 @@ const InvoiceEmail = () => { > - + diff --git a/app/javascript/src/components/Profile/BankAccountDetails/AddressDetails.tsx b/app/javascript/src/components/Profile/BankAccountDetails/AddressDetails.tsx deleted file mode 100644 index bedb0b7538..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/AddressDetails.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react"; - -const AddressDetails = ({ - fields, - handleAddressDetails, - handleBankDetails, - recipientDetails, -}) => { - const legalType = fields.filter( - field => field["group"][0]["key"] == "legalType" - )[0]; - - const countries = fields.filter( - field => field["group"][0]["key"] == "address.country" - )[0]; - - const postCode = fields.filter( - field => field["group"][0]["key"] == "address.postCode" - )[0]; - - return ( -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
- - handleAddressDetails(name, value, { - validationRegexp: "^.{1,255}$", - }) - } - /> -
-
-
- -
- - handleAddressDetails(name, value, { validationRegexp: regexp }) - } - /> -
-
-
-
- -
- - handleAddressDetails(name, value, { - validationRegexp: "^.{1,255}$", - }) - } - /> -
-
-
- ); -}; - -export default AddressDetails; diff --git a/app/javascript/src/components/Profile/BankAccountDetails/BankDetails.tsx b/app/javascript/src/components/Profile/BankAccountDetails/BankDetails.tsx deleted file mode 100644 index 9fb943399f..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/BankDetails.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useEffect } from "react"; - -import { XIcon, ShieldSVG } from "miruIcons"; -import { isEmpty } from "ramda"; - -import wiseApi from "apis/wise"; - -import AddressDetails from "./AddressDetails"; -import BankDetailInput from "./BillingDetailInput"; - -const BankDetails = ({ - bankRequirements, - firstName, - setFirstName, - lastName, - setLastName, - recipientDetails, - setRecipientDetails, - validRecipientDetails, - setValidRecipientDetails, - setBankDetailsModal, - submitBankDetails, -}) => { - useEffect(() => { - setRecipientDetails({ - ...recipientDetails, - type: bankRequirements[0]["type"], - }); - }, []); - - const updateDetails = (isValid, key, value) => { - const valid = { ...validRecipientDetails }; - const details = { ...recipientDetails }; - - valid["details"][`${key}`] = isValid; - details["details"][`${key}`] = value; - setRecipientDetails(details); - setValidRecipientDetails(valid); - }; - - const updateAddressDetails = (isValid, key, value) => { - const valid = { ...validRecipientDetails }; - const details = { ...recipientDetails }; - - valid["details"]["address"][`${key}`] = isValid; - details["details"]["address"][`${key}`] = value; - setRecipientDetails(details); - setValidRecipientDetails(valid); - }; - - const handleBankDetails = async (key, value, fieldDetails) => { - const validationAsync = fieldDetails["validationAsync"]; - - if (validationAsync) { - try { - const url = `${validationAsync.url}?${validationAsync.params[0]["key"]}=${value}`; - await wiseApi.validateAccountDetail(url); - updateDetails(true, key, value); - } catch (error) { - if (error.response.status == 400) { - updateDetails(false, key, value); - } - } - } else if (fieldDetails["validationRegexp"]) { - value.match(new RegExp(fieldDetails["validationRegexp"])) - ? updateDetails(true, key, value) - : updateDetails(false, key, value); - } else { - updateDetails(true, key, value); - } - }; - - const handleAddressDetails = (key, value, fieldDetails) => { - if (fieldDetails["validationRegexp"]) { - value.match(new RegExp(fieldDetails["validationRegexp"])) - ? updateAddressDetails(true, key, value) - : updateAddressDetails(false, key, value); - } else { - updateAddressDetails(true, key, value); - } - }; - - const isFormValid = - Object.values(validRecipientDetails["details"]).every(value => value) && - Object.values(validRecipientDetails["details"]["address"]).every( - value => value - ) && - !isEmpty(firstName) && - !isEmpty(lastName); - - return ( - <> -
-
- {/*content*/} -
- {/*header*/} -
-

Enter Bank Details

- -
- {/*Info*/} -
- -

- We don't store your bank details on our servers to ensure - security of your information -

-
- {/*body*/} -
- -
- setFirstName(value)} - /> - setLastName(value)} - /> -
- {bankRequirements[0]["fields"].map(field => ( - - ))} - -
- {/*footer*/} -
- -
-
-
-
-
- - ); -}; - -export default BankDetails; diff --git a/app/javascript/src/components/Profile/BankAccountDetails/BankInfo.tsx b/app/javascript/src/components/Profile/BankAccountDetails/BankInfo.tsx deleted file mode 100644 index 4a6abdddbc..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/BankInfo.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect } from "react"; - -import Logger from "js-logger"; - -import wiseApi from "apis/wise"; - -const BankInfo = ({ - recipientId, - sourceCurrency, - targetCurrency, - fetchAccountRequirements, - setBankDetailsModal, - setIsLoading, - setFirstName, - setLastName, - setRecipientDetails, - setIsUpdate, -}) => { - useEffect(() => { - fetchRecipientDetails(recipientId); - }, []); - - useEffect(() => { - fetchAccountRequirements(sourceCurrency, targetCurrency, true); - }, []); - - const fetchRecipientDetails = async recipientId => { - try { - setIsLoading(true); - const response = await wiseApi.fetchRecipient(recipientId); - const name = response.data["accountHolderName"].split(" "); - setRecipientDetails(response.data); - setFirstName(name.shift()); - setLastName(name.join(" ")); - setIsUpdate(true); - } catch (error) { - Logger.error(error); - } finally { - setIsLoading(false); - } - }; - - return ( -
-

You have already submitted your bank details.

- -
- ); -}; - -export default BankInfo; diff --git a/app/javascript/src/components/Profile/BankAccountDetails/BillingDetailInput.tsx b/app/javascript/src/components/Profile/BankAccountDetails/BillingDetailInput.tsx deleted file mode 100644 index b436082409..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/BillingDetailInput.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; - -const BillingDetailInput = ({ field, handleBankDetails, recipientDetails }) => { - const group = field["group"][0]; - - if (group.type == "text") { - return ( -
- -
- - handleBankDetails(name, value, group) - } - /> -
-
- ); - } - - return ( -
- -
- -
-
- ); -}; - -export default BillingDetailInput; diff --git a/app/javascript/src/components/Profile/BankAccountDetails/CurrencyDropdown.tsx b/app/javascript/src/components/Profile/BankAccountDetails/CurrencyDropdown.tsx deleted file mode 100644 index 44f0c31f7b..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/CurrencyDropdown.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useEffect } from "react"; - -import getSymbolFromCurrency from "currency-symbol-map"; -import Logger from "js-logger"; - -import wiseApi from "apis/wise"; - -const CurrencyDropdown = ({ - currencies, - setCurrencies, - currency, - setCurrency, - setIsLoading, - setBankDetailsModal, - fetchAccountRequirements, -}) => { - useEffect(() => { - fetchCurrencyList(); - }, []); - - useEffect(() => { - if (currency) { - fetchAccountRequirements("USD", currency); - } - }, [currency]); - - const fetchCurrencyList = async () => { - try { - setIsLoading(true); - const response = await wiseApi.fetchCurrencies(); - const list = response.data.map(currency => ({ - label: `${currency} (${getSymbolFromCurrency(currency)})`, - value: currency, - })); - setCurrencies(list); - } catch (error) { - Logger.error(error); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
- -
-
- - -
-
- ); -}; - -export default CurrencyDropdown; diff --git a/app/javascript/src/components/Profile/BankAccountDetails/index.tsx b/app/javascript/src/components/Profile/BankAccountDetails/index.tsx deleted file mode 100644 index 8874d8e94a..0000000000 --- a/app/javascript/src/components/Profile/BankAccountDetails/index.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, { useState, useEffect } from "react"; - -import { - bankFieldValidationRequirements, - separateAddressFields, -} from "helpers"; -import Logger from "js-logger"; -import { isEmpty } from "ramda"; - -import profilesApi from "apis/profiles"; -import wiseApi from "apis/wise"; -import Loader from "common/Loader"; -import { sendGAPageView } from "utils/googleAnalytics"; - -import BankDetails from "./BankDetails"; -import BankInfo from "./BankInfo"; -import CurrencyDropdown from "./CurrencyDropdown"; - -import Header from "../Header"; - -const BankAccountDetails = () => { - const [isUpdate, setIsUpdate] = useState(); - const [billingDetails, setBillingDetails] = useState({}); - const [currencies, setCurrencies] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [currency, setCurrency] = useState(); - const [bankDetailsModal, setBankDetailsModal] = useState(false); - const [bankRequirements, setBankRequirements] = useState(); - const [recipientDetails, setRecipientDetails] = useState({ - details: { address: {} }, - }); - - const [validRecipientDetails, setValidRecipientDetails] = useState({ - details: { address: {} }, - }); - - const [firstName, setFirstName] = useState(); - const [lastName, setLastName] = useState(); - - useEffect(() => { - sendGAPageView(); - fetchProfileDetails(); - }, []); - - const fetchProfileDetails = async () => { - try { - setIsLoading(true); - const response = await profilesApi.get(); - setBillingDetails(response.data); - } catch (error) { - Logger.error(error); - } finally { - setIsLoading(false); - } - }; - - const createRecipient = async payload => { - try { - const response = await wiseApi.createRecipient(payload); - const billingResponse = await profilesApi.post(response.data); - setBillingDetails(billingResponse.data); - } catch { - setBankDetailsModal(true); - throw new Error("Error while creating recipient"); - } finally { - setIsLoading(false); - } - }; - - const updateRecipient = async payload => { - try { - const response = await wiseApi.updateRecipient(payload); - const billingResponse = await profilesApi.put( - billingDetails.id, - response.data - ); - setBillingDetails(billingResponse.data); - setRecipientDetails(response.data); - } catch { - setBankDetailsModal(true); - throw new Error("Error while creating recipient"); - } finally { - setIsLoading(false); - } - }; - - // TODO: Try to remove duplicate code as much as possible and optimize - const fetchAccountRequirements = async ( - sourceCurrency, - targetCurrency, - isUpdate = false - ) => { - try { - setIsLoading(true); - const response = await wiseApi.fetchAccountRequirements( - sourceCurrency, - targetCurrency - ); - const data = response.data; - const validRecipientDetails = bankFieldValidationRequirements( - data, - isUpdate - ); - setValidRecipientDetails(validRecipientDetails); - if (!isUpdate) { - setRecipientDetails({ details: { address: {} } }); - } - - const fields = data.map(requirement => - separateAddressFields(requirement) - ); - setBankRequirements(fields); - } catch (error) { - Logger.error(error); - } finally { - setIsLoading(false); - } - }; - - const submitBankDetails = () => { - const payload = { - ...recipientDetails, - accountHolderName: `${firstName} ${lastName}`, - currency: currency || billingDetails.targetCurrency, - }; - setIsLoading(true); - setBankDetailsModal(false); - isUpdate ? updateRecipient(payload) : createRecipient(payload); - }; - - const renderBankDetails = billingDetails => { - if (isEmpty(billingDetails)) { - return
; - } else if (!billingDetails.recipientId) { - return ( - - ); - } - - return ( - - ); - }; - - return ( -
-
-
- {isLoading && } -
Bank Account Details
- {renderBankDetails(billingDetails)} - {bankDetailsModal && ( - - )} -
-
- ); -}; - -export default BankAccountDetails; diff --git a/app/javascript/src/components/Profile/Billing/Table/TableHeader.tsx b/app/javascript/src/components/Profile/Billing/Table/TableHeader.tsx deleted file mode 100644 index b3612c9fb2..0000000000 --- a/app/javascript/src/components/Profile/Billing/Table/TableHeader.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; - -const TableHeader = () => ( - - - DATE - - - DESCRIPTION - - - TEAM MEMBERS - - - TOTAL BILL AMT - - - PAYMENT TYPE - - - -); - -export default TableHeader; diff --git a/app/javascript/src/components/Profile/Billing/Table/TableRow.tsx b/app/javascript/src/components/Profile/Billing/Table/TableRow.tsx deleted file mode 100644 index 80e931dc42..0000000000 --- a/app/javascript/src/components/Profile/Billing/Table/TableRow.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState } from "react"; - -import { DownloadSimpleIcon } from "miruIcons"; - -const TableRow = ({ data }) => { - const [isSending, setIsSending] = useState(false); - - return ( - - - {data.date} - - - {data.description} - - - {data.team_members} - - - {data.total_bill_amt} - - - {data.payment_type} - - -
- -
- - - ); -}; - -export default TableRow; diff --git a/app/javascript/src/components/Profile/Billing/Table/index.tsx b/app/javascript/src/components/Profile/Billing/Table/index.tsx deleted file mode 100644 index b3def7b154..0000000000 --- a/app/javascript/src/components/Profile/Billing/Table/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -import TableHeader from "./TableHeader"; -import TableRow from "./TableRow"; - -const data = []; - -const Table = () => ( - - - - - - {data.map((data, index) => ( - - ))} - -
-); - -export default Table; diff --git a/app/javascript/src/components/Profile/Billing/index.tsx b/app/javascript/src/components/Profile/Billing/index.tsx deleted file mode 100644 index eb3acbf21a..0000000000 --- a/app/javascript/src/components/Profile/Billing/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; - -import { Divider } from "common/Divider"; - -import Table from "./Table"; - -import Header from "../Header"; - -const Billing = () => ( -
-
-
-
-
-
Next Billing Date
-
-
-
- -
-
-

- Add-Ons -

-

1 team member

-

- -$ per user per month -

-
-
-

-$/mo

-

- charged every month{" "} -

-
-
- -
-
Total
-
-

-$

-

- plus taxes{" "} -

-
-
-
-
-

Billing History

- - - - -); - -export default Billing; diff --git a/app/javascript/src/components/Profile/Common/DetailsHeader.tsx b/app/javascript/src/components/Profile/Common/DetailsHeader.tsx new file mode 100644 index 0000000000..0fcb2dd2a4 --- /dev/null +++ b/app/javascript/src/components/Profile/Common/DetailsHeader.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import { getYear } from "date-fns"; + +import CustomYearPicker from "common/CustomYearPicker"; + +const DetailsHeader = ({ + title, + subTitle, + showButtons = false, + editAction, + isDisableUpdateBtn = false, + showYearPicker = false, + currentYear = getYear(new Date()), + setCurrentYear, +}: Iprops) => ( +
+ {title} + {subTitle && {subTitle}} + {showYearPicker && ( + + )} +
+
+ +
+
+
+); + +interface Iprops { + title: string; + subTitle: string; + showButtons?: boolean; + editAction?: () => any; + isDisableUpdateBtn?: boolean; + showYearPicker?: boolean; + currentYear?: number; + setCurrentYear?: () => any; +} + +export default DetailsHeader; diff --git a/app/javascript/src/components/Profile/Common/EditHeader.tsx b/app/javascript/src/components/Profile/Common/EditHeader.tsx new file mode 100644 index 0000000000..e6fb76f331 --- /dev/null +++ b/app/javascript/src/components/Profile/Common/EditHeader.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +import { getYear } from "date-fns"; +import { XIcon } from "miruIcons"; + +import CustomYearPicker from "common/CustomYearPicker"; + +const EditHeader = ({ + title, + subTitle, + showButtons = false, + cancelAction, + saveAction, + isDisableUpdateBtn = false, + showYearPicker = false, + currentYear = getYear(new Date()), + setCurrentYear, +}: Iprops) => ( + <> +
+

{title}

+ {subTitle && {subTitle}} + {showYearPicker && ( + + )} +
+
+ + +
+
+
+
+

+ {title} +

+
+ +
+
+ +); + +interface Iprops { + title: string; + subTitle: string; + showButtons?: boolean; + cancelAction?: () => any; + saveAction?: () => any; + isDisableUpdateBtn?: boolean; + showYearPicker?: boolean; + currentYear?: number; + setCurrentYear?: () => any; +} + +export default EditHeader; diff --git a/app/javascript/src/components/Profile/CommonComponents/Header/index.tsx b/app/javascript/src/components/Profile/CommonComponents/Header/index.tsx deleted file mode 100644 index 1621d1400a..0000000000 --- a/app/javascript/src/components/Profile/CommonComponents/Header/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const Header = () => ( -
Profile & Settings
-); - -export default React.memo(Header); diff --git a/app/javascript/src/components/Profile/context/CompensationDetailsState.tsx b/app/javascript/src/components/Profile/Context/CompensationDetailsState.tsx similarity index 100% rename from app/javascript/src/components/Profile/context/CompensationDetailsState.tsx rename to app/javascript/src/components/Profile/Context/CompensationDetailsState.tsx diff --git a/app/javascript/src/components/Profile/context/EmploymentDetailsState.tsx b/app/javascript/src/components/Profile/Context/EmploymentDetailsState.tsx similarity index 100% rename from app/javascript/src/components/Profile/context/EmploymentDetailsState.tsx rename to app/javascript/src/components/Profile/Context/EmploymentDetailsState.tsx diff --git a/app/javascript/src/components/Profile/context/PersonalDetailsState.tsx b/app/javascript/src/components/Profile/Context/PersonalDetailsState.tsx similarity index 97% rename from app/javascript/src/components/Profile/context/PersonalDetailsState.tsx rename to app/javascript/src/components/Profile/Context/PersonalDetailsState.tsx index 3695068aa7..585967581c 100644 --- a/app/javascript/src/components/Profile/context/PersonalDetailsState.tsx +++ b/app/javascript/src/components/Profile/Context/PersonalDetailsState.tsx @@ -1,4 +1,5 @@ export const PersonalDetailsState = { + id: "", first_name: "", last_name: "", date_of_birth: "", diff --git a/app/javascript/src/components/Profile/DetailsHeader.tsx b/app/javascript/src/components/Profile/DetailsHeader.tsx deleted file mode 100644 index bf845db9bc..0000000000 --- a/app/javascript/src/components/Profile/DetailsHeader.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState } from "react"; - -import { getYear } from "date-fns"; - -import CustomYearPicker from "common/CustomYearPicker"; - -const DetailsHeader = ({ - title, - subTitle, - showButtons = false, - editAction, - isDisableUpdateBtn = false, - showYearPicker = false, -}: Iprops) => { - const [currentYear, setCurrentYear] = useState(getYear(new Date())); - - return ( -
- {title} - {subTitle} - {showYearPicker && ( - - )} -
-
- -
-
-
- ); -}; - -interface Iprops { - title: string; - subTitle: string; - showButtons?: boolean; - editAction?: () => any; - isDisableUpdateBtn?: boolean; - showYearPicker?: boolean; -} - -export default DetailsHeader; diff --git a/app/javascript/src/components/Profile/GoogleCalendar/Header.tsx b/app/javascript/src/components/Profile/GoogleCalendar/Header.tsx deleted file mode 100644 index 7095040695..0000000000 --- a/app/javascript/src/components/Profile/GoogleCalendar/Header.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -const Header = ({ title }: Iprops) => ( - <> -
- {title} -
-
-

- {title} -

-
- -); - -interface Iprops { - title: string; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/GoogleCalendar/index.tsx b/app/javascript/src/components/Profile/GoogleCalendar/index.tsx deleted file mode 100644 index cab76b88fb..0000000000 --- a/app/javascript/src/components/Profile/GoogleCalendar/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useEffect, useState } from "react"; - -import Logger from "js-logger"; -import { GoogleCalendarIcon, IntegrateIcon } from "miruIcons"; -import { Button, Switch } from "StyledComponents"; - -import companiesApi from "apis/companies"; -import googleCalendarApi from "apis/googleCalendar"; -import teamApi from "apis/team"; -import Loader from "common/Loader/index"; -import { useUserContext } from "context/UserContext"; - -import Header from "./Header"; - -const GoogleCalendar = () => { - const { - isAdminUser: isAdmin, - calendarEnabled, - calendarConnected, - } = useUserContext(); - - const [connectGoogleCalendar, setConnectGoogleCalendar] = - useState(calendarConnected); - const [enabled, setEnabled] = useState(calendarEnabled); - const [apiCallNeeded, setApiCallNeeded] = useState(false); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (isAdmin) { - const companiesData = async () => { - setLoading(true); - try { - const { - data: { company_details }, - } = await companiesApi.index(); - setEnabled(company_details.calendar_enabled); - } catch (error) { - Logger.log(error); - } finally { - setLoading(false); - } - }; - companiesData(); - } - }, []); - - useEffect(() => { - if (apiCallNeeded) { - enableCalendar(); - setApiCallNeeded(false); - } - }, [apiCallNeeded]); - - if (loading) { - return ; - } - - const enableCalendar = async () => { - try { - const payload = { team: { calendar_enabled: enabled } }; - await teamApi.updateTeamMembers(payload); - } catch (error) { - Logger.log(error); - } - }; - - const toggleEnabled = () => { - setEnabled(prevEnabled => !prevEnabled); - setApiCallNeeded(true); - }; - - const handleConnectCalendar = async () => { - setConnectGoogleCalendar(true); - const { data } = await googleCalendarApi.redirect(); - window.location.replace(data.url); - }; - - const handleDisconnectCalendar = async () => { - setConnectGoogleCalendar(false); - await googleCalendarApi.callback(); - window.location.replace("/settings/integrations"); - }; - - const showConnectDisconnectBtn = () => { - if (enabled) { - return connectGoogleCalendar ? ( -
- - -
- ) : ( - - ); - } - }; - - return ( -
-
-
-
-
-
-
- -
- {isAdmin && } -
- - Google Calendar - -

- Connect your google calendar to automatically sync your meetings - with Miru -

- {showConnectDisconnectBtn()} -
-
-
-
- ); -}; - -export default GoogleCalendar; diff --git a/app/javascript/src/components/Profile/Header.tsx b/app/javascript/src/components/Profile/Header.tsx deleted file mode 100644 index 2e583ab0c4..0000000000 --- a/app/javascript/src/components/Profile/Header.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from "react"; - -import { getYear } from "date-fns"; -import { XIcon } from "miruIcons"; - -import CustomYearPicker from "common/CustomYearPicker"; - -const Header = ({ - title, - subTitle, - showButtons = false, - cancelAction, - saveAction, - isDisableUpdateBtn = false, - showYearPicker = false, -}: Iprops) => { - const [currentYear, setCurrentYear] = useState(getYear(new Date())); - - return ( - <> -
- {title} - {subTitle} - {showYearPicker && ( - - )} -
-
- - -
-
-
-
-

- {title} -

-
- -
-
- - ); -}; - -interface Iprops { - title: string; - subTitle: string; - showButtons?: boolean; - cancelAction?: () => any; - saveAction?: () => any; - isDisableUpdateBtn?: boolean; - showYearPicker?: boolean; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/Layout.tsx b/app/javascript/src/components/Profile/Layout.tsx deleted file mode 100644 index 90c5dffcd0..0000000000 --- a/app/javascript/src/components/Profile/Layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { Fragment, useState } from "react"; - -import { useUserContext } from "context/UserContext"; - -import Header from "./CommonComponents/Header"; -import { CompensationDetailsState } from "./context/CompensationDetailsState"; -import { EmploymentDetailsState } from "./context/EmploymentDetailsState"; -import EntryContext from "./context/EntryContext"; -import { PersonalDetailsState } from "./context/PersonalDetailsState"; -import RouteConfig from "./RouteConfig"; -import SubNav from "./SubNav"; - -const Layout = ({ isAdminUser, user, company }) => { - const { isDesktop } = useUserContext(); - - const [settingsStates, setSettingsStates] = useState({ - profileSettings: PersonalDetailsState, - employmentDetails: EmploymentDetailsState, - compensationDetails: CompensationDetailsState, - organizationSettings: {}, - bankAccDetails: {}, - paymentSettings: {}, - billing: {}, - }); - - const { profileSettings, employmentDetails } = settingsStates; - const setUserState = (key, value) => { - setSettingsStates(previousSettings => ({ - ...previousSettings, - ...{ [key]: { ...previousSettings[key], ...value } }, - })); - }; - - return ( - - {isDesktop && ( - -
-
-
-
-
- -
-
- -
-
-
- )} - {!isDesktop && } -
- ); -}; - -export default Layout; diff --git a/app/javascript/src/components/Profile/Layout/Header.tsx b/app/javascript/src/components/Profile/Layout/Header.tsx new file mode 100644 index 0000000000..5f9e952013 --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/Header.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import { ArrowLeftIcon } from "miruIcons"; +import { useNavigate } from "react-router-dom"; +import { Button } from "StyledComponents"; + +import { useProfileContext } from "context/Profile/ProfileContext"; + +const Header = () => { + const navigate = useNavigate(); + const { personalDetails, isCalledFromSettings } = useProfileContext(); + + const getHeaderContent = () => { + if (isCalledFromSettings) { + return Profile & Settings; + } + + return ( +
+ + + {`${personalDetails.first_name} ${personalDetails.last_name}`} + +
+ ); + }; + + return ( +
+ {getHeaderContent()} +
+ ); +}; + +export default Header; diff --git a/app/javascript/src/components/Profile/Layout/MobileNav.tsx b/app/javascript/src/components/Profile/Layout/MobileNav.tsx deleted file mode 100644 index 18a9e2222d..0000000000 --- a/app/javascript/src/components/Profile/Layout/MobileNav.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import React, { useEffect, useState } from "react"; - -import { - ClientsIcon as BuildingsIcon, - UserIcon, - PaymentsIcon, - ProjectsIcon, - MobileIcon, -} from "miruIcons"; -import { Outlet, useNavigate, useParams } from "react-router-dom"; - -import WorkspaceApi from "apis/workspaces"; -import Loader from "common/Loader/index"; -import withLayout from "common/Mobile/HOC/withLayout"; -import { useUserContext } from "context/UserContext"; - -import { TeamUrl } from "./TeamUrl"; -import { UserDetails } from "./UserDetails"; - -const getSettingsNavUrls = memberId => [ - { - groupName: "Personal", - navItems: [ - { - url: "/settings/profile", - text: "PERSONAL DETAILS", - icon: , - }, - { - url: "/settings/employment", - text: "EMPLOYMENT DETAILS", - icon: , - }, - { - url: "/settings/devices", - text: "ALLOCATED DEVICES", - icon: , - }, - //Todo: Uncomment while API integration - // { - // url: "/settings/compensation", - // text: "COMPENSATION", - // icon: , - // }, - ], - }, - - { - isCompanyDetails: true, - navItems: [ - { - url: "/settings/organization", - text: "ORG. SETTINGS", - icon: , - }, - { - url: "/settings/payment", - text: "PAYMENT SETTINGS", - icon: , - }, - // { - // url: "/settings/leaves", - // text: "Leaves", - // icon: , - // }, - // { - // url: "/settings/holidays", - // text: "Holidays", - // icon: , - // }, - ], - }, -]; - -const getEmployeeSettingsNavUrls = memberId => [ - { - groupName: "Personal", - navItems: [ - { - url: "/settings/profile", - text: "PERSONAL DETAILS", - icon: , - }, - { - url: "/settings/employment", - text: "EMPLOYMENT DETAILS", - icon: , - }, - { - url: "/settings/devices", - text: "ALLOCATED DEVICES", - icon: , - }, - { - url: "/settings/compensation", - text: "COMPENSATION", - icon: , - }, - ], - }, -]; - -const MobileNav = () => { - const { isAdminUser: isAdmin, isDesktop, user } = useUserContext(); - const { memberId } = useParams(); - const AdminUrlList = getSettingsNavUrls(memberId); - const EmployeeUrlList = getEmployeeSettingsNavUrls(memberId); - const navigate = useNavigate(); - const [currentWorkspace, setCurrentWorkspace] = useState({}); - const [isLoading, setIsLoading] = useState(false); - - const getCurrentWorkspace = async () => { - const res = await WorkspaceApi.get(); - const { workspaces } = res.data; - workspaces.find(wrk => { - if (wrk.id == user.current_workspace_id) { - setCurrentWorkspace(wrk); - } - }); - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - getCurrentWorkspace(); - }, []); - - useEffect(() => { - if (isDesktop) { - navigate("/settings/profile"); - } - }, [isDesktop]); - - const mobileView = () => ( -
- - - -
- ); - - const DisplayView = withLayout(mobileView, true, true); - - if (isLoading) { - return ( -
- -
- ); - } - - return ; -}; - -export default MobileNav; diff --git a/app/javascript/src/components/Profile/Layout/NavItem.tsx b/app/javascript/src/components/Profile/Layout/NavItem.tsx deleted file mode 100644 index 7f4690f6eb..0000000000 --- a/app/javascript/src/components/Profile/Layout/NavItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; - -import { RightArrowIcon } from "miruIcons"; - -interface NavItemProps { - icon?: any; - text: string; - url: string; -} - -const NavItem = ({ icon, text, url }: NavItemProps) => ( - -
  • -
    -
    {icon}
    -

    - {text} -

    -
    -
    - -
    -
  • -
    -); - -export default NavItem; diff --git a/app/javascript/src/components/Profile/Layout/Navigation/AdminNav.tsx b/app/javascript/src/components/Profile/Layout/Navigation/AdminNav.tsx new file mode 100644 index 0000000000..a1c082a68f --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/Navigation/AdminNav.tsx @@ -0,0 +1,62 @@ +import React, { Fragment, useState } from "react"; + +import { MinusIcon, PlusIcon } from "miruIcons"; + +import { useUserContext } from "context/UserContext"; + +import List from "./List"; + +import { SETTINGS } from "../routes"; + +const AdminNav = () => { + const { companyRole, company } = useUserContext(); + const [openedSubNav, setOpenedSubNav] = useState({ + personal: true, + organization: false, + }); + + const toggleSection = section => { + setOpenedSubNav({ + personal: section === "personal", + organization: section === "organization", + }); + }; + + const personalSettings = SETTINGS.filter( + ({ category }) => category === "personal" + ); + + const organizationalSettings = SETTINGS.filter( + ({ category }) => category === "organization" + ); + + const renderSection = (title, section, settingsList) => ( + +
    toggleSection(section)} + > + {title} +
    + {openedSubNav[section] ? ( + + ) : ( + + )} +
    +
    + {openedSubNav[section] && ( + + )} +
    + ); + + return ( +
    + {renderSection("Personal", "personal", personalSettings)} + {renderSection(company.name, "organization", organizationalSettings)} +
    + ); +}; + +export default AdminNav; diff --git a/app/javascript/src/components/Profile/Layout/Navigation/List.tsx b/app/javascript/src/components/Profile/Layout/Navigation/List.tsx new file mode 100644 index 0000000000..d28445adeb --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/Navigation/List.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import { NavLink } from "react-router-dom"; + +const getActiveClassName = isActive => { + if (isActive) { + return "pl-4 py-5 border-l-8 border-miru-han-purple-600 bg-miru-gray-200 text-miru-han-purple-600 block w-full flex items-center"; + } + + return "pl-6 py-5 border-b-1 border-miru-gray-400 block w-full flex items-center"; +}; + +const List = ({ settingsList, companyRole }) => ( +
      + {settingsList.map((setting, index) => { + if (setting.isTab && setting.authorisedRoles.includes(companyRole)) { + return ( +
    • + getActiveClassName(isActive)} + to={setting.path} + > + {setting.icon} + {setting.label} + +
    • + ); + } + })} +
    +); + +export default List; diff --git a/app/javascript/src/components/Profile/Layout/Navigation/MobileNav.tsx b/app/javascript/src/components/Profile/Layout/Navigation/MobileNav.tsx new file mode 100644 index 0000000000..9673d0ed9c --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/Navigation/MobileNav.tsx @@ -0,0 +1,92 @@ +/* eslint-disable no-unused-vars */ + +import React, { Fragment, useEffect, useState } from "react"; + +import { Outlet, useNavigate } from "react-router-dom"; + +import WorkspaceApi from "apis/workspaces"; +import Loader from "common/Loader/index"; +import withLayout from "common/Mobile/HOC/withLayout"; +import { useUserContext } from "context/UserContext"; + +import List from "./List"; +import UserInformation from "./UserInformation"; + +import { SETTINGS } from "../routes"; + +const MobileNav = () => { + const { companyRole, isDesktop, user, isAdminUser } = useUserContext(); + const navigate = useNavigate(); + const [currentWorkspace, setCurrentWorkspace] = useState({}); + const [isLoading, setIsLoading] = useState(false); + + const personalSettings = SETTINGS.filter( + ({ category }) => category === "personal" + ); + + const organizationalSettings = SETTINGS.filter( + ({ category }) => category === "organization" + ); + + const getCurrentWorkspace = async () => { + const res = await WorkspaceApi.get(); + const { workspaces } = res.data; + workspaces.find(wrk => { + if (wrk.id == user.current_workspace_id) { + setCurrentWorkspace(wrk); + } + }); + setIsLoading(false); + }; + + const renderSection = (title, settingsList) => ( + +
    + {title} +
    + +
    + ); + + useEffect(() => { + setIsLoading(true); + getCurrentWorkspace(); + }, []); + + useEffect(() => { + if (isDesktop) { + navigate("/settings/profile"); + } + }, [isDesktop]); + + const mobileView = () => ( +
    + +
    + {isAdminUser ? ( + + {renderSection("Personal", personalSettings)} + {renderSection(currentWorkspace.name, organizationalSettings)} + + ) : ( + {renderSection("Personal", personalSettings)} + )} +
    + +
    + ); + + const DisplayView = withLayout(mobileView, true, true); + + if (isLoading) { + return ( +
    + +
    + ); + } + + return ; +}; + +export default MobileNav; diff --git a/app/javascript/src/components/Profile/CommonComponents/UserInformation/index.tsx b/app/javascript/src/components/Profile/Layout/Navigation/UserInformation.tsx similarity index 68% rename from app/javascript/src/components/Profile/CommonComponents/UserInformation/index.tsx rename to app/javascript/src/components/Profile/Layout/Navigation/UserInformation.tsx index a35c0813e1..a233111961 100644 --- a/app/javascript/src/components/Profile/CommonComponents/UserInformation/index.tsx +++ b/app/javascript/src/components/Profile/Layout/Navigation/UserInformation.tsx @@ -1,14 +1,34 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; -import { EditIcon, ImageIcon, DeleteIcon } from "miruIcons"; -import { Avatar, MoreOptions, Toastr, Tooltip } from "StyledComponents"; +import { UserAvatarSVG, DeleteIcon, ImageIcon, EditIcon } from "miruIcons"; +import { MoreOptions, Toastr, Tooltip } from "StyledComponents"; -import profileApi from "apis/profile"; -import { useUserContext } from "context/UserContext"; +import teamApi from "apis/team"; +import teamsApi from "apis/teams"; +import { useProfileContext } from "context/Profile/ProfileContext"; + +const UserInformation = () => { + const { + personalDetails: { first_name, last_name, id }, + } = useProfileContext(); -const UserInformation = ({ firstName, lastName, designation }) => { const [showProfileOptions, setShowProfileOptions] = useState(false); - const { avatarUrl, setCurrentAvatarUrl } = useUserContext(); + const [imageUrl, setImageUrl] = useState(null); + + const getAvatar = async () => { + try { + const responseData = await teamsApi.get(id); + setImageUrl(responseData.data.avatar_url); + } catch { + Toastr.error("Error in getting Profile Image"); + } + }; + + useEffect(() => { + if (id) { + getAvatar(); + } + }, [id]); const validateFileSize = file => { const sizeInKB = file.size / 1024; @@ -29,23 +49,25 @@ const UserInformation = ({ firstName, lastName, designation }) => { setShowProfileOptions(false); const file = e.target.files[0]; validateFileSize(file); - setCurrentAvatarUrl(URL.createObjectURL(file)); + setImageUrl(URL.createObjectURL(file)); const payload = createFormData(file); - const headers = { "Content-Type": "multipart/form-data", }; - - await profileApi.upadteAvatar(payload, { headers }); + await teamApi.updateTeamMemberAvatar(id, payload, { headers }); } catch (error) { Toastr.error(error.message); } }; const handleDeleteProfileImage = async () => { - setShowProfileOptions(false); - await profileApi.removeAvatar(); - setCurrentAvatarUrl(null); + try { + setShowProfileOptions(false); + await teamApi.destroyTeamMemberAvatar(id); + setImageUrl(null); + } catch { + Toastr.error("Error in deleting Profile Image"); + } }; return ( @@ -54,7 +76,10 @@ const UserInformation = ({ firstName, lastName, designation }) => {
    - +
    -
    +
    {showProfileOptions && (
  • @@ -82,7 +107,7 @@ const UserInformation = ({ firstName, lastName, designation }) => { type="file" onChange={handleProfileImageChange} /> - {avatarUrl && ( + {imageUrl && (
  • { )}
    - {`${firstName} ${lastName}`} + {`${first_name} ${last_name}`} -

    - {designation} -

    diff --git a/app/javascript/src/components/Profile/Layout/Navigation/index.tsx b/app/javascript/src/components/Profile/Layout/Navigation/index.tsx new file mode 100644 index 0000000000..b70f435c6f --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/Navigation/index.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +import { useProfileContext } from "context/Profile/ProfileContext"; +import { useUserContext } from "context/UserContext"; + +import AdminNav from "./AdminNav"; +import List from "./List"; +import UserInformation from "./UserInformation"; + +import { SETTINGS } from "../routes"; + +const SideNav = () => { + const { isCalledFromSettings } = useProfileContext(); + const { isAdminUser, companyRole } = useUserContext(); + const personalSettings = SETTINGS.filter( + ({ category }) => category === "personal" + ); + + const EmployeeNav = () => ( + + ); + + return ( +
    + +
    + {isCalledFromSettings && isAdminUser ? : } +
    +
    + ); +}; + +export default SideNav; diff --git a/app/javascript/src/components/Team/Details/Layout/OutletWrapper.tsx b/app/javascript/src/components/Profile/Layout/OutletWrapper.tsx similarity index 100% rename from app/javascript/src/components/Team/Details/Layout/OutletWrapper.tsx rename to app/javascript/src/components/Profile/Layout/OutletWrapper.tsx diff --git a/app/javascript/src/components/Profile/Layout/RouteConfig.tsx b/app/javascript/src/components/Profile/Layout/RouteConfig.tsx new file mode 100644 index 0000000000..14226146cf --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/RouteConfig.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from "react"; + +import { Routes, Route, Navigate } from "react-router-dom"; + +import ErrorPage from "common/Error"; +import Layout from "components/Profile/index"; +import { useProfileContext } from "context/Profile/ProfileContext"; +import { useUserContext } from "context/UserContext"; + +import { SETTINGS } from "./routes"; + +import UserDetailsView from "../Personal/User"; + +const ProtectedRoute = ({ role, authorisedRoles, children }) => { + if (authorisedRoles.includes(role)) { + return children; + } + + return ; +}; + +const RouteConfig = () => { + const { companyRole } = useUserContext(); + const { setIsCalledFromSettings, setIsCalledFromTeam } = useProfileContext(); + + useEffect(() => { + if (window.location.pathname.startsWith("/settings")) { + setIsCalledFromSettings(true); + } else { + setIsCalledFromSettings(false); + } + + if (window.location.pathname.startsWith("/team")) { + setIsCalledFromTeam(true); + } else { + setIsCalledFromTeam(false); + } + }, [window.location]); + + return ( + + {window.location.pathname.startsWith("/team") && ( + } path=":memberId"> + {SETTINGS.filter(({ category }) => category === "personal").map( + ({ path, authorisedRoles, Component }) => ( + + + + } + /> + ) + )} + + )} + {window.location.pathname.startsWith("/settings") && ( + } path="/*"> + } path="profile" /> + {SETTINGS.map(({ path, authorisedRoles, Component }) => ( + + + + } + /> + ))} + + )} + } path="*" /> + + ); +}; + +export default RouteConfig; diff --git a/app/javascript/src/components/Profile/Layout/TeamUrl.tsx b/app/javascript/src/components/Profile/Layout/TeamUrl.tsx deleted file mode 100644 index e8c8c16254..0000000000 --- a/app/javascript/src/components/Profile/Layout/TeamUrl.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; - -import NavItem from "./NavItem"; - -export const TeamUrl = ({ urlList, currentWorkspaceName }) => ( -
    -
      - {urlList.map((item, index) => ( -
    • - {item?.isCompanyDetails ? ( - <> -

      - {currentWorkspaceName} -

      -
        - {item?.navItems?.map(navItem => ( - - ))} -
      - - ) : ( - <> -

      - {item.groupName} -

      -
        - {item?.navItems?.map(navItem => ( - - ))} -
      - - )} -
    • - ))} -
    -
    -); diff --git a/app/javascript/src/components/Profile/Layout/UserDetails.tsx b/app/javascript/src/components/Profile/Layout/UserDetails.tsx deleted file mode 100644 index 383a53bed7..0000000000 --- a/app/javascript/src/components/Profile/Layout/UserDetails.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import React, { useEffect, useState } from "react"; - -import { DeleteIcon, EditIcon, ImageIcon } from "miruIcons"; -import { Avatar, MobileMoreOptions, Toastr, Tooltip } from "StyledComponents"; - -import profileApi from "apis/profile"; -import teamsApi from "apis/teams"; -import { useProfile } from "components/Profile/context/EntryContext"; -import { useUserContext } from "context/UserContext"; -import { employmentMapper, teamsMapper } from "mapper/teams.mapper"; - -export const UserDetails = () => { - const { setUserState, profileSettings, employmentDetails } = useProfile(); - const { first_name, last_name } = profileSettings; - const { - current_employment: { designation }, - } = employmentDetails; - - const [showImageUpdateOptions, setShowImageUpdateOptions] = - useState(false); - const { avatarUrl, setCurrentAvatarUrl, companyRole } = useUserContext(); - const getDetails = async () => { - try { - if (!first_name && !last_name) { - const userData = await profileApi.index(); - if (userData.status && userData.status == 200) { - const addressData = await profileApi.getAddress( - userData.data.user.id - ); - - const userObj = teamsMapper( - userData.data.user, - addressData.data.addresses[0] - ); - setUserState("profileSettings", userObj); - } - - if (companyRole !== "client" && !designation) { - const employmentData: any = await teamsApi.getEmploymentDetails( - userData.data.user.id - ); - - const previousEmploymentData = await teamsApi.getPreviousEmployments( - userData.data.user.id - ); - if (employmentData.status && employmentData.status == 200) { - const employmentObj = employmentMapper( - employmentData?.data?.employment, - previousEmploymentData?.data?.previous_employments - ); - setUserState("employmentDetails", employmentObj); - } - } - } - } catch { - Toastr.error("Something went wrong"); - } - }; - - const validateFileSize = file => { - const sizeInKB = file.size / 1024; - if (sizeInKB > 100) { - throw new Error("Image size needs to be less than 100 KB"); - } - }; - - const createFormData = file => { - const formData = new FormData(); - formData.append("user[avatar]", file); - - return formData; - }; - - const handleProfileImageChange = async e => { - try { - setShowImageUpdateOptions(false); - const file = e.target.files[0]; - validateFileSize(file); - setCurrentAvatarUrl(URL.createObjectURL(file)); - const payload = createFormData(file); - const headers = { - "Content-Type": "multipart/form-data", - }; - await profileApi.upadteAvatar(payload, { headers }); - } catch (error) { - Toastr.error(error.message); - } - }; - - const handleDeleteProfileImage = async () => { - setShowImageUpdateOptions(false); - await profileApi.removeAvatar(); - setCurrentAvatarUrl(null); - }; - - useEffect(() => { - getDetails(); - }, []); - - return ( -
    -
    -
    -
    - - -
    -
    - {showImageUpdateOptions ? ( - -
  • - - -
  • -
  • -
    - -
    -

    - Delete -

    -
  • - - ) : null} -
    - -
    - - {`${first_name} ${last_name}`} - -

    {designation}

    -
    -
    - -
    -
    -
    - ); -}; diff --git a/app/javascript/src/components/Profile/Layout/routes.tsx b/app/javascript/src/components/Profile/Layout/routes.tsx new file mode 100644 index 0000000000..b6f26fc5e7 --- /dev/null +++ b/app/javascript/src/components/Profile/Layout/routes.tsx @@ -0,0 +1,182 @@ +import React from "react"; + +import { + UserIcon, + ProjectsIcon, + MobileIcon, + PaymentsIcon, + CalendarIcon, + CakeIcon, + ClientsIcon, + ReminderIcon, +} from "miruIcons"; + +import OrgDetails from "components/Profile/Organization/Details"; +import OrgEdit from "components/Profile/Organization/Edit"; +import Holidays from "components/Profile/Organization/Holidays"; +import Leaves from "components/Profile/Organization/Leaves"; +import PaymentSettings from "components/Profile/Organization/Payment"; +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"; + +const { ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT } = Roles; + +export const SETTINGS = [ + { + label: "PROFILE SETTINGS", + path: "profile", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], + Component: UserDetailsView, + category: "personal", + isTab: true, + }, + { + label: "PROFILE SETTINGS", + path: "profile/edit", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], + Component: UserDetailsEdit, + category: "personal", + isTab: false, + }, + { + label: "EMPLOYMENT DETAILS", + path: "employment", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + Component: EmploymentDetails, + category: "personal", + isTab: true, + }, + { + label: "EMPLOYMENT DETAILS", + path: "employment/edit", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + Component: EmploymentDetailsEdit, + category: "personal", + isTab: false, + }, + { + label: "ALLOCATED DEVICES", + path: "devices", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + Component: AllocatedDevicesDetails, + category: "personal", + isTab: true, + }, + { + label: "ALLOCATED DEVICES", + path: "devices/edit", + icon: , + authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + Component: AllocatedDevicesEdit, + 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", + // path: "compensation", + // icon: , + // authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + // Component: CompensationDetails, + // category: "personal", + //isTab: false, + //}, + // { + // label: "COMPENSATION", + // path: "compensation/edit", + // icon: , + // authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], + // Component: CompensationDetailsEdit, + // category: "personal", + // isTab: false, + //}, + { + label: "ORG. SETTINGS", + path: "organization", + icon: , + authorisedRoles: [ADMIN, OWNER], + Component: OrgDetails, + category: "organization", + isTab: true, + }, + { + label: "ORG. SETTINGS", + path: "organization/edit", + icon: , + authorisedRoles: [ADMIN, OWNER], + Component: OrgEdit, + category: "organization", + isTab: false, + }, + { + label: "PAYMENT SETTINGS", + path: "payment", + icon: , + authorisedRoles: [ADMIN, OWNER], + Component: PaymentSettings, + category: "organization", + isTab: true, + }, + { + label: "LEAVES", + path: "leaves", + icon: , + authorisedRoles: [ADMIN, OWNER], + Component: Leaves, + category: "organization", + isTab: true, + }, + { + label: "HOLIDAYS", + path: "holidays", + icon: , + authorisedRoles: [ADMIN, OWNER], + Component: Holidays, + category: "organization", + isTab: true, + }, + // Uncomment when Integrating with API + // { + // label: "Integration", + // path: "/integrations", + // icon: , + // authorisedRoles: [ADMIN, OWNER], + // Component: GoogleCalendar, + // category: "organization", + // isTab: false, + // }, + // { + // path: "/import", + // Component: OrganizationImport, + // authorisedRoles: [ADMIN, OWNER], + // category: "organization", + // isTab: false, + // }, + // { + // path: "/billing", + // Component: Billing, + // authorisedRoles: [ADMIN, OWNER], + // category: "organization", + //isTab: false, + // }, +]; diff --git a/app/javascript/src/components/Profile/Organization/Billing/index.tsx b/app/javascript/src/components/Profile/Organization/Billing/index.tsx index 622ec511f7..811ff5ab8b 100644 --- a/app/javascript/src/components/Profile/Organization/Billing/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Billing/index.tsx @@ -5,14 +5,14 @@ import { sendGAPageView } from "utils/googleAnalytics"; import Table from "./Table"; -import Header from "../../Header"; +import EditHeader from "../../Common/EditHeader"; const Billing = () => { useEffect(() => sendGAPageView(), []); return (
    -
    { return (
    -
    ( -
    - Holidays - -
    -
    - -
    -
    -
    -); - -interface Iprops { - editAction?: () => any; - currentYear: any; - setCurrentYear: () => any; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/Organization/Holidays/Details/index.tsx b/app/javascript/src/components/Profile/Organization/Holidays/Details/index.tsx index b6adc3d059..2c19731b2d 100644 --- a/app/javascript/src/components/Profile/Organization/Holidays/Details/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Holidays/Details/index.tsx @@ -4,8 +4,8 @@ import dayjs from "dayjs"; import { Tooltip } from "StyledComponents"; import HolidayModal from "common/HolidayModal"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; -import Header from "./Header"; import TableHeader from "./TableHeader"; import TableRow from "./TableRow"; @@ -52,10 +52,15 @@ const Details = ({ return ( -
    diff --git a/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/Header.tsx b/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/Header.tsx deleted file mode 100644 index 3ac346025a..0000000000 --- a/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/Header.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; - -import CustomYearPicker from "common/CustomYearPicker"; - -const Header = ({ - cancelAction, - saveAction, - currentYear, - setCurrentYear, - isDisableUpdateBtn, -}: Iprops) => ( -
    - Holidays - -
    -
    - - -
    -
    -
    -); - -interface Iprops { - cancelAction?: () => any; - saveAction?: () => any; - isDisableUpdateBtn?: boolean; - currentYear: any; - setCurrentYear: () => any; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/index.tsx b/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/index.tsx index f5dfa91966..3916f7c33c 100644 --- a/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Holidays/EditHolidays/index.tsx @@ -9,9 +9,9 @@ import CustomReactSelect from "common/CustomReactSelect"; import CustomToggle from "common/CustomToggle"; import SingleYearDatePicker from "common/CustomYearPicker/SingleYearDatePicker"; import { Divider } from "common/Divider"; +import EditHeader from "components/Profile/Common/EditHeader"; import { allocationFrequency } from "constants/leaveType"; -import Header from "./Header"; import { customStyles } from "./utils"; const EditHolidays = ({ @@ -43,12 +43,16 @@ const EditHolidays = ({ updateHolidayDetails, }) => ( <> -
    diff --git a/app/javascript/src/components/Profile/Organization/Import/index.tsx b/app/javascript/src/components/Profile/Organization/Import/index.tsx index 5e380e3bcc..f71ecae6c1 100644 --- a/app/javascript/src/components/Profile/Organization/Import/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Import/index.tsx @@ -6,7 +6,7 @@ import Loader from "common/Loader/index"; import ImportCard from "./importCard"; import ImportModal from "./importModal"; -import Header from "../../Header"; +import EditHeader from "../../Common/EditHeader"; const importList = [ { @@ -77,7 +77,7 @@ const Import = () => { return (
    -
    ( -
    - - - - - - -); - -export default CustomTableHeader; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx deleted file mode 100644 index dd5ed3232a..0000000000 --- a/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { Fragment } from "react"; - -const CustomTableRow = ({ leave, key }) => { - const { - customLeaveType, - customLeaveTotal, - customAllocationPeriod, - employees, - } = leave; - - return ( - - - - - - ); -}; - -export default CustomTableRow; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/Header.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/Header.tsx deleted file mode 100644 index 1450acb53b..0000000000 --- a/app/javascript/src/components/Profile/Organization/Leaves/Details/Header.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; - -import CustomYearPicker from "common/CustomYearPicker"; - -const Header = ({ - editAction, - showYearPicker = false, - currentYear, - setCurrentYear, -}: Iprops) => ( -
    - Leaves - {showYearPicker && ( - - )} -
    -
    - -
    -
    -
    -); - -interface Iprops { - editAction?: () => any; - showYearPicker?: boolean; - currentYear: any; - setCurrentYear: () => any; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx index fa847be6d1..a980b21a2b 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx @@ -1,25 +1,27 @@ import React from "react"; -import CustomTableHeader from "./CustomTableHeader"; -import CustomTableRow from "./CustomTableRow"; -import Header from "./Header"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; + import TableHeader from "./TableHeader"; import TableRow from "./TableRow"; const Details = ({ leavesList, - customLeavesList, showYearPicker, editAction, currentYear, setCurrentYear, }) => ( <> -
    @@ -39,23 +41,6 @@ const Details = ({
    - LEAVE TYPE - - TOTAL - - Employees -
    - - {customLeaveType} - - - {customLeaveTotal} {customAllocationPeriod} - - {employees.map((emp, index) => ( - - {emp.label}{" "} - {index < employees.length - 1 && ,} - - ))} -
    -
    -
    - Customised Leaves -
    - - - - {customLeavesList.length > 0 ? ( - customLeavesList.map((leave, index) => ( - - )) - ) : ( -
    No data found
    - )} -
    -
    -
    ); diff --git a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/Header.tsx b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/Header.tsx deleted file mode 100644 index 12454cef3d..0000000000 --- a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/Header.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; - -import CustomYearPicker from "common/CustomYearPicker"; - -const Header = ({ - cancelAction, - saveAction, - showYearPicker = false, - currentYear, - setCurrentYear, - isDisableUpdateBtn, -}: Iprops) => ( -
    - Leaves - {showYearPicker && ( - - )} -
    -
    - - -
    -
    -
    -); - -interface Iprops { - cancelAction?: () => any; - saveAction?: () => any; - isDisableUpdateBtn?: boolean; - showYearPicker?: boolean; - currentYear: any; - setCurrentYear: () => any; -} - -export default Header; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx index 2af69d3b89..db8a7b4776 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx @@ -6,9 +6,9 @@ import { Button } from "StyledComponents"; import { CustomInputText } from "common/CustomInputText"; import CustomReactSelect from "common/CustomReactSelect"; import { Divider } from "common/Divider"; +import EditHeader from "components/Profile/Common/EditHeader"; import { allocationFrequency, getAllocationPeriod } from "constants/leaveType"; -import Header from "./Header"; import { ColorOption, iconColorStyles, @@ -18,10 +18,8 @@ import { const EditLeaves = ({ leaveBalanceList, - customLeavesList, - handleOnChangeLeaves, - handleOnChangeCustomLeaves, - handleDeleteLeave, + updateCondition, + handleDeleteLeaveBalance, handleAddLeaveType, handleLeaveTypeChange, iconOptions, @@ -33,8 +31,6 @@ const EditLeaves = ({ currentYear, setCurrentYear, isDisableUpdateBtn, - handleAddCustomLeave, - employees, }) => { const getAllocationPeriodValue = (allocationFrequency, allocationPeriod) => { const availableOptions = getAllocationPeriod(allocationFrequency); @@ -47,13 +43,16 @@ const EditLeaves = ({ return ( <> -
    @@ -90,7 +89,7 @@ const EditLeaves = ({
    {e.icon}
    )} handleOnChange={e => - handleOnChangeLeaves("leaveIcon", e, index) + updateCondition("leaveIcon", e, index) } /> )} handleOnChange={e => - handleOnChangeLeaves("leaveColor", e, index) + updateCondition("leaveColor", e, index) } />
    @@ -129,7 +128,7 @@ const EditLeaves = ({ value={leaveBalance.total || ""} wrapperClassName="w-full lg:w-2/12 lg:mr-2 mb-6 lg:mb-0" onChange={e => - handleOnChangeLeaves("total", e.target.value, index) + updateCondition("total", e.target.value, index) } /> null, }} handleOnChange={e => - handleOnChangeLeaves("allocationPeriod", e.value, index) + updateCondition("allocationPeriod", e.value, index) } options={getAllocationPeriod( leaveBalance.allocationFrequency @@ -163,13 +162,8 @@ const EditLeaves = ({ IndicatorSeparator: () => null, }} handleOnChange={e => { - handleOnChangeLeaves( - "allocationFrequency", - e.value, - index - ); - - handleOnChangeLeaves( + updateCondition("allocationFrequency", e.value, index); + updateCondition( "allocationPeriod", getAllocationPeriodValue( leaveBalance.allocationFrequency, @@ -199,7 +193,7 @@ const EditLeaves = ({ value={leaveBalance.carryForwardDays || ""} wrapperClassName="w-full lg:w-4/12 mb-6 lg:mb-0" onChange={e => - handleOnChangeLeaves( + updateCondition( "carryForwardDays", e.target.value, index @@ -207,13 +201,15 @@ const EditLeaves = ({ } />
    - + +
    {leaveBalanceList.length - 1 != index && ( @@ -223,124 +219,17 @@ const EditLeaves = ({ )}
    ))} - -
    -
    -
    - -
    -
    Customised Leaves
    -
    - {customLeavesList.map((customLeave, index) => ( -
    -
    - handleLeaveTypeChange(e, index, true)} - /> - - handleOnChangeCustomLeaves( - "customLeaveTotal", - e.target.value, - index - ) - } - /> - null, - }} - handleOnChange={e => - handleOnChangeCustomLeaves( - "customAllocationPeriod", - e, - index - ) - } - value={getAllocationPeriodValue( - "", - customLeave.allocationPeriod - )} - /> -
    -
    -
    - null, - }} - handleOnChange={e => { - handleOnChangeCustomLeaves("employees", e, index); - }} - /> -
    - -
    -
    - {leaveBalanceList.length - 1 != index && ( -
    - -
    - )}
    - ))} - +
    -
    - {!isDesktop && - (leaveBalanceList.length > 0 || customLeavesList.legth > 0) && ( + {!isDesktop && leaveBalanceList.length > 0 && (
    )} +
    ); diff --git a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js index 2f4dd3b07d..99672ab2b0 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js +++ b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js @@ -74,14 +74,6 @@ export const customStyles = { letterSpacing: "2px", zIndex: 5, }), - valueContainer: provided => ({ - ...provided, - overflow: "visible", - }), - multiValue: provided => ({ - ...provided, - fontSize: "14px", - }), placeholder: base => ({ ...base, position: "absolute", diff --git a/app/javascript/src/components/Profile/Organization/Leaves/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/index.tsx index 4072d7983f..f8efdfa10f 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/index.tsx @@ -5,7 +5,6 @@ import { getYear } from "date-fns"; import { useNavigate } from "react-router-dom"; import leavesApi from "apis/leaves"; -import teamApi from "apis/team"; import Loader from "common/Loader/index"; import { leaveIcons, leaveColors } from "constants/leaveType"; import { useUserContext } from "context/UserContext"; @@ -13,26 +12,18 @@ import { sendGAPageView } from "utils/googleAnalytics"; import Details from "./Details"; import EditLeaves from "./EditLeaves"; -import { - makeLeavePayload, - makeLeavesList, - makeCustomLeavesList, - makeCustomLeavePayload, -} from "./utils"; +import { makeLeavePayload, makeLeavesList } from "./utils"; const Leaves = () => { const [leaveBalanceList, setLeaveBalanceList] = useState([]); - const [leaves, setLeaves] = useState([]); - const [customLeavesList, setCustomLeavesList] = useState([]); const [iconOptions, setIconOptions] = useState(leaveIcons); const [colorOptions, setColorOptions] = useState(leaveColors); const [isLoading, setIsLoading] = useState(true); const [currentYear, setCurrentYear] = useState(getYear(new Date())); + const [leaves, setLeaves] = useState([]); const [currentYearLeaves, setCurrentYearLeaves] = useState([]); - const [currentYearCustomLeaves, setCurrentYearCustomLeaves] = useState([]); const [isEditable, setIsEditable] = useState(false); const [isDisableUpdateBtn, setIsDisableUpdateBtn] = useState(false); - const [employees, setEmployees] = useState>([]); const { isDesktop } = useUserContext(); const navigate = useNavigate(); @@ -41,7 +32,6 @@ const Leaves = () => { useEffect(() => { fetchLeaves(); - fetchEmployees(); }, []); useEffect(() => { @@ -58,19 +48,6 @@ const Leaves = () => { setIsLoading(false); }; - const fetchEmployees = async () => { - const res = await teamApi.get(); - const empData = res.data.combinedDetails; - const empList = empData - .filter(emp => emp.isTeamMember) - .map(emp => ({ - value: emp.id, - label: emp.name, - })); - - setEmployees(empList); - }; - useEffect(() => { if (leaves.length) { updateLeaveBalanceList(); @@ -79,23 +56,12 @@ const Leaves = () => { const updateLeaveBalanceList = (allLeaves = leaves) => { const currentLeaves = allLeaves.find(leave => leave.year == currentYear); - if (currentLeaves) { - if (currentLeaves?.leave_types.length) { - setLeaveBalanceList(makeLeavesList(currentLeaves?.leave_types)); - setCurrentYearLeaves(makeLeavesList(currentLeaves?.leave_types)); - } - - if (currentLeaves?.custom_leaves.length) { - setCustomLeavesList(makeCustomLeavesList(currentLeaves?.custom_leaves)); - setCurrentYearCustomLeaves( - makeCustomLeavesList(currentLeaves?.custom_leaves) - ); - } + if (currentLeaves?.leave_types.length) { + setLeaveBalanceList(makeLeavesList(currentLeaves?.leave_types)); + setCurrentYearLeaves(makeLeavesList(currentLeaves?.leave_types)); } else { setLeaveBalanceList([]); - setCustomLeavesList([]); setCurrentYearLeaves([]); - setCurrentYearCustomLeaves([]); } }; @@ -116,32 +82,12 @@ const Leaves = () => { ]); }; - const handleAddCustomLeave = () => { - setCustomLeavesList([ - ...customLeavesList, - ...[ - { - customLeaveType: "", - customLeaveTotal: 0, - customAllocationPeriod: "days", - employees: [], - }, - ], - ]); - }; - - const handleOnChangeLeaves = (type, value, index) => { + const updateCondition = (type, value, index) => { const editLeaveList = [...leaveBalanceList]; editLeaveList[index][type] = value; setLeaveBalanceList([...editLeaveList]); }; - const handleOnChangeCustomLeaves = (type, value, index) => { - const editCustomLeaveList = [...customLeavesList]; - editCustomLeaveList[index][type] = value; - setCustomLeavesList([...editCustomLeaveList]); - }; - const handleUpdateDetails = async () => { const payload = { add_leave_types: [], @@ -149,15 +95,8 @@ const Leaves = () => { removed_leave_type_ids: [], }; - const customPayload = { - add_custom_leaves: [], - update_custom_leaves: [], - remove_custom_leaves: [], - }; - setIsDisableUpdateBtn(true); - //filtering out removed leaves const removedLeaves = currentYearLeaves .filter( currentLeave => @@ -165,17 +104,7 @@ const Leaves = () => { ) .map(removedLeave => removedLeave.id); - //filtering out removed custom leaves - const removedCustomLeaves = currentYearCustomLeaves - .filter( - currentLeave => - !customLeavesList.some(leave => leave.id === currentLeave.id) - ) - .map(removedLeave => removedLeave.id); - - //updating payload payload.removed_leave_type_ids.push(...removedLeaves); - customPayload.remove_custom_leaves.push(...removedCustomLeaves); const leavesList = leaveBalanceList.filter( leave => @@ -187,7 +116,6 @@ const Leaves = () => { }) ); - //updating leaves payload for add and update leavesList.forEach(leave => { if (leave.id) { payload.updated_leave_types.push(makeLeavePayload(leave)); @@ -196,38 +124,12 @@ const Leaves = () => { } }); - const customLeaves = customLeavesList.filter( - customLeave => - !currentYearCustomLeaves.some(currentLeave => { - const leaveJSON = JSON.stringify(customLeave); - const currentLeaveJSON = JSON.stringify(currentLeave); - - return leaveJSON === currentLeaveJSON; - }) - ); - - //updating custom leaves payload for add and update - customLeaves.forEach(customLeave => { - if (customLeave.id) { - customPayload.update_custom_leaves.push( - makeCustomLeavePayload(customLeave) - ); - } else { - customPayload.add_custom_leaves.push( - makeCustomLeavePayload(customLeave) - ); - } - }); - - updateLeaveDetails(payload, customPayload); + updateLeaveDetails(payload); }; - const updateLeaveDetails = async (payload, customPayload) => { + const updateLeaveDetails = async payload => { try { await leavesApi.updateLeaveWithLeaveTypes(currentYear, payload); - await leavesApi.customLeaves(currentYear, { - custom_leaves: customPayload, - }); setIsDisableUpdateBtn(false); setIsEditable(false); fetchLeaves(); @@ -246,21 +148,13 @@ const Leaves = () => { } }; - const handleDeleteLeave = (leave, isCustom = false) => { - if (isCustom) { - setCustomLeavesList(customLeavesList.filter(prev => prev !== leave)); - } else { - setLeaveBalanceList(leaveBalanceList.filter(prev => prev !== leave)); - } + const handleDeleteLeaveBalance = leave => { + setLeaveBalanceList(leaveBalanceList.filter(prev => prev !== leave)); }; - const handleLeaveTypeChange = (e, index, isCustom = false) => { + const handleLeaveTypeChange = (e, index) => { const result = e.target.value.replace(/[^a-zA-Z- ]/g, ""); - if (isCustom) { - handleOnChangeCustomLeaves("customLeaveType", result, index); - } else { - handleOnChangeLeaves("leaveType", result, index); - } + updateCondition("leaveType", result, index); }; const handleIconSelect = () => { @@ -294,27 +188,22 @@ const Leaves = () => { showYearPicker colorOptions={colorOptions} currentYear={currentYear} - customLeavesList={customLeavesList} - employees={employees} - handleAddCustomLeave={handleAddCustomLeave} handleAddLeaveType={handleAddLeaveType} handleCancelAction={handleCancelAction} - handleDeleteLeave={handleDeleteLeave} + handleDeleteLeaveBalance={handleDeleteLeaveBalance} handleLeaveTypeChange={handleLeaveTypeChange} - handleOnChangeCustomLeaves={handleOnChangeCustomLeaves} - handleOnChangeLeaves={handleOnChangeLeaves} iconOptions={iconOptions} isDesktop={isDesktop} isDisableUpdateBtn={isDisableUpdateBtn} leaveBalanceList={leaveBalanceList} setCurrentYear={setCurrentYear} + updateCondition={updateCondition} updateLeaveDetails={handleUpdateDetails} /> ) : (
    setIsEditable(true)} leavesList={currentYearLeaves} setCurrentYear={setCurrentYear} diff --git a/app/javascript/src/components/Profile/Organization/Leaves/utils.ts b/app/javascript/src/components/Profile/Organization/Leaves/utils.ts index 8a2fde8bbe..3b3c0ac0f6 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/utils.ts +++ b/app/javascript/src/components/Profile/Organization/Leaves/utils.ts @@ -39,14 +39,6 @@ export const makeLeavePayload = leave => ({ carry_forward_days: leave.carryForwardDays, }); -export const makeCustomLeavePayload = leave => ({ - id: leave.id, - name: leave.customLeaveType, - allocation_value: leave.customLeaveTotal, - allocation_period: leave.customAllocationPeriod, - user_ids: leave.employees.map(emp => emp.value), -}); - export const makeLeavesList = leaveTypes => leaveTypes.map(leaveType => ({ id: leaveType.id, @@ -58,15 +50,3 @@ export const makeLeavesList = leaveTypes => allocationFrequency: leaveType.allocation_frequency, carryForwardDays: leaveType.carry_forward_days, })); - -export const makeCustomLeavesList = customLeaves => - customLeaves.map(leave => ({ - id: leave.id, - customLeaveType: leave.name, - customLeaveTotal: leave.allocation_value, - customAllocationPeriod: leave.allocation_period, - employees: leave.users.map(emp => ({ - value: emp.id, - label: emp.full_name, - })), - })); diff --git a/app/javascript/src/components/Profile/Organization/Payment/StaticPage.tsx b/app/javascript/src/components/Profile/Organization/Payment/StaticPage.tsx index 207515576b..57105d7ec2 100644 --- a/app/javascript/src/components/Profile/Organization/Payment/StaticPage.tsx +++ b/app/javascript/src/components/Profile/Organization/Payment/StaticPage.tsx @@ -5,7 +5,7 @@ import { ConnectSVG, StripeLogoSVG, disconnectAccountSVG } from "miruIcons"; import Loader from "common/Loader/index"; import { ApiStatus as PaymentSettingsStatus } from "constants/index"; -import Header from "../../Header"; +import EditHeader from "../../Common/EditHeader"; const StaticPage = ({ isStripeConnected, @@ -14,7 +14,7 @@ const StaticPage = ({ status, }) => ( <> -
    { const initialErrState = { @@ -20,8 +21,12 @@ const CompensationEditPage = () => { deduction_amount_err: "", }; const navigate = useNavigate(); + const { memberId } = useParams(); const { isDesktop, company } = useUserContext(); - + const { isCalledFromSettings } = useProfileContext(); + const navigateToPath = isCalledFromSettings + ? "/settings" + : `/team/${memberId}`; const [isLoading, setIsLoading] = useState(false); const [earnings, setEarnings] = useState>( CompensationDetailsState.earnings @@ -99,7 +104,7 @@ const CompensationEditPage = () => { const handleCancelDetails = () => { setIsLoading(true); - navigate(`/settings/compensation`, { replace: true }); + navigate(`${navigateToPath}/compensation`, { replace: true }); }; return ( diff --git a/app/javascript/src/components/Profile/UserDetail/CompensationDetails/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/Compensation/StaticPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/CompensationDetails/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/Compensation/StaticPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/CompensationDetails/index.tsx b/app/javascript/src/components/Profile/Personal/Compensation/index.tsx similarity index 71% rename from app/javascript/src/components/Profile/UserDetail/CompensationDetails/index.tsx rename to app/javascript/src/components/Profile/Personal/Compensation/index.tsx index b7e3eb9ae0..e8aa230db3 100644 --- a/app/javascript/src/components/Profile/UserDetail/CompensationDetails/index.tsx +++ b/app/javascript/src/components/Profile/Personal/Compensation/index.tsx @@ -1,19 +1,22 @@ import React, { Fragment, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import Loader from "common/Loader/index"; import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; -import DetailsHeader from "components/Profile/DetailsHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import StaticPage from "./StaticPage"; +import DetailsHeader from "../../Common/DetailsHeader"; + const CompensationDetails = () => { const { isDesktop, company } = useUserContext(); - const { setUserState, compensationDetails } = useProfile(); + const { updateDetails, compensationDetails, isCalledFromSettings } = + useProfileContext(); const navigate = useNavigate(); + const { memberId } = useParams(); const [isLoading, setIsLoading] = useState(false); const getDetails = async () => { @@ -29,10 +32,14 @@ const CompensationDetails = () => { amount: "147500", }, }; - setUserState("compensationDetails", compensationData); + updateDetails("compensationDetails", compensationData); setIsLoading(false); }; + const handleEditClick = () => { + navigate(`edit`, { replace: true }); + }; + useEffect(() => { setIsLoading(true); getDetails(); @@ -43,17 +50,15 @@ const CompensationDetails = () => { {isDesktop ? ( - navigate(`/settings/compensation/edit`, { replace: true }) - } /> ) : ( )} diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Device.ts b/app/javascript/src/components/Profile/Personal/Devices/Device.ts similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Device.ts rename to app/javascript/src/components/Profile/Personal/Devices/Device.ts diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/EditPage.tsx b/app/javascript/src/components/Profile/Personal/Devices/Edit/EditPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/EditPage.tsx rename to app/javascript/src/components/Profile/Personal/Devices/Edit/EditPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/MobileEditPage.tsx b/app/javascript/src/components/Profile/Personal/Devices/Edit/MobileEditPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/MobileEditPage.tsx rename to app/javascript/src/components/Profile/Personal/Devices/Edit/MobileEditPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/index.tsx b/app/javascript/src/components/Profile/Personal/Devices/Edit/index.tsx similarity index 70% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/index.tsx rename to app/javascript/src/components/Profile/Personal/Devices/Edit/index.tsx index fe54e59fed..fb3b638544 100644 --- a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/Edit/index.tsx +++ b/app/javascript/src/components/Profile/Personal/Devices/Edit/index.tsx @@ -1,11 +1,13 @@ /* eslint-disable no-unused-vars */ import React, { Fragment, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import deviceApi from "apis/devices"; import Loader from "common/Loader/index"; import { MobileDetailsHeader } from "common/Mobile/MobileDetailsHeader"; +import EditHeader from "components/Profile/Common/EditHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import EditPage from "./EditPage"; @@ -26,7 +28,13 @@ const AllocatedDevicesEdit = () => { }; const navigate = useNavigate(); const { isDesktop, user } = useUserContext(); + const { memberId } = useParams(); + const { isCalledFromSettings } = useProfileContext(); + const navigateToPath = isCalledFromSettings + ? "/settings" + : `/team/${memberId}`; + const currentUserId = isCalledFromSettings ? user.id : memberId; const [isLoading, setIsLoading] = useState(false); const [devices, setDevices] = useState([]); const [errDetails, setErrDetails] = useState(initialErrState); @@ -37,15 +45,19 @@ const AllocatedDevicesEdit = () => { }, []); const getDevicesDetail = async () => { - const res: any = await deviceApi.get(user.id); + const res: any = await deviceApi.get(currentUserId); const devicesDetails: Device[] = res.data.devices; setDevices(devicesDetails); setIsLoading(false); }; + const handleUpdateDetails = () => { + //Todo: API integration for update details + }; + const handleCancelDetails = () => { setIsLoading(true); - navigate(`/settings/devices`, { replace: true }); + navigate(`${navigateToPath}/devices`, { replace: true }); }; const addAnotherDevice = () => { @@ -68,20 +80,14 @@ const AllocatedDevicesEdit = () => { {isDesktop && ( -
    -

    Allocated Devices

    -
    - - -
    -
    + {isLoading ? ( ) : ( @@ -92,7 +98,7 @@ const AllocatedDevicesEdit = () => { {!isDesktop && ( {isLoading ? ( diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/Devices/StaticPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/Devices/StaticPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/helpers.ts b/app/javascript/src/components/Profile/Personal/Devices/helpers.ts similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/helpers.ts rename to app/javascript/src/components/Profile/Personal/Devices/helpers.ts diff --git a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/index.tsx b/app/javascript/src/components/Profile/Personal/Devices/index.tsx similarity index 72% rename from app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/index.tsx rename to app/javascript/src/components/Profile/Personal/Devices/index.tsx index ab614a9b8b..5f06c02ea8 100644 --- a/app/javascript/src/components/Profile/UserDetail/AllocatedDevicesDetails/index.tsx +++ b/app/javascript/src/components/Profile/Personal/Devices/index.tsx @@ -1,11 +1,12 @@ import React, { Fragment, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import deviceApi from "apis/devices"; import Loader from "common/Loader/index"; import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import DetailsHeader from "components/Profile/DetailsHeader"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import { Device } from "./Device"; @@ -13,13 +14,16 @@ import StaticPage from "./StaticPage"; const AllocatedDevicesDetails = () => { const { user, isDesktop } = useUserContext(); + const { isCalledFromSettings } = useProfileContext(); const navigate = useNavigate(); + const { memberId } = useParams(); + const currentUserId = isCalledFromSettings ? user.id : memberId; const [isLoading, setIsLoading] = useState(false); const [devices, setDevices] = useState([]); const getDevicesDetail = async () => { - const res: any = await deviceApi.get(user.id); + const res: any = await deviceApi.get(currentUserId); const devicesDetails: Device[] = res.data.devices; setDevices(devicesDetails); setIsLoading(false); @@ -31,7 +35,7 @@ const AllocatedDevicesDetails = () => { }, []); const handleEdit = () => { - navigate(`/settings/devices/edit`, { replace: true }); + navigate(`edit`, { replace: true }); }; return ( @@ -46,8 +50,8 @@ const AllocatedDevicesDetails = () => { /> ) : ( )} diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/MobileEditPage.tsx b/app/javascript/src/components/Profile/Personal/Employment/Edit/MobileEditPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/MobileEditPage.tsx rename to app/javascript/src/components/Profile/Personal/Employment/Edit/MobileEditPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/Employment/Edit/StaticPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/Employment/Edit/StaticPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/index.tsx b/app/javascript/src/components/Profile/Personal/Employment/Edit/index.tsx similarity index 86% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/index.tsx rename to app/javascript/src/components/Profile/Personal/Employment/Edit/index.tsx index 6c3748b63f..3681a59f8f 100644 --- a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/Edit/index.tsx +++ b/app/javascript/src/components/Profile/Personal/Employment/Edit/index.tsx @@ -4,14 +4,15 @@ import React, { Fragment, useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { useOutsideClick } from "helpers"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import * as Yup from "yup"; import teamsApi from "apis/teams"; import Loader from "common/Loader/index"; import { MobileDetailsHeader } from "common/Mobile/MobileDetailsHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; -import { employmentSchema } from "components/Team/Details/EmploymentDetails/Edit/validationSchema"; +import EditHeader from "components/Profile/Common/EditHeader"; +import { employmentSchema } from "components/Profile/Schema/employmentSchema"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import { employmentMapper } from "mapper/teams.mapper"; @@ -36,8 +37,10 @@ const EmploymentDetailsEdit = () => { role_err: "", }; const { user } = useUserContext(); - const { setUserState, employmentDetails } = useProfile(); + const { updateDetails, employmentDetails, isCalledFromSettings } = + useProfileContext(); const navigate = useNavigate(); + const { memberId } = useParams(); const { isDesktop } = useUserContext(); const DOJRef = useRef(null); @@ -64,12 +67,18 @@ const EmploymentDetailsEdit = () => { const [resignedAt, setResignedAt] = useState(null); const [joinedAt, setJoinedAt] = useState(null); + const currentUserId = isCalledFromSettings ? user.id : memberId; + + const navigateToPath = isCalledFromSettings + ? "/settings" + : `/team/${memberId}`; + useOutsideClick(DOJRef, () => setShowDOJDatePicker({ visibility: false })); useOutsideClick(DORRef, () => setShowDORDatePicker({ visibility: false })); const getDetails = async () => { - const curr: any = await teamsApi.getEmploymentDetails(user.id); - const prev: any = await teamsApi.getPreviousEmployments(user.id); + const curr: any = await teamsApi.getEmploymentDetails(currentUserId); + const prev: any = await teamsApi.getPreviousEmployments(currentUserId); setDateFormat(curr.data.date_format); setJoinedAt(curr.data.employment.joined_at); setResignedAt(curr.data.employment.resigned_at); @@ -89,7 +98,7 @@ const EmploymentDetailsEdit = () => { employmentData.current_employment.employment_type = employeeTypes[0].value; } - setUserState("employmentDetails", employmentData); + updateDetails("employmentDetails", employmentData); if (employmentData.previous_employments?.length > 0) { setPreviousEmployments(employmentData.previous_employments); } @@ -103,7 +112,7 @@ const EmploymentDetailsEdit = () => { const handleOnChangeEmployeeType = empType => { setEmployeeType(empType); - setUserState("employmentDetails", { + updateDetails("employmentDetails", { ...employmentDetails, ...{ current_employment: { @@ -115,7 +124,7 @@ const EmploymentDetailsEdit = () => { }; const updateCurrentEmploymentDetails = (value, type) => { - setUserState("employmentDetails", { + updateDetails("employmentDetails", { ...employmentDetails, ...{ current_employment: { @@ -139,7 +148,7 @@ const EmploymentDetailsEdit = () => { const handleDOJDatePicker = date => { setShowDOJDatePicker({ visibility: !showDOJDatePicker.visibility }); setJoinedAt(date); - setUserState("employmentDetails", { + updateDetails("employmentDetails", { ...employmentDetails, ...{ current_employment: { @@ -155,7 +164,7 @@ const EmploymentDetailsEdit = () => { const handleDORDatePicker = date => { setShowDORDatePicker({ visibility: !showDORDatePicker.visibility }); setResignedAt(date); - setUserState("employmentDetails", { + updateDetails("employmentDetails", { ...employmentDetails, ...{ current_employment: { @@ -246,11 +255,11 @@ const EmploymentDetailsEdit = () => { }, }; - await teamsApi.updatePreviousEmployments(user.id, { + await teamsApi.updatePreviousEmployments(currentUserId, { employments: payload, }); setIsLoading(false); - navigate(`/settings/employment`, { replace: true }); + navigate(`${navigateToPath}/employment`, { replace: true }); } catch (err) { setIsLoading(false); const errObj = initialErrState; @@ -269,32 +278,21 @@ const EmploymentDetailsEdit = () => { const handleCancelDetails = () => { setIsLoading(true); - navigate(`/settings/employment`, { replace: true }); + navigate(`${navigateToPath}/employment`, { replace: true }); }; return ( {isDesktop && ( -
    -

    - Employment Details -

    -
    - - -
    -
    + {isLoading ? ( ) : ( @@ -327,7 +325,7 @@ const EmploymentDetailsEdit = () => { {!isDesktop && ( {isLoading ? ( diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/Employment/StaticPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/Employment/StaticPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/helpers.ts b/app/javascript/src/components/Profile/Personal/Employment/helpers.ts similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/helpers.ts rename to app/javascript/src/components/Profile/Personal/Employment/helpers.ts diff --git a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/index.tsx b/app/javascript/src/components/Profile/Personal/Employment/index.tsx similarity index 61% rename from app/javascript/src/components/Profile/UserDetail/EmploymentDetails/index.tsx rename to app/javascript/src/components/Profile/Personal/Employment/index.tsx index 233734e00b..493bf51585 100644 --- a/app/javascript/src/components/Profile/UserDetail/EmploymentDetails/index.tsx +++ b/app/javascript/src/components/Profile/Personal/Employment/index.tsx @@ -1,12 +1,12 @@ import React, { Fragment, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import teamsApi from "apis/teams"; import Loader from "common/Loader/index"; import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; -import DetailsHeader from "components/Profile/DetailsHeader"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import { employmentMapper } from "mapper/teams.mapper"; @@ -14,21 +14,29 @@ import StaticPage from "./StaticPage"; const EmploymentDetails = () => { const { user, isDesktop } = useUserContext(); - const { setUserState, employmentDetails } = useProfile(); + const { updateDetails, employmentDetails, isCalledFromSettings } = + useProfileContext(); const navigate = useNavigate(); + const { memberId } = useParams(); + const [isLoading, setIsLoading] = useState(false); + const currentUserId = isCalledFromSettings ? user.id : memberId; const getDetails = async () => { - const res1: any = await teamsApi.getEmploymentDetails(user.id); - const res: any = await teamsApi.getPreviousEmployments(user.id); + const res1: any = await teamsApi.getEmploymentDetails(currentUserId); + const res: any = await teamsApi.getPreviousEmployments(currentUserId); const employmentData = employmentMapper( res1.data.employment, res.data.previous_employments ); - setUserState("employmentDetails", employmentData); + updateDetails("employmentDetails", employmentData); setIsLoading(false); }; + const handleEditClick = () => { + navigate(`edit`, { replace: true }); + }; + useEffect(() => { setIsLoading(true); getDetails(); @@ -39,18 +47,16 @@ const EmploymentDetails = () => { {isDesktop && ( - navigate(`/settings/employment/edit`, { replace: true }) - } /> )} {!isDesktop && ( )} 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..7faeb1e511 --- /dev/null +++ b/app/javascript/src/components/Profile/Personal/NotificationPreferences/index.tsx @@ -0,0 +1,72 @@ +/* eslint-disable no-unused-vars */ +import React, { Fragment, useEffect, useState } from "react"; + +import { useParams } from "react-router-dom"; + +import preferencesApi from "apis/preferences"; +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, isDesktop } = 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, + }); + setIsLoading(false); + }; + + useEffect(() => { + setIsLoading(true); + getPreferences(); + }, []); + + return ( + + {isDesktop ? ( + + ) : ( + + )} + {isLoading ? ( + + ) : ( +
    + Weekly Email Reminder + +
    + )} +
    + ); +}; + +export default NotificationPreferences; diff --git a/app/javascript/src/components/Profile/UserDetail/Edit/MobileEditPage.tsx b/app/javascript/src/components/Profile/Personal/User/Edit/MobileEditPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/Edit/MobileEditPage.tsx rename to app/javascript/src/components/Profile/Personal/User/Edit/MobileEditPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/Edit/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/User/Edit/StaticPage.tsx similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/Edit/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/User/Edit/StaticPage.tsx diff --git a/app/javascript/src/components/Profile/UserDetail/Edit/index.tsx b/app/javascript/src/components/Profile/Personal/User/Edit/index.tsx similarity index 71% rename from app/javascript/src/components/Profile/UserDetail/Edit/index.tsx rename to app/javascript/src/components/Profile/Personal/User/Edit/index.tsx index 6544492b01..48279ffae6 100644 --- a/app/javascript/src/components/Profile/UserDetail/Edit/index.tsx +++ b/app/javascript/src/components/Profile/Personal/User/Edit/index.tsx @@ -9,9 +9,11 @@ import { useNavigate } from "react-router-dom"; import * as Yup from "yup"; import profileApi from "apis/profile"; +import teamsApi from "apis/teams"; import Loader from "common/Loader/index"; import { MobileDetailsHeader } from "common/Mobile/MobileDetailsHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; +import EditHeader from "components/Profile/Common/EditHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import { teamsMapper } from "mapper/teams.mapper"; @@ -42,6 +44,10 @@ const UserDetailsEdit = () => { const navigate = useNavigate(); const { isDesktop } = useUserContext(); + const { + personalDetails: { id }, + isCalledFromSettings, + } = useProfileContext(); const wrapperRef = useRef(null); @@ -52,7 +58,7 @@ const UserDetailsEdit = () => { const [isLoading, setIsLoading] = useState(false); const [addrId, setAddrId] = useState(); const [userId, setUserId] = useState(); - const { setUserState, profileSettings } = useProfile(); + const { updateDetails, personalDetails } = useProfileContext(); const [changePassword, setChangePassword] = useState(false); const [showCurrentPassword, setShowCurrentPassword] = useState(false); @@ -63,6 +69,8 @@ const UserDetailsEdit = () => { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const navigateToPath = isCalledFromSettings ? "/settings" : `/team/${id}`; + useOutsideClick(wrapperRef, () => setShowDatePicker({ visibility: false })); const assignCountries = async allCountries => { const countryData = await allCountries.map(country => ({ @@ -74,12 +82,12 @@ const UserDetailsEdit = () => { }; const getDetails = async () => { - const data = await profileApi.index(); - const addressData = await profileApi.getAddress(data.data.user.id); - setUserId(data.data.user.id); + const data = await teamsApi.get(id); + const addressData = await teamsApi.getAddress(id); + setUserId(data.data.id); - const userObj = teamsMapper(data.data.user, addressData.data.addresses[0]); - setUserState("profileSettings", userObj); + const userObj = teamsMapper(data.data, addressData.data.addresses[0]); + updateDetails("personalDetails", userObj); if (userObj.addresses?.address_type?.length > 0) { setAddrType( addressOptions.find( @@ -96,22 +104,22 @@ const UserDetailsEdit = () => { const allCountries = Country.getAllCountries(); assignCountries(allCountries); getDetails(); - }, []); + }, [id]); const cancelPasswordChange = () => { setChangePassword(false); - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ confirmPassword: "", password: "", currentPassword: "" }, }); }; const handleOnChangeCountry = selectCountry => { - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ addresses: { - ...profileSettings.addresses, + ...personalDetails.addresses, ...{ country: selectCountry.value, state: "", city: "" }, }, }, @@ -120,11 +128,11 @@ const UserDetailsEdit = () => { const handleOnChangeAddrType = addreType => { setAddrType(addreType); - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ addresses: { - ...profileSettings.addresses, + ...personalDetails.addresses, ...{ address_type: addreType.value }, }, }, @@ -133,15 +141,15 @@ const UserDetailsEdit = () => { const updateBasicDetails = (value, type, isAddress = false) => { if (isAddress) { - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ - addresses: { ...profileSettings.addresses, ...{ [type]: value } }, + addresses: { ...personalDetails.addresses, ...{ [type]: value } }, }, }); } else { - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ [type]: value }, }); } @@ -149,12 +157,12 @@ const UserDetailsEdit = () => { const handleDatePicker = date => { setShowDatePicker({ visibility: !showDatePicker.visibility }); - const formattedDate = dayjs(date, profileSettings.date_format).format( - profileSettings.date_format + const formattedDate = dayjs(date, personalDetails.date_format).format( + personalDetails.date_format ); - setUserState("profileSettings", { - ...profileSettings, + updateDetails("personalDetails", { + ...personalDetails, ...{ date_of_birth: formattedDate }, }); }; @@ -163,10 +171,10 @@ const UserDetailsEdit = () => { try { await schema.validate( { - ...profileSettings, + ...personalDetails, ...{ - is_email: profileSettings.email_id - ? profileSettings.email_id.length > 0 + is_email: personalDetails.email_id + ? personalDetails.email_id.length > 0 : false, changePassword, }, @@ -175,54 +183,60 @@ const UserDetailsEdit = () => { ); const userSchema = { - first_name: profileSettings.first_name, - last_name: profileSettings.last_name, - date_of_birth: profileSettings.date_of_birth + first_name: personalDetails.first_name, + last_name: personalDetails.last_name, + date_of_birth: personalDetails.date_of_birth ? dayjs - .utc(profileSettings.date_of_birth, profileSettings.date_format) + .utc(personalDetails.date_of_birth, personalDetails.date_format) .toISOString() : null, - phone: profileSettings.phone_number - ? profileSettings.phone_number + phone: personalDetails.phone_number + ? personalDetails.phone_number : null, - personal_email_id: profileSettings.email_id, + personal_email_id: personalDetails.email_id, social_accounts: { - linkedin_url: profileSettings.linkedin, - github_url: profileSettings.github, + linkedin_url: personalDetails.linkedin, + github_url: personalDetails.github, }, }; if (changePassword) { - userSchema["current_password"] = profileSettings.currentPassword; - userSchema["password"] = profileSettings.password; - userSchema["password_confirmation"] = profileSettings.confirmPassword; + userSchema["current_password"] = personalDetails.currentPassword; + userSchema["password"] = personalDetails.password; + userSchema["password_confirmation"] = personalDetails.confirmPassword; } const payload = { address: { - address_line_1: profileSettings.addresses.address_line_1, - address_line_2: profileSettings.addresses.address_line_2, - address_type: profileSettings.addresses.address_type, - city: profileSettings.addresses.city, - state: profileSettings.addresses.state, - country: profileSettings.addresses.country, - pin: profileSettings.addresses.pin, + address_line_1: personalDetails.addresses.address_line_1, + address_line_2: personalDetails.addresses.address_line_2, + address_type: personalDetails.addresses.address_type, + city: personalDetails.addresses.city, + state: personalDetails.addresses.state, + country: personalDetails.addresses.country, + pin: personalDetails.addresses.pin, }, }; - await profileApi.update({ - user: userSchema, - }); + if (isCalledFromSettings) { + await profileApi.update({ + user: userSchema, + }); + } else { + await teamsApi.updateUser(id, { + user: userSchema, + }); + } if (addrId) { - await profileApi.updateAddress(userId, addrId, { - address: { ...profileSettings.addresses }, + await teamsApi.updateAddress(userId, addrId, { + address: { ...personalDetails.addresses }, }); } else { - await profileApi.createAddress(userId, payload); + await teamsApi.createAddress(userId, payload); } setErrDetails(initialErrState); - navigate(`/settings/profile`, { replace: true }); + navigate(`${navigateToPath}/profile`, { replace: true }); } catch (err) { setIsLoading(false); const errObj = initialErrState; @@ -245,7 +259,7 @@ const UserDetailsEdit = () => { const handleCancelDetails = () => { setIsLoading(true); - navigate(`/settings/profile`, { replace: true }); + navigate(`${navigateToPath}/profile`, { replace: true }); }; const handleCurrentPasswordChange = event => { @@ -266,23 +280,14 @@ const UserDetailsEdit = () => { {isDesktop && ( -
    -

    Personal Details

    -
    - - -
    -
    + {isLoading ? ( ) : ( @@ -294,7 +299,7 @@ const UserDetailsEdit = () => { confirmPassword={confirmPassword} countries={countries} currentPassword={currentPassword} - dateFormat={profileSettings.date_format} + dateFormat={personalDetails.date_format} errDetails={errDetails} getErr={getErr} handleConfirmPasswordChange={handleConfirmPasswordChange} @@ -305,7 +310,7 @@ const UserDetailsEdit = () => { handlePasswordChange={handlePasswordChange} handlePhoneNumberChange={handlePhoneNumberChange} password={password} - personalDetails={profileSettings} + personalDetails={personalDetails} setChangePassword={setChangePassword} setErrDetails={setErrDetails} setShowConfirmPassword={setShowConfirmPassword} @@ -325,7 +330,7 @@ const UserDetailsEdit = () => { {!isDesktop && ( {isLoading ? ( @@ -338,7 +343,7 @@ const UserDetailsEdit = () => { changePassword={changePassword} countries={countries} currentPassword={currentPassword} - dateFormat={profileSettings.date_format} + dateFormat={personalDetails.date_format} errDetails={errDetails} handleCancelDetails={handleCancelDetails} handleCurrentPasswordChange={handleCurrentPasswordChange} @@ -347,7 +352,7 @@ const UserDetailsEdit = () => { handleOnChangeCountry={handleOnChangeCountry} handlePhoneNumberChange={handlePhoneNumberChange} handleUpdateDetails={handleUpdateDetails} - personalDetails={profileSettings} + personalDetails={personalDetails} setChangePassword={setChangePassword} setErrDetails={setErrDetails} setShowConfirmPassword={setShowConfirmPassword} diff --git a/app/javascript/src/components/Profile/UserDetail/Edit/validationSchema.ts b/app/javascript/src/components/Profile/Personal/User/Edit/validationSchema.ts similarity index 100% rename from app/javascript/src/components/Profile/UserDetail/Edit/validationSchema.ts rename to app/javascript/src/components/Profile/Personal/User/Edit/validationSchema.ts diff --git a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/MobilePersonalDetails.tsx b/app/javascript/src/components/Profile/Personal/User/MobilePersonalDetails.tsx similarity index 85% rename from app/javascript/src/components/Profile/UserDetail/UserDetailsView/MobilePersonalDetails.tsx rename to app/javascript/src/components/Profile/Personal/User/MobilePersonalDetails.tsx index 05f970c74b..bcdad03f85 100644 --- a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/MobilePersonalDetails.tsx +++ b/app/javascript/src/components/Profile/Personal/User/MobilePersonalDetails.tsx @@ -19,6 +19,7 @@ const MobilePersonalDetails = ({ date_format, }, handleEditClick, + isCalledFromSettings, }) => { const { address_line_1 = "", @@ -116,20 +117,22 @@ const MobilePersonalDetails = ({ -
    - - - Password - -
    - + {isCalledFromSettings && ( +
    + + + Password + +
    + +
    -
    + )}
    ); }; diff --git a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/StaticPage.tsx b/app/javascript/src/components/Profile/Personal/User/StaticPage.tsx similarity index 83% rename from app/javascript/src/components/Profile/UserDetail/UserDetailsView/StaticPage.tsx rename to app/javascript/src/components/Profile/Personal/User/StaticPage.tsx index 3cb9058be5..3d63e829f9 100644 --- a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/StaticPage.tsx +++ b/app/javascript/src/components/Profile/Personal/User/StaticPage.tsx @@ -5,7 +5,11 @@ import customParseFormat from "dayjs/plugin/customParseFormat"; import { InfoIcon, KeyIcon, PhoneIcon, MapPinIcon, GlobeIcon } from "miruIcons"; dayjs.extend(customParseFormat); -const StaticPage = ({ personalDetails, handleEditClick }) => ( +const StaticPage = ({ + personalDetails, + handleEditClick, + isCalledFromSettings, +}) => (
    @@ -112,26 +116,28 @@ const StaticPage = ({ personalDetails, handleEditClick }) => (
    -
    -
    -
    - - - Password - -
    -
    -
    - + {isCalledFromSettings && ( +
    +
    +
    + + + Password + +
    +
    +
    + +
    -
    + )}
    ); diff --git a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/index.tsx b/app/javascript/src/components/Profile/Personal/User/index.tsx similarity index 64% rename from app/javascript/src/components/Profile/UserDetail/UserDetailsView/index.tsx rename to app/javascript/src/components/Profile/Personal/User/index.tsx index add5b182fe..ef1f91a768 100644 --- a/app/javascript/src/components/Profile/UserDetail/UserDetailsView/index.tsx +++ b/app/javascript/src/components/Profile/Personal/User/index.tsx @@ -1,15 +1,12 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - import React, { Fragment, useEffect, useState } from "react"; -import { Outlet, useNavigate } from "react-router-dom"; +import { Outlet, useNavigate, useParams } from "react-router-dom"; -import profileApi from "apis/profile"; import teamsApi from "apis/teams"; import Loader from "common/Loader/index"; import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; -import DetailsHeader from "components/Profile/DetailsHeader"; +import DetailsHeader from "components/Profile/Common/DetailsHeader"; +import { useProfileContext } from "context/Profile/ProfileContext"; import { useUserContext } from "context/UserContext"; import { employmentMapper, teamsMapper } from "mapper/teams.mapper"; import { sendGAPageView } from "utils/googleAnalytics"; @@ -18,32 +15,35 @@ import MobilePersonalDetails from "./MobilePersonalDetails"; import StaticPage from "./StaticPage"; const UserDetailsView = () => { - const { setUserState, profileSettings } = useProfile(); + const { updateDetails, personalDetails, isCalledFromSettings } = + useProfileContext(); const [isLoading, setIsLoading] = useState(false); - const { isDesktop, companyRole } = useUserContext(); + const { user, isDesktop, companyRole } = useUserContext(); + const { memberId } = useParams(); + const UserId = window.location.pathname.startsWith("/settings") + ? user.id + : memberId; const getData = async () => { setIsLoading(true); - const res = await profileApi.index(); + const res = await teamsApi.get(UserId); if (res.status && res.status == 200) { - const addressData = await profileApi.getAddress(res.data.user.id); - const userObj = teamsMapper(res.data.user, addressData.data.addresses[0]); + const addressData = await teamsApi.getAddress(UserId); + const userObj = teamsMapper(res.data, addressData.data.addresses[0]); - setUserState("profileSettings", userObj); + updateDetails("personalDetails", userObj); if (companyRole !== "client") { - const employmentData: any = await teamsApi.getEmploymentDetails( - res?.data?.user.id - ); + const employmentData: any = await teamsApi.getEmploymentDetails(UserId); const previousEmploymentData: any = - await teamsApi.getPreviousEmployments(res?.data?.user.id); + await teamsApi.getPreviousEmployments(UserId); if (employmentData.status && employmentData.status == 200) { const employmentObj = employmentMapper( employmentData.data.employment, previousEmploymentData.data.previous_employments ); - setUserState("employmentDetails", employmentObj); + updateDetails("employmentDetails", employmentObj); } } setIsLoading(false); @@ -60,7 +60,7 @@ const UserDetailsView = () => { const navigate = useNavigate(); const handleEditClick = () => { - navigate(`/settings/profile/edit`, { replace: true }); + navigate(`edit`, { replace: true }); }; return ( @@ -79,7 +79,8 @@ const UserDetailsView = () => { ) : ( )} @@ -87,16 +88,19 @@ const UserDetailsView = () => { {!isDesktop && ( {isLoading ? ( ) : ( )} diff --git a/app/javascript/src/components/Profile/RouteConfig.tsx b/app/javascript/src/components/Profile/RouteConfig.tsx deleted file mode 100644 index 1982b5f5ec..0000000000 --- a/app/javascript/src/components/Profile/RouteConfig.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useEffect } from "react"; - -import { Routes, Route, Navigate } from "react-router-dom"; - -import profileApi from "apis/profile"; -import ErrorPage from "common/Error"; -import { useUserContext } from "context/UserContext"; -import { teamsMapper } from "mapper/teams.mapper"; - -import { useProfile } from "./context/EntryContext"; -import { SETTINGS_ROUTES } from "./routes"; - -const ProtectedRoute = ({ role, authorisedRoles, children }) => { - if (authorisedRoles.includes(role)) { - return children; - } - - return ; -}; - -const RouteConfig = () => { - const { companyRole } = useUserContext(); - const { - setUserState, - profileSettings: { first_name, last_name }, - } = useProfile(); - - const getData = async () => { - if (!first_name && !last_name) { - const res = await profileApi.index(); - if (res.status && res.status == 200) { - const addressData = await profileApi.getAddress(res.data.user.id); - const userObj = teamsMapper( - res.data.user, - addressData.data.addresses[0] - ); - setUserState("profileSettings", userObj); - } - } - }; - - useEffect(() => { - getData(); - }, []); - - return ( - - {SETTINGS_ROUTES.map(({ path, authorisedRoles, Component }) => ( - - - - } - /> - ))} - } path="*" /> - - ); -}; - -export default RouteConfig; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/validationSchema.ts b/app/javascript/src/components/Profile/Schema/employmentSchema.ts similarity index 100% rename from app/javascript/src/components/Team/Details/EmploymentDetails/Edit/validationSchema.ts rename to app/javascript/src/components/Profile/Schema/employmentSchema.ts diff --git a/app/javascript/src/components/Profile/SubNav.tsx b/app/javascript/src/components/Profile/SubNav.tsx deleted file mode 100644 index 76150998b1..0000000000 --- a/app/javascript/src/components/Profile/SubNav.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import React, { useState } from "react"; - -import { MinusIcon, PlusIcon } from "miruIcons"; -import { NavLink } from "react-router-dom"; - -import { useUserContext } from "context/UserContext"; - -import UserInformation from "./CommonComponents/UserInformation"; -import { companySettingsList, personalSettingsList } from "./constants"; - -const SubNav = ({ isAdmin, firstName, company, lastName, designation }) => { - const { companyRole } = useUserContext(); - - const getActiveClassName = isActive => { - if (isActive) { - return "pl-4 py-5 border-l-8 border-miru-han-purple-600 bg-miru-gray-200 text-miru-han-purple-600 block w-full flex items-center"; - } - - return "pl-6 py-5 border-b-1 border-miru-gray-400 block w-full flex items-center"; - }; - - const [openedSubNav, setOpenedSubNav] = useState({ - personal: true, - company: false, - }); - - const getAdminLinks = () => ( -
      -
      - setOpenedSubNav({ - ...openedSubNav, - personal: !openedSubNav.personal, - }) - } - > - Personal - -
      - {openedSubNav.personal && ( -
        - {personalSettingsList.map((setting, index) => { - if (setting.authorisedRoles.includes(companyRole)) { - return ( -
      • - -
      • - ); - } - })} -
      - )} -
      - setOpenedSubNav({ ...openedSubNav, company: !openedSubNav.company }) - } - > - {company.name} - -
      - {openedSubNav.company && ( -
        - {companySettingsList.map((setting, index) => { - if (setting.authorisedRoles.includes(companyRole)) { - return ( -
      • - -
      • - ); - } - })} -
      - )} -
    - ); - - const getEmployeeLinks = () => ( -
      - {personalSettingsList.map((setting, index) => { - if (setting.authorisedRoles.includes(companyRole)) { - return ( -
    • - -
    • - ); - } - })} -
    - ); - - const SideBarNavItem = ({ label, link, icon }) => ( - getActiveClassName(isActive)} - to={link} - > - {icon} - {label} - - ); - - return ( -
    - -
    - {isAdmin ? getAdminLinks() : getEmployeeLinks()} -
    -
    - ); -}; - -export default SubNav; diff --git a/app/javascript/src/components/Profile/UserDetail/index.tsx b/app/javascript/src/components/Profile/UserDetail/index.tsx deleted file mode 100644 index 12aff71ab5..0000000000 --- a/app/javascript/src/components/Profile/UserDetail/index.tsx +++ /dev/null @@ -1,456 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -import React, { useEffect, useState } from "react"; - -import { - DeleteIconSVG, - EditImageButtonSVG, - PasswordIconSVG, - PasswordIconTextSVG, - PlusIconSVG, -} from "miruIcons"; -import { Toastr } from "StyledComponents"; -import * as Yup from "yup"; - -import profileApi from "apis/profile"; -import { Divider } from "common/Divider"; -import Loader from "common/Loader/index"; -import { sendGAPageView } from "utils/googleAnalytics"; - -import { useProfile } from "../context/EntryContext"; -import Header from "../Header"; - -const userProfileSchema = Yup.object().shape({ - firstName: Yup.string().required("First Name cannot be blank"), - lastName: Yup.string().required("Last Name cannot be blank"), - changePassword: Yup.boolean(), - password: Yup.string().when("changePassword", { - is: true, - then: Yup.string().required("Please enter password"), - }), - currentPassword: Yup.string().when("changePassword", { - is: true, - then: Yup.string().required("Please enter current password"), - }), - - confirmPassword: Yup.string().when("changePassword", { - is: true, - then: Yup.string().oneOf( - [Yup.ref("password"), null], - "Passwords don't match" - ), - }), -}); - -const UserDetails = () => { - const initialErrState = { - firstNameErr: "", - lastNameErr: "", - passwordErr: "", - currentPasswordErr: "", - confirmPasswordErr: "", - }; - - const { setUserState } = useProfile(); - const [profileImage, setProfileImage] = useState(""); - const [imageFile, setImageFile] = useState(null); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [email, setEmail] = useState(""); - const [currentPassword, setCurrentPassword] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [changePassword, setChangePassword] = useState(false); - const [showPassword, setShowPassword] = useState(false); - const [showCurrentPassword, setShowCurrentPassword] = - useState(false); - - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - const [isDetailUpdated, setIsDetailUpdated] = useState(false); - const [errDetails, setErrDetails] = useState(initialErrState); - const [isLoading, setIsLoading] = useState(false); - - const handleProfileImageChange = e => { - const imageFile = e.target.files[0]; - setProfileImage(URL.createObjectURL(imageFile)); - setImageFile(imageFile); - setIsDetailUpdated(true); - }; - - const handleUpdateProfile = async () => { - try { - await userProfileSchema.validate( - { - firstName, - lastName, - changePassword, - password, - confirmPassword, - currentPassword, - }, - { abortEarly: false } - ); - await updateProfile(); - } catch (err) { - setIsLoading(false); - const errObj = initialErrState; - err.inner.map(item => { - errObj[`${item.path}Err`] = item.message; - }); - setErrDetails(errObj); - } - }; - - const updateProfile = async () => { - try { - setIsLoading(true); - const formD = new FormData(); - formD.append("user[first_name]", firstName); - formD.append("user[last_name]", lastName); - if (changePassword) { - formD.append("user[current_password]", currentPassword); - formD.append("user[password]", password); - formD.append("user[password_confirmation]", confirmPassword); - } - - if (imageFile) { - formD.append("user[avatar]", imageFile); - } - await profileApi.update(formD); - setIsDetailUpdated(false); - setErrDetails(initialErrState); - setUserState("profileSettings", { - firstName, - lastName, - }); - setIsLoading(false); - } catch { - setIsLoading(false); - Toastr.error("Error in Updating user Details"); - } - }; - - const handleFirstNameChange = event => { - setFirstName(event.target.value); - setIsDetailUpdated(true); - setErrDetails({ ...errDetails, firstNameErr: "" }); - }; - - const handleLastNameChange = event => { - setLastName(event.target.value); - setIsDetailUpdated(true); - setErrDetails({ ...errDetails, lastNameErr: "" }); - }; - - const handleCurrentPasswordChange = event => { - setCurrentPassword(event.target.value); - setIsDetailUpdated(true); - setErrDetails({ ...errDetails, currentPasswordErr: "" }); - }; - - const handlePasswordChange = event => { - setPassword(event.target.value); - setIsDetailUpdated(true); - setErrDetails({ ...errDetails, passwordErr: "" }); - }; - - const handleConfirmPasswordChange = event => { - setConfirmPassword(event.target.value); - setIsDetailUpdated(true); - }; - - const getData = async () => { - setIsLoading(true); - const data = await profileApi.index(); - if (data.status && data.status == 200) { - setFirstName(data.data.user.first_name); - setLastName(data.data.user.last_name); - setProfileImage(data.data.user.avatar_url); - setEmail(data.data.user.email); - setUserState("profileSettings", { - firstName: data.data.user.first_name, - lastName: data.data.user.last_name, - email: data.data.user.email, - }); - setIsLoading(false); - } else { - setFirstName(""); - setLastName(""); - setProfileImage(""); - setEmail(""); - setIsLoading(false); - } - }; - - useEffect(() => { - sendGAPageView(); - getData(); - }, []); - - const handleCancelAction = () => { - getData(); - setIsDetailUpdated(false); - setErrDetails(initialErrState); - setChangePassword(false); - setCurrentPassword(""); - setPassword(""); - setConfirmPassword(""); - }; - - const handleDeleteLogo = async () => { - const removeProfile = await profileApi.removeAvatar(); - if (removeProfile.status === 200) { - setImageFile(null); - setProfileImage(""); - } - }; - - const getErr = errMsg =>

    {errMsg}

    ; - - return ( -
    -
    - {isLoading ? ( -
    - -
    - ) : ( -
    -
    -
    Basic Details
    -
    - Profile Picture - {profileImage ? ( -
    -
    - profile_pic -
    - - - -
    - ) : ( - <> -
    - -
    - - - )} -
    - -
    -
    - - {errDetails.firstNameErr && getErr(errDetails.firstNameErr)} -
    -
    - - {errDetails.lastNameErr && getErr(errDetails.lastNameErr)} -
    -
    -
    -
    - - setEmail(event.target.value)} - /> -
    -
    -
    - -
    -
    Password
    -
    -
    - {!changePassword && ( -
    -

    setChangePassword(true)} - > - CHANGE PASSWORD -

    -
    - )} - {changePassword && ( -
    -
    -
    - -
    - - -
    - {errDetails.currentPasswordErr && - getErr(errDetails.currentPasswordErr)} -
    -
    -
    - -
    - - -
    - {errDetails.passwordErr && - getErr(errDetails.passwordErr)} -
    -
    - -
    - - -
    - {errDetails.confirmPasswordErr && - getErr(errDetails.confirmPasswordErr)} -
    -
    -

    { - setChangePassword(false); - setErrDetails(initialErrState); - }} - > - CANCEL -

    -
    -
    - )} -
    -
    -
    -
    - )} -
    - ); -}; - -export default UserDetails; diff --git a/app/javascript/src/components/Profile/constants.js b/app/javascript/src/components/Profile/constants.js deleted file mode 100644 index 317fb0f3d7..0000000000 --- a/app/javascript/src/components/Profile/constants.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; - -import { - ClientsIcon as BuildingsIcon, - CalendarIcon, - CakeIcon, - PaymentsIcon, - UserIcon, - MobileIcon, - ProjectsIcon, -} from "miruIcons"; - -import { Roles } from "constants/index"; - -const { ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT } = Roles; - -export const personalSettingsList = [ - { - label: "PROFILE SETTINGS", - link: "/settings/profile", - icon: , - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], - }, - { - label: "EMPLOYMENT DETAILS", - link: "/settings/employment", - icon: , - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - label: "ALLOCATED DEVICES", - link: "/settings/devices", - icon: , - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - //TODO: Uncomment when Integrating with API - // { - // label: "COMPENSATION", - // link: "/settings/compensation", - // icon: , - // authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - // }, -]; - -export const companySettingsList = [ - { - label: "ORG. SETTINGS", - link: "/settings/organization", - icon: , - authorisedRoles: [ADMIN, OWNER], - }, - { - label: "PAYMENT SETTINGS", - link: "/settings/payment", - icon: , - authorisedRoles: [ADMIN, OWNER], - }, - { - label: "LEAVES", - link: "/settings/leaves", - icon: , - authorisedRoles: [ADMIN, OWNER], - }, - { - label: "HOLIDAYS", - link: "/settings/holidays", - icon: , - authorisedRoles: [ADMIN, OWNER], - }, - // { - // label: "Integration", - // link: "/settings/integrations", - // icon: , - // authorisedRoles: [ADMIN, OWNER], - // }, -]; diff --git a/app/javascript/src/components/Profile/context/EntryContext.tsx b/app/javascript/src/components/Profile/context/EntryContext.tsx deleted file mode 100644 index 8fa61bc78b..0000000000 --- a/app/javascript/src/components/Profile/context/EntryContext.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createContext, useContext } from "react"; - -import { CompensationDetailsState } from "./CompensationDetailsState"; -import { EmploymentDetailsState } from "./EmploymentDetailsState"; -import { PersonalDetailsState } from "./PersonalDetailsState"; - -const EntryContext = createContext({ - profileSettings: PersonalDetailsState, - employmentDetails: EmploymentDetailsState, - compensationDetails: CompensationDetailsState, - organizationSettings: {}, - bankAccDetails: {}, - paymentSettings: {}, - billing: {}, - setUserState: (key, value) => {}, //eslint-disable-line -}); - -export const useProfile = () => useContext(EntryContext); - -export default EntryContext; diff --git a/app/javascript/src/components/Profile/index.tsx b/app/javascript/src/components/Profile/index.tsx new file mode 100644 index 0000000000..3ad5076614 --- /dev/null +++ b/app/javascript/src/components/Profile/index.tsx @@ -0,0 +1,98 @@ +import React, { Fragment, useState, useEffect } from "react"; + +import { useLocation, useParams } from "react-router-dom"; + +import { ProfileContext } from "context/Profile/ProfileContext"; +import { useUserContext } from "context/UserContext"; + +import { CompensationDetailsState } from "./Context/CompensationDetailsState"; +import { EmploymentDetailsState } from "./Context/EmploymentDetailsState"; +import { PersonalDetailsState } from "./Context/PersonalDetailsState"; +import Header from "./Layout/Header"; +import SideNav from "./Layout/Navigation"; +import MobileNav from "./Layout/Navigation/MobileNav"; +import OutletWrapper from "./Layout/OutletWrapper"; + +const Layout = () => { + const { isDesktop } = useUserContext(); + const location = useLocation(); + const { memberId } = useParams(); + const [settingsStates, setSettingsStates] = useState({ + personalDetails: PersonalDetailsState, + employmentDetails: EmploymentDetailsState, + documentDetails: {}, + deviceDetails: {}, + compensationDetails: CompensationDetailsState, + reimburstmentDetails: {}, + }); + + const [isCalledFromSettings, setIsCalledFromSettings] = useState(false); + const [isCalledFromTeam, setIsCalledFromTeam] = useState(false); + const [showMobileNav, setShowMobileNav] = useState(false); + + useEffect(() => { + if (location.pathname.startsWith("/settings")) { + setIsCalledFromSettings(true); + } else { + setIsCalledFromSettings(false); + } + + if (location.pathname.startsWith("/team")) { + setIsCalledFromTeam(true); + } else { + setIsCalledFromTeam(false); + } + + const mobileNavVisibility = + location.pathname === "/settings" || + location.pathname === "/settings/" || + location.pathname === `/team/${memberId}` || + location.pathname === `/team/${memberId}/`; + + setShowMobileNav(mobileNavVisibility); + }, [location]); + + const updateDetails = (key, value) => { + setSettingsStates(previousSettings => ({ + ...previousSettings, + ...{ [key]: { ...previousSettings[key], ...value } }, + })); + }; + + return ( + + {isDesktop && ( + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + )} + {!isDesktop && ( + + {showMobileNav && } + + + )} +
    + ); +}; + +export default Layout; diff --git a/app/javascript/src/components/Profile/routes.ts b/app/javascript/src/components/Profile/routes.ts deleted file mode 100644 index 7ff1b29e98..0000000000 --- a/app/javascript/src/components/Profile/routes.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Roles } from "constants/index"; - -import MobileNav from "./Layout/MobileNav"; -import OrgDetails from "./Organization/Details"; -import OrgEdit from "./Organization/Edit"; -import Holidays from "./Organization/Holidays"; -import Leaves from "./Organization/Leaves"; -import PaymentSettings from "./Organization/Payment"; -import AllocatedDevicesDetails from "./UserDetail/AllocatedDevicesDetails"; -import AllocatedDevicesEdit from "./UserDetail/AllocatedDevicesDetails/Edit"; -import CompensationDetails from "./UserDetail/CompensationDetails"; -import CompensationDetailsEdit from "./UserDetail/CompensationDetails/Edit"; -import UserDetailsEdit from "./UserDetail/Edit"; -import EmploymentDetails from "./UserDetail/EmploymentDetails"; -import EmploymentDetailsEdit from "./UserDetail/EmploymentDetails/Edit"; -import UserDetailsView from "./UserDetail/UserDetailsView"; - -const { ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT } = Roles; - -export const SETTINGS_ROUTES = [ - { - path: "/profile", - Component: UserDetailsView, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], - }, - { - path: "/profile/edit", - Component: UserDetailsEdit, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], - }, - { - path: "/employment", - Component: EmploymentDetails, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/employment/edit", - Component: EmploymentDetailsEdit, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/devices", - Component: AllocatedDevicesDetails, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/devices/edit", - Component: AllocatedDevicesEdit, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/compensation", - Component: CompensationDetails, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/compensation/edit", - Component: CompensationDetailsEdit, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE], - }, - { - path: "/", - Component: MobileNav, - authorisedRoles: [ADMIN, OWNER, BOOK_KEEPER, EMPLOYEE, CLIENT], - }, - { - path: "/organization", - Component: OrgDetails, - authorisedRoles: [ADMIN, OWNER], - }, - { - path: "/organization/edit", - Component: OrgEdit, - authorisedRoles: [ADMIN, OWNER], - }, - { - path: "/payment", - Component: PaymentSettings, - authorisedRoles: [ADMIN, OWNER], - }, - // { - // path: "/import", - // Component: OrganizationImport, - // authorisedRoles: [ADMIN, OWNER], - // }, - // { - // path: "/billing", - // Component: Billing, - // authorisedRoles: [ADMIN, OWNER], - // }, - { - path: "/leaves", - Component: Leaves, - authorisedRoles: [ADMIN, OWNER], - }, - { - path: "/holidays", - Component: Holidays, - authorisedRoles: [ADMIN, OWNER], - }, - // { - // path: "/integrations", - // Component: GoogleCalendar, - // authorisedRoles: [ADMIN, OWNER], - // }, -]; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/CompensationDetailsState.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/CompensationDetailsState.tsx deleted file mode 100644 index f993aa7b2d..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/CompensationDetailsState.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const CompensationDetailsState = { - earnings: [], - deductions: [], - total: { - amount: 0, - }, -}; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/EditPage.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/Edit/EditPage.tsx deleted file mode 100644 index 235addf5ad..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/EditPage.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React from "react"; - -import { currencyFormat } from "helpers"; -import { - DeleteIcon, - CoinsIcon, - EarningsIconSVG, - DeductionIconSVG, -} from "miruIcons"; -import "react-phone-number-input/style.css"; -import { Button } from "StyledComponents"; - -import { CustomInputText } from "common/CustomInputText"; - -const EditPage = ({ - handleAddEarning, - handleAddDeduction, - updateDeductionValues, - updateEarningsValues, - handleDeleteEarning, - handleDeleteDeduction, - earnings, - deductions, - total, - currency, -}) => ( -
    -
    -
    - - - Earnings - -
    -
    - {earnings.length > 0 && - earnings.map((earning, index) => ( -
    -
    -
    - { - updateEarningsValues(earning, e); - }} - /> -
    -
    - { - updateEarningsValues(earning, e); - }} - /> -
    -
    - -
    - ))} -
    - -
    -
    -
    -
    -
    -
    - - - Deductions - -
    -
    - {deductions.length > 0 && - deductions.map((deduction, index) => ( -
    -
    -
    - { - updateDeductionValues(deduction, e); - }} - /> -
    -
    - { - updateDeductionValues(deduction, e); - }} - /> -
    -
    - -
    - ))} -
    - -
    -
    -
    -
    -
    -
    - - - Total - -
    -
    - - {currencyFormat(currency, total)} - -
    -
    -
    -); - -export default EditPage; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/MobileEditPage.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/Edit/MobileEditPage.tsx deleted file mode 100644 index e700055902..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/MobileEditPage.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from "react"; - -import { currencyFormat } from "helpers"; -import { - CoinsIcon, - DeductionIconSVG, - DeleteIcon, - EarningsIconSVG, -} from "miruIcons"; -import "react-phone-number-input/style.css"; -import { Button } from "StyledComponents"; - -import { CustomInputText } from "common/CustomInputText"; - -const MobileEditPage = ({ - handleAddEarning, - handleAddDeduction, - updateDeductionValues, - updateEarningsValues, - handleDeleteEarning, - handleDeleteDeduction, - handleCancelDetails, - handleUpdateDetails, - earnings, - deductions, - total, - currency, -}) => ( -
    -
    -
    - - - Earnings - -
    -
    - {earnings.length > 0 ? ( - earnings.map((earning, index) => ( -
    -
    -
    - { - updateEarningsValues(earning, e); - }} - /> -
    - -
    -
    - { - updateEarningsValues(earning, e); - }} - /> -
    -
    - )) - ) : ( -
    No Earnings found
    - )} -
    - -
    -
    -
    -
    -
    - - - Deductions - -
    -
    - {deductions.length > 0 ? ( - deductions.map((deduction, index) => ( -
    -
    -
    - { - updateDeductionValues(deduction, e); - }} - /> -
    - -
    -
    - { - updateDeductionValues(deduction, e); - }} - /> -
    -
    - )) - ) : ( -
    No deductions found
    - )} -
    - -
    -
    -
    -
    -
    - - - Total - -
    -
    - - {currencyFormat(currency, total)} - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -); - -export default MobileEditPage; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/index.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/Edit/index.tsx deleted file mode 100644 index 214ef161e9..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/Edit/index.tsx +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { Fragment, useEffect, useState } from "react"; - -import { useNavigate } from "react-router-dom"; - -import Loader from "common/Loader/index"; -import { CompensationDetailsState } from "components/Profile/context/CompensationDetailsState"; -import Header from "components/Profile/Header"; -import { useUserContext } from "context/UserContext"; - -import EditPage from "./EditPage"; -import MobileEditPage from "./MobileEditPage"; - -const CompensationEditPage = () => { - //TODO: add state for errDetails after API integration - // const initialErrState = { - // earning_type_err: "", - // earning_amount_err: "", - // deduction_type_err: "", - // deduction_amount_err: "", - // }; - const navigate = useNavigate(); - const { isDesktop, company } = useUserContext(); - - const [isLoading, setIsLoading] = useState(false); - const [earnings, setEarnings] = useState>( - CompensationDetailsState.earnings - ); - - const [deductions, setDeductions] = useState>( - CompensationDetailsState.deductions - ); - - const [total, setTotal] = useState( - CompensationDetailsState.total.amount - ); - - useEffect(() => { - setIsLoading(true); - getDevicesDetail(); - }, []); - - useEffect(() => { - const totalEarnings = earnings.reduce( - (accumulator, currentValue) => accumulator + currentValue["amount"], - 0 - ); - - const totalDeductions = deductions.reduce( - (accumulator, currentValue) => accumulator + currentValue["amount"], - 0 - ); - setTotal(totalEarnings - totalDeductions); - }, [deductions, earnings]); - - const getDevicesDetail = async () => { - setIsLoading(false); - }; - - const handleAddDeduction = () => { - const newDeduction = [...deductions, { deduction_type: "", amount: "" }]; - setDeductions(newDeduction); - }; - - const handleAddEarning = () => { - const newEarning = [...earnings, { earning_type: "", amount: "" }]; - setEarnings(newEarning); - }; - - const handleDeleteDeduction = deduction => { - setDeductions(deductions.filter(d => d !== deduction)); - }; - - const handleDeleteEarning = earning => { - setEarnings(earnings.filter(e => e !== earning)); - }; - - const updateEarningsValues = (earning, event) => { - const { name, value } = event.target; - const updatedEarnings = earnings.map(e => - e == earning ? { ...e, [name]: value } : e - ); - setEarnings(updatedEarnings); - }; - - const updateDeductionValues = (deduction, event) => { - const { name, value } = event.target; - const updatedDeductions = deductions.map(d => - d == deduction ? { ...d, [name]: value } : d - ); - setDeductions(updatedDeductions); - }; - - const handleUpdateDetails = () => { - //Todo: API integration for update details - }; - - const handleCancelDetails = () => { - setIsLoading(true); - navigate(`/settings/compensation`, { replace: true }); - }; - - return ( - -
    - {isLoading ? ( - - ) : ( - - {isDesktop && ( - - )} - {!isDesktop && ( - - )} - - )} - - ); -}; - -export default CompensationEditPage; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/StaticPage.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/StaticPage.tsx deleted file mode 100644 index 42c702af43..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/StaticPage.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; - -import { currencyFormat } from "helpers"; -import { EarningsIconSVG, DeductionIconSVG, CoinsIcon } from "miruIcons"; - -const StaticPage = ({ compensationDetails, currency }) => { - const { earnings, deductions, total } = compensationDetails; - - return ( -
    -
    -
    - - - Earnings - -
    -
    - {earnings ? ( - earnings.map((earning, index) => ( -
    -
    - - Earning Type - -

    - {earning.type || "-"} -

    -
    -
    - - Amount - -

    - {currencyFormat(currency, earning.amount) || "-"} -

    -
    -
    - )) - ) : ( -
    No earning(s) found
    - )} -
    -
    -
    -
    - - - Deductions - -
    -
    - {deductions ? ( - deductions.map((deduction, index) => ( -
    -
    - - Deduction Type - -

    - {deduction.type} -

    -
    -
    - - Amount - -

    - {currencyFormat(currency, deduction.amount) || "-"} -

    -
    -
    - )) - ) : ( -
    No deduction(s) found
    - )} -
    -
    -
    -
    - - - Total - -
    -
    - - {currencyFormat(currency, total.amount)} - -
    -
    -
    - ); -}; - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/CompensationDetails/index.tsx b/app/javascript/src/components/Team/Details/CompensationDetails/index.tsx deleted file mode 100644 index b7e3eb9ae0..0000000000 --- a/app/javascript/src/components/Team/Details/CompensationDetails/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Fragment, useEffect, useState } from "react"; - -import { useNavigate } from "react-router-dom"; - -import Loader from "common/Loader/index"; -import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import { useProfile } from "components/Profile/context/EntryContext"; -import DetailsHeader from "components/Profile/DetailsHeader"; -import { useUserContext } from "context/UserContext"; - -import StaticPage from "./StaticPage"; - -const CompensationDetails = () => { - const { isDesktop, company } = useUserContext(); - const { setUserState, compensationDetails } = useProfile(); - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - - const getDetails = async () => { - //fetch compensation details from backend and store it in compensationData - const compensationData = { - earnings: [ - { type: "Monthly Salary", amount: "125000" }, - { type: "SGST (9%)", amount: "11250" }, - { type: "CGST (9%)", amount: "11250" }, - ], - deductions: [{ type: "TDS", amount: "12500" }], - total: { - amount: "147500", - }, - }; - setUserState("compensationDetails", compensationData); - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - getDetails(); - }, []); - - return ( - - {isDesktop ? ( - - navigate(`/settings/compensation/edit`, { replace: true }) - } - /> - ) : ( - - )} - {isLoading ? ( - - ) : ( - - )} - - ); -}; -export default CompensationDetails; diff --git a/app/javascript/src/components/Team/Details/DeviceDetails/StaticPage.tsx b/app/javascript/src/components/Team/Details/DeviceDetails/StaticPage.tsx deleted file mode 100644 index 3b548b116d..0000000000 --- a/app/javascript/src/components/Team/Details/DeviceDetails/StaticPage.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; - -const StaticPage = () => ( -
    -
    -
    - - Devices - -
    -
    -
    -
    - - Device Type - -

    Laptop

    -
    -
    - Model -

    - Macbook Pro 13-inch, 2020, Four Thunderbolt 3 ports -

    -
    -
    -
    -
    - - Serial Number - -

    cf4c6be4c742

    -
    -
    - Memory -

    16 GB 3733 MHz LPDDR4X

    -
    -
    -
    -
    - Graphics -

    - Intel Iris Plus Graphics 1536 MB -

    -
    -
    - - Processor - -

    - 2 GHz Quad-Core Intel Core i5 -

    -
    -
    -
    -
    -
    -); - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/DeviceDetails/index.tsx b/app/javascript/src/components/Team/Details/DeviceDetails/index.tsx deleted file mode 100644 index 033f11f9a0..0000000000 --- a/app/javascript/src/components/Team/Details/DeviceDetails/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Fragment } from "react"; - -import StaticPage from "./StaticPage"; - -const DeviceDetails = () => ( - -
    -

    Device Details

    -
    - -
    -); - -export default DeviceDetails; diff --git a/app/javascript/src/components/Team/Details/DocumentDetails/index.tsx b/app/javascript/src/components/Team/Details/DocumentDetails/index.tsx deleted file mode 100644 index d0ae3ca09b..0000000000 --- a/app/javascript/src/components/Team/Details/DocumentDetails/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Fragment } from "react"; - -const DocumentDetails = () => ( - -
    -

    Document Details

    -
    -
    - -); - -export default DocumentDetails; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/StaticPage.tsx b/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/StaticPage.tsx deleted file mode 100644 index cb674a7de1..0000000000 --- a/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/StaticPage.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import React from "react"; - -import dayjs from "dayjs"; -import { ProjectsIcon, CalendarIcon, DeleteIcon } from "miruIcons"; -import "react-phone-number-input/style.css"; -import { Button } from "StyledComponents"; - -import CustomDatePicker from "common/CustomDatePicker"; -import { CustomInputText } from "common/CustomInputText"; -import { CustomReactSelect } from "common/CustomReactSelect"; -import { ErrorSpan } from "common/ErrorSpan"; - -const inputClass = - "form__input block w-full appearance-none bg-white p-4 text-base h-12 focus-within:border-miru-han-purple-1000"; - -const labelClass = - "absolute top-0.5 left-1 h-6 z-1 origin-0 bg-white p-2 text-base font-medium duration-300"; - -const StaticPage = ({ - employeeTypes, - employeeType, - handleOnChangeEmployeeType, - updateCurrentEmploymentDetails, - employmentDetails, - showDORDatePicker, - showDOJDatePicker, - setShowDOJDatePicker, - setShowDORDatePicker, - handleDOJDatePicker, - handleDORDatePicker, - errDetails, - DOJRef, - DORRef, - previousEmployments, - handleDeletePreviousEmployment, - handleAddPastEmployment, - updatePreviousEmploymentValues, - dateFormat, - joinedAt, - resignedAt, -}) => { - const getDOJ = joinedAt && dayjs(joinedAt, dateFormat).format(dateFormat); - - const getDOR = resignedAt && dayjs(resignedAt, dateFormat).format(dateFormat); - - return ( -
    -
    -
    - - - Current
    Employment -
    -
    -
    -
    -
    - { - updateCurrentEmploymentDetails(e.target.value, "employee_id"); - }} - /> - {errDetails.employee_id_err && ( - - )} -
    -
    - { - updateCurrentEmploymentDetails(e.target.value, "designation"); - }} - /> - {errDetails.designation_err && ( - - )} -
    -
    -
    -
    - { - updateCurrentEmploymentDetails(e.target.value, "email"); - }} - /> - {errDetails.email_err && ( - - )} -
    -
    - - {errDetails.employment_err && ( - - )} -
    -
    -
    -
    -
    - setShowDOJDatePicker({ - visibility: !showDOJDatePicker.visibility, - }) - } - > - - -
    - {errDetails.joined_at_err && ( - - )} - {showDOJDatePicker.visibility && ( - - )} -
    -
    -
    - setShowDORDatePicker({ - visibility: !showDORDatePicker.visibility, - }) - } - > - - -
    - {errDetails.resigned_at_err && ( - - )} - {showDORDatePicker.visibility && ( - - )} -
    -
    -
    -
    -
    -
    - - - Previous
    Employment -
    -
    -
    - {previousEmployments.length > 0 && - previousEmployments.map((previous, index) => ( -
    -
    -
    - { - updatePreviousEmploymentValues(previous, e); - }} - /> -
    -
    - { - updatePreviousEmploymentValues(previous, e); - }} - /> -
    -
    - -
    - ))} -
    - -
    -
    -
    -
    -
    - ); -}; - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/index.tsx b/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/index.tsx deleted file mode 100644 index 2dc800dc87..0000000000 --- a/app/javascript/src/components/Team/Details/EmploymentDetails/Edit/index.tsx +++ /dev/null @@ -1,330 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { Fragment, useEffect, useRef, useState } from "react"; - -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import { useOutsideClick } from "helpers"; -import { useNavigate, useParams } from "react-router-dom"; -import * as Yup from "yup"; - -import teamsApi from "apis/teams"; -import Loader from "common/Loader/index"; -import { useTeamDetails } from "context/TeamDetailsContext"; -import { useUserContext } from "context/UserContext"; -import { employmentMapper } from "mapper/teams.mapper"; - -import StaticPage from "./StaticPage"; -import { employmentSchema } from "./validationSchema"; - -dayjs.extend(utc); - -const schema = Yup.object().shape(employmentSchema); - -const EmploymentDetails = () => { - const initialErrState = { - employee_id_err: "", - employment_type_err: "", - email_err: "", - designation_err: "", - joined_at_err: "", - resigned_at_err: "", - company_name_err: "", - role_err: "", - }; - - const { memberId } = useParams(); - const { - updateDetails, - details: { employmentDetails }, - } = useTeamDetails(); - const navigate = useNavigate(); - const { isDesktop } = useUserContext(); - - const DOJRef = useRef(null); - const DORRef = useRef(null); - - const InitialPrevEmployments = { - added_employments: [], - updated_employments: [], - removed_employment_ids: [], - }; - - const [previousEmployments, setPreviousEmployments] = useState([]); - const [employeeType, setEmployeeType] = useState({ label: "", value: "" }); - const [showDOJDatePicker, setShowDOJDatePicker] = useState({ - visibility: false, - }); - - const [showDORDatePicker, setShowDORDatePicker] = useState({ - visibility: false, - }); - const [errDetails, setErrDetails] = useState(initialErrState); - const [isLoading, setIsLoading] = useState(false); - const [dateFormat, setDateFormat] = useState("DD-MM-YYYY"); - const [resignedAt, setResignedAt] = useState(null); - const [joinedAt, setJoinedAt] = useState(null); - - useOutsideClick(DOJRef, () => setShowDOJDatePicker({ visibility: false })); - useOutsideClick(DORRef, () => setShowDORDatePicker({ visibility: false })); - - const employeeTypes = [ - { label: "Salaried Employee", value: "salaried" }, - { label: "Contractor", value: "contractor" }, - ]; - - const getDetails = async () => { - const curr: any = await teamsApi.getEmploymentDetails(memberId); - const prev: any = await teamsApi.getPreviousEmployments(memberId); - setDateFormat(curr.data.date_format); - setJoinedAt(curr.data.employment.joined_at); - setResignedAt(curr.data.employment.resigned_at); - const employmentData = employmentMapper( - curr.data.employment, - prev.data.previous_employments - ); - if (employmentData.current_employment?.employment_type?.length > 0) { - setEmployeeType( - employeeTypes.find( - item => - item.value === employmentData.current_employment.employment_type - ) - ); - } else { - setEmployeeType(employeeTypes[0]); - employmentData.current_employment.employment_type = - employeeTypes[0].value; - } - updateDetails("employment", employmentData); - if (employmentData.previous_employments?.length > 0) { - setPreviousEmployments(employmentData.previous_employments); - } - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - getDetails(); - }, []); - - const handleOnChangeEmployeeType = empType => { - setEmployeeType(empType); - updateDetails("employment", { - ...employmentDetails, - ...{ - current_employment: { - ...employmentDetails.current_employment, - ...{ employment_type: empType.value }, - }, - }, - }); - }; - - const updateCurrentEmploymentDetails = (value, type) => { - updateDetails("employment", { - ...employmentDetails, - ...{ - current_employment: { - ...employmentDetails.current_employment, - ...{ [type]: value }, - }, - }, - }); - }; - - const updatePreviousEmploymentValues = (previous, event) => { - const { name, value } = event.target; - const updatedPreviousEmployments = previousEmployments.map(prevEmployment => - prevEmployment == previous - ? { ...prevEmployment, [name]: value } - : prevEmployment - ); - setPreviousEmployments(updatedPreviousEmployments); - }; - - const handleDOJDatePicker = date => { - setShowDOJDatePicker({ visibility: !showDOJDatePicker.visibility }); - setJoinedAt(date); - updateDetails("employment", { - ...employmentDetails, - ...{ - current_employment: { - ...employmentDetails.current_employment, - ...{ - joined_at: - dateFormat == "DD-MM-YYYY" - ? date - : dayjs(date).format("DD-MM-YYYY"), - }, - }, - }, - }); - }; - - const handleDORDatePicker = date => { - setShowDORDatePicker({ visibility: !showDORDatePicker.visibility }); - setResignedAt(date); - updateDetails("employment", { - ...employmentDetails, - ...{ - current_employment: { - ...employmentDetails.current_employment, - ...{ - resigned_at: - dateFormat == "DD-MM-YYYY" - ? date - : dayjs(date).format("DD-MM-YYYY"), - }, - }, - }, - }); - }; - - const handleAddPastEmployment = () => { - const pastEmployments = [ - ...previousEmployments, - { company_name: "", role: "" }, - ]; - setPreviousEmployments(pastEmployments); - }; - - const handleDeletePreviousEmployment = previous => { - setPreviousEmployments( - previousEmployments.filter(prev => prev !== previous) - ); - }; - - const handleUpdateDetails = async () => { - setIsLoading(true); - const getDifference = (array1, array2) => - array1.filter(object1 => !array2.some(object2 => object1 === object2)); - - //creating an array which includes removed records - const removed = employmentDetails.previous_employments.filter( - e => !previousEmployments.includes(e) - ); - - //creating an array which includes updated and added records - const unSortedEmployments = getDifference( - previousEmployments, - employmentDetails.previous_employments - ); - - const pastEmployments = InitialPrevEmployments; - - //sorting new entries and updated entries into - unSortedEmployments.map(unSorted => { - if (unSorted.id) { - pastEmployments.updated_employments.push(unSorted); - } else { - pastEmployments.added_employments.push(unSorted); - } - }); - - //Extracting removed records id - if (removed.length > 0) { - removed.map(remove => { - if (pastEmployments.updated_employments.length > 0) { - pastEmployments.updated_employments.filter(updated => { - if (updated.id !== remove.id) { - pastEmployments.removed_employment_ids.push(remove?.id); - } - }); - } else { - pastEmployments.removed_employment_ids.push(remove?.id); - } - }); - } - updateEmploymentDetails(pastEmployments); - }; - - const updateEmploymentDetails = async updatedPreviousEmployments => { - try { - await schema.validate(employmentDetails, { abortEarly: false }); - const payload = { - ...updatedPreviousEmployments, - current_employment: employmentDetails.current_employment, - }; - - await teamsApi.updatePreviousEmployments(memberId, { - employments: payload, - }); - setIsLoading(false); - navigate(`/team/${memberId}/employment`, { replace: true }); - } catch (err) { - setIsLoading(false); - const errObj = initialErrState; - if (err.inner) { - err.inner.map(item => { - if (item.path.includes("current_employment")) { - errObj[`${item.path.split(".").pop()}_err`] = item.message; - } else { - errObj[`${item.path}_err`] = item.message; - } - }); - setErrDetails(errObj); - } - } - }; - - const handleCancelDetails = () => { - setIsLoading(true); - navigate(`/team/${memberId}/employment`, { replace: true }); - }; - - return ( - - {isDesktop && ( - -
    -

    - Employement Details -

    -
    - - -
    -
    - {isLoading ? ( - - ) : ( - - )} -
    - )} -
    - ); -}; - -export default EmploymentDetails; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/EmploymentDetailsState.tsx b/app/javascript/src/components/Team/Details/EmploymentDetails/EmploymentDetailsState.tsx deleted file mode 100644 index 6627e53426..0000000000 --- a/app/javascript/src/components/Team/Details/EmploymentDetails/EmploymentDetailsState.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export const EmploymentDetailsState = { - current_employment: { - employee_id: "", - email: "", - employment_type: "", - designation: "", - joined_at: "", - resigned_at: "", - }, - previous_employments: [ - { - company_name: "", - role: "", - id: "", - }, - ], -}; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/StaticPage.tsx b/app/javascript/src/components/Team/Details/EmploymentDetails/StaticPage.tsx deleted file mode 100644 index c2862aeda4..0000000000 --- a/app/javascript/src/components/Team/Details/EmploymentDetails/StaticPage.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from "react"; - -import { ProjectsIcon } from "miruIcons"; - -const StaticPage = ({ employmentDetails }) => ( -
    -
    -
    - - - Current
    Employment -
    -
    -
    -
    -
    - - Employee ID - -

    - {employmentDetails.current_employment.employee_id} -

    -
    -
    - - Designation - -

    - {employmentDetails.current_employment.designation} -

    -
    -
    -
    -
    - - Email ID (Official) - -

    - {employmentDetails.current_employment.email} -

    -
    -
    - - Employee Type - -

    - {employmentDetails.current_employment.employment_type} -

    -
    -
    -
    -
    - - Date of Joining - -

    - {employmentDetails.current_employment.joined_at} -

    -
    -
    - - Date of Resignation - -

    - {employmentDetails.current_employment.resigned_at} -

    -
    -
    -
    -
    -
    -
    - - - Previous
    Employment -
    -
    -
    - {employmentDetails?.previous_employments[0]?.company_name ? ( - employmentDetails.previous_employments.map((previous, index) => ( -
    -
    - - Company - -

    - {previous.company_name} -

    -
    -
    - Role -

    {previous.role}

    -
    -
    - )) - ) : ( -
    No previous employments found
    - )} -
    -
    -
    -); -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/EmploymentDetails/index.tsx b/app/javascript/src/components/Team/Details/EmploymentDetails/index.tsx deleted file mode 100644 index 282f979a5a..0000000000 --- a/app/javascript/src/components/Team/Details/EmploymentDetails/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { Fragment, useEffect, useState } from "react"; - -import { useParams, useNavigate } from "react-router-dom"; - -import teamsApi from "apis/teams"; -import Loader from "common/Loader/index"; -import { useTeamDetails } from "context/TeamDetailsContext"; -import { employmentMapper } from "mapper/teams.mapper"; - -import StaticPage from "./StaticPage"; - -const EmploymentDetails = () => { - const { - updateDetails, - details: { employmentDetails }, - } = useTeamDetails(); - const { memberId } = useParams(); - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - - const getDetails = async () => { - const res1: any = await teamsApi.getEmploymentDetails(memberId); - const res: any = await teamsApi.getPreviousEmployments(memberId); - const employmentData = employmentMapper( - res1.data.employment, - res.data.previous_employments - ); - updateDetails("employment", employmentData); - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - getDetails(); - }, []); - - return ( - -
    -

    Employment Details

    - -
    - {isLoading ? ( - - ) : ( - - )} -
    - ); -}; -export default EmploymentDetails; diff --git a/app/javascript/src/components/Team/Details/Layout/Header.tsx b/app/javascript/src/components/Team/Details/Layout/Header.tsx deleted file mode 100644 index ba8b4a393c..0000000000 --- a/app/javascript/src/components/Team/Details/Layout/Header.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; - -import { ArrowLeftIcon } from "miruIcons"; -import { useNavigate } from "react-router-dom"; - -import { useTeamDetails } from "context/TeamDetailsContext"; - -const Header = () => { - const navigate = useNavigate(); - const { - details: { personalDetails }, - } = useTeamDetails(); - - return ( -
    -
    - -

    - {`${personalDetails.first_name} ${personalDetails.last_name}`} -

    -
    -
    - ); -}; -export default Header; diff --git a/app/javascript/src/components/Team/Details/Layout/MobileNav.tsx b/app/javascript/src/components/Team/Details/Layout/MobileNav.tsx deleted file mode 100644 index cff6cc2a1b..0000000000 --- a/app/javascript/src/components/Team/Details/Layout/MobileNav.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import React from "react"; - -import { UserIcon, ProjectsIcon, MobileIcon } from "miruIcons"; -import { useParams } from "react-router-dom"; - -import withLayout from "common/Mobile/HOC/withLayout"; - -import { TeamUrl } from "./TeamUrl"; -import { UserInformation } from "./UserInformation"; - -const getTeamUrls = memberId => [ - { - url: `/team/${memberId}/details`, - text: "PERSONAL DETAILS", - icon: , - }, - { - url: `/team/${memberId}/employment`, - text: "EMPLOYMENT DETAILS", - icon: , - }, - { - url: "/settings/devices", - text: "ALLOCATED DEVICES", - icon: , - }, -]; - -const MobileNav = () => { - const { memberId } = useParams(); - const urlList = getTeamUrls(memberId); - - const mobileView = () => ( -
    - - -
    - ); - - const DisplayView = withLayout(mobileView, true, true); - - return ; -}; - -export default MobileNav; diff --git a/app/javascript/src/components/Team/Details/Layout/SideNav.tsx b/app/javascript/src/components/Team/Details/Layout/SideNav.tsx deleted file mode 100644 index 2623f4e598..0000000000 --- a/app/javascript/src/components/Team/Details/Layout/SideNav.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, { useEffect, useState } from "react"; - -import { DeleteIcon, EditIcon, ImageIcon, UserAvatarSVG } from "miruIcons"; -import { NavLink, useParams } from "react-router-dom"; -import { MoreOptions, Toastr, Tooltip } from "StyledComponents"; - -import teamApi from "apis/team"; -import teamsApi from "apis/teams"; -import { useTeamDetails } from "context/TeamDetailsContext"; - -const getActiveClassName = isActive => { - if (isActive) { - return "pl-4 py-5 border-l-8 border-miru-han-purple-600 bg-miru-gray-200 text-miru-han-purple-600 block"; - } - - return "pl-6 py-5 border-b-1 border-miru-gray-400 block"; -}; - -const getTeamUrls = memberId => [ - { - url: `/team/${memberId}`, - text: "PERSONAL DETAILS", - }, - { - url: `/team/${memberId}/employment`, - text: "EMPLOYMENT DETAILS", - }, - //Todo: Uncomment while API integration - // { - // url: `/team/${memberId}/compensation`, - // text: "COMPENSATION", - // }, -]; - -const UserInformation = ({ memberId }) => { - const { - details: { - personalDetails: { first_name, last_name }, - }, - } = useTeamDetails(); - - const [showProfileOptions, setShowProfileOptions] = useState(false); - const [imageUrl, setImageUrl] = useState(null); - - const getAvatar = async () => { - try { - const responseData = await teamsApi.get(memberId); - setImageUrl(responseData.data.avatar_url); - } catch { - Toastr.error("Error in getting Profile Image"); - } - }; - - useEffect(() => { - getAvatar(); - }, []); - - const validateFileSize = file => { - const sizeInKB = file.size / 1024; - if (sizeInKB > 100) { - throw new Error("Image size needs to be less than 100 KB"); - } - }; - - const createFormData = file => { - const formData = new FormData(); - formData.append("user[avatar]", file); - - return formData; - }; - - const handleProfileImageChange = async e => { - try { - setShowProfileOptions(false); - const file = e.target.files[0]; - validateFileSize(file); - setImageUrl(URL.createObjectURL(file)); - const payload = createFormData(file); - await teamApi.updateTeamMemberAvatar(memberId, payload); - } catch (error) { - Toastr.error(error.message); - } - }; - - const handleDeleteProfileImage = async () => { - try { - setShowProfileOptions(false); - await teamApi.destroyTeamMemberAvatar(memberId); - setImageUrl(null); - Toastr.success("Image deleted successfully"); - } catch { - Toastr.success("Error in deleting Profile Image"); - } - }; - - return ( -
    -
    -
    -
    -
    - - -
    -
    -
    - {showProfileOptions && ( - -
  • - - - {imageUrl && ( -
  • - - Delete -
  • - )} - -
    - )} - -
    - - {`${first_name} ${last_name}`} - -
    -
    - -
    -
    -
    - ); -}; - -const TeamUrl = ({ urlList }) => ( -
    -
      - {urlList.map((item, index) => ( -
    • - getActiveClassName(isActive)} - to={item.url} - > - {item.text} - -
    • - ))} -
    -
    -); - -const SideNav = () => { - const { memberId } = useParams(); - const urlList = getTeamUrls(memberId); - - return ( -
    - - -
    - ); -}; - -export default SideNav; diff --git a/app/javascript/src/components/Team/Details/Layout/TeamUrl.tsx b/app/javascript/src/components/Team/Details/Layout/TeamUrl.tsx deleted file mode 100644 index 8c905aedcb..0000000000 --- a/app/javascript/src/components/Team/Details/Layout/TeamUrl.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; - -import { RightArrowIcon } from "miruIcons"; -import { NavLink } from "react-router-dom"; - -const getActiveClassName = isActive => { - if (isActive) { - return "pl-4 py-5 border-l-8 border-miru-han-purple-600 bg-miru-gray-200 text-miru-han-purple-600 block"; - } - - return "pl-6 py-5 border-b-1 border-miru-gray-400 block"; -}; - -export const TeamUrl = ({ urlList }) => ( -
    -
      - {urlList.map((item, index) => ( -
    • - getActiveClassName(isActive)} - to={item.url} - > -
      -
      - {item.icon} - {item.text} -
      -
      - -
      -
      -
      -
    • - ))} -
    -
    -); diff --git a/app/javascript/src/components/Team/Details/Layout/UserInformation.tsx b/app/javascript/src/components/Team/Details/Layout/UserInformation.tsx deleted file mode 100644 index d471a8f269..0000000000 --- a/app/javascript/src/components/Team/Details/Layout/UserInformation.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import React, { useEffect, useState } from "react"; - -import { UserAvatarSVG, DeleteIcon, ImageIcon, EditIcon } from "miruIcons"; -import { useParams } from "react-router-dom"; -import { MobileMoreOptions, Toastr, Tooltip } from "StyledComponents"; - -import teamApi from "apis/team"; -import teamsApi from "apis/teams"; -import { useTeamDetails } from "context/TeamDetailsContext"; -import { teamsMapper } from "mapper/teams.mapper"; - -export const UserInformation = () => { - const [showImageUpdateOptions, setShowImageUpdateOptions] = - useState(false); - const [userImageUrl, setUserImageUrl] = useState(null); - const { - details: { personalDetails }, - updateDetails, - } = useTeamDetails(); - const { memberId } = useParams(); - - const getDetails = async () => { - try { - const res: any = await teamsApi.get(memberId); - const addRes = await teamsApi.getAddress(memberId); - const teamsObj = teamsMapper(res.data, addRes.data.addresses[0]); - updateDetails("personal", teamsObj); - } catch { - Toastr.error("Something went wrong"); - } - }; - - const validateFileSize = file => { - const sizeInKB = file.size / 1024; - if (sizeInKB > 100) { - throw new Error("Image size needs to be less than 100 KB"); - } - }; - - const createFormData = file => { - const formData = new FormData(); - formData.append("user[avatar]", file); - - return formData; - }; - - const handleProfileImageChange = async e => { - try { - setShowImageUpdateOptions(false); - const file = e.target.files[0]; - validateFileSize(file); - setUserImageUrl(URL.createObjectURL(file)); - const payload = createFormData(file); - await teamApi.updateTeamMemberAvatar(memberId, payload); - } catch (error) { - Toastr.error(error.message); - } - }; - - const handleDeleteProfileImage = async () => { - setShowImageUpdateOptions(false); - await teamApi.destroyTeamMemberAvatar(memberId); - setUserImageUrl(null); - }; - - const getAvatar = async () => { - const responseData = await teamsApi.get(memberId); - setUserImageUrl(responseData.data.avatar_url); - }; - - useEffect(() => { - getAvatar(); - getDetails(); - }, []); - - return ( -
    -
    -
    - - -
    - {showImageUpdateOptions ? ( - -
  • - - -
  • - {userImageUrl && ( -
  • -
    - -
    -

    - Delete -

    -
  • - )} -
    - ) : null} -
    - -
    - - {`${personalDetails.first_name} ${personalDetails.last_name}`} - -
    -
    - -
    -
    -
    - ); -}; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/MobileEditPage.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/Edit/MobileEditPage.tsx deleted file mode 100644 index 323eb3143a..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/MobileEditPage.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import React from "react"; - -import dayjs from "dayjs"; -import { - CalendarIcon, - GlobeIcon, - InfoIcon, - MapPinIcon, - PhoneIcon, -} from "miruIcons"; -import PhoneInput from "react-phone-number-input"; -import flags from "react-phone-number-input/flags"; -import "react-phone-number-input/style.css"; - -import CustomDatePicker from "common/CustomDatePicker"; -import { CustomInputText } from "common/CustomInputText"; -import { CustomReactSelect } from "common/CustomReactSelect"; -import { Divider } from "common/Divider"; -import { ErrorSpan } from "common/ErrorSpan"; - -const inputClass = - "form__input block w-full appearance-none bg-white p-4 text-sm h-12 focus-within:border-miru-han-purple-1000"; - -const labelClass = - "absolute top-0.5 left-1 h-6 z-1 origin-0 bg-white p-2 text-sm font-medium duration-300"; - -const MobileEditDetails = ({ - addrType, - addressOptions, - countries, - handleOnChangeAddrType, - handleOnChangeCountry, - handleCancelDetails, - handleUpdateDetails, - updateBasicDetails, - personalDetails, - showDatePicker, - setShowDatePicker, - handleDatePicker, - errDetails, - handlePhoneNumberChange, - wrapperRef, - dateFormat, -}) => ( -
    -
    - - Basic Details - -
    -
    - { - updateBasicDetails(e.target.value, "first_name", false, ""); - }} - /> - {errDetails.first_name_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "last_name", false, ""); - }} - /> - {errDetails.last_name_err && ( - - )} -
    -
    -
    -
    -
    - setShowDatePicker({ visibility: !showDatePicker.visibility }) - } - > - { - updateBasicDetails(e.target.value, "date_of_birth", false); - }} - /> - -
    - {showDatePicker.visibility && ( - handleDatePicker(e, true)} - date={ - personalDetails.date_of_birth - ? personalDetails.date_of_birth - : dayjs() - } - /> - )} -
    -
    -
    - -
    - - Contact - Details - -
    -
    -
    - - -
    -
    -
    - { - updateBasicDetails(e.target.value, "email_id", false); - }} - /> - {errDetails.email_id_err && ( - - )} -
    -
    -
    - -
    - - Address - -
    -
    - -
    -
    - { - updateBasicDetails(e.target.value, "address_line_1", true); - }} - /> - {errDetails.address_line_1_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "address_line_2", true); - }} - /> -
    -
    -
    - handleOnChangeCountry(value)} - isErr={!!errDetails.country_err} - label="Country" - name="current_country_select" - options={countries} - value={{ - label: personalDetails.addresses.country, - value: personalDetails.addresses.country, - }} - /> - {errDetails.country_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "state", true); - }} - /> - {errDetails.state_err && ( - - )} -
    -
    -
    -
    - { - updateBasicDetails(e.target.value, "city", true); - }} - /> - {errDetails.city_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "pin", true); - }} - /> - {errDetails.pin_err && ( - - )} -
    -
    -
    -
    - -
    - - - Social Profiles - -
    -
    - { - updateBasicDetails(e.target.value, "linkedin", false, ""); - }} - /> -
    -
    - { - updateBasicDetails(e.target.value, "github", false, ""); - }} - /> -
    -
    -
    - -
    - - -
    -
    -); - -export default MobileEditDetails; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/StaticPage.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/Edit/StaticPage.tsx deleted file mode 100644 index 2678781432..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/StaticPage.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import React from "react"; - -import dayjs from "dayjs"; -import { - GlobeIcon, - CalendarIcon, - PhoneIcon, - MapPinIcon, - InfoIcon, -} from "miruIcons"; -import PhoneInput from "react-phone-number-input"; -import flags from "react-phone-number-input/flags"; -import "react-phone-number-input/style.css"; - -import CustomDatePicker from "common/CustomDatePicker"; -import { CustomInputText } from "common/CustomInputText"; -import { CustomReactSelect } from "common/CustomReactSelect"; -import { ErrorSpan } from "common/ErrorSpan"; - -const inputClass = - "form__input block w-full appearance-none bg-white p-4 text-base h-12 focus-within:border-miru-han-purple-1000"; - -const labelClass = - "absolute top-0.5 left-1 h-6 z-1 origin-0 bg-white p-2 text-base font-medium duration-300"; - -const StaticPage = ({ - addressOptions, - addrType, - countries, - handleOnChangeAddrType, - handleOnChangeCountry, - updateBasicDetails, - personalDetails, - showDatePicker, - setShowDatePicker, - handleDatePicker, - errDetails, - handlePhoneNumberChange, - wrapperRef, - dateFormat, -}) => ( -
    -
    -
    - - Basic - Details - -
    -
    -
    -
    - { - updateBasicDetails(e.target.value, "first_name", false, ""); - }} - /> - {errDetails.first_name_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "last_name", false, ""); - }} - /> - {errDetails.last_name_err && ( - - )} -
    -
    -
    -
    - setShowDatePicker({ visibility: !showDatePicker.visibility }) - } - > - { - updateBasicDetails(e.target.value, "date_of_birth", false); - }} - /> - -
    - {showDatePicker.visibility && ( - handleDatePicker(e, true)} - date={ - personalDetails.date_of_birth - ? personalDetails.date_of_birth - : dayjs() - } - /> - )} -
    -
    -
    -
    -
    - - - Contact Details - -
    -
    -
    -
    -
    - - -
    -
    -
    - { - updateBasicDetails(e.target.value, "email_id", false); - }} - /> - {errDetails.email_id_err && ( - - )} -
    -
    -
    -
    -
    -
    - - - Address - -
    -
    -
    -
    - -
    -
    -
    - { - updateBasicDetails(e.target.value, "address_line_1", true); - }} - /> - {errDetails.address_line_1_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "address_line_2", true); - }} - /> -
    -
    -
    - handleOnChangeCountry(value)} - isErr={!!errDetails.country_err} - label="Country" - name="current_country_select" - options={countries} - value={{ - label: personalDetails.addresses.country, - value: personalDetails.addresses.country, - }} - /> - {errDetails.country_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "state", true); - }} - /> - {errDetails.state_err && ( - - )} -
    -
    -
    -
    - { - updateBasicDetails(e.target.value, "city", true); - }} - /> - {errDetails.city_err && ( - - )} -
    -
    - { - updateBasicDetails(e.target.value, "pin", true); - }} - /> - {errDetails.pin_err && ( - - )} -
    -
    -
    -
    -
    -
    - - - Social Profiles - -
    -
    -
    -
    - { - updateBasicDetails(e.target.value, "linkedin", false); - }} - /> -
    -
    - { - updateBasicDetails(e.target.value, "github", false); - }} - /> -
    -
    -
    -
    -
    -); - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/index.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/Edit/index.tsx deleted file mode 100644 index 2d180199e5..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/index.tsx +++ /dev/null @@ -1,303 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { Fragment, useEffect, useRef, useState } from "react"; - -import { Country } from "country-state-city"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import { useOutsideClick } from "helpers"; -import { useNavigate, useParams } from "react-router-dom"; -import * as Yup from "yup"; - -import teamsApi from "apis/teams"; -import Loader from "common/Loader/index"; -import { MobileDetailsHeader } from "common/Mobile/MobileDetailsHeader"; -import { useTeamDetails } from "context/TeamDetailsContext"; -import { useUserContext } from "context/UserContext"; -import { teamsMapper } from "mapper/teams.mapper"; - -import MobileEditPage from "./MobileEditPage"; -import StaticPage from "./StaticPage"; -import { userSchema } from "./validationSchema"; - -dayjs.extend(utc); - -const addressOptions = [ - { label: "Current", value: "current" }, - { label: "Permanent", value: "permanent" }, -]; - -const schema = Yup.object().shape(userSchema); - -const EmploymentDetails = () => { - const initialErrState = { - first_name_err: "", - last_name_err: "", - address_line_1_err: "", - country_err: "", - state_err: "", - city_err: "", - email_id_err: "", - pin_err: "", - }; - - const { memberId } = useParams(); - const { - updateDetails, - details: { personalDetails }, - } = useTeamDetails(); - const navigate = useNavigate(); - const { isDesktop } = useUserContext(); - const wrapperRef = useRef(null); - - const [addrType, setAddrType] = useState({ label: "", value: "" }); - const [showDatePicker, setShowDatePicker] = useState({ visibility: false }); - const [countries, setCountries] = useState([]); - const [errDetails, setErrDetails] = useState(initialErrState); - const [isLoading, setIsLoading] = useState(false); - const [addrId, setAddrId] = useState(); - - useOutsideClick(wrapperRef, () => setShowDatePicker({ visibility: false })); - - const assignCountries = async allCountries => { - const countryData = await allCountries.map(country => ({ - value: country.name, - label: country.name, - code: country.isoCode, - })); - setCountries(countryData); - }; - - const getDetails = async () => { - const res: any = await teamsApi.get(memberId); - const addRes = await teamsApi.getAddress(memberId); - const teamsObj = teamsMapper(res.data, addRes.data.addresses[0]); - updateDetails("personal", teamsObj); - if (teamsObj.addresses?.address_type?.length > 0) { - setAddrType( - addressOptions.find( - item => item.value === teamsObj.addresses.address_type - ) - ); - } - setAddrId(addRes.data.addresses[0]?.id); - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - const allCountries = Country.getAllCountries(); - assignCountries(allCountries); - getDetails(); - }, []); - - const handleOnChangeCountry = selectCountry => { - updateDetails("personal", { - ...personalDetails, - ...{ - addresses: { - ...personalDetails.addresses, - ...{ country: selectCountry.value, state: "", city: "" }, - }, - }, - }); - }; - - const handleOnChangeAddrType = addreType => { - setAddrType(addreType); - updateDetails("personal", { - ...personalDetails, - ...{ - addresses: { - ...personalDetails.addresses, - ...{ address_type: addreType.value }, - }, - }, - }); - }; - - const updateBasicDetails = (value, type, isAddress = false) => { - if (isAddress) { - updateDetails("personal", { - ...personalDetails, - ...{ - addresses: { ...personalDetails.addresses, ...{ [type]: value } }, - }, - }); - } else { - updateDetails("personal", { - ...personalDetails, - ...{ [type]: value }, - }); - } - }; - - const handleDatePicker = date => { - setShowDatePicker({ visibility: !showDatePicker.visibility }); - const formattedDate = dayjs(date, personalDetails.date_format).format( - personalDetails.date_format - ); - - updateDetails("personal", { - ...personalDetails, - ...{ date_of_birth: formattedDate }, - }); - }; - - const handleUpdateDetails = async () => { - try { - await schema.validate( - { - ...personalDetails, - ...{ - is_email: personalDetails.email_id - ? personalDetails.email_id.length > 0 - : false, - }, - }, - { abortEarly: false } - ); - - await teamsApi.updateUser(memberId, { - user: { - first_name: personalDetails.first_name, - last_name: personalDetails.last_name, - date_of_birth: personalDetails.date_of_birth - ? dayjs - .utc(personalDetails.date_of_birth, personalDetails.date_format) - .toISOString() - : null, - phone: personalDetails.phone_number || "", - personal_email_id: personalDetails.email_id, - social_accounts: { - linkedin_url: personalDetails.linkedin, - github_url: personalDetails.github, - }, - }, - }); - - const payload = { - address: { - address_line_1: personalDetails.addresses.address_line_1, - address_line_2: personalDetails.addresses.address_line_2, - address_type: personalDetails.addresses.address_type, - city: personalDetails.addresses.city, - state: personalDetails.addresses.state, - country: personalDetails.addresses.country, - pin: personalDetails.addresses.pin, - }, - }; - if (addrId) { - await teamsApi.updateAddress(memberId, addrId, { - address: { ...personalDetails.addresses }, - }); - } else { - await teamsApi.createAddress(memberId, payload); - } - - setErrDetails(initialErrState); - navigate(`/team/${memberId}`, { replace: true }); - } catch (err) { - setIsLoading(false); - const errObj = initialErrState; - if (err.inner) { - err.inner.map(item => { - if (item.path.includes("addresses")) { - errObj[`${item.path.split(".").pop()}_err`] = item.message; - } else { - errObj[`${item.path}_err`] = item.message; - } - }); - setErrDetails(errObj); - } - } - }; - - const handlePhoneNumberChange = phoneNumber => { - updateBasicDetails(phoneNumber, "phone_number", false); - }; - - const handleCancelDetails = () => { - setIsLoading(true); - navigate(`/team/${memberId}`, { replace: true }); - }; - - return ( - - {isDesktop && ( - -
    -

    Personal Details

    -
    - - -
    -
    - {isLoading ? ( - - ) : ( - - )} -
    - )} - {!isDesktop && ( - - - {isLoading ? ( -
    - -
    - ) : ( - - )} -
    - )} -
    - ); -}; - -export default EmploymentDetails; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/validationSchema.ts b/app/javascript/src/components/Team/Details/PersonalDetails/Edit/validationSchema.ts deleted file mode 100644 index 39c3640535..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/Edit/validationSchema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Yup from "yup"; - -export const userSchema = { - first_name: Yup.string() - .required("Please enter first name") - .max(20, "Maximum 20 characters are allowed"), - last_name: Yup.string() - .required("Please enter last name") - .max(20, "Maximum 20 characters are allowed"), - addresses: Yup.object().shape({ - address_line_1: Yup.string().required("Please enter address line 1"), - country: Yup.string().required("Please enter country"), - state: Yup.string().required("Please enter state"), - city: Yup.string().required("Please enter city"), - pin: Yup.string().required("Please enter zipcode"), - }), - is_email: Yup.boolean(), - email_id: Yup.string() - .nullable() - .when("is_email", { - is: true, - then: Yup.string().email("Please enter valid email"), - }), -}; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/MobilePersonalDetails.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/MobilePersonalDetails.tsx deleted file mode 100644 index 107f7f70a5..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/MobilePersonalDetails.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from "react"; - -import dayjs from "dayjs"; -import { GlobeIcon, InfoIcon, MapPinIcon, PhoneIcon } from "miruIcons"; - -import { Divider } from "common/Divider"; -import { InfoDescription } from "common/Mobile/InfoDescription"; - -const MobilePersonalDetails = ({ - personalDetails: { - first_name, - last_name, - date_of_birth, - phone_number, - email_id, - addresses, - linkedin, - github, - date_format, - }, -}) => ( -
    -
    - - Basic Details - -
    -
    - -
    -
    - -
    -
    -
    - -
    - - Contact - Details - -
    -
    - -
    -
    - -
    -
    -
    - -
    - - Address - -
    -
    - {addresses && ( - - )} -
    -
    -
    - -
    - - - Social Profiles - -
    -
    - -
    -
    - -
    -
    -
    -
    -); - -export default MobilePersonalDetails; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/PersonalDetailsState.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/PersonalDetailsState.tsx deleted file mode 100644 index 13cd48ed6a..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/PersonalDetailsState.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export const PersonalDetailsState = { - first_name: "", - last_name: "", - date_of_birth: "", - phone_number: "", - email_id: "", - addresses: { - id: "", - address_type: "", - address_line_1: "", - address_line_2: "", - country: "", - state: "", - city: "", - pin: "", - }, - linkedin: "", - github: "", - date_format: "", -}; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/StaticPage.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/StaticPage.tsx deleted file mode 100644 index bb41d5b976..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/StaticPage.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; - -import dayjs from "dayjs"; -import customParseFormat from "dayjs/plugin/customParseFormat"; -import { GlobeIcon, InfoIcon, MapPinIcon, PhoneIcon } from "miruIcons"; - -dayjs.extend(customParseFormat); - -const StaticPage = ({ personalDetails }) => ( -
    -
    -
    - - Basic - Details - -
    -
    -
    -
    - Name -

    - {personalDetails.first_name} {personalDetails.last_name} -

    -
    -
    - - Date of Birth - -

    - {personalDetails.date_of_birth && - dayjs( - personalDetails.date_of_birth, - personalDetails.date_format - ).format(personalDetails.date_format)} -

    -
    -
    -
    -
    -
    -
    - - - Contact Details - -
    -
    -
    -
    - - Phone Number - -

    - {personalDetails.phone_number} -

    -
    -
    - - Email ID (Personal) - -

    - {personalDetails.email_id} -

    -
    -
    -
    -
    -
    -
    - - - Address - -
    -
    -
    -
    - Address -

    - {personalDetails.addresses && ( - <> - {personalDetails.addresses.address_line_1}, - {personalDetails.addresses.address_line_2} - {personalDetails.addresses.city}, - {personalDetails.addresses.state}, - {personalDetails.addresses.country} - - {personalDetails.addresses.pin} - - )} -

    -
    -
    -
    -
    -
    -
    - - - Social Profiles - -
    -
    -
    -
    - LinkedIn -

    - {personalDetails.linkedin} -

    -
    -
    - Github -

    - {personalDetails.github} -

    -
    -
    -
    -
    -
    -); - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/PersonalDetails/index.tsx b/app/javascript/src/components/Team/Details/PersonalDetails/index.tsx deleted file mode 100644 index ba5f00db75..0000000000 --- a/app/javascript/src/components/Team/Details/PersonalDetails/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { Fragment, useEffect, useState } from "react"; - -import { Outlet, useParams, useNavigate } from "react-router-dom"; - -import teamsApi from "apis/teams"; -import Loader from "common/Loader/index"; -import { MobileEditHeader } from "common/Mobile/MobileEditHeader"; -import { useTeamDetails } from "context/TeamDetailsContext"; -import { useUserContext } from "context/UserContext"; -import { teamsMapper } from "mapper/teams.mapper"; - -import MobilePersonalDetails from "./MobilePersonalDetails"; -import StaticPage from "./StaticPage"; - -const PersonalDetails = () => { - const { memberId } = useParams(); - const { isDesktop } = useUserContext(); - const { - updateDetails, - details: { personalDetails }, - } = useTeamDetails(); - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - - const getDetails = async () => { - const res: any = await teamsApi.get(memberId); - const addRes = await teamsApi.getAddress(memberId); - const teamsObj = teamsMapper(res.data, addRes.data.addresses[0]); - updateDetails("personal", teamsObj); - setIsLoading(false); - }; - - useEffect(() => { - setIsLoading(true); - getDetails(); - }, []); - - return ( - - {isDesktop && ( - -
    -

    Personal Details

    - -
    - {isLoading ? ( - - ) : ( - - )} -
    - )} - {!isDesktop && ( - - - {isLoading ? ( - - ) : ( - - )} - - )} - -
    - ); -}; - -export default PersonalDetails; diff --git a/app/javascript/src/components/Team/Details/ReimburstmentDetails/StaticPage.tsx b/app/javascript/src/components/Team/Details/ReimburstmentDetails/StaticPage.tsx deleted file mode 100644 index dd1144e016..0000000000 --- a/app/javascript/src/components/Team/Details/ReimburstmentDetails/StaticPage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; - -import { PenIcon, DeleteIcon } from "miruIcons"; - -const StaticPage = () => ( -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    DATEDESCRIPTIONSTORE/MODEAMOUNT -
    22.04.2022 - Medium annual team subscription - Online/Netbanking₹7,500 - - -
    22.04.2022 - Medium annual team subscription - Online/Netbanking₹7,500 - - -
    -
    -); - -export default StaticPage; diff --git a/app/javascript/src/components/Team/Details/ReimburstmentDetails/index.tsx b/app/javascript/src/components/Team/Details/ReimburstmentDetails/index.tsx deleted file mode 100644 index d8bb67d259..0000000000 --- a/app/javascript/src/components/Team/Details/ReimburstmentDetails/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Fragment } from "react"; - -import StaticPage from "./StaticPage"; - -const ReimburstmentDetails = () => ( - -
    -

    Reimburstment Details

    -
    - -
    -); - -export default ReimburstmentDetails; diff --git a/app/javascript/src/components/Team/Details/index.tsx b/app/javascript/src/components/Team/Details/index.tsx deleted file mode 100644 index 2b92994079..0000000000 --- a/app/javascript/src/components/Team/Details/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Fragment, useState } from "react"; - -import { TeamDetailsContext } from "context/TeamDetailsContext"; -import { useUserContext } from "context/UserContext"; - -import { CompensationDetailsState } from "./CompensationDetails/CompensationDetailsState"; -import { EmploymentDetailsState } from "./EmploymentDetails/EmploymentDetailsState"; -import Header from "./Layout/Header"; -import OutletWrapper from "./Layout/OutletWrapper"; -import SideNav from "./Layout/SideNav"; -import { PersonalDetailsState } from "./PersonalDetails/PersonalDetailsState"; - -const TeamDetails = () => { - const [details, setDetails] = useState({ - personalDetails: PersonalDetailsState, - employmentDetails: EmploymentDetailsState, - documentDetails: {}, - deviceDetails: {}, - compensationDetails: CompensationDetailsState, - reimburstmentDetails: {}, - }); - const { isDesktop } = useUserContext(); - const updateDetails = (key, payload) => { - setDetails({ ...details, [`${key}Details`]: payload }); - }; - - return ( - - {isDesktop && ( - -
    -
    -
    - -
    -
    - -
    -
    - - )} - {!isDesktop && } - - ); -}; - -export default TeamDetails; diff --git a/app/javascript/src/components/Team/List/Table/TableRow.tsx b/app/javascript/src/components/Team/List/Table/TableRow.tsx index f857cb62c0..76c6ad8f91 100644 --- a/app/javascript/src/components/Team/List/Table/TableRow.tsx +++ b/app/javascript/src/components/Team/List/Table/TableRow.tsx @@ -48,7 +48,7 @@ const TableRow = ({ item }) => { const handleRowClick = () => { if (!status) return; - navigate(`/team/${id}`, { replace: true }); + navigate(`/team/${id}/profile`, { replace: true }); }; return ( diff --git a/app/javascript/src/components/Team/RouteConfig.tsx b/app/javascript/src/components/Team/RouteConfig.tsx deleted file mode 100644 index 1fe95241f1..0000000000 --- a/app/javascript/src/components/Team/RouteConfig.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; - -import { Route, Routes } from "react-router-dom"; - -import Details from "./Details"; -import CompensationDetails from "./Details/CompensationDetails"; -import CompensationEdit from "./Details/CompensationDetails/Edit"; -import EmploymentDetails from "./Details/EmploymentDetails"; -import EmploymentEdit from "./Details/EmploymentDetails/Edit"; -import MobileNav from "./Details/Layout/MobileNav"; -import PersonalDetails from "./Details/PersonalDetails"; -import PersonalEdit from "./Details/PersonalDetails/Edit"; - -const RouteConfig = () => ( - - } path=":memberId"> - } /> - } path="edit" /> - } path="options" /> - } path="details" /> - } path="employment" /> - } path="employment_edit" /> - } path="compensation" /> - } path="compensation_edit" /> - - -); - -export default RouteConfig; diff --git a/app/javascript/src/constants/routes.ts b/app/javascript/src/constants/routes.ts index ced02d73a3..15f30499e1 100644 --- a/app/javascript/src/constants/routes.ts +++ b/app/javascript/src/constants/routes.ts @@ -10,6 +10,7 @@ import InvoiceEmail from "components/InvoiceEmail"; import InvoicesRouteConfig from "components/Invoices/InvoicesRouteConfig"; import LeaveManagement from "components/LeaveManagement"; import Success from "components/payments/Success"; +import ProfileRouteConfig from "components/Profile/Layout/RouteConfig"; import Projects from "components/Projects"; import AccountsAgingReport from "components/Reports/AccountsAgingReport"; import InvalidLink from "components/Team/List/InvalidLink"; @@ -19,7 +20,6 @@ import { Roles, Paths } from "constants/index"; import Clients from "../components/Clients"; import ClientDetails from "../components/Clients/Details"; import Payments from "../components/payments"; -import ProfileLayout from "../components/Profile/Layout"; import ProjectDetails from "../components/Projects/Details"; import ReportList from "../components/Reports/List"; import OutstandingInvoiceReport from "../components/Reports/OutstandingInvoiceReport"; @@ -27,7 +27,6 @@ import RevenueByClientReport from "../components/Reports/RevenueByClientReport"; import TimeEntryReports from "../components/Reports/TimeEntryReport"; import TotalHoursReport from "../components/Reports/totalHoursLogged"; import PlanSelection from "../components/Subscriptions/PlanSelection"; -import RouteConfig from "../components/Team/RouteConfig"; import TimesheetEntries from "../components/TimesheetEntries"; const ClientsRoutes = [ @@ -70,13 +69,13 @@ const LeaveManagementRoutes = [ { path: "*", Component: ErrorPage }, ]; -const TeamRoutes = [{ path: "*", Component: RouteConfig }]; +const TeamRoutes = [{ path: "*", Component: ProfileRouteConfig }]; const TeamsRoutes = [{ path: "*", Component: TeamsRouteConfig }]; const InvoiceRoutes = [{ path: "*", Component: InvoicesRouteConfig }]; -const SettingsRoutes = [{ path: "*", Component: ProfileLayout }]; +const SettingsRoutes = [{ path: "*", Component: ProfileRouteConfig }]; const ExpenseRoutes = [ { path: "", Component: Expenses }, diff --git a/app/javascript/src/context/Profile/ProfileContext.tsx b/app/javascript/src/context/Profile/ProfileContext.tsx new file mode 100644 index 0000000000..10cf333c1b --- /dev/null +++ b/app/javascript/src/context/Profile/ProfileContext.tsx @@ -0,0 +1,23 @@ +import { createContext, useContext } from "react"; + +import { CompensationDetailsState } from "components/Profile/Context/CompensationDetailsState"; +import { EmploymentDetailsState } from "components/Profile/Context/EmploymentDetailsState"; +import { PersonalDetailsState } from "components/Profile/Context/PersonalDetailsState"; +// Context Creation + +export const ProfileContext = createContext({ + personalDetails: PersonalDetailsState, + employmentDetails: EmploymentDetailsState, + documentDetails: {}, + deviceDetails: {}, + compensationDetails: CompensationDetailsState, + reimburstmentDetails: {}, + updateDetails: (key, payload) => {}, //eslint-disable-line + isCalledFromSettings: false, + setIsCalledFromSettings: val => {}, //eslint-disable-line + isCalledFromTeam: false, + setIsCalledFromTeam: val => {}, //eslint-disable-line +}); + +// Custom Hooks +export const useProfileContext = () => useContext(ProfileContext); diff --git a/app/javascript/src/context/TeamDetailsContext.tsx b/app/javascript/src/context/TeamDetailsContext.tsx deleted file mode 100644 index d9467d3537..0000000000 --- a/app/javascript/src/context/TeamDetailsContext.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext, useContext } from "react"; - -import { CompensationDetailsState } from "components/Team/Details/CompensationDetails/CompensationDetailsState"; -import { EmploymentDetailsState } from "components/Team/Details/EmploymentDetails/EmploymentDetailsState"; -import { PersonalDetailsState } from "components/Team/Details/PersonalDetails/PersonalDetailsState"; -// Context Creation - -export const TeamDetailsContext = createContext({ - details: { - personalDetails: PersonalDetailsState, - employmentDetails: EmploymentDetailsState, - documentDetails: {}, - deviceDetails: {}, - compensationDetails: CompensationDetailsState, - reimburstmentDetails: {}, - }, - updateDetails: (key, payload) => {}, //eslint-disable-line -}); - -// Custom Hooks -export const useTeamDetails = () => useContext(TeamDetailsContext); diff --git a/app/javascript/src/mapper/teams.mapper.ts b/app/javascript/src/mapper/teams.mapper.ts index 015f06d44c..bf1fbabf33 100644 --- a/app/javascript/src/mapper/teams.mapper.ts +++ b/app/javascript/src/mapper/teams.mapper.ts @@ -1,6 +1,7 @@ import dayjs from "dayjs"; export const teamsMapper = (user, address) => ({ + id: user.id, first_name: user.first_name, last_name: user.last_name, date_of_birth: diff --git a/app/models/company.rb b/app/models/company.rb index 4f8b528854..07eb91e3f6 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, dependent: :destroy resourcify diff --git a/app/models/invoice.rb b/app/models/invoice.rb index e502ee0655..e515ee5c86 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -75,6 +75,7 @@ class Invoice < ApplicationRecord before_validation :set_external_view_key, on: :create after_commit :refresh_invoice_index after_save :lock_timesheet_entries, if: :draft? + after_discard :unlock_timesheet_entries, if: :draft? validates :issue_date, :due_date, :invoice_number, presence: true validates :due_date, comparison: { greater_than_or_equal_to: :issue_date }, if: :not_waived @@ -202,4 +203,9 @@ def lock_timesheet_entries timesheet_entry_ids = invoice_line_items.pluck(:timesheet_entry_id) TimesheetEntry.where(id: timesheet_entry_ids).update!(locked: true) end + + def unlock_timesheet_entries + timesheet_entry_ids = invoice_line_items.pluck(:timesheet_entry_id) + TimesheetEntry.where(id: timesheet_entry_ids).update!(locked: false) + end end diff --git a/app/models/invoice_line_item.rb b/app/models/invoice_line_item.rb index b060e001b4..b4b8ba1f66 100644 --- a/app/models/invoice_line_item.rb +++ b/app/models/invoice_line_item.rb @@ -30,6 +30,8 @@ class InvoiceLineItem < ApplicationRecord belongs_to :invoice belongs_to :timesheet_entry, optional: true + before_destroy :unlock_timesheet_entry + validates :name, :date, :rate, :quantity, presence: true validates :rate, numericality: { greater_than_or_equal_to: 0 } validates :quantity, numericality: { greater_than_or_equal_to: 0 } @@ -67,4 +69,12 @@ def total_cost def formatted_date CompanyDateFormattingService.new(date, company: invoice.company).process end + + private + + def unlock_timesheet_entry + if invoice.draft? + timesheet_entry.update!(locked: false) + end + end end diff --git a/app/models/notification_preference.rb b/app/models/notification_preference.rb new file mode 100644 index 0000000000..6027ca309b --- /dev/null +++ b/app/models/notification_preference.rb @@ -0,0 +1,30 @@ +# 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 :notification_enabled, inclusion: { in: [true, false] } +end diff --git a/app/models/user.rb b/app/models/user.rb index f2b6b854fc..299e2d7771 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, dependent: :destroy rolify strict: true diff --git a/app/policies/profile_policy.rb b/app/policies/profile_policy.rb index 9c840768c2..6733733109 100644 --- a/app/policies/profile_policy.rb +++ b/app/policies/profile_policy.rb @@ -1,14 +1,6 @@ # frozen_string_literal: true class ProfilePolicy < ApplicationPolicy - def show? - user - end - - def remove_avatar? - user - end - def update? user end diff --git a/app/policies/team_members/avatar_policy.rb b/app/policies/team_members/avatar_policy.rb index 77ec917b9d..a3d6486806 100644 --- a/app/policies/team_members/avatar_policy.rb +++ b/app/policies/team_members/avatar_policy.rb @@ -17,6 +17,6 @@ def authorize_current_user return false end - user_owner_role? || user_admin_role? + has_owner_or_admin_role? || record_belongs_to_user? end end diff --git a/app/policies/team_members/detail_policy.rb b/app/policies/team_members/detail_policy.rb index 7251b69303..a01f35cab8 100644 --- a/app/policies/team_members/detail_policy.rb +++ b/app/policies/team_members/detail_policy.rb @@ -21,6 +21,6 @@ def authorize_current_user return false end - user_owner_role? || user_admin_role? + has_owner_or_admin_role? || record_belongs_to_user? end end 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/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 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/app/views/devise/shared/_footer.html.erb b/app/views/devise/shared/_footer.html.erb index b4745af509..461f460aa4 100644 --- a/app/views/devise/shared/_footer.html.erb +++ b/app/views/devise/shared/_footer.html.erb @@ -45,7 +45,7 @@ <%= image_tag "Instagram.svg", height: '16px', width: '16px', alt: 'Instagram logo' %> <%= image_tag "Twitter.svg", height: '16px', width: '16px', alt: 'Twitter logo' %> diff --git a/app/views/internal_api/v1/team_members/details/_detail.json.jbuilder b/app/views/internal_api/v1/team_members/details/_detail.json.jbuilder index 19c2ae45c5..dace39a008 100644 --- a/app/views/internal_api/v1/team_members/details/_detail.json.jbuilder +++ b/app/views/internal_api/v1/team_members/details/_detail.json.jbuilder @@ -1,5 +1,5 @@ # frozen_string_literal: true -json.extract! user, :first_name, :last_name, :date_of_birth, :phone, :personal_email_id, :social_accounts +json.extract! user, :id, :first_name, :last_name, :date_of_birth, :phone, :personal_email_id, :social_accounts json.date_format current_company.date_format json.avatar_url user.avatar_url diff --git a/app/views/mailers/client_payment_mailer/payment.html.erb b/app/views/mailers/client_payment_mailer/payment.html.erb index 411725e923..7072439f70 100644 --- a/app/views/mailers/client_payment_mailer/payment.html.erb +++ b/app/views/mailers/client_payment_mailer/payment.html.erb @@ -120,7 +120,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -218,7 +218,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/app/views/mailers/invoice_mailer/invoice.html.erb b/app/views/mailers/invoice_mailer/invoice.html.erb index 59af5c9fd6..98ac2e0ebe 100644 --- a/app/views/mailers/invoice_mailer/invoice.html.erb +++ b/app/views/mailers/invoice_mailer/invoice.html.erb @@ -127,7 +127,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -239,7 +239,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/app/views/mailers/payment_mailer/client_payment_mailer.html.erb b/app/views/mailers/payment_mailer/client_payment_mailer.html.erb index 1e4ce178a7..a560323dac 100644 --- a/app/views/mailers/payment_mailer/client_payment_mailer.html.erb +++ b/app/views/mailers/payment_mailer/client_payment_mailer.html.erb @@ -120,7 +120,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -218,7 +218,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/app/views/mailers/payment_mailer/payment.html.erb b/app/views/mailers/payment_mailer/payment.html.erb index b96b2086d4..6068afc698 100644 --- a/app/views/mailers/payment_mailer/payment.html.erb +++ b/app/views/mailers/payment_mailer/payment.html.erb @@ -91,7 +91,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -196,7 +196,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/app/views/mailers/send_payment_reminder_mailer/send_payment_reminder.html.erb b/app/views/mailers/send_payment_reminder_mailer/send_payment_reminder.html.erb index 3a4fc26b51..4f770a5e3c 100644 --- a/app/views/mailers/send_payment_reminder_mailer/send_payment_reminder.html.erb +++ b/app/views/mailers/send_payment_reminder_mailer/send_payment_reminder.html.erb @@ -158,7 +158,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -285,7 +285,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/app/views/mailers/send_reminder_mailer/send_reminder.html.erb b/app/views/mailers/send_reminder_mailer/send_reminder.html.erb index e059278916..57c462284d 100644 --- a/app/views/mailers/send_reminder_mailer/send_reminder.html.erb +++ b/app/views/mailers/send_reminder_mailer/send_reminder.html.erb @@ -126,7 +126,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px', style: 'padding-left: 10px' %> @@ -238,7 +238,7 @@ <%= image_tag attachments['Instagram.png'].url, height: '16px' ,width:'16px', style: 'margin: 0px 20px;' %> <%= image_tag attachments['Twitter.png'].url, height: '16px' ,width:'16px' %> diff --git a/config/application.rb b/config/application.rb index 56a9aec35f..45d52a6abb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,6 +44,7 @@ class Application < Rails::Application config.react.camelize_props = true # Use a real queuing backend for Active Job (and separate queues per environment). - config.active_job.queue_adapter = :sidekiq + config.active_job.queue_adapter = :solid_queue + config.mission_control.jobs.base_controller_class = "MissionControlController" end end diff --git a/config/environments/production.rb b/config/environments/production.rb index c2c0b2a67f..d8759cd46d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -70,7 +70,7 @@ # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque + config.active_job.queue_adapter = :solid_queue # config.active_job.queue_name_prefix = "miru_web_production" config.action_mailer.perform_caching = false @@ -101,5 +101,4 @@ host = ENV.fetch("APP_BASE_URL") config.action_mailer.default_url_options = { host: } config.action_mailer.asset_host = host - config.active_job.queue_adapter = :sidekiq end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb deleted file mode 100644 index f55d73c9e3..0000000000 --- a/config/initializers/sidekiq.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -Sidekiq.configure_client do |config| - config.redis = { - url: ENV["REDIS_URL"], network_timeout: 5, - ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } - } -end - -Sidekiq.configure_server do |config| - config.redis = { - url: ENV["REDIS_URL"], network_timeout: 5, - ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } - } -end diff --git a/config/routes.rb b/config/routes.rb index a087ed0cc3..abac2dff2f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "sidekiq/web" - class ActionDispatch::Routing::Mapper def draw(routes_name) instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) @@ -9,6 +7,8 @@ def draw(routes_name) end Rails.application.routes.draw do + mount MissionControl::Jobs::Engine, at: "/jobs" + namespace :admin do resources :users resources :timesheet_entries @@ -38,16 +38,6 @@ def draw(routes_name) mount LetterOpenerWeb::Engine, at: "/sent_emails" end - Sidekiq::Web.use Rack::Auth::Basic do |username, password| - ActiveSupport::SecurityUtils.secure_compare( - ::Digest::SHA256.hexdigest(username), - ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) & - ActiveSupport::SecurityUtils.secure_compare( - ::Digest::SHA256.hexdigest(password), - ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"])) - end - mount Sidekiq::Web, at: "/sidekiq" - draw(:internal_api) draw(:api) diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index b7127daaff..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 @@ -138,18 +139,12 @@ resources :providers, only: [:index, :update] end - resources :team, only: [:index, :destroy] do - resource :details, only: [:show, :update], controller: "team_members/details" - end - resources :users, concerns: :addressable do resources :previous_employments, only: [:create, :index, :show, :update], controller: "users/previous_employments" resources :devices, only: [:create, :index, :show, :update], controller: "users/devices" end - resource :profile, only: [:update, :show], controller: "profile" do - delete "/remove_avatar", to: "profile#remove_avatar" - end + resource :profile, only: [:update], controller: "profile" resources :vendors, only: [:create] resources :expense_categories, only: [:create] diff --git a/config/sidekiq.yml b/config/sidekiq.yml deleted file mode 100644 index 4507137ed3..0000000000 --- a/config/sidekiq.yml +++ /dev/null @@ -1,19 +0,0 @@ -development: - :concurrency: 1 -production: - :concurrency: 1 -:queues: - - default - - mailers - - action_mailbox_routing - - action_mailbox_incineration - - active_storage_analysis - - active_storage_purge -:scheduler: - schedule: - update_invoice_status: - cron: '0 0 * * *' # Runs every day at 12AM UTC - class: UpdateInvoiceStatusToOverdueJob - weekly_reminder: - cron: '0 14 * * 1 Asia/Kolkata' # Runs every monday at 14:00 - class: WeeklyReminderToUserJob diff --git a/config/solid_queue.yml b/config/solid_queue.yml new file mode 100644 index 0000000000..0f18aeba3e --- /dev/null +++ b/config/solid_queue.yml @@ -0,0 +1,25 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + recurring_tasks: + update_invoice_status: + class: UpdateInvoiceStatusToOverdueJob + schedule: '0 0 * * *' # Runs every day at 12AM UTC + weekly_reminder: + class: WeeklyReminderToUserJob + schedule: '0 14 * * 1 Asia/Kolkata' # Runs every Monday at 14:00 + workers: + - queues: "*" + threads: 5 + processes: 1 + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default 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/migrate/20240701052603_create_solid_queue_tables.solid_queue.rb b/db/migrate/20240701052603_create_solid_queue_tables.solid_queue.rb new file mode 100644 index 0000000000..a4b60e6f97 --- /dev/null +++ b/db/migrate/20240701052603_create_solid_queue_tables.solid_queue.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# This migration comes from solid_queue (originally 20231211200639) +class CreateSolidQueueTables < ActiveRecord::Migration[7.0] + def change + create_table :solid_queue_jobs do |t| + t.string :queue_name, null: false + t.string :class_name, null: false, index: true + t.text :arguments + t.integer :priority, default: 0, null: false + t.string :active_job_id, index: true + t.datetime :scheduled_at + t.datetime :finished_at, index: true + t.string :concurrency_key + + t.timestamps + + t.index [ :queue_name, :finished_at ], name: "index_solid_queue_jobs_for_filtering" + t.index [ :scheduled_at, :finished_at ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table :solid_queue_scheduled_executions do |t| + t.references :job, index: { unique: true }, null: false + t.string :queue_name, null: false + t.integer :priority, default: 0, null: false + t.datetime :scheduled_at, null: false + + t.datetime :created_at, null: false + + t.index [ :scheduled_at, :priority, :job_id ], name: "index_solid_queue_dispatch_all" + end + + create_table :solid_queue_ready_executions do |t| + t.references :job, index: { unique: true }, null: false + t.string :queue_name, null: false + t.integer :priority, default: 0, null: false + + t.datetime :created_at, null: false + + t.index [ :priority, :job_id ], name: "index_solid_queue_poll_all" + t.index [ :queue_name, :priority, :job_id ], name: "index_solid_queue_poll_by_queue" + end + + create_table :solid_queue_claimed_executions do |t| + t.references :job, index: { unique: true }, null: false + t.bigint :process_id + t.datetime :created_at, null: false + + t.index [ :process_id, :job_id ] + end + + create_table :solid_queue_blocked_executions do |t| + t.references :job, index: { unique: true }, null: false + t.string :queue_name, null: false + t.integer :priority, default: 0, null: false + t.string :concurrency_key, null: false + t.datetime :expires_at, null: false + + t.datetime :created_at, null: false + + t.index [ :expires_at, :concurrency_key ], name: "index_solid_queue_blocked_executions_for_maintenance" + end + + create_table :solid_queue_failed_executions do |t| + t.references :job, index: { unique: true }, null: false + t.text :error + t.datetime :created_at, null: false + end + + create_table :solid_queue_pauses do |t| + t.string :queue_name, null: false, index: { unique: true } + t.datetime :created_at, null: false + end + + create_table :solid_queue_processes do |t| + t.string :kind, null: false + t.datetime :last_heartbeat_at, null: false, index: true + t.bigint :supervisor_id, index: true + + t.integer :pid, null: false + t.string :hostname + t.text :metadata + + t.datetime :created_at, null: false + end + + create_table :solid_queue_semaphores do |t| + t.string :key, null: false, index: { unique: true } + t.integer :value, default: 1, null: false + t.datetime :expires_at, null: false, index: true + + t.timestamps + + t.index [ :key, :value ], name: "index_solid_queue_semaphores_on_key_and_value" + end + + add_foreign_key :solid_queue_blocked_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + add_foreign_key :solid_queue_claimed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + add_foreign_key :solid_queue_failed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + add_foreign_key :solid_queue_ready_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + add_foreign_key :solid_queue_scheduled_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + end +end diff --git a/db/migrate/20240701052604_add_missing_index_to_blocked_executions.solid_queue.rb b/db/migrate/20240701052604_add_missing_index_to_blocked_executions.solid_queue.rb new file mode 100644 index 0000000000..4fc4080c10 --- /dev/null +++ b/db/migrate/20240701052604_add_missing_index_to_blocked_executions.solid_queue.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# This migration comes from solid_queue (originally 20240110143450) +class AddMissingIndexToBlockedExecutions < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_index :solid_queue_blocked_executions, [ :concurrency_key, :priority, :job_id ], + name: "index_solid_queue_blocked_executions_for_release", algorithm: :concurrently + end +end diff --git a/db/migrate/20240701053139_validate_create_solid_queue_tables.rb b/db/migrate/20240701053139_validate_create_solid_queue_tables.rb new file mode 100644 index 0000000000..048d1d649b --- /dev/null +++ b/db/migrate/20240701053139_validate_create_solid_queue_tables.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ValidateCreateSolidQueueTables < ActiveRecord::Migration[7.0] + def change + validate_foreign_key :solid_queue_blocked_executions, :solid_queue_jobs + validate_foreign_key :solid_queue_claimed_executions, :solid_queue_jobs + validate_foreign_key :solid_queue_failed_executions, :solid_queue_jobs + validate_foreign_key :solid_queue_ready_executions, :solid_queue_jobs + validate_foreign_key :solid_queue_scheduled_executions, :solid_queue_jobs + end +end diff --git a/db/migrate/20240702060427_create_recurring_executions.solid_queue.rb b/db/migrate/20240702060427_create_recurring_executions.solid_queue.rb new file mode 100644 index 0000000000..eeeaa8aefd --- /dev/null +++ b/db/migrate/20240702060427_create_recurring_executions.solid_queue.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# This migration comes from solid_queue (originally 20240218110712) +class CreateRecurringExecutions < ActiveRecord::Migration[7.1] + def change + create_table :solid_queue_recurring_executions do |t| + t.references :job, index: { unique: true }, null: false + t.string :task_key, null: false + t.datetime :run_at, null: false + t.datetime :created_at, null: false + + t.index [ :task_key, :run_at ], unique: true + end + + add_foreign_key :solid_queue_recurring_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade, + validate: false + end +end diff --git a/db/migrate/20240702063359_validate_foreign_key_on_recurring_executions.rb b/db/migrate/20240702063359_validate_foreign_key_on_recurring_executions.rb new file mode 100644 index 0000000000..e6396ba156 --- /dev/null +++ b/db/migrate/20240702063359_validate_foreign_key_on_recurring_executions.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ValidateForeignKeyOnRecurringExecutions < ActiveRecord::Migration[7.1] + def change + validate_foreign_key :solid_queue_recurring_executions, :solid_queue_jobs + end +end diff --git a/db/schema.rb b/db/schema.rb index 4e7bd6c553..56e2e5d8a3 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_07_02_063359) 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 @@ -459,6 +470,109 @@ t.datetime "updated_at", null: false end + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + create_table "stripe_connected_accounts", force: :cascade do |t| t.string "account_id", null: false t.bigint "company_id", null: false @@ -599,12 +713,20 @@ 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" add_foreign_key "project_members", "projects" add_foreign_key "project_members", "users" add_foreign_key "projects", "clients" + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "stripe_connected_accounts", "companies" add_foreign_key "timeoff_entries", "leave_types" add_foreign_key "timeoff_entries", "users" diff --git a/deployment/fly/fly.production.toml b/deployment/fly/fly.production.toml index 29eef1671f..36f21b9f4e 100644 --- a/deployment/fly/fly.production.toml +++ b/deployment/fly/fly.production.toml @@ -7,7 +7,7 @@ primary_region = "ewr" [processes] web = "bin/rails fly:server" - worker = "bundle exec sidekiq -e production -C config/sidekiq.yml" + worker = "bundle exec rake solid_queue:start" [build] dockerfile = "Dockerfile" diff --git a/deployment/fly/fly.staging.toml b/deployment/fly/fly.staging.toml index 8a49b686b5..f5d39b4016 100644 --- a/deployment/fly/fly.staging.toml +++ b/deployment/fly/fly.staging.toml @@ -7,7 +7,7 @@ primary_region = "ewr" [processes] web = "bin/rails fly:server" - worker = "bundle exec sidekiq -e production -C config/sidekiq.yml" + worker = "bundle exec rake solid_queue:start" [build] dockerfile = "Dockerfile" diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index be907abdff..74fb09e857 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -56,7 +56,7 @@ const config = { announcementBar: { id: "star_us", content: - '⭐️ If you like Miru, give it a star on GitHub and follow us on Twitter!', + '⭐️ If you like Miru, give it a star on GitHub and follow us on Twitter!', isCloseable: true, backgroundColor: "#5B34EA", textColor: "#ffffff", @@ -110,7 +110,7 @@ const config = { }, { label: "Twitter", - href: "https://twitter.com/getmiru", + href: "https://x.com/getmiru", }, ], }, diff --git a/spec/factories/invoices.rb b/spec/factories/invoices.rb index da757bdb22..4c9b7c8932 100644 --- a/spec/factories/invoices.rb +++ b/spec/factories/invoices.rb @@ -14,7 +14,7 @@ # amount_paid { Faker::Number.decimal(r_digits: 2) } # amount_due { Faker::Number.decimal(r_digits: 2) } # discount { Faker::Number.decimal(r_digits: 2) } - status { [:draft, :paid, :overdue].sample } + status { :draft } external_view_key { "#{SecureRandom.hex}" } factory :invoice_with_invoice_line_items do transient do diff --git a/spec/models/invoice_line_item_spec.rb b/spec/models/invoice_line_item_spec.rb index e2b6605246..d9c8cd73b1 100644 --- a/spec/models/invoice_line_item_spec.rb +++ b/spec/models/invoice_line_item_spec.rb @@ -19,6 +19,10 @@ end end + describe "callbacks" do + it { is_expected.to callback(:unlock_timesheet_entry).before(:destroy) } + end + describe "Associations" do describe "belongs to" do it { is_expected.to belong_to(:invoice) } diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 26d668742c..ac595f9218 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -113,6 +113,13 @@ end end + describe "callbacks" do + it { is_expected.to callback(:set_external_view_key).before(:validation).on(:create) } + it { is_expected.to callback(:refresh_invoice_index).after(:commit) } + it { is_expected.to callback(:lock_timesheet_entries).after(:save).if(:draft?) } + it { is_expected.to callback(:unlock_timesheet_entries).after(:discard).if(:draft?) } + end + describe ".client_name" do it { is_expected.to delegate_method(:name).to(:client).with_prefix(:client) } end diff --git a/spec/requests/internal_api/v1/invoices/bulk_download/index_spec.rb b/spec/requests/internal_api/v1/invoices/bulk_download/index_spec.rb index a858a74599..6a9d42a899 100644 --- a/spec/requests/internal_api/v1/invoices/bulk_download/index_spec.rb +++ b/spec/requests/internal_api/v1/invoices/bulk_download/index_spec.rb @@ -38,7 +38,8 @@ end it "check if bulk invoice download job is queued" do - expect(BulkInvoiceDownloadJob).to be_processed_in :default + subject + expect(BulkInvoiceDownloadJob).to have_been_enqueued.on_queue("default") end end @@ -52,17 +53,18 @@ end context "when user is book_keeper" do - let(:role) { :book_keeper } + let(:role) { :book_keeper } - it "send the download request successfully" do - subject - expect(response).to have_http_status(:accepted) - end + it "send the download request successfully" do + subject + expect(response).to have_http_status(:accepted) + end - it "check if bulk invoice download job is queued" do - expect(BulkInvoiceDownloadJob).to be_processed_in :default + it "check if bulk invoice download job is queued" do + subject + expect(BulkInvoiceDownloadJob).to have_been_enqueued.on_queue("default") + end end - end end context "when unauthenticated" do diff --git a/spec/requests/internal_api/v1/invoices/create_spec.rb b/spec/requests/internal_api/v1/invoices/create_spec.rb index 4a83738bc1..a96c5aaf27 100644 --- a/spec/requests/internal_api/v1/invoices/create_spec.rb +++ b/spec/requests/internal_api/v1/invoices/create_spec.rb @@ -23,7 +23,6 @@ invoice_number: "SAI-C1-03", client: company.clients.first, client_id: company.clients.first.id, - status: :draft, invoice_line_item: { name: "Test", description: "test description", @@ -72,7 +71,6 @@ :invoice, client: company.clients.first, client_id: company.clients.first.id, - status: :draft, invoice_line_item: { name: "Test", description: "test description", @@ -99,7 +97,6 @@ :invoice, client: company.clients.first, client_id: company.clients.first.id, - status: :draft, invoice_line_item: { name: "Test", description: "test description", @@ -122,8 +119,7 @@ invoice: attributes_for( :invoice, client: company.clients.first, - client_id: company.clients.first.id, - status: :draft + client_id: company.clients.first.id ) ) expect(response).to have_http_status(:unauthorized) diff --git a/spec/requests/internal_api/v1/payments/new_spec.rb b/spec/requests/internal_api/v1/payments/new_spec.rb index 366c8965d7..5acc748940 100644 --- a/spec/requests/internal_api/v1/payments/new_spec.rb +++ b/spec/requests/internal_api/v1/payments/new_spec.rb @@ -11,7 +11,7 @@ let!(:client1_viewed_invoice1) { create(:invoice, client: client1, company:, status: "viewed") } let!(:client1_paid_invoice1) { create(:invoice, client: client1, company:, status: "paid") } let!(:client1_overdue_invoice1) { create(:invoice, client: client1, company:, status: "overdue") } - let!(:client1_draft_invoice1) { create(:invoice, client: client1, company:, status: "draft") } + let!(:client1_draft_invoice1) { create(:invoice, client: client1, company:) } context "when user is an admin" do before do diff --git a/spec/requests/internal_api/v1/profile/remove_avatar_spec.rb b/spec/requests/internal_api/v1/profile/remove_avatar_spec.rb deleted file mode 100644 index e6dffd6e2c..0000000000 --- a/spec/requests/internal_api/v1/profile/remove_avatar_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe "InternalApi::V1::Profile#remove_avatar", type: :request do - let(:user) { create(:user, :with_avatar) } - let(:company) { create(:company) } - - describe "remove avatar" do - before do - user.add_role :employee, company - sign_in user - send_request :delete, remove_avatar_internal_api_v1_profile_path, headers: auth_headers(user) - end - - it "removes user's avatar" do - expect(response).to have_http_status(:ok) - expect(user.reload.avatar.attached?).to be_falsey - end - end -end diff --git a/spec/requests/internal_api/v1/profile/show_spec.rb b/spec/requests/internal_api/v1/profile/show_spec.rb deleted file mode 100644 index 51ee64fa7a..0000000000 --- a/spec/requests/internal_api/v1/profile/show_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe "InternalApi::V1::Profile#show", type: :request do - let(:user) { create(:user, :with_avatar) } - let(:company) { create(:company) } - - describe "index action" do - before do - user.add_role :employee, company - sign_in user - send_request :get, internal_api_v1_profile_path, headers: auth_headers(user) - end - - it "fetches user details & avatar" do - expect(response).to have_http_status(:ok) - expect(json_response["user"]["avatar_url"]).to eq(user.avatar_url) - expect(json_response["user"]["first_name"]).to eq(user.first_name) - expect(json_response["user"]["last_name"]).to eq(user.last_name) - end - end -end diff --git a/spec/requests/internal_api/v1/team_members/avatar/destroy_spec.rb b/spec/requests/internal_api/v1/team_members/avatar/destroy_spec.rb index 3c195efa6c..b406570893 100644 --- a/spec/requests/internal_api/v1/team_members/avatar/destroy_spec.rb +++ b/spec/requests/internal_api/v1/team_members/avatar/destroy_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" -RSpec.describe "InternalApi::V1::Profile#remove_avatar", type: :request do +RSpec.describe "InternalApi::V1::TeamMembers::AvatarController#destroy", type: :request do let(:company) { create(:company) } let(:company2) { create(:company) } let(:admin) { create(:user, current_workspace_id: company.id) } @@ -45,6 +45,21 @@ end end + context "when employee wants to remove own avatar" do + before do + create(:employment, user:, company:) + + user.add_role :employee, company + sign_in user + send_request :delete, internal_api_v1_team_avatar_path(user.id), headers: auth_headers(user) + end + + it "removes user's avatar" do + expect(response).to have_http_status(:ok) + expect(user.reload.avatar.attached?).to be_falsey + end + end + context "when logged in admin wants to remove avatar of employee from a different company" do before do create(:employment, user: admin, company:) @@ -76,4 +91,21 @@ expect(response).to have_http_status(:not_found) end end + + context "when logged in employee wants to remove avatar of another employee from a same company" do + before do + create(:employment, user:, company:) + create(:employment, user: user2, company:) + + user.add_role :employee, company + user2.add_role :employee, company + sign_in user + send_request :delete, internal_api_v1_team_avatar_path(user2.id), headers: auth_headers(user) + end + + it "is unsuccessful" do + expect(response).to have_http_status(:forbidden) + expect(json_response["errors"]).to eq("You are not authorized to perform this action.") + end + end end diff --git a/spec/requests/internal_api/v1/team_members/avatar/update_spec.rb b/spec/requests/internal_api/v1/team_members/avatar/update_spec.rb index 1ed20bb50e..a19a9e68a5 100644 --- a/spec/requests/internal_api/v1/team_members/avatar/update_spec.rb +++ b/spec/requests/internal_api/v1/team_members/avatar/update_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" -RSpec.describe "InternalApi::V1::TeamMembers::AvatarController::#update", type: :request do +RSpec.describe "InternalApi::V1::TeamMembers::AvatarController#update", type: :request do let(:company) { create(:company) } let(:company2) { create(:company) } let(:admin) { create(:user, current_workspace_id: company.id) } diff --git a/spec/requests/internal_api/v1/team_members/details/show_spec.rb b/spec/requests/internal_api/v1/team_members/details/show_spec.rb index a577b086a8..fe24bd08cd 100644 --- a/spec/requests/internal_api/v1/team_members/details/show_spec.rb +++ b/spec/requests/internal_api/v1/team_members/details/show_spec.rb @@ -2,11 +2,11 @@ require "rails_helper" -RSpec.describe "Details#show", type: :request do +RSpec.describe "InternalApi::V1::TeamMembers::DetailsController#show", type: :request do let(:company) { create(:company) } let(:company2) { create(:company) } - let(:user) { create(:user, current_workspace_id: company.id) } - let(:user2) { create(:user, current_workspace_id: company2.id) } + let(:user) { create(:user, :with_avatar, current_workspace_id: company.id) } + let(:user2) { create(:user, :with_avatar, current_workspace_id: company2.id) } let(:employment) { create(:employment, user:, company:) } context "when Owner wants to see details of employee of his company" do @@ -52,9 +52,15 @@ send_request :get, internal_api_v1_team_details_path(employment.user_id), headers: auth_headers(user) end - it "is unsuccessful" do - expect(response).to have_http_status(:forbidden) - expect(json_response["errors"]).to eq("You are not authorized to perform this action.") + it "is successful" do + expect(response).to have_http_status(:ok) + expect(json_response["avatar_url"]).to eq(JSON.parse(user.avatar_url.to_json)) + expect(json_response["first_name"]).to eq(JSON.parse(user.first_name.to_json)) + expect(json_response["last_name"]).to eq(JSON.parse(user.last_name.to_json)) + expect(json_response["personal_email_id"]).to eq(JSON.parse(user.personal_email_id.to_json)) + expect(json_response["date_of_birth"]).to eq(JSON.parse(user.date_of_birth.to_json)) + expect(json_response["phone"]).to eq(JSON.parse(user.phone.to_json)) + expect(json_response["social_accounts"]).to eq(JSON.parse(user.social_accounts.to_json)) end end diff --git a/spec/requests/internal_api/v1/team_members/details/update_spec.rb b/spec/requests/internal_api/v1/team_members/details/update_spec.rb index 96bc0d24f3..8bb24619de 100644 --- a/spec/requests/internal_api/v1/team_members/details/update_spec.rb +++ b/spec/requests/internal_api/v1/team_members/details/update_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" -RSpec.describe "InternalApi::V1::TeamMembers::DetailsController::#update", type: :request do +RSpec.describe "InternalApi::V1::TeamMembers::DetailsController#update", type: :request do let(:company) { create(:company) } let(:company2) { create(:company) } let(:user) { create(:user, current_workspace_id: company.id) } @@ -79,9 +79,14 @@ }), headers: auth_headers(user) end - it "is unsuccessful" do - expect(response).to have_http_status(:forbidden) - expect(json_response["errors"]).to eq("You are not authorized to perform this action.") + it "is successful" do + expect(response).to have_http_status(:ok) + expect(json_response["first_name"]).to eq(JSON.parse(@user_details["first_name"].to_json)) + expect(json_response["last_name"]).to eq(JSON.parse(@user_details["last_name"].to_json)) + expect(json_response["personal_email_id"]).to eq(JSON.parse(@user_details["personal_email_id"].to_json)) + expect(json_response["date_of_birth"]).to eq(JSON.parse(@user_details["date_of_birth"].to_json)) + expect(json_response["phone"]).to eq(JSON.parse(@user_details["phone"].to_json)) + expect(json_response["social_accounts"]).to eq(JSON.parse(@user_details["social_accounts"].to_json)) end end diff --git a/spec/services/update_invoice_status_to_overdue_service_spec.rb b/spec/services/update_invoice_status_to_overdue_service_spec.rb index ff751706aa..e9cf43e8e8 100644 --- a/spec/services/update_invoice_status_to_overdue_service_spec.rb +++ b/spec/services/update_invoice_status_to_overdue_service_spec.rb @@ -10,7 +10,7 @@ let!(:client1_sent_invoice2) { create(:invoice, client: client1, status: "sent", due_date: Date.current - 1) } let!(:client1_sent_invoice3) { create(:invoice, client: client1, status: "sent", due_date: Date.current - 2) } let(:client1_paid_invoice2) { create(:invoice, client: client1, status: "paid", due_date: Date.current - 1) } - let!(:client1_draft_invoice1) { create(:invoice, client: client1, status: "draft", due_date: Date.current - 1) } + let!(:client1_draft_invoice1) { create(:invoice, client: client1, due_date: Date.current - 1) } let!(:client1_viewed_invoice1) { create(:invoice, client: client1, status: "viewed", due_date: Date.current + 1) } let!(:client1_viewed_invoice2) { create(:invoice, client: client1, status: "viewed", due_date: Date.current - 1) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c06bce7fac..c6eed21be9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,6 @@ require "simplecov" require "pundit/rspec" -require "sidekiq/testing" require "webmock/rspec" require "rspec/retry" # require "buildkite/test_collector" diff --git a/spec/system/invoices/edit_invoice_spec.rb b/spec/system/invoices/edit_invoice_spec.rb index 367bcc6cba..4aee07270c 100644 --- a/spec/system/invoices/edit_invoice_spec.rb +++ b/spec/system/invoices/edit_invoice_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe "Edit Invoice", type: :system do - let!(:invoice) { create :invoice_with_invoice_line_items, status: :draft } + let!(:invoice) { create :invoice_with_invoice_line_items } let(:client) { invoice.client } let!(:company) { invoice.company } let(:admin) { create(:user, current_workspace_id: company.id) } diff --git a/spec/system/invoices/invoice_history_spec.rb b/spec/system/invoices/invoice_history_spec.rb index 0858304396..7b6dc73a8c 100644 --- a/spec/system/invoices/invoice_history_spec.rb +++ b/spec/system/invoices/invoice_history_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe "View Invoice Logs", type: :system do - let!(:invoice) { create :invoice_with_invoice_line_items, status: :draft } + let!(:invoice) { create :invoice_with_invoice_line_items } let(:client) { invoice.client } let!(:company) { invoice.company } let(:admin) { create(:user, current_workspace_id: company.id) } diff --git a/spec/system/invoices/send_invoice_spec.rb b/spec/system/invoices/send_invoice_spec.rb index c847cb22ea..035e7f95bc 100644 --- a/spec/system/invoices/send_invoice_spec.rb +++ b/spec/system/invoices/send_invoice_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe "Send Invoice", type: :system do - let(:invoice) { create :invoice_with_invoice_line_items, status: :draft } + let(:invoice) { create :invoice_with_invoice_line_items } let(:client) { invoice.client } let(:company) { invoice.company } let(:admin) { create(:user, current_workspace_id: company.id) }