237 lines
6.4 KiB
Vue
237 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, onUnmounted, 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');
|
|
}
|
|
|
|
function clearAsk() {
|
|
askAnswer.value = '';
|
|
askError.value = '';
|
|
askSources.value = [];
|
|
}
|
|
|
|
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 ?? [];
|
|
await nextTick();
|
|
document
|
|
.getElementById('home-ai-assistant')
|
|
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
function onAskHotkey(e: KeyboardEvent) {
|
|
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'k') return;
|
|
const tag = (e.target as HTMLElement | null)?.tagName;
|
|
if (tag === 'TEXTAREA' || (tag === 'INPUT' && (e.target as HTMLInputElement).id !== 'home-hero-ask')) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
document.getElementById('home-hero-ask')?.focus();
|
|
}
|
|
|
|
onMounted(async () => {
|
|
window.addEventListener('keydown', onAskHotkey);
|
|
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;
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', onAskHotkey);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="dashboard">
|
|
<HomeHero
|
|
:greeting="greeting"
|
|
:first-name="firstName"
|
|
:user-initial="userInitial"
|
|
:alerts="home?.alerts ?? []"
|
|
:asking="asking"
|
|
:ask-answer="askAnswer"
|
|
:ask-error="askError"
|
|
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
|
|
id="home-ai-assistant"
|
|
v-model:ask-query="askQuery"
|
|
:asking="asking"
|
|
:answer="askAnswer"
|
|
:error="askError"
|
|
:sources="askSources"
|
|
@ask="submitAsk"
|
|
@clear="clearAsk"
|
|
/>
|
|
</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>
|