feat: migrate CRM screens to Ant Design Vue and expand home dashboard

Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
This commit is contained in:
2026-11-03 16:51:51 +03:30
parent 631a3267d1
commit 26792f9a37
80 changed files with 19982 additions and 24020 deletions
+95 -166
View File
@@ -5,6 +5,20 @@ import { api } from '@/lib/api';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import { brand, type NoteAiPayloadDto, type NoteAiSuggestionDto } from '@mento/shared';
import { rewriteGregorianDates } from '@/lib/jalali';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Checkbox as ACheckbox,
Flex as AFlex,
Input as AInput,
Space as ASpace,
Textarea as ATextarea,
Typography,
} from 'ant-design-vue';
const { Text: ATypographyText, Paragraph: ATypographyParagraph, Title: ATypographyTitle } =
Typography;
const props = defineProps<{
noteId: string;
@@ -148,193 +162,108 @@ function formatPayloadSummary(value: NoteAiPayloadDto | null) {
</script>
<template>
<section class="ai-panel">
<header>
<strong>پیشنهاد {{ brand.aiName }}</strong>
<button
<a-card :bordered="true" :style="{ marginTop: '12px' }">
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
<a-typography-text strong>پیشنهاد {{ brand.aiName }}</a-typography-text>
<a-button
v-if="!isPending"
type="button"
class="link"
type="link"
:disabled="loading"
@click="analyze"
>
{{ loading ? '…' : 'تحلیل مجدد' }}
</button>
<button
</a-button>
<a-button
v-else-if="!suggestion"
type="button"
class="link"
type="link"
:disabled="loading"
@click="analyze"
>
{{ loading ? 'در حال تحلیل…' : 'تحلیل با AI' }}
</button>
</header>
</a-button>
</a-flex>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading && !suggestion" class="muted">در حال تحلیل یادداشت…</p>
<p v-else-if="suggestion?.status === 'failed'" class="error">
{{ suggestion.errorMessage || 'تحلیل ناموفق بود' }}
<button type="button" class="link" @click="analyze">تلاش دوباره</button>
</p>
<p v-else-if="!suggestion" class="muted">
<button type="button" class="link" :disabled="loading" @click="analyze">
تحلیل این یادداشت با AI
</button>
</p>
<a-alert v-if="error" type="error" show-icon :message="error" :style="{ marginTop: '8px' }" />
<a-typography-text v-else-if="loading && !suggestion" type="secondary">
در حال تحلیل یادداشت…
</a-typography-text>
<a-flex v-else-if="suggestion?.status === 'failed'" vertical :gap="4">
<a-alert
type="error"
show-icon
:message="suggestion.errorMessage || 'تحلیل ناموفق بود'"
/>
<a-button type="link" @click="analyze">تلاش دوباره</a-button>
</a-flex>
<a-button
v-else-if="!suggestion"
type="link"
:disabled="loading"
@click="analyze"
>
تحلیل این یادداشت با AI
</a-button>
<template v-else-if="isPending && payload">
<p v-if="formatPayloadSummary(payload)" class="summary">{{ formatPayloadSummary(payload) }}</p>
<a-typography-paragraph
v-if="formatPayloadSummary(payload)"
type="secondary"
:style="{ marginTop: '8px' }"
>
{{ formatPayloadSummary(payload) }}
</a-typography-paragraph>
<div v-if="payload.nextAction" class="block">
<label class="check">
<input v-model="applyNextAction" type="checkbox" />
کار بعدی
</label>
<input v-model="nextTitle" type="text" placeholder="عنوان کار بعدی" />
<JalaliDateInput v-model="nextDueAt" mode="datetime" />
</div>
<a-flex v-if="payload.nextAction" vertical :gap="8" :style="{ marginTop: '8px' }">
<a-checkbox v-model:checked="applyNextAction">کار بعدی</a-checkbox>
<a-input v-model:value="nextTitle" placeholder="عنوان کار بعدی" style="width: 100%" />
<JalaliDateInput v-model="nextDueAt" mode="datetime" style="width: 100%" />
</a-flex>
<div v-if="memoryItems.length" class="block">
<p class="block-title">حافظه مشتری (با تأیید شما ذخیره می‌شود)</p>
<p v-if="!hasPerson" class="warn">برای ذخیره حافظه، یادداشت باید به یک شخص متصل باشد.</p>
<label
<a-flex v-if="memoryItems.length" vertical :gap="8" :style="{ marginTop: '12px' }">
<a-typography-title :level="5" :style="{ margin: 0 }">
حافظه مشتری (با تأیید شما ذخیره می‌شود)
</a-typography-title>
<a-alert
v-if="!hasPerson"
type="warning"
show-icon
message="برای ذخیره حافظه، یادداشت باید به یک شخص متصل باشد."
/>
<a-flex
v-for="(_, index) in memoryItems"
:key="index"
class="memory-row"
:gap="8"
align="flex-start"
>
<input
v-model="selectedMemory[index]"
type="checkbox"
<a-checkbox
v-model:checked="selectedMemory[index]"
:disabled="!hasPerson"
/>
<textarea v-model="memoryEdits[index]" rows="2" />
</label>
</div>
<a-textarea v-model:value="memoryEdits[index]" :rows="2" style="width: 100%" />
</a-flex>
</a-flex>
<div class="actions">
<button type="button" class="btn" :disabled="applying" @click="apply">
{{ applying ? '…' : 'اعمال انتخاب‌شده' }}
</button>
<button type="button" class="btn btn-ghost" :disabled="applying" @click="dismiss">
رد کردن
</button>
</div>
<a-space :size="8" wrap :style="{ marginTop: '12px' }">
<a-button type="primary" :disabled="applying" :loading="applying" @click="apply">
اعمال انتخاب‌شده
</a-button>
<a-button :disabled="applying" @click="dismiss">رد کردن</a-button>
</a-space>
</template>
<p v-else-if="suggestion?.status === 'applied'" class="muted ok">پیشنهاد اعمال شد.</p>
<p v-else-if="suggestion?.status === 'dismissed'" class="muted">پیشنهاد رد شد.</p>
</section>
<a-typography-text
v-else-if="suggestion?.status === 'applied'"
type="success"
:style="{ display: 'block', marginTop: '8px' }"
>
پیشنهاد اعمال شد.
</a-typography-text>
<a-typography-text
v-else-if="suggestion?.status === 'dismissed'"
type="secondary"
:style="{ display: 'block', marginTop: '8px' }"
>
پیشنهاد رد شد.
</a-typography-text>
</a-card>
</template>
<style scoped>
.ai-panel {
margin-top: 0.75rem;
padding: 0.75rem 0.85rem;
border: 1px dashed var(--accent);
border-radius: var(--radius);
background: rgba(255, 255, 255, 0.65);
display: grid;
gap: 0.6rem;
min-width: 0;
overflow-wrap: anywhere;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.link {
border: none;
background: transparent;
color: var(--accent);
cursor: pointer;
font: inherit;
}
.block {
display: grid;
gap: 0.45rem;
}
.block-title {
margin: 0;
font-weight: 600;
}
.check {
display: flex;
align-items: center;
gap: 0.4rem;
}
.memory-row {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.45rem;
align-items: start;
}
input[type='text'],
textarea {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.55rem 0.7rem;
font: inherit;
width: 100%;
min-width: 0;
max-width: 100%;
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.actions .btn {
min-width: 0;
flex: 1 1 auto;
}
@media (max-width: 520px) {
.actions {
flex-direction: column;
}
.actions .btn,
.actions .btn-ghost {
width: 100%;
}
}
.summary {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.muted {
margin: 0;
color: var(--muted);
}
.ok {
color: #067647;
}
.warn {
margin: 0;
color: #b54708;
font-size: 0.88rem;
}
.error {
margin: 0;
color: #b42318;
}
</style>