From d683932d1b6bc5c56e0d9b727cf2b05ec85fc63d Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 7 Oct 2026 13:17:29 +0330 Subject: [PATCH] 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 --- apps/api/src/activities/activities.service.ts | 2 +- apps/web/src/views/ActivitiesView.vue | 20 ++++++ apps/web/src/views/ActivityDetailView.vue | 27 +++++++ apps/web/src/views/ActivityFormView.vue | 70 +++++++++++-------- apps/web/src/views/DealFormView.vue | 47 +------------ 5 files changed, 91 insertions(+), 75 deletions(-) diff --git a/apps/api/src/activities/activities.service.ts b/apps/api/src/activities/activities.service.ts index 1a261f9..d8e4c42 100644 --- a/apps/api/src/activities/activities.service.ts +++ b/apps/api/src/activities/activities.service.ts @@ -628,7 +628,7 @@ export class ActivitiesService { const search = buildSearchWhere(q, (t) => [ { title: ci(t) }, { description: ci(t) }, - { tags: { has: t } }, + { tags: { has: t.replace(/^#/, '') } }, { person: { OR: [ diff --git a/apps/web/src/views/ActivitiesView.vue b/apps/web/src/views/ActivitiesView.vue index e1c4603..76ccd45 100644 --- a/apps/web/src/views/ActivitiesView.vue +++ b/apps/web/src/views/ActivitiesView.vue @@ -983,6 +983,10 @@ onUnmounted(() => { {{ item.title }}

{{ subtitle(item) }}

+

+ #{{ t }} + +{{ toFaDigits(item.tags.length - 3) }} +

@@ -1727,6 +1731,22 @@ time.note { gap: 0.25rem; } +.row-tags { + margin: 0.2rem 0 0; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.row-tags span { + background: #f0fdfa; + color: #0f766e; + border-radius: 999px; + padding: 0.05rem 0.45rem; + font-size: 0.7rem; + font-weight: 600; +} + .customer { display: grid; gap: 0.15rem; diff --git a/apps/web/src/views/ActivityDetailView.vue b/apps/web/src/views/ActivityDetailView.vue index 63a80db..c00ef1a 100644 --- a/apps/web/src/views/ActivityDetailView.vue +++ b/apps/web/src/views/ActivityDetailView.vue @@ -316,6 +316,16 @@ onMounted(() => { {{ subtitle }}

+
+ + #{{ t }} + +
@@ -612,6 +622,23 @@ h1 { flex-shrink: 0; } +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-top: 0.5rem; +} + +.tag { + background: #f0fdfa; + color: #0f766e; + border-radius: 999px; + padding: 0.15rem 0.55rem; + font-size: 0.76rem; + font-weight: 600; + text-decoration: none; +} + .type-ico.sm { width: 28px; height: 28px; diff --git a/apps/web/src/views/ActivityFormView.vue b/apps/web/src/views/ActivityFormView.vue index ded9ea6..61a5543 100644 --- a/apps/web/src/views/ActivityFormView.vue +++ b/apps/web/src/views/ActivityFormView.vue @@ -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([]); const deals = ref([]); const users = ref([]); @@ -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(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('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 }); - -

در حال بارگذاری…

-
+
@@ -865,9 +867,12 @@ watch([id, isNew], () => void load(), { immediate: true });
-
+
- + @@ -875,7 +880,7 @@ watch([id, isNew], () => void load(), { immediate: true }); @@ -884,6 +889,13 @@ watch([id, isNew], () => void load(), { immediate: true }); v-model="tagsRaw" placeholder="پیگیری، فوری (با کاما یا #)" /> +
+ #{{ t }} +
+ + چند برچسب را با کاما (،) یا # جدا کنید؛ برچسب‌ها در جزئیات و لیست فعالیت‌ها نمایش داده می‌شوند و با + جستجوی همان کلمه پیدا می‌شوند. +

{{ error }}

@@ -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; } diff --git a/apps/web/src/views/DealFormView.vue b/apps/web/src/views/DealFormView.vue index 297c908..f5285a2 100644 --- a/apps/web/src/views/DealFormView.vue +++ b/apps/web/src/views/DealFormView.vue @@ -68,7 +68,6 @@ const pageTitle = computed(() => (isNew.value ? 'ایجاد معامله جدی const loading = ref(false); const saving = ref(false); const error = ref(''); -const tab = ref<'main' | 'notes'>('main'); const showOthers = ref(true); const productsOpen = ref(false); const openMenu = ref(''); @@ -373,7 +372,6 @@ async function submit() { const message = validate(); if (message) { error.value = message; - tab.value = 'main'; return; } saving.value = true; @@ -473,15 +471,10 @@ onMounted(load); - -

در حال بارگذاری…

-
+

@@ -804,7 +797,7 @@ onMounted(load);

-
+