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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user