Files
CRM_Montzar/apps/web/src/views/HomeView.vue
T
MahdiandCursor 7757c0837d Add Ask Mento, customer mood, and Pipecat speech-to-text.
Ship assistant answers, contact moods, voice dictation, and the related CRM UI so these can be deployed to production.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 14:20:55 +03:30

207 lines
5.5 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/money';
import { toFaDigits } from '@/lib/jalali';
import HomeHero from '@/components/dashboard/HomeHero.vue';
import HomeKpiRow from '@/components/dashboard/HomeKpiRow.vue';
import HomeSalesTrend from '@/components/dashboard/HomeSalesTrend.vue';
import HomeSourcesChart from '@/components/dashboard/HomeSourcesChart.vue';
import HomeAiAssistant from '@/components/dashboard/HomeAiAssistant.vue';
import HomeRecentActivities from '@/components/dashboard/HomeRecentActivities.vue';
import HomeMiniPipeline from '@/components/dashboard/HomeMiniPipeline.vue';
import HomeTopCustomers from '@/components/dashboard/HomeTopCustomers.vue';
import axios from 'axios';
import type {
ActivityDto,
AskMentoResponse,
AskMentoSourceDto,
HomeDashboardDto,
PaginatedResponse,
PipelineBoardDto,
} from '@mento/shared';
const auth = useAuthStore();
const loading = ref(true);
const home = ref<HomeDashboardDto | null>(null);
const activities = ref<ActivityDto[]>([]);
const board = ref<PipelineBoardDto | null>(null);
const askQuery = ref('');
const asking = ref(false);
const askAnswer = ref('');
const askError = ref('');
const askSources = ref<AskMentoSourceDto[]>([]);
const firstName = computed(() => {
const parts = auth.user?.fullName?.trim().split(/\s+/) ?? [];
return parts[0] ?? 'کاربر';
});
const userInitial = computed(() => firstName.value.slice(0, 1));
const greeting = computed(() => {
const hour = new Date().getHours();
if (hour < 12) return 'صبح بخیر';
if (hour < 17) return 'ظهر بخیر';
return 'عصر بخیر';
});
function formatWhen(value: string | null) {
if (!value) return '';
try {
return new Intl.DateTimeFormat('fa-IR', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(value));
} catch {
return value;
}
}
function formatKpiValue(key: string, value: number) {
if (key === 'sales') {
return formatMoney(value, home.value?.currency ?? 'IRR');
}
return toFaDigits(value);
}
function formatChange(value: number | null) {
if (value == null) return '';
const sign = value >= 0 ? '+' : '';
return `${sign}${toFaDigits(value)}٪`;
}
function formatTrendAmount(value: number) {
return formatMoney(value, home.value?.currency ?? 'IRR');
}
async function submitAsk(preset?: string) {
const q = (preset || askQuery.value).trim();
if (!q || asking.value) return;
if (preset) askQuery.value = preset;
asking.value = true;
askError.value = '';
askAnswer.value = '';
askSources.value = [];
try {
const { data } = await api.post<AskMentoResponse>('/ai/ask', { question: q });
askAnswer.value = data.answer;
askSources.value = data.sources ?? [];
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
askError.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'پرسش ناموفق بود');
} else {
askError.value = 'پرسش ناموفق بود';
}
} finally {
asking.value = false;
}
}
onMounted(async () => {
loading.value = true;
try {
const [homeRes, actRes, boardRes] = await Promise.all([
api.get<HomeDashboardDto>('/reports/home'),
api.get<PaginatedResponse<ActivityDto>>('/activities', {
params: { limit: 5, page: 1 },
}),
api.get<PipelineBoardDto>('/pipelines/board'),
]);
home.value = homeRes.data;
activities.value = actRes.data.items;
board.value = boardRes.data;
} catch {
home.value = null;
} finally {
loading.value = false;
}
});
</script>
<template>
<section class="dashboard">
<HomeHero
:greeting="greeting"
:first-name="firstName"
:user-initial="userInitial"
:alerts="home?.alerts ?? []"
v-model:ask-query="askQuery"
@ask="submitAsk"
/>
<p v-if="loading" class="muted center">در حال بارگذاری داشبورد…</p>
<template v-else>
<HomeKpiRow
:kpis="home?.kpis ?? []"
:currency="home?.currency ?? 'IRR'"
:format-value="formatKpiValue"
:format-change="formatChange"
/>
<div class="mid-grid">
<HomeSalesTrend
:trend="home?.salesTrend ?? []"
:change-percent="home?.kpis?.[0]?.changePercent ?? null"
:format-change="formatChange"
:format-amount="formatTrendAmount"
/>
<HomeSourcesChart :sources="home?.sources ?? []" />
<HomeAiAssistant
v-model:ask-query="askQuery"
:asking="asking"
:answer="askAnswer"
:error="askError"
:sources="askSources"
@ask="submitAsk"
/>
</div>
<div class="bottom-grid">
<HomeRecentActivities :activities="activities" :format-when="formatWhen" />
<HomeMiniPipeline :board="board" :format-money="formatMoney" />
<HomeTopCustomers :customers="home?.topCustomers ?? []" />
</div>
</template>
</section>
</template>
<style scoped>
.dashboard {
display: grid;
gap: 1.15rem;
}
.mid-grid {
display: grid;
grid-template-columns: 1.25fr 1fr 0.88fr;
gap: 0.8rem;
align-items: stretch;
}
.bottom-grid {
display: grid;
grid-template-columns: 1fr 1.35fr 0.78fr;
gap: 0.8rem;
}
.muted {
color: #94a3b8;
}
.center {
text-align: center;
}
@media (max-width: 1200px) {
.mid-grid,
.bottom-grid {
grid-template-columns: 1fr;
}
}
</style>