CREATING COOL CRM OPERATIONS DASHBOARD FOR AUTOMATING COMMUNICATIONS FOR YOUR RAILS APP

By: Saurav

2017-10-11 22:15:00 UTC

I have used active admin in almost all B2B and B2C products I have developed and one thing common among all the requirements was to develop a CRM type admin interface for operations team. I can't go in detail about each product but based on my experiences I wanted to share how to develop a cool CRM or operation automation platform for your rails app.

I consider myself a pool swimmer in this vast ocean of software development and have to learn a lot. So, the more I become successful at integrating complex requirements the more complex cases I will be simplifying with my blogs. For now, lets deal with a basic requirement.

For a reservation based application, the operation team wanted to have complete control over the process. Once the renter books a particular date for an entity, the operation team should get notifications through email as well as on the internal dashboard. After the owner approves the request, the operation team moves ahead with invoicing. The team wanted to have control over payments and wanted to rely on sending invoice through email. Once the payment is received, the team will make the contact and pickup address available through email. They should be able to track the status of payments as well as send email asking for reviews after the reservation date was over.

For a simple reservation model, in the rails app, I decided to construct the admin dashboard using active admin. I won't explain all the schema in my rails app. for this case I would only talk about one resource i.e. Reservation

Gems I specifically needed:

gem 'devise'
gem 'activeadmin'

Installing active admin is easy Check Active Admin Docs.
Run bundle now.

I will re-post few steps from the website itself

Run rails g active_admin:install which will generate following files:

app/admin/dashboard.rb
app/assets/javascripts/active_admin.js
app/assets/stylesheets/active_admin.scss
config/initializers/active_admin.rb

Now run:

rake db:migrate
rake db:seed
rails server

Visit http://localhost:3000/admin and log in as the default user:

User: admin@example.com
Password: password

To register an existing model with Active Admin:

rails generate active_admin:resource MyModel

Now, in my case I wanted the admin to have complete control over the process and have a dashboard which shows all the resources (including reservation resource). So, I ran the command above for all the models in my rails app.

For any model, activeadmin registers the model involved. The permitted parameters as well as the customization code goes in that block.

I wanted to have scopes in my admin dashboard to have easy access to viewall, requested, approved, billed, paid and onhold reservation. I declared the scopes in my Reservation model itself (in your rails model, not activeadmin directory). In the schema, I am using a status enumeration to hold status of the reservation (You can also use state machine gem and have a state machine instead):

scope :viewall, -> { }
scope :requested, -> { where(:status => "requested")}
scope :approved, -> {where(:status => "approved")}
scope :billed, -> {where(:status => "billed")}
scope :paid, -> {where(:status => "paid")}
scope :onhold, ->{where(:status => "onhold")}

enum status: {:requested => 'requested', :approved => 'approved', :billed => 'billed', :paid => 'paid', :onhold  => 'onhold'}

For changing states, I decided to have class methods in my Reservation class (In app /models directory) which can be called from anywhere on the resource itself. The clas methods which we will need are:

def set_as_requested
 self.status = :requested
end

def mark_as_approved
 self.status = :approved 
end

def set_as_billed
 self.status = :billed
end

def set_as_paid
 self.status = :paid
end

def set_as_onhold
 self.status = :onhold    		
end

Having set the basic active admin setup for all the resource, now its time to focus on the reservation dashboard and give the operation team the automation they desire.

In the ActiveAdmin.register Reservation do block, to have the scopes as link which admin can click and view scoped resources, I added:

scope :requested
scope :approved
scope :billed
scope :paid
scope :onhold
scope :viewall

I also had to permit parameters from model so activeadmin would be able to edit them.

permit_params :start_time, :end_time, :numberofguests, :booking_date, :total_cost, :status

Now, for the view, by default all columns would be shown. You can specify the columns shown on the reservation dashboard using index block and give them custom names:

index do 
	column :id
	column "Booking Date", :booking_date
	column "Total Cost", :total_cost
	column "Start Time", :start_time
	column "End Time", :end_time
	column "Number of Guests", :numberofguests
	column "User id", :user_id	

I wanted to have edit, view and delete links for the resources but without any header name so I added a block:

	column "" do |resource|
        	links = ''.html_safe
        	links += link_to I18n.t('active_admin.edit'), edit_resource_path(resource), :class => "member_link edit_link"
        	links += link_to I18n.t('active_admin.view'), resource_path(resource), :class => "member_link view_link"
        	links += link_to I18n.t('active_admin.delete'), resource_path(resource), :method => :delete, :confirm => I18n.t('active_admin.delete_confirmation'), :class => "member_link delete_link"
        	links
        end

The admin should be able to see the status of reservation, so I added:

	column "Status", :status 

