Files
CRM_Montzar/apps/web/src/components/MentoMemoryPanel.vue
T
s.esmaeili 1ae87c97b7 feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography
- Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout
- Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions
- Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components
- Add draggable bottom-left QuickCreate floating action button
2026-09-12 01:33:07 +03:30

266 lines
6.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { brand, type MemoryItemDto, type PaginatedResponse } from '@mento/shared';
const props = defineProps<{
personId: string;
}>();
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const items = ref<MemoryItemDto[]>([]);
const draft = ref('');
const editingId = ref<string | null>(null);
const editContent = ref('');
const sourceLabel: Record<string, string> = {
note: 'از یادداشت',
manual: 'دستی',
};
async function load() {
if (!props.personId) return;
loading.value = true;
error.value = '';
try {
const { data } = await api.get<PaginatedResponse<MemoryItemDto>>('/memory', {
params: { personId: props.personId, limit: 100 },
});
items.value = data.items;
} catch {
error.value = 'بارگذاری حافظه مشتری ناموفق بود';
} finally {
loading.value = false;
}
}
async function addItem() {
if (!draft.value.trim()) return;
saving.value = true;
error.value = '';
try {
await api.post('/memory', {
personId: props.personId,
content: draft.value.trim(),
});
draft.value = '';
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ذخیره ناموفق بود');
} else {
error.value = 'ذخیره ناموفق بود';
}
} finally {
saving.value = false;
}
}
function startEdit(item: MemoryItemDto) {
editingId.value = item.id;
editContent.value = item.content;
}
function cancelEdit() {
editingId.value = null;
editContent.value = '';
}
async function saveEdit(id: string) {
if (!editContent.value.trim()) return;
saving.value = true;
error.value = '';
try {
await api.patch(`/memory/${id}`, { content: editContent.value.trim() });
cancelEdit();
await load();
} catch {
error.value = 'ویرایش ناموفق بود';
} finally {
saving.value = false;
}
}
async function removeItem(id: string) {
if (!confirm('این مورد از حافظه حذف شود؟')) return;
try {
await api.delete(`/memory/${id}`);
if (editingId.value === id) cancelEdit();
await load();
} catch {
error.value = 'حذف ناموفق بود';
}
}
function formatWhen(value: string) {
try {
return new Intl.DateTimeFormat('fa-IR', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(value));
} catch {
return value;
}
}
const hasItems = computed(() => items.value.length > 0);
watch(() => props.personId, load, { immediate: true });
onMounted(load);
</script>
<template>
<section class="memory-panel">
<header>
<div>
<p class="eyebrow">{{ brand.memoryEn }}</p>
<h2>حافظه مشتری</h2>
<p class="lead">نکات پایدار درباره این شخص — فقط با تأیید شما ذخیره می‌شود.</p>
</div>
</header>
<form class="add-form" @submit.prevent="addItem">
<textarea
v-model="draft"
rows="2"
placeholder="مثلاً: بودجه تا ۵۰۰ میلیون · ترجیح تماس صبح‌ها"
/>
<button class="btn btn-ghost" type="submit" :disabled="saving || !draft.trim()">
افزودن
</button>
</form>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
<p v-else-if="!hasItems" class="muted">هنوز موردی در حافظه ثبت نشده.</p>
<ul v-else class="items">
<li v-for="item in items" :key="item.id">
<template v-if="editingId === item.id">
<textarea v-model="editContent" rows="2" />
<div class="row-actions">
<button type="button" class="btn" :disabled="saving" @click="saveEdit(item.id)">
ذخیره
</button>
<button type="button" class="btn btn-ghost" @click="cancelEdit">انصراف</button>
</div>
</template>
<template v-else>
<p class="content">{{ item.content }}</p>
<p class="meta">
<span>{{ sourceLabel[item.sourceType] ?? item.sourceType }}</span>
<span v-if="item.confirmedBy"> · {{ item.confirmedBy.fullName }}</span>
<span> · {{ formatWhen(item.updatedAt) }}</span>
</p>
<div class="row-actions">
<button type="button" @click="startEdit(item)">ویرایش</button>
<button type="button" class="danger" @click="removeItem(item.id)">حذف</button>
</div>
</template>
</li>
</ul>
</section>
</template>
<style scoped>
.memory-panel {
display: grid;
gap: 0.85rem;
padding: 1rem;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.eyebrow {
margin: 0;
color: var(--accent);
font-weight: 700;
font-size: 0.85rem;
}
h2 {
margin: 0.15rem 0 0;
font-size: 1.15rem;
}
.lead {
margin: 0.35rem 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.add-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.6rem;
align-items: start;
}
textarea {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.75rem 0.9rem;
background: #fff;
font: inherit;
width: 100%;
resize: vertical;
}
.items {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.75rem;
}
.items li {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.75rem 0.85rem;
background: #fff;
}
.content {
margin: 0;
white-space: pre-wrap;
}
.meta {
margin: 0.35rem 0 0;
color: var(--muted);
font-size: 0.85rem;
}
.row-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.row-actions button {
border: none;
background: transparent;
color: var(--accent);
cursor: pointer;
font: inherit;
}
.row-actions button.danger {
color: #b42318;
}
.muted {
color: var(--muted);
}
.error {
color: #b42318;
}
</style>