feat(forms): merge notes tab into main info for activity and deal forms; show activity hashtags

- Activity form: notes/tags card inline, live hashtag preview, focus notes when required
- Hashtags shown in activity detail (clickable search) and list rows; search accepts #tag
- Deal form: remove mobile tabs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 13:17:29 +03:30
co-authored by Cursor
parent 13668c4ba4
commit d683932d1b
5 changed files with 91 additions and 75 deletions
+40 -30
View File
@@ -71,7 +71,6 @@ const isNew = computed(() =>
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const tab = ref<'main' | 'notes'>('main');
const people = ref<PersonDto[]>([]);
const deals = ref<DealDto[]>([]);
const users = ref<OrgUserDto[]>([]);
@@ -298,7 +297,6 @@ function resetForm() {
reminderSentAt.value = null;
reminderNotifyApp.value = true;
reminderNotifySms.value = false;
tab.value = 'main';
error.value = '';
}
@@ -310,8 +308,18 @@ watch(personId, () => {
}
});
const noteCard = ref<HTMLElement | null>(null);
const tagPreview = computed(() => parseTags(tagsRaw.value).map((t) => t.replace(/^#/, '')));
function focusNote() {
void nextTick(() => {
noteCard.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
noteCard.value?.querySelector<HTMLTextAreaElement>('textarea')?.focus({ preventScroll: true });
});
}
watch(needsNote, (on) => {
if (on) tab.value = 'notes';
if (on && !loading.value) focusNote();
});
async function load() {
@@ -470,8 +478,7 @@ async function submit(next: 'detail' | 'new' = 'detail') {
const message = validate();
if (message) {
error.value = message;
if (needsNote.value && !description.value.trim()) tab.value = 'notes';
else tab.value = 'main';
if (needsNote.value && !description.value.trim()) focusNote();
return;
}
saving.value = true;
@@ -579,16 +586,11 @@ watch([id, isNew], () => void load(), { immediate: true });
</template>
</UiPageHeader>
<nav class="tabs">
<button type="button" :class="{ on: tab === 'main' }" @click="tab = 'main'">اطلاعات اصلی</button>
<button type="button" :class="{ on: tab === 'notes' }" @click="tab = 'notes'">توضیحات و یادداشت</button>
</nav>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<div v-else class="layout">
<form class="form" @submit.prevent="submit('detail')">
<div v-show="tab === 'main'" class="stack">
<div class="stack">
<div class="card pair">
<div class="field">
<label>نوع فعالیت <i>*</i></label>
@@ -865,9 +867,12 @@ watch([id, isNew], () => void load(), { immediate: true });
</div>
</div>
<div v-show="tab === 'notes'" class="card note-card">
<div ref="noteCard" class="card note-card">
<div class="note-head">
<label for="description">توضیحات و یادداشت</label>
<label for="description">
توضیحات و یادداشت
<i v-if="needsNote" class="req-star">*</i>
</label>
<VoiceToTextButton
@transcript="(text) => (description = appendSpokenText(description, text))"
/>
@@ -875,7 +880,7 @@ watch([id, isNew], () => void load(), { immediate: true });
<UiTextarea
id="description"
v-model="description"
:rows="8"
:rows="4"
placeholder="جزئیات تماس، نتیجه جلسه یا یادداشت مرتبط را بنویسید…"
/>
<label for="activity-tags" class="tags-label">هشتگ / برچسب</label>
@@ -884,6 +889,13 @@ watch([id, isNew], () => void load(), { immediate: true });
v-model="tagsRaw"
placeholder="پیگیری، فوری (با کاما یا #)"
/>
<div v-if="tagPreview.length" class="tag-preview">
<span v-for="t in tagPreview" :key="t" class="tag-chip">#{{ t }}</span>
</div>
<small class="hint">
چند برچسب را با کاما (،) یا # جدا کنید؛ برچسب‌ها در جزئیات و لیست فعالیت‌ها نمایش داده می‌شوند و با
جستجوی همان کلمه پیدا می‌شوند.
</small>
</div>
<p v-if="error" class="error">{{ error }}</p>
@@ -1018,26 +1030,24 @@ watch([id, isNew], () => void load(), { immediate: true });
text-decoration: none;
}
.tabs {
.tag-preview {
display: flex;
gap: var(--sp-4);
border-bottom: 1px solid var(--color-border);
flex-wrap: wrap;
gap: 0.3rem;
}
.tabs button {
border: 0;
background: none;
padding: var(--sp-2) 0;
font: inherit;
color: var(--color-text-muted);
cursor: pointer;
transition: all var(--transition-fast);
.tag-chip {
background: #f0fdfa;
color: #0f766e;
border-radius: 999px;
padding: 0.15rem 0.55rem;
font-size: 0.78rem;
font-weight: 600;
}
.tabs .on {
color: var(--color-primary);
font-weight: 700;
box-shadow: inset 0 -2px 0 var(--color-primary);
.req-star {
color: var(--color-danger);
font-style: normal;
}
.layout {
@@ -1690,7 +1700,7 @@ watch([id, isNew], () => void load(), { immediate: true });
}
.note-card textarea {
min-height: 12rem;
min-height: 7rem;
resize: vertical;
}