The admin should be able to see the status of an invoice being sent, payment being received, able to undo changes or put the resource on hold and then take next step. Also, all this process should be automated, so I added:

	column "Invoice sent?" do |reservations|
    		link_to "Yes, Mark as Billed", markasbilled_admin_reservation_path(reservations)  
	end

	column "Payment Received?" do |reservations|
		link_to "Yes, Mark as Paid", markaspaid_admin_reservation_path(reservations)
	end

	column "Put on Hold?" do |reservations|
    		link_to "Yes, Put on hold", markasonhold_admin_reservation_path(reservations)
	end

	column "Mark as just Requested?" do |reservations|
    		link_to "Yes, Go back and mark as Requestd", markasrequested_admin_reservation_path(reservations)
	end

The methods being called in the block would be part of the reservation admin controller member_action. For the flow here I would explain it later but in actual code, add controller block before index (view)

The admin should be able to see the status review being submitted, so I added:

column "Reviewed", :reviewed

If its a no, they should click a button which will send the renter an email request to add a review. This process should be automated, and done through click of a button.

   column "Ask Reserver for reviews?" do |reservations|
        link_to "Yes, Send Email", askforreview_admin_reservation_path(reservations)
    end

end

Lets move on to contoller and the automating methods now.

In the controller block, I added the helper methods which will be used by member_actions to be able to automate communications.

controller do 

For sending emails I am using sendgrid. I created a class EmailNotifier in my lib folder which provides a public method to send email. I wrapped external api using a private methods under begin rescuce block with proper error catching. This implementation is a topic for another blog post. We can also have background task for the purpose of sending emails using sidekiq gem but for now lets just say we have the public interface available which I am using here in the next helper method.

Note: To allow lib files to be available across and in active admin as well, I added

