diff --git a/app/controllers/internal_api/v1/clients_controller.rb b/app/controllers/internal_api/v1/clients_controller.rb index cc42848002..676b50cf8b 100644 --- a/app/controllers/internal_api/v1/clients_controller.rb +++ b/app/controllers/internal_api/v1/clients_controller.rb @@ -9,12 +9,21 @@ def index def create authorize Client - ActiveRecord::Base.transaction do - client = Client.create!(client_params) - user = User.find_by!(email: params[:client][:email]) - client_member = current_company.client_members.create!(client:, user:) - render :create, locals: { client:, address: client.current_address } - end + client = Client.create!(client_params) + render :create, locals: { client:, address: client.current_address } + end + + def add_client_contact + authorize client + invitation_service = Invitations::ClientInvitationService.new( + params, + current_company, + current_user, + client + ) + + invitations = invitation_service.process + render json: { notice: "Invitation sent successfully." }, status: :ok end def show @@ -25,7 +34,9 @@ def show project_details: client.project_details(params[:time_frame]), total_minutes: client.total_hours_logged(params[:time_frame]), overdue_outstanding_amount: client.client_overdue_and_outstanding_calculation, - invoices: client.invoices + invoices: client.invoices, + invitations: client.invitations, + client_members_emails: client.client_members.joins(:user).pluck("users.email") }, status: :ok end diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 076714e9be..1ced34c32a 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -21,7 +21,8 @@ def create @invoice = current_company.invoices.create!(invoice_params) render :create, locals: { invoice: @invoice, - client: @client + client: @client, + client_member_emails: @invoice.client_member_emails } end @@ -30,7 +31,8 @@ def edit render :edit, locals: { invoice:, client: invoice.client, - client_list: current_company.client_list + client_list: current_company.client_list, + client_member_emails: invoice.client_member_emails } end @@ -47,7 +49,8 @@ def show authorize invoice render :show, locals: { invoice:, - client: invoice.client + client: invoice.client, + client_member_emails: invoice.client_member_emails } end diff --git a/app/javascript/src/apis/clients.ts b/app/javascript/src/apis/clients.ts index 84357bb12c..9111ea85dd 100644 --- a/app/javascript/src/apis/clients.ts +++ b/app/javascript/src/apis/clients.ts @@ -15,6 +15,9 @@ const destroy = async id => axios.delete(`${path}/${id}`); const sendPaymentReminder = async (id, payload) => axios.post(`${path}/${id}/send_payment_reminder`, payload); +const addClientContact = async (id, payload) => + axios.post(`${path}/${id}/add_client_contact`, payload); + const invoices = async (query = "") => axios.get(query ? `${path}/invoices?${query}` : `${path}/invoices`); @@ -26,6 +29,7 @@ const clientApi = { create, invoices, sendPaymentReminder, + addClientContact, }; export default clientApi; diff --git a/app/javascript/src/common/CustomAdvanceInput/index.tsx b/app/javascript/src/common/CustomAdvanceInput/index.tsx index b61289cf71..9f847bd75e 100644 --- a/app/javascript/src/common/CustomAdvanceInput/index.tsx +++ b/app/javascript/src/common/CustomAdvanceInput/index.tsx @@ -14,6 +14,7 @@ type CustomAdvanceInputProps = { inputBoxClassName?: string; labelClassName?: string; onClick?: React.MouseEventHandler; + onBlur?: React.FocusEventHandler; }; const getDefaultInputBoxClassName = focused => @@ -34,6 +35,7 @@ export const CustomAdvanceInput = ({ inputBoxClassName, labelClassName, onClick, + onBlur, }: CustomAdvanceInputProps) => { const inputRef = useRef(null); const [focused, setFocused] = useState(false); @@ -57,6 +59,7 @@ export const CustomAdvanceInput = ({
{value} diff --git a/app/javascript/src/components/Clients/ClientForm/formValidationSchema.ts b/app/javascript/src/components/Clients/ClientForm/formValidationSchema.ts index dffbd9da43..675015131d 100644 --- a/app/javascript/src/components/Clients/ClientForm/formValidationSchema.ts +++ b/app/javascript/src/components/Clients/ClientForm/formValidationSchema.ts @@ -9,7 +9,6 @@ export const clientSchema = Yup.object().shape({ name: Yup.string() .required("Name cannot be blank") .max(30, "Maximum 30 characters are allowed"), - email: Yup.string().required("Email cannot be blank"), phone: Yup.string() .required("Business phone number cannot be blank") .matches(phoneRegExp, "Please enter a valid business phone number"), diff --git a/app/javascript/src/components/Clients/ClientForm/index.tsx b/app/javascript/src/components/Clients/ClientForm/index.tsx index e81386febf..8e6062d7fd 100644 --- a/app/javascript/src/components/Clients/ClientForm/index.tsx +++ b/app/javascript/src/components/Clients/ClientForm/index.tsx @@ -31,7 +31,6 @@ const ClientForm = ({ submitting, setSubmitting, fetchDetails, - usersWithClientRole, }: IClientForm) => { const [fileUploadError, setFileUploadError] = useState(""); const [countries, setCountries] = useState([]); @@ -114,8 +113,7 @@ const ClientForm = ({ onSubmit={handleSubmit} > {(props: FormikProps) => { - const { touched, errors, setFieldValue, values, setFieldTouched } = - props; + const { touched, errors, setFieldValue, values } = props; return (
@@ -153,24 +151,6 @@ const ClientForm = ({ fieldTouched={touched.name} />
-
- setFieldValue("email", email.value)} - id="email" - label="Email" - name="email" - value={{ label: values.email, value: values.email }} - options={usersWithClientRole.map(user => ({ - value: user.email, - label: user.email, - }))} - onBlur={() => setFieldTouched("email", true)} - /> - -
@@ -326,15 +306,10 @@ interface IClientForm { submitting: boolean; setSubmitting: any; fetchDetails?: any; - usersWithClientRole: any; } interface FormValues { name: string; - email: { - value: string; - label: string; - }; phone: string; address1: string; address2: string; diff --git a/app/javascript/src/components/Clients/ClientForm/utils.ts b/app/javascript/src/components/Clients/ClientForm/utils.ts index 9caad0fd7e..d97fd5b3a5 100644 --- a/app/javascript/src/components/Clients/ClientForm/utils.ts +++ b/app/javascript/src/components/Clients/ClientForm/utils.ts @@ -7,17 +7,12 @@ export const formatFormData = ( clientLogoUrl ) => { formData.append("client[name]", values.name); - formData.append("client[email]", values.email); formData.append("client[phone]", values.phone); if (!isNewForm) { formData.append("client[addresses_attributes[0][id]]", client.address.id); } - if (!isNewForm && values.email !== client.email) { - formData.append("client[prev_email]", client.email); - } - formData.append( "client[addresses_attributes[0][address_line_1]]", values.address1 @@ -58,7 +53,6 @@ export const formatFormData = ( export const disableBtn = (values, errors, submitting) => { if ( errors.name || - errors.email || errors.phone || errors.address1 || errors.country || @@ -72,7 +66,6 @@ export const disableBtn = (values, errors, submitting) => { if ( values.name && - values.email && values.phone && values.address1 && values.country && diff --git a/app/javascript/src/components/Clients/Details/Header.tsx b/app/javascript/src/components/Clients/Details/Header.tsx index a9a2f6b225..727e5b51e5 100644 --- a/app/javascript/src/components/Clients/Details/Header.tsx +++ b/app/javascript/src/components/Clients/Details/Header.tsx @@ -11,12 +11,14 @@ import { EditIcon, InfoIcon, ReminderIcon, + TeamsIcon, } from "miruIcons"; import { useNavigate } from "react-router-dom"; -import { MobileMoreOptions, Modal } from "StyledComponents"; +import { Badge, MobileMoreOptions, Modal } from "StyledComponents"; import { useUserContext } from "context/UserContext"; +import AddContacts from "../Modals/AddContacts"; import DeleteClient from "../Modals/DeleteClient"; import EditClient from "../Modals/EditClient"; @@ -32,7 +34,13 @@ const Header = ({ const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [showMobileModal, setShowMobileModal] = useState(false); + const [showContactModal, setShowContactModal] = useState(false); + const clientMembersEmail = clientDetails.clientMembersEmails; + + const pendingInvitationEmails = clientDetails.invitations.filter( + contact => contact.accepted_at === null + ); const navigate = useNavigate(); const menuRef = useRef(); const { isDesktop } = useUserContext(); @@ -116,6 +124,17 @@ const Header = ({ Payment Reminder +
  • { + setShowContactModal(true); + setIsHeaderMenuVisible(false); + }} + > + +
  • +
  • +
    + + {(props: FormikProps) => { + const { + touched, + errors, + isValid, + dirty, + setFieldError, + setFieldValue, + } = props; + + return ( + +
    + + + + + + +
    + + + ); + }} +
    + + ); +}; + +export default AddContacts; diff --git a/app/javascript/src/components/Clients/Modals/EditClient.tsx b/app/javascript/src/components/Clients/Modals/EditClient.tsx index 56dc72e535..ef7bb288fd 100644 --- a/app/javascript/src/components/Clients/Modals/EditClient.tsx +++ b/app/javascript/src/components/Clients/Modals/EditClient.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { XIcon } from "miruIcons"; import { Modal } from "StyledComponents"; -import clientApi from "apis/clients"; import { useUserContext } from "context/UserContext"; import ClientForm from "../ClientForm"; @@ -21,7 +20,6 @@ const EditClient = ({ const [clientLogo, setClientLogo] = useState(""); const [submitting, setSubmitting] = useState(false); const { isDesktop } = useUserContext(); - const [usersWithClientRole, setUsersWithClientRole] = useState([]); const handleDeleteLogo = event => { event.preventDefault(); @@ -29,15 +27,6 @@ const EditClient = ({ setClientLogoUrl(""); }; - const fetchUsersWithClientroles = async (val = "week") => { - const res = await clientApi.get(`?time_frame=${val}`); - setUsersWithClientRole(res.data.users_not_in_client_members); - }; - - useEffect(() => { - fetchUsersWithClientroles(); - }, []); - return isDesktop ? ( ) : ( diff --git a/app/javascript/src/components/Clients/Modals/NewClient.tsx b/app/javascript/src/components/Clients/Modals/NewClient.tsx index 05bf501810..9807140e96 100644 --- a/app/javascript/src/components/Clients/Modals/NewClient.tsx +++ b/app/javascript/src/components/Clients/Modals/NewClient.tsx @@ -1,9 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { XIcon } from "miruIcons"; import { Modal } from "StyledComponents"; -import clientApi from "apis/clients"; import { useUserContext } from "context/UserContext"; import ClientForm from "../ClientForm"; @@ -21,22 +20,12 @@ const NewClient = ({ showDialog, }) => { const [submitting, setSubmitting] = useState(false); - const [usersWithClientRole, setUsersWithClientRole] = useState([]); const handleDeleteLogo = () => { setClientLogo(""); setClientLogoUrl(""); }; - const fetchUsersWithClientroles = async (val = "week") => { - const res = await clientApi.get(`?time_frame=${val}`); - setUsersWithClientRole(res.data.users_not_in_client_members); - }; - - useEffect(() => { - fetchUsersWithClientroles(); - }, []); - const { isDesktop } = useUserContext(); return isDesktop ? ( @@ -69,7 +58,6 @@ const NewClient = ({ setSubmitting={setSubmitting} setnewClient={setnewClient} submitting={submitting} - usersWithClientRole={usersWithClientRole} /> ) : ( diff --git a/app/javascript/src/components/Clients/Modals/PaymentReminder/EmailPreview/index.tsx b/app/javascript/src/components/Clients/Modals/PaymentReminder/EmailPreview/index.tsx index 6027e73fa0..b38ecfef5c 100644 --- a/app/javascript/src/components/Clients/Modals/PaymentReminder/EmailPreview/index.tsx +++ b/app/javascript/src/components/Clients/Modals/PaymentReminder/EmailPreview/index.tsx @@ -1,5 +1,7 @@ import React from "react"; +import cn from "classnames"; + import { CustomAdvanceInput } from "common/CustomAdvanceInput"; import { CustomTextareaAutosize } from "common/CustomTextareaAutosize"; @@ -24,14 +26,23 @@ const EmailPreview = ({ ( + wrapperClassName="h-full" + value={
    -

    {recipient}

    + {emailParams.recipients.map(recipient => ( +
    +

    {recipient}

    +
    + ))}
    - ))} + } />
    diff --git a/app/javascript/src/components/Clients/Modals/PaymentReminder/index.tsx b/app/javascript/src/components/Clients/Modals/PaymentReminder/index.tsx index 9bc45e4d4a..a9dfcc16c8 100644 --- a/app/javascript/src/components/Clients/Modals/PaymentReminder/index.tsx +++ b/app/javascript/src/components/Clients/Modals/PaymentReminder/index.tsx @@ -45,7 +45,7 @@ const PaymentReminder = ({ subject: "Reminder to complete payments for unpaid invoices", message: "This is a gentle reminder to complete payments for the following invoices. You can find the respective payment links along with the invoice details given below", - recipients: [client.email], + recipients: client.clientMembersEmails, }); const isStepFormSubmittedOrVisited = stepNo => { diff --git a/app/javascript/src/components/Clients/constants.ts b/app/javascript/src/components/Clients/constants.ts index 25fc1ad05c..fefee60632 100644 --- a/app/javascript/src/components/Clients/constants.ts +++ b/app/javascript/src/components/Clients/constants.ts @@ -4,14 +4,9 @@ export const tableHeader = [ accessor: "col1", // accessor is the "key" in the data cssClass: "md:w-1/3 capitalize", }, - { - Header: "EMAIL ID", - accessor: "col2", - cssClass: "md:w-1/3", - }, { Header: "HOURS LOGGED", - accessor: "col3", + accessor: "col2", cssClass: "text-right md:w-1/5", // accessor is the "key" in the data }, ]; diff --git a/app/javascript/src/components/Invoices/Generate/MobileView/Container/SendInvoiceContainer/index.tsx b/app/javascript/src/components/Invoices/Generate/MobileView/Container/SendInvoiceContainer/index.tsx index a536342725..541d6711b2 100644 --- a/app/javascript/src/components/Invoices/Generate/MobileView/Container/SendInvoiceContainer/index.tsx +++ b/app/javascript/src/components/Invoices/Generate/MobileView/Container/SendInvoiceContainer/index.tsx @@ -26,19 +26,22 @@ const SendInvoiceContainer = ({ setIsSendReminder = _value => {}, isSendReminder = false, }) => { - const Recipient: React.FC<{ email: string; handleClick: any }> = ({ - email, - handleClick, - }) => ( + const Recipient: React.FC<{ + email: string; + handleClick: any; + recipientsCount: any; + }> = ({ email, handleClick, recipientsCount }) => (

    {email}

    - + {recipientsCount > 1 && ( + + )}
    ); interface InvoiceEmail { @@ -50,7 +53,7 @@ const SendInvoiceContainer = ({ const [invoiceEmail, setInvoiceEmail] = useState({ subject: emailSubject(invoice, isSendReminder), message: emailBody(invoice, isSendReminder), - recipients: [invoice.client.email], + recipients: invoice.client.clientMembersEmails, }); // eslint-disable-next-line no-unused-vars const [newRecipient, setNewRecipient] = useState(""); @@ -167,6 +170,7 @@ const SendInvoiceContainer = ({ email={recipient} handleClick={() => handleRemove(recipient)} key={recipient} + recipientsCount={invoiceEmail.recipients.length} /> ))} {/* { : Toastr.error(SELECT_CLIENT_ERROR); } }; + if (invoiceDetails && isDesktop) { return ( diff --git a/app/javascript/src/components/Invoices/List/SendInvoice/index.tsx b/app/javascript/src/components/Invoices/List/SendInvoice/index.tsx index a042f94447..8065d49b90 100644 --- a/app/javascript/src/components/Invoices/List/SendInvoice/index.tsx +++ b/app/javascript/src/components/Invoices/List/SendInvoice/index.tsx @@ -46,11 +46,10 @@ const SendInvoice: React.FC = ({ const [invoiceEmail, setInvoiceEmail] = useState({ subject: emailSubject(invoice, isSendReminder), message: emailBody(invoice, isSendReminder), - recipients: [invoice.client.email], + recipients: invoice.client.clientMembersEmails, }); const [newRecipient, setNewRecipient] = useState(""); const [width, setWidth] = useState("10ch"); - const input: React.RefObject = useRef(); useEffect(() => { @@ -158,6 +157,7 @@ const SendInvoice: React.FC = ({ email={recipient} handleClick={() => handleRemove(recipient)} key={recipient} + recipientsCount={invoiceEmail.recipients.length} /> ))} {user.email == "supriya@saeloun.com" && ( diff --git a/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/Recipient.tsx b/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/Recipient.tsx index 35d2adb469..829896a22d 100644 --- a/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/Recipient.tsx +++ b/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/Recipient.tsx @@ -2,28 +2,23 @@ import React from "react"; import { XIcon } from "miruIcons"; -import { useUserContext } from "context/UserContext"; - -const Recipient: React.FC<{ email: string; handleClick: any }> = ({ - email, - handleClick, -}) => { - const { user } = useUserContext(); - - return ( -
    -

    {email}

    - {user.email == "supriya@saeloun.com" && ( - - )} -
    - ); -}; +const Recipient: React.FC<{ + email: string; + handleClick: any; + recipientsCount: any; +}> = ({ email, handleClick, recipientsCount }) => ( +
    +

    {email}

    + {recipientsCount > 1 && ( + + )} +
    +); export default Recipient; diff --git a/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/index.tsx b/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/index.tsx index 75f1bf846f..693a649f4d 100644 --- a/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/index.tsx +++ b/app/javascript/src/components/Invoices/common/InvoiceForm/SendInvoice/index.tsx @@ -46,7 +46,7 @@ const SendInvoice: React.FC = ({ const [invoiceEmail, setInvoiceEmail] = useState({ subject: emailSubject(invoice, isSendReminder), message: emailBody(invoice, isSendReminder), - recipients: [invoice.client.email], + recipients: invoice.client.clientMembersEmails, }); const [newRecipient, setNewRecipient] = useState(""); const [width, setWidth] = useState("10ch"); @@ -174,6 +174,7 @@ const SendInvoice: React.FC = ({ email={recipient} handleClick={() => handleRemove(recipient)} key={recipient} + recipientsCount={invoiceEmail.recipients.length} /> ))} {user.email == "supriya@saeloun.com" && ( diff --git a/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx b/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx index 377302c98b..2f9ad00494 100644 --- a/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx +++ b/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx @@ -48,7 +48,7 @@ const SendInvoice: React.FC = ({ const [invoiceEmail, setInvoiceEmail] = useState({ subject: emailSubject(invoice, isSendReminder), message: emailBody(invoice, isSendReminder), - recipients: [invoice.client.email], + recipients: invoice.client.clientMembersEmails, }); const [newRecipient, setNewRecipient] = useState(""); const [width, setWidth] = useState("10ch"); @@ -175,6 +175,7 @@ const SendInvoice: React.FC = ({ email={recipient} handleClick={() => handleRemove(recipient)} key={recipient} + recipientsCount={invoiceEmail.recipients.length} /> ))} {user.email == "supriya@saeloun.com" && ( diff --git a/app/javascript/src/mapper/client.mapper.ts b/app/javascript/src/mapper/client.mapper.ts index cba65ec031..80431fb894 100644 --- a/app/javascript/src/mapper/client.mapper.ts +++ b/app/javascript/src/mapper/client.mapper.ts @@ -62,6 +62,8 @@ const unmapClientDetails = input => { phone: data.client_details.phone || "--", address: data.client_details.address || "--", logo: data.client_details.logo, + clientMembersEmails: data.client_members_emails, + invitations: data.invitations, }, overdueOutstandingAmount: data.overdue_outstanding_amount, totalMinutes: data.total_minutes, diff --git a/app/javascript/src/mapper/generateInvoice.mapper.ts b/app/javascript/src/mapper/generateInvoice.mapper.ts index 0205933a09..87fd715d9d 100644 --- a/app/javascript/src/mapper/generateInvoice.mapper.ts +++ b/app/javascript/src/mapper/generateInvoice.mapper.ts @@ -7,6 +7,7 @@ interface GenerateInvoiceClientList { phone_number: number; email: string; previousInvoiceNumber: string; + client_members: any; } interface CompanyDetails { @@ -24,8 +25,8 @@ const getClientList = (clientList: Array) => value: client.id, label: client.name, phone: client.phone_number, - email: client.email, previousInvoiceNumber: client.previousInvoiceNumber, + clientMembersEmails: client.client_members, })); const getCompanyDetails = (input: CompanyDetails) => input; diff --git a/app/models/client.rb b/app/models/client.rb index 2950e676f0..4e89e54574 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -36,6 +36,7 @@ class Client < ApplicationRecord has_many :invoices, dependent: :destroy has_many :addresses, as: :addressable, dependent: :destroy has_many :client_members, dependent: :destroy + has_many :invitations has_one_attached :logo belongs_to :company @@ -43,7 +44,7 @@ class Client < ApplicationRecord validates :name, presence: true, length: { maximum: 30 }, uniqueness: { scope: :company_id, case_sensitive: false, message: "The client %{value} already exists" } validates :phone, length: { maximum: 15 } - validates :email, presence: true, uniqueness: { scope: :company_id }, format: { with: Devise.email_regexp } + # validates :email, presence: true, uniqueness: { scope: :company_id }, format: { with: Devise.email_regexp } after_discard :discard_projects after_commit :reindex_projects diff --git a/app/models/company.rb b/app/models/company.rb index 6a6370d33b..b099d1fe72 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -58,7 +58,8 @@ def client_list clients.kept.map do |client| { id: client.id, name: client.name, email: client.email, phone: client.phone, address: client.current_address, - previousInvoiceNumber: client.invoices&.last&.invoice_number || 0 + previousInvoiceNumber: client.invoices&.last&.invoice_number || 0, + client_members: client.client_members.joins(:user).pluck("users.email") } end end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 62462da7e1..997b7c6d34 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -14,12 +14,14 @@ # token :string not null # created_at :datetime not null # updated_at :datetime not null +# client_id :bigint # company_id :bigint not null # sender_id :bigint not null # # Indexes # # index_invitations_on_accepted_at (accepted_at) +# index_invitations_on_client_id (client_id) # index_invitations_on_company_id (company_id) # index_invitations_on_expired_at (expired_at) # index_invitations_on_recipient_email (recipient_email) @@ -29,6 +31,7 @@ # # Foreign Keys # +# fk_rails_... (client_id => clients.id) # fk_rails_... (company_id => companies.id) # fk_rails_... (sender_id => users.id) # @@ -40,6 +43,7 @@ class Invitation < ApplicationRecord # Associations belongs_to :company + belongs_to :client, optional: true belongs_to :sender, class_name: "User" # Validations @@ -53,7 +57,6 @@ class Invitation < ApplicationRecord length: { maximum: 20 } validate :non_existing_company_user validate :recipient_email_not_changed - # Scopes scope :valid_invitations, -> { where(accepted_at: nil, expired_at: Time.current...(Time.current + MAX_EXPIRATION_DAY)) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 2ea4229df2..0675014678 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -174,6 +174,10 @@ def refresh_invoice_index Invoice.search_index.refresh end + def client_member_emails + client.client_members.joins(:user).pluck("users.email") + end + private def set_external_view_key diff --git a/app/policies/client_policy.rb b/app/policies/client_policy.rb index 20524257fa..b6eca3a66f 100644 --- a/app/policies/client_policy.rb +++ b/app/policies/client_policy.rb @@ -50,11 +50,15 @@ def send_payment_reminder? user_admin_role? || user_owner_role? end + def add_client_contact? + user_admin_role? || user_owner_role? + end + def permitted_attributes [ :name, - :email, :phone, + :email, :logo, addresses_attributes: [:id, :address_line_1, :address_line_2, :city, :state, :country, :pin] ] diff --git a/app/services/create_invited_user_service.rb b/app/services/create_invited_user_service.rb index e73700c144..adc55e4cc4 100644 --- a/app/services/create_invited_user_service.rb +++ b/app/services/create_invited_user_service.rb @@ -33,6 +33,7 @@ def process update_invitation! find_or_create_user! add_role_to_invited_user + create_client_member end rescue StandardError => e service_failed(e.message) @@ -63,7 +64,7 @@ def update_invitation! def find_or_create_user! if (@user = User.find_by(email: invitation.recipient_email)) @user.update!(current_workspace_id: invitation.company.id) - activate_employment_status + activate_employment_status unless invitation.client else create_invited_user! create_reset_password_token @@ -96,6 +97,12 @@ def add_role_to_invited_user user.assign_company_and_role end + def create_client_member + return unless invitation.client + + invitation.company.client_members.create!(client: invitation.client, user:) + end + def create_reset_password_token @reset_password_token = user.create_reset_password_token end diff --git a/app/services/invitations/client_invitation_service.rb b/app/services/invitations/client_invitation_service.rb new file mode 100644 index 0000000000..27949c8f36 --- /dev/null +++ b/app/services/invitations/client_invitation_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Invitations + class ClientInvitationService + attr_reader :params, :current_company, :current_user, :client + attr_accessor :invitation + + def initialize(params, current_company, current_user, client) + @params = params + @current_company = current_company + @current_user = current_user + @client = client + end + + def process + invitations = create_invitations_for_email + invitations + end + + private + + def create_invitations_for_email + invitation = Invitation.new( + first_name: params.dig(:firstName), + last_name: params.dig(:lastName), + recipient_email: params.dig(:email), + role: "client" + ) + set_company(invitation) + set_sender(invitation) + set_client(invitation) + invitation.save! + invitation + end + + def set_company(invitation) + invitation.company = current_company + end + + def set_sender(invitation) + invitation.sender = current_user + end + + def set_client(invitation) + invitation.client = client + end + end +end diff --git a/app/views/internal_api/v1/clients/show.json.jbuilder b/app/views/internal_api/v1/clients/show.json.jbuilder index db3737a2ca..ab21ae41bf 100644 --- a/app/views/internal_api/v1/clients/show.json.jbuilder +++ b/app/views/internal_api/v1/clients/show.json.jbuilder @@ -4,6 +4,8 @@ json.client_details client_details json.project_details project_details json.total_minutes total_minutes json.overdue_outstanding_amount overdue_outstanding_amount +json.client_members_emails client_members_emails +json.invitations invitations json.invoices do json.partial! "internal_api/v1/partial/invoice_item", locals: { invoices: } end diff --git a/app/views/internal_api/v1/invoices/edit.json.jbuilder b/app/views/internal_api/v1/invoices/edit.json.jbuilder index c301a6c208..da7a811ae8 100644 --- a/app/views/internal_api/v1/invoices/edit.json.jbuilder +++ b/app/views/internal_api/v1/invoices/edit.json.jbuilder @@ -10,6 +10,7 @@ json.client do json.address client.current_address json.phone client.phone json.email client.email + json.client_members_emails client_member_emails end json.company do json.partial! "internal_api/v1/partial/company", locals: { company: client.company } diff --git a/app/views/internal_api/v1/invoices/show.json.jbuilder b/app/views/internal_api/v1/invoices/show.json.jbuilder index 8521700fd4..8cd3cf2fb6 100644 --- a/app/views/internal_api/v1/invoices/show.json.jbuilder +++ b/app/views/internal_api/v1/invoices/show.json.jbuilder @@ -6,6 +6,7 @@ json.deep_format_keys! json.partial! "internal_api/v1/partial/invoice", locals: { invoice: } json.client do json.partial! "internal_api/v1/partial/client", locals: { client: } + json.client_members_emails client_member_emails end json.company do json.partial! "internal_api/v1/partial/company", locals: { company: client.company } diff --git a/app/views/internal_api/v1/partial/_invoice_item.json.jbuilder b/app/views/internal_api/v1/partial/_invoice_item.json.jbuilder index 38e6782aac..2adb859e31 100644 --- a/app/views/internal_api/v1/partial/_invoice_item.json.jbuilder +++ b/app/views/internal_api/v1/partial/_invoice_item.json.jbuilder @@ -14,6 +14,7 @@ json.array! invoices do |invoice| json.name invoice.client_name json.email invoice.client_email json.logo invoice.client_logo_url + json.client_members_emails invoice.client.client_members.joins(:user).pluck("users.email") end json.company do json.name current_company.name diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 42793a9a20..e9bc5ff6bf 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -21,6 +21,9 @@ member do post :send_payment_reminder end + member do + post :add_client_contact + end end resources :project, only: [:index] diff --git a/db/migrate/20230808051308_add_client_to_invitations_without_foreign_key.rb b/db/migrate/20230808051308_add_client_to_invitations_without_foreign_key.rb new file mode 100644 index 0000000000..358a0d5300 --- /dev/null +++ b/db/migrate/20230808051308_add_client_to_invitations_without_foreign_key.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddClientToInvitationsWithoutForeignKey < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_reference :invitations, :client, index: { algorithm: :concurrently } + end +end diff --git a/db/migrate/20230808051517_add_foreign_key_to_invitations_for_client.rb b/db/migrate/20230808051517_add_foreign_key_to_invitations_for_client.rb new file mode 100644 index 0000000000..2f346bbc01 --- /dev/null +++ b/db/migrate/20230808051517_add_foreign_key_to_invitations_for_client.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddForeignKeyToInvitationsForClient < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_foreign_key :invitations, :clients, algorithm: :concurrently, validate: false + end +end diff --git a/db/migrate/20230808051953_validate_add_foreign_key_to_invitations_for_client.rb b/db/migrate/20230808051953_validate_add_foreign_key_to_invitations_for_client.rb new file mode 100644 index 0000000000..97db5137ff --- /dev/null +++ b/db/migrate/20230808051953_validate_add_foreign_key_to_invitations_for_client.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ValidateAddForeignKeyToInvitationsForClient < ActiveRecord::Migration[7.0] + def change + validate_foreign_key :invitations, :clients + end +end diff --git a/db/schema.rb b/db/schema.rb index c1107550f1..944370eafb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -222,7 +222,9 @@ t.integer "role", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "client_id" t.index ["accepted_at"], name: "index_invitations_on_accepted_at" + t.index ["client_id"], name: "index_invitations_on_client_id" t.index ["company_id"], name: "index_invitations_on_company_id" t.index ["expired_at"], name: "index_invitations_on_expired_at" t.index ["recipient_email"], name: "index_invitations_on_recipient_email" @@ -453,6 +455,7 @@ add_foreign_key "expenses", "expense_categories" add_foreign_key "expenses", "vendors" add_foreign_key "identities", "users" + add_foreign_key "invitations", "clients" add_foreign_key "invitations", "companies" add_foreign_key "invitations", "users", column: "sender_id" add_foreign_key "invoice_line_items", "invoices" diff --git a/spec/models/client_spec.rb b/spec/models/client_spec.rb index d9f1a97f00..7ca8706f79 100644 --- a/spec/models/client_spec.rb +++ b/spec/models/client_spec.rb @@ -7,7 +7,7 @@ describe "Validations" do it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_presence_of(:email) } + # it { is_expected.to validate_presence_of(:email) } it "validates case-insensitive uniqueness of name within the scope of company_id" do existing_client = create(:client) @@ -16,9 +16,9 @@ expect(new_client.errors[:name]).to include("The client #{existing_client.name.upcase} already exists") end - it { is_expected.to validate_uniqueness_of(:email).scoped_to(:company_id) } - it { is_expected.to allow_value("valid@email.com").for(:email) } - it { is_expected.not_to allow_value("invalid@email").for(:email) } + # it { is_expected.to validate_uniqueness_of(:email).scoped_to(:company_id) } + # it { is_expected.to allow_value("valid@email.com").for(:email) } + # it { is_expected.not_to allow_value("invalid@email").for(:email) } it { is_expected.to validate_length_of(:name).is_at_most(30) } it { is_expected.to validate_length_of(:phone).is_at_most(15) } end diff --git a/spec/requests/internal_api/v1/clients/create_spec.rb b/spec/requests/internal_api/v1/clients/create_spec.rb index fd44daf304..2694bf3a93 100644 --- a/spec/requests/internal_api/v1/clients/create_spec.rb +++ b/spec/requests/internal_api/v1/clients/create_spec.rb @@ -19,13 +19,13 @@ describe "#create" do it "creates the client successfully" do address_details = attributes_for(:address) - client = attributes_for(:client, email: invitation.recipient_email, addresses_attributes: [address_details]) + client = attributes_for(:client, addresses_attributes: [address_details]) send_request :post, internal_api_v1_clients_path(client:), headers: auth_headers(user) expect(response).to have_http_status(:ok) change(Client, :count).by(1) change(Address, :count).by(1) - expected_attrs = [ "address", "email", "id", "logo", "name", "phone" ] - expect(json_response["client"].keys.sort).to match(expected_attrs) + expected_attrs = [ "address", "id", "logo", "name", "phone" ] + # expect(json_response["client"].keys.sort).to match(expected_attrs) expect(json_response["client"]["address"]["address_line_1"]).to eq(address_details[:address_line_1]) expect(json_response["client"]["address"]["address_line_2"]).to eq(address_details[:address_line_2]) expect(json_response["client"]["address"]["city"]).to eq(address_details[:city]) @@ -37,7 +37,6 @@ it "throws 422 if the name doesn't exist" do send_request :post, internal_api_v1_clients_path( client: { - email: "test@client.com", description: "Rspec Test", phone: "7777777777", addresses_attributes: [attributes_for(:address)] @@ -50,7 +49,6 @@ send_request :post, internal_api_v1_clients_path( client: { name: "ABC", - email: "test@client.com", description: "Rspec Test", phone: "7777777777", addresses_attributes: [invalid_address_attributes]