feat: Contact Exports (#7258)
This commit is contained in:
@@ -42,6 +42,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def export
|
||||
column_names = params['column_names']
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||
end
|
||||
|
||||
# returns online contacts
|
||||
def active
|
||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
|
||||
@@ -75,6 +75,10 @@ class ContactAPI extends ApiClient {
|
||||
destroyAvatar(contactId) {
|
||||
return axios.delete(`${this.url}/${contactId}/avatar`);
|
||||
}
|
||||
|
||||
exportContacts() {
|
||||
return axios.get(`${this.url}/export`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
||||
@@ -73,6 +73,13 @@
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"EXPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Export",
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to dowanlod.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:search-query="searchQuery"
|
||||
:segments-id="segmentsId"
|
||||
:on-search-submit="onSearchSubmit"
|
||||
:on-export-submit="onExportSubmit"
|
||||
this-selected-contact-id=""
|
||||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
@@ -92,6 +93,7 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
@@ -110,6 +112,7 @@ export default {
|
||||
AddCustomViews,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
segmentsId: {
|
||||
@@ -386,6 +389,16 @@ export default {
|
||||
this.$store.dispatch('contacts/clearContactFilters');
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
onExportSubmit() {
|
||||
try {
|
||||
this.$store.dispatch('contacts/export');
|
||||
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
setParamsForEditSegmentModal() {
|
||||
// Here we are setting the params for edit segment modal to show the existing values.
|
||||
|
||||
|
||||
@@ -87,6 +87,16 @@
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="upload"
|
||||
class="clear"
|
||||
@click="onExportSubmit"
|
||||
>
|
||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -127,6 +137,10 @@ export default {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onExportSubmit: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onToggleFilter: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
|
||||
@@ -140,6 +140,20 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
export: async ({ commit }) => {
|
||||
try {
|
||||
await ContactAPI.exportContacts();
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
if (error.response?.data?.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
class Account::ContactsExportJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(account_id, column_names)
|
||||
account = Account.find(account_id)
|
||||
headers = valid_headers(column_names)
|
||||
generate_csv(account, headers)
|
||||
file_url = account_contact_export_url(account)
|
||||
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url)&.deliver_later
|
||||
end
|
||||
|
||||
def generate_csv(account, headers)
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << headers
|
||||
account.contacts.each do |contact|
|
||||
csv << headers.map { |header| contact.send(header) }
|
||||
end
|
||||
end
|
||||
|
||||
attach_export_file(account, csv_data)
|
||||
end
|
||||
|
||||
def valid_headers(column_names)
|
||||
columns = (column_names.presence || default_columns)
|
||||
headers = columns.map { |column| column if Contact.column_names.include?(column) }
|
||||
headers.compact
|
||||
end
|
||||
|
||||
def attach_export_file(account, csv_data)
|
||||
return if csv_data.blank?
|
||||
|
||||
account.contacts_export.attach(
|
||||
io: StringIO.new(csv_data),
|
||||
filename: "#{account.name}_#{account.id}_contacts.csv",
|
||||
content_type: 'text/csv'
|
||||
)
|
||||
end
|
||||
|
||||
def account_contact_export_url(account)
|
||||
Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export)
|
||||
end
|
||||
|
||||
def default_columns
|
||||
%w[id name email phone_number]
|
||||
end
|
||||
end
|
||||
@@ -43,6 +43,14 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_export_complete(file_url)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url = file_url
|
||||
subject = "Your contact's export file is available to download."
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_emails
|
||||
|
||||
@@ -77,6 +77,8 @@ class Account < ApplicationRecord
|
||||
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
|
||||
has_many :working_hours, dependent: :destroy_async
|
||||
|
||||
has_one_attached :contacts_export
|
||||
|
||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||
enum status: { active: 0, suspended: 1 }
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class ContactPolicy < ApplicationPolicy
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def export?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def search?
|
||||
true
|
||||
end
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<p>Hi</p>
|
||||
|
||||
|
||||
<p>Your contact export file is ready to download.</p>
|
||||
|
||||
<p>
|
||||
Click <a href="{{ action_url }}">here</a> to download the export file.
|
||||
</p>
|
||||
Reference in New Issue
Block a user