config.autoload_paths += %W(#{Rails.root}/lib) 

in config/application.rb in the class Application block. This is an important step.

    def send_email_with_reservation_details(reservation, subject, message, user)
      from = 'operationteam@xyz.com'          
      content = "custom html content"
      notifier = EmailNotifier.new(from, user, subject, content)
      notifier.send
    end

For Twilio, I followed the same methodology as sendgrid class and provided a helper method here for the admin member_action to use.

   def send_text(send_to, message)
      @notifier = Twilionotifier.new
      @notifier.notify_through_text(send_to, message)
    end

end

I have deleted few other methods which is crucial for the app to work but not important for the sake of this blog.

Finally, you need to add member_actions to be able to interact with active record in active admin block. Just like we create member routes to add additional routes apart from the seven routes in rails app routes.rb, we have to create member_action routes for active admin buttons we created in the reservation dashboard. In each active admin resource, you can use "resource" to refer to the active admin model (Class) you are using so in this case, resource is the Reservation.

For marking as requested, if in case, someone accidentaly clicked other buttons to change the state, the member action will be as below. Declaring class methods in the app/models comes in handy now, as we can call those methods to implement database changes. In the method below, I called set_default_role class method which updates the status of reservation ro requested.

Note: There are few methods which I would have added to the helper (I am using it in my app) but here, I am just using them. These methods are used to interact with the database.

member_action :markasrequested, method: :get do
    resource.set_as_requested

    if resource.save!        
    	redirect_to admin_reservations_path, notice: "Status marked as Requested!"        
    else
    	redirect_to admin_reservations_path, notice: "Error while changing status!'"
    end

end

Similarly, I added other member_actions using get action and also used helpers for sending appropriate emails and texts. I also included notification to be sent to appropriate users on their home page.

Mark as billed sends automated email and text to the reserver to check their email. Before this step, the operation team has to send invoice email having a method to pay for the reservation.

member_action :markasbilled, method: :get do
    resource.set_as_billed

    if resource.save!
    	redirect_to admin_reservations_path, notice: "Status marked as billed!"
        @user = User.find_by_id(resource.user_id)        
        @notification = Notification.new send_to: "#{@user.email}" , message: "Hi! The reservation id number #{resource.id} for #{resource.booking_date} was accepted. We have sent you an invoice, please check you email inbox"
        @notification.save!        

        begin 
        	send_email_with_reservation_details(resource, "Reservation payment invoice sent", "Hi! The reservation id number #{resource.id} for #{resource.booking_date} was accepted. We have sent you an invoice, please check your email inbox for the email id you used to login", ["#{@user.email}"])        

        rescue
        	p error in sending email
        end

        if (getpayment(@user) == true)
        	begin
            	send_text(getpayment(@user).contact_number, "Hi! The reservation id number #{resource.id} for #{resource.booking_date} was accepted. We have sent you an invoice, please check your email inbox for 
            	the email id you used to login")
            rescue
        		p error in sending text
        	end
        end
    else
    	redirect_to admin_reservations_path, notice: "Error while changing status!'"
    end

end

Mark as paid sends automated email and text to the renter and the owner and disclose details and pickup address. Before this step, the operation team has to must have received payments

member_action :markaspaid , method: :get do
    resource.set_as_paid
    if resource.save!
    	redirect_to admin_reservations_path, notice: "Status marked as paid!"
        @user = User.find_by_id(resource.user_id)        
        @notification = Notification.new send_to: "#{@user.email}" , message: "Hi! The payment on reservation id #{resource.id} for #{resource.booking_date} was accepted. Here are the owner details: Here are the owner and pickup details: Contact: #{get_owners_contact(resource)} \n #{get_pickup_address(resource)}"
        @notification.save!        

        @owner_notification = Notification.new send_to: "#{get_reservation_listing_owner(resource).email}" , message: "Hi! The payment on reservation id #{resource.id} for #{resource.booking_date} was accepted. We have made your contact informations available to #{@user.email}. Please get in touch or wait from the renter to contact you"
        @owner_notification.save!

        begin

        	send_email_with_reservation_details(resource, "Your payment was accepted", "Hi! The payment on reservation id #{resource.id} for #{resource.booking_date} was accepted. We have made your contact informations available to #{@user.email}. Please get in touch or wait from the renter to contact you", ["#{get_reservation_listing_owner(resource).email}"]) 
        rescue

        end

        begin

        	send_email_with_reservation_details(resource, "Your payment was accepted", "Hi! The payment on reservation id #{resource.id} for #{resource.booking_date} was accepted. Here are the owner and pickup details: Contact: #{get_owners_contact(resource)} \n #{get_pickup_address(resource)}", ["#{@user.email}"])        

        rescue

        end


        if (getpayment(@user) == true)

        begin
            
            send_text(getpayment(@user).contact_number, "Hi! The payment on reservation id #{resource.id} for #{resource.booking_date} was accepted. Here are the owner and pickup details: Contact: #{get_owners_contact(resource)} \n #{get_pickup_address(resource)}")

        rescue

        end

        end

    else
    	redirect_to admin_reservations_path, notice: "Error while changing status!'"
    end
end

Mark as onhold marks a reservation on hold and informs the reserver to contact the operation team

member_action :markasonhold, method: :get do
    resource.set_as_onhold

   if resource.save!
    	redirect_to admin_reservations_path, notice: "Status marked as onhold"
        @user = User.find_by_id(resource.user_id)        
        @notification = Notification.new send_to: "#{@user.email}" , message: "Hi!The reservation id number #{resource.id} for #{resource.booking_date} has been put on hold, please contact us to resolve the situation"
        @notification.save!       

        begin
        	send_email_with_reservation_details(resource, "Reservation is on hold", "The reservation id number #{resource.id} for #{resource.booking_date} has been put on hold, please contact us to resolve the situation", ["#{@user.email}"])         
        rescue

        end

        if (getpayment(@user) == true)

        	begin

            	send_text(getpayment(@user).contact_number, "The reservation id number #{resource.id} for #{resource.booking_date} has been put on hold, please contact us to resolve the situation")
            rescue

            end
        end
    else
    	redirect_to admin_reservations_path, notice: "Error while changing status!'"
    end
end

Finally, ask for review sends a review request to the reserver only after the reservation date has gone by and the reserver hasn't added a review yet. This process can be automated totally by background task, but this method gives extra power to the operation team to automate sending the request of review whenever they want.

member_action :askforreview, method: :get do

    @reserver = User.find_by_id(resource.user_id)
    
    #send an email
        @notification = Notification.new send_to: "#{@reserver.email}" , message: "Hi!We Hope you enjoyed the reservation id number #{resource.id} for #{resource.booking_date}. For us you are a very important customer and we would request your reviews for the trip. Please go to the listing page and submit a review. We will release money to the owner only when you are satisfied."

        if @notification.save! 

        begin
            send_email_with_reservation_details(resource, "Need reviews for your last trip. Owner is waiting for your reviews", "We hope you enjoyed the reservation id number #{resource.id} for #{resource.booking_date}. For us you are a very important customer and we would request your reviews for the trip. Please go to the listing page and submit a review. We will release money to the owner only when you are satisfied and had a great experience. We will judge it from your ratings", ["#{@reserver.email}"]) 
        rescue

        end
            redirect_to admin_reservations_path, notice: "Email for review has been sent"


            if (getpayment(@reserver) == true)

            begin
                send_text(getpayment(@reserver).contact_number, "We hope you enjoyed the reservation id number #{resource.id} for #{resource.booking_date}. For us you are a very important customer and we would request your reviews for the trip. Please go to the listing page and submit a review. We will release money to the owner only when you are satisfied and had a great experience. We will judge it from your ratings")

            rescue
            
            end

            end
        else
            redirect_to admin_reservations_path, notice: "Oops! Error while changing sending review request, please call the renter and ask for reviews"                        
        end

end



end

Activeadmin

This is how the basic automated operation dashboard looks like.

Owned & Maintained by Saurav Prakash

If you like what you see, you can help me cover server costs or buy me a cup of coffee though donation :)