feat: Companies page (#12842)

# Pull Request Template

## Description

This PR introduces a new Companies section in the Chatwoot dashboard. It
lists all companies associated with the account and includes features
such as **search**, **sorting**, and **pagination** to enable easier
navigation and efficient management.

Fixes
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshot
<img width="1619" height="1200" alt="image"
src="https://github.com/user-attachments/assets/21f0a666-c3d6-4dec-bd02-1e38e0cd9542"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
Sivin Varghese
2025-11-18 15:29:15 +05:30
committed by GitHub
parent 58ca82c720
commit e33f28dc33
17 changed files with 763 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store';
import { debounce } from '@chatwoot/utils';
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue';
const DEBOUNCE_DELAY = 300;
const store = useStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const searchQuery = computed(() => route.query?.search || '');
const searchValue = ref(searchQuery.value);
const pageNumber = computed(() => Number(route.query?.page) || 1);
const activeSort = computed(() => {
const sortParam = route.query?.sort || 'name';
return sortParam.startsWith('-') ? sortParam.slice(1) : sortParam;
});
const activeOrdering = computed(() => {
const sortParam = route.query?.sort || 'name';
return sortParam.startsWith('-') ? '-' : '';
});
const companies = useMapGetter('companies/getCompaniesList');
const meta = useMapGetter('companies/getMeta');
const uiFlags = useMapGetter('companies/getUIFlags');
const isFetchingList = computed(() => uiFlags.value.fetchingList);
const sortParam = computed(() => {
return activeOrdering.value === '-'
? `-${activeSort.value}`
: activeSort.value;
});
const updateURLParams = (page, search = '', sort = '') => {
const query = {
...route.query,
page: page.toString(),
};
if (search) {
query.search = search;
} else {
delete query.search;
}
if (sort) {
query.sort = sort;
} else {
delete query.sort;
}
router.replace({ query });
};
const fetchCompanies = async (page, search, sort) => {
const currentPage = page ?? pageNumber.value;
const currentSearch = search ?? searchQuery.value;
const currentSort = sort ?? sortParam.value;
// Only update URL if arguments were explicitly provided
if (page !== undefined || search !== undefined || sort !== undefined) {
updateURLParams(currentPage, currentSearch, currentSort);
}
if (currentSearch) {
await store.dispatch('companies/search', {
search: currentSearch,
page: currentPage,
sort: currentSort,
});
} else {
await store.dispatch('companies/get', {
page: currentPage,
sort: currentSort,
});
}
};
const onSearch = debounce(query => {
searchValue.value = query;
fetchCompanies(1, query, sortParam.value);
}, DEBOUNCE_DELAY);
const onPageChange = page => {
fetchCompanies(page, searchValue.value, sortParam.value);
};
const handleSort = ({ sort, order }) => {
const newSortParam = order === '-' ? `-${sort}` : sort;
fetchCompanies(1, searchValue.value, newSortParam);
};
onMounted(() => {
searchValue.value = searchQuery.value;
fetchCompanies();
});
</script>
<template>
<CompaniesListLayout
:search-value="searchValue"
:header-title="t('COMPANIES.HEADER')"
:current-page="pageNumber"
:total-items="Number(meta.totalCount || 0)"
:active-sort="activeSort"
:active-ordering="activeOrdering"
:is-fetching-list="isFetchingList"
@update:current-page="onPageChange"
@update:sort="handleSort"
@search="onSearch"
>
<div v-if="isFetchingList" class="flex items-center justify-center p-8">
<span class="text-n-slate-11 text-base">{{
t('COMPANIES.LOADING')
}}</span>
</div>
<div
v-else-if="companies.length === 0"
class="flex items-center justify-center p-8"
>
<span class="text-n-slate-11 text-base">{{
t('COMPANIES.EMPTY_STATE.TITLE')
}}</span>
</div>
<div v-else class="flex flex-col gap-4 p-4">
<CompaniesCard
v-for="company in companies"
:id="company.id"
:key="company.id"
:name="company.name"
:domain="company.domain"
:contacts-count="company.contactsCount || 0"
:description="company.description"
:avatar-url="company.avatarUrl"
:updated-at="company.updatedAt"
/>
</div>
</CompaniesListLayout>
</template>

View File

@@ -0,0 +1,26 @@
import { frontendURL } from '../../../helper/URLHelper';
import CompaniesIndex from './pages/CompaniesIndex.vue';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
const commonMeta = {
featureFlag: FEATURE_FLAGS.COMPANIES,
permissions: ['administrator', 'agent'],
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
export const routes = [
{
path: frontendURL('accounts/:accountId/companies'),
component: CompaniesIndex,
meta: commonMeta,
children: [
{
path: '',
name: 'companies_dashboard_index',
component: CompaniesIndex,
meta: commonMeta,
},
],
},
];

View File

@@ -2,6 +2,7 @@ import settings from './settings/settings.routes';
import conversation from './conversation/conversation.routes';
import { routes as searchRoutes } from '../../modules/search/search.routes';
import { routes as contactRoutes } from './contacts/routes';
import { routes as companyRoutes } from './companies/routes';
import { routes as notificationRoutes } from './notifications/routes';
import { routes as inboxRoutes } from './inbox/routes';
import { frontendURL } from '../../helper/URLHelper';
@@ -23,6 +24,7 @@ export default {
...conversation.routes,
...settings.routes,
...contactRoutes,
...companyRoutes,
...searchRoutes,
...notificationRoutes,
...helpcenterRoutes.routes,