feat: add DISC profiles and a sales copilot as an org module
Help reps capture a customer's likely behavior style and get negotiation guidance, with the feature gated from super admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
||||
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
||||
@@ -36,6 +37,8 @@ import {
|
||||
PERSON_IMPORT_SOURCES,
|
||||
} from '@mento/shared';
|
||||
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
||||
import { canUseBehavior } from '@/lib/behavior-access';
|
||||
import DiscBadge from '@/components/behavior/DiscBadge.vue';
|
||||
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
||||
|
||||
type Tab = 'people' | 'companies';
|
||||
@@ -58,6 +61,12 @@ const moodFilter = computed<PersonMood | 'all'>(() => {
|
||||
const value = typeof route.query.mood === 'string' ? route.query.mood : 'all';
|
||||
return PERSON_MOODS.includes(value as PersonMood) ? (value as PersonMood) : 'all';
|
||||
});
|
||||
const auth = useAuthStore();
|
||||
const canBehavior = computed(() => canUseBehavior(auth.user));
|
||||
const discFilter = computed(() => {
|
||||
const value = typeof route.query.disc === 'string' ? route.query.disc.toUpperCase() : '';
|
||||
return /^[DISC](\/[DISC]){0,3}$/.test(value) ? value : '';
|
||||
});
|
||||
const sourceFilter = computed<PersonImportSource | 'all'>(() => {
|
||||
const value = typeof route.query.source === 'string' ? route.query.source : 'all';
|
||||
return PERSON_IMPORT_SOURCES.includes(value as PersonImportSource)
|
||||
@@ -135,6 +144,7 @@ const peopleSavedFilters = computed(() => ({
|
||||
sort: sort.value === 'name' ? undefined : sort.value,
|
||||
mood: moodFilter.value === 'all' ? undefined : moodFilter.value,
|
||||
source: sourceFilter.value === 'all' ? undefined : sourceFilter.value,
|
||||
disc: discFilter.value || undefined,
|
||||
}));
|
||||
|
||||
function applyPeopleSavedFilters(filters: Record<string, unknown>) {
|
||||
@@ -223,6 +233,7 @@ async function load() {
|
||||
mood: moodFilter.value === 'all' ? undefined : moodFilter.value,
|
||||
importSource:
|
||||
sourceFilter.value === 'all' ? undefined : sourceFilter.value,
|
||||
disc: discFilter.value || undefined,
|
||||
},
|
||||
}),
|
||||
api.get<PersonImportSourceStatDto[]>('/people/source-stats').catch(() => ({
|
||||
@@ -280,6 +291,10 @@ function setMood(next: PersonMood | 'all') {
|
||||
patchQuery({ mood: next === 'all' ? undefined : next, page: undefined });
|
||||
}
|
||||
|
||||
function setDisc(next: string) {
|
||||
patchQuery({ disc: next || undefined, page: undefined });
|
||||
}
|
||||
|
||||
function setSource(next: PersonImportSource | 'all') {
|
||||
patchQuery({
|
||||
source: next === 'all' ? undefined : next,
|
||||
@@ -394,7 +409,7 @@ function onImportSettled() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.tab, route.query.q, route.query.page, route.query.sort, route.query.mood],
|
||||
() => [route.query.tab, route.query.q, route.query.page, route.query.sort, route.query.mood, route.query.disc, route.query.source],
|
||||
() => {
|
||||
q.value = typeof route.query.q === 'string' ? route.query.q : '';
|
||||
void load();
|
||||
@@ -412,6 +427,9 @@ onMounted(load);
|
||||
lead="مدیریت و مشاهده اشخاص و شرکتها"
|
||||
>
|
||||
<template #actions>
|
||||
<RouterLink v-if="canBehavior" :to="{ name: 'disc-report' }">
|
||||
<UiButton variant="outline" size="sm">گزارش DISC</UiButton>
|
||||
</RouterLink>
|
||||
<UiButton variant="ghost" size="sm" :disabled="busy" :loading="pageBusy && busyMessage.includes('خروجی')" @click.stop="exportExcel">
|
||||
<span class="btn-full">خروجی اکسل</span>
|
||||
<span class="btn-short">خروجی</span>
|
||||
@@ -538,6 +556,13 @@ onMounted(load);
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'people' && canBehavior" class="source-stats">
|
||||
<button type="button" class="source-chip" :class="{ on: !discFilter }" @click="setDisc('')">همه تیپها</button>
|
||||
<button v-for="type in ['D', 'I', 'S', 'C']" :key="type" type="button" class="source-chip" :class="{ on: discFilter === type }" @click="setDisc(type)">
|
||||
{{ type }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
@@ -551,6 +576,7 @@ onMounted(load);
|
||||
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
|
||||
{{ p.fullName }}
|
||||
</RouterLink>
|
||||
<DiscBadge v-if="canBehavior" :label="p.discLabel" />
|
||||
<UiDropdownMenu
|
||||
:open="menuId === `m:${p.id}`"
|
||||
align="end"
|
||||
@@ -648,10 +674,11 @@ onMounted(load);
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
|
||||
{{ p.fullName }}
|
||||
</RouterLink>
|
||||
<div v-if="p.previousFullName" class="muted tiny">
|
||||
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
|
||||
{{ p.fullName }}
|
||||
</RouterLink>
|
||||
<DiscBadge v-if="canBehavior" :label="p.discLabel" />
|
||||
<div v-if="p.previousFullName" class="muted tiny">
|
||||
قبلی: {{ p.previousFullName }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user