diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index ed9d3ebc8..83f1ad186 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController end def agents - @agents ||= Current.account.users.order_by_full_name + @agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] }) end end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 55428b24a..c1e90d957 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -1,17 +1,32 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController + RESULTS_PER_PAGE = 15 protect_from_forgery with: :null_session before_action :check_authorization + before_action :set_current_page, only: [:index, :active, :search] before_action :fetch_contact, only: [:show, :update] def index - @contacts = Current.account.contacts + contacts = Current.account.contacts.where.not(email: [nil, '']).or(Current.account.contacts.where.not(phone_number: [nil, ''])) + @contacts_count = contacts.count + @contacts = fetch_contact_last_seen_at(contacts) + end + + def search + render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return + + contacts = Current.account.contacts.where.not(email: [nil, '']).or(Current.account.contacts.where.not(phone_number: [nil, ''])) + .where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%") + @contacts_count = contacts.count + @contacts = fetch_contact_last_seen_at(contacts) end # returns online contacts def active - @contacts = Current.account.contacts.where(id: ::OnlineStatusTracker + contacts = Current.account.contacts.where(id: ::OnlineStatusTracker .get_available_contact_ids(Current.account.id)) + @contacts_count = contacts.count + @contacts = contacts.page(@current_page) end def show; end @@ -36,13 +51,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController }, status: :unprocessable_entity end - def search - render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return + private - @contacts = Current.account.contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%") + def set_current_page + @current_page = params[:page] || 1 end - private + def fetch_contact_last_seen_at(contacts) + contacts.left_outer_joins(:conversations) + .select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at') + .group('contacts.id') + .includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }]) + .page(@current_page).per(RESULTS_PER_PAGE) + end def build_contact_inbox return if params[:inbox_id].blank? diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index a2a879376..0f573bbb9 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :check_authorization def index - @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, :avatar_attachment)) + @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] })) end def create diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 051b25679..2aa7cb321 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -62,9 +62,7 @@ class ConversationFinder end def find_all_conversations - @conversations = current_account.conversations.includes( - :assignee, :inbox, :taggings, contact: [:avatar_attachment] - ).where(inbox_id: @inbox_ids) + @conversations = current_account.conversations.where(inbox_id: @inbox_ids) end def filter_by_assignee_type @@ -106,6 +104,9 @@ class ConversationFinder end def conversations + @conversations = @conversations.includes( + :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } } + ) current_page ? @conversations.latest.page(current_page) : @conversations.latest end end diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb index 5295dce74..63c72bd8e 100644 --- a/app/finders/message_finder.rb +++ b/app/finders/message_finder.rb @@ -11,7 +11,7 @@ class MessageFinder private def conversation_messages - @conversation.messages.includes(:attachments, :sender) + @conversation.messages.includes(:attachments, :sender, sender: { avatar_attachment: [:blob] }) end def messages diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 0988141d3..6429c53d2 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -6,9 +6,17 @@ class ContactAPI extends ApiClient { super('contacts', { accountScoped: true }); } + get(page) { + return axios.get(`${this.url}?page=${page}`); + } + getConversations(contactId) { return axios.get(`${this.url}/${contactId}/conversations`); } + + search(search = '', page = 1) { + return axios.get(`${this.url}/search?q=${search}&page=${page}`); + } } export default new ContactAPI(); diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 7f5ef39c9..7f9041b93 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -26,3 +26,4 @@ @import 'views/signup'; @import 'plugins/multiselect'; +@import 'plugins/dropdown'; diff --git a/app/javascript/dashboard/assets/scss/app.scss b/app/javascript/dashboard/assets/scss/app.scss index 92edc8e42..3220f0326 100644 --- a/app/javascript/dashboard/assets/scss/app.scss +++ b/app/javascript/dashboard/assets/scss/app.scss @@ -2,6 +2,7 @@ @import 'shared/assets/stylesheets/colors'; @import 'shared/assets/stylesheets/spacing'; @import 'shared/assets/stylesheets/font-size'; +@import 'shared/assets/stylesheets/font-weights'; @import 'variables'; @import '~spinkit/scss/spinners/7-three-bounce'; diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss new file mode 100644 index 000000000..b4dada351 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -0,0 +1,27 @@ +.dropdown-pane.sleek { + @include elegant-card; + @include border-light; + padding-left: 0; + padding-right: 0; + right: -12px; + top: 48px; + width: auto; + + &::before { + @include arrow(top, var(--color-border-light), 14px); + position: absolute; + right: 6px; + top: -14px; + } + + &::after { + @include arrow(top, $color-white, var(--space-slab)); + position: absolute; + right: var(--space-small); + top: -12px; + } + + .dropdown>li>a:hover { + background: var(--color-background); + } +} diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 43f4640e0..77b12d9f8 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -30,7 +30,7 @@ -
+
diff --git a/app/javascript/dashboard/components/widgets/EmptyState.vue b/app/javascript/dashboard/components/widgets/EmptyState.vue index b6447ffd5..6bb8ecaed 100644 --- a/app/javascript/dashboard/components/widgets/EmptyState.vue +++ b/app/javascript/dashboard/components/widgets/EmptyState.vue @@ -1,12 +1,11 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue new file mode 100644 index 000000000..d34902d62 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue new file mode 100644 index 000000000..0661c9d68 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Footer.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Footer.vue new file mode 100644 index 000000000..70f27121a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Footer.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue new file mode 100644 index 000000000..6489b1ee9 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/routes.js b/app/javascript/dashboard/routes/dashboard/contacts/routes.js new file mode 100644 index 000000000..06a13c7db --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/routes.js @@ -0,0 +1,12 @@ +/* eslint arrow-body-style: 0 */ +import ContactsView from './components/ContactsView'; +import { frontendURL } from '../../../helper/URLHelper'; + +export const routes = [ + { + path: frontendURL('accounts/:accountId/contacts'), + name: 'contacts_dashboard', + roles: ['administrator', 'agent'], + component: ContactsView, + }, +]; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index bd9a52ab7..cbf6656d8 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -45,6 +45,7 @@