feat(activities): split customer column, wider search, custom activity types with type-specific fields

- Table: separate assignee and customer columns; search by assignee and activity type
- ActivityTypeDef model with per-type field definitions (text, select, checklist, participants, ...)
- Default types incl. meeting subject/participants and task checklist; admins manage types
- Participants see activities in their lists and get notified

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 12:29:23 +03:30
co-authored by Cursor
parent aa2f39256a
commit 13668c4ba4
17 changed files with 3371 additions and 147 deletions
+66 -33
View File
@@ -12,9 +12,16 @@ import UserAvatar from '@/components/UserAvatar.vue';
import UserMultiSelect from '@/components/UserMultiSelect.vue';
import ActivityCommentSheet from '@/components/activities/ActivityCommentSheet.vue';
import ActivityPostponeSheet from '@/components/activities/ActivityPostponeSheet.vue';
import type { ActivityDto, ActivityPriority, ActivityType, OrgUserDto } from '@mento/shared';
import ActivityFieldsView from '@/components/activities/ActivityFieldsView.vue';
import type {
ActivityChecklistItem,
ActivityDto,
ActivityPriority,
OrgUserDto,
} from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
import { canReassignRecords } from '@/lib/roles';
import { activityTypeView, typeColorStyle, useActivityTypes } from '@/lib/activity-types';
const route = useRoute();
const router = useRouter();
@@ -44,19 +51,33 @@ const assigneeNames = computed(() => {
return list.length ? list.join('، ') : '—';
});
const typeLabel: Record<ActivityType, string> = {
call: 'تماس',
meeting: 'جلسه',
task: 'وظیفه',
note: 'پیام',
};
const activityTypes = useActivityTypes();
const typeView = computed(() => (activity.value ? activityTypeView(activity.value) : null));
const typeDef = computed(() => (activity.value ? activityTypes.defOf(activity.value) : null));
const fieldsBusy = ref(false);
const typeIcon: Record<ActivityType, 'phone' | 'users' | 'check' | 'chat'> = {
call: 'phone',
meeting: 'users',
task: 'check',
note: 'chat',
};
/** تیک چک‌لیست بدون باز کردن فرم ویرایش */
async function toggleChecklistItem(key: string, itemId: string, done: boolean) {
const a = activity.value;
if (!a || fieldsBusy.value) return;
const list = (a.fields?.[key] as ActivityChecklistItem[] | undefined) ?? [];
const nextFields = {
...(a.fields ?? {}),
[key]: list.map((it) => (it.id === itemId ? { ...it, done } : it)),
};
activity.value = { ...a, fields: nextFields };
fieldsBusy.value = true;
error.value = '';
try {
const { data } = await api.patch<ActivityDto>(`/activities/${a.id}`, { fields: nextFields });
activity.value = { ...data, participantUsers: a.participantUsers };
} catch {
activity.value = a;
error.value = 'ثبت تغییر چک‌لیست ناموفق بود.';
} finally {
fieldsBusy.value = false;
}
}
const priorityLabel: Record<ActivityPriority, string> = {
very_low: 'خیلی کم',
@@ -254,6 +275,7 @@ watch(id, () => {
onMounted(() => {
void load();
void loadUsers();
void activityTypes.load();
});
</script>
@@ -295,18 +317,18 @@ onMounted(() => {
{{ subtitle }}
</p>
</div>
<span class="type-ico" :class="activity.type">
<UiIcon :name="typeIcon[activity.type]" :size="22" />
<span class="type-ico" :style="typeColorStyle(typeView!.color)">
<UiIcon :name="typeView!.icon" :size="22" />
</span>
</header>
<dl>
<div>
<dt>
<UiIcon :name="typeIcon[activity.type]" :size="15" />
<UiIcon :name="typeView!.icon" :size="15" />
نوع فعالیت
</dt>
<dd>{{ typeLabel[activity.type] }}</dd>
<dd>{{ typeView!.name }}</dd>
</div>
<div>
<dt>
@@ -369,6 +391,23 @@ onMounted(() => {
</dl>
</article>
<article v-if="typeDef?.fields.length" class="card type-card">
<h3>
<span class="type-ico sm" :style="typeColorStyle(typeView!.color)">
<UiIcon :name="typeView!.icon" :size="14" />
</span>
اطلاعات {{ typeView!.name }}
</h3>
<ActivityFieldsView
:fields="typeDef.fields"
:values="activity.fields ?? {}"
:participant-users="activity.participantUsers"
:busy="fieldsBusy"
editable
@toggle-item="toggleChecklistItem"
/>
</article>
<article class="card note">
<button class="note-head" type="button" @click="noteOpen = !noteOpen">
<strong>
@@ -573,24 +612,18 @@ h1 {
flex-shrink: 0;
}
.type-ico.call {
background: #ecfdf5;
color: #0f766e;
.type-ico.sm {
width: 28px;
height: 28px;
}
.type-ico.meeting {
background: #f5f3ff;
color: #6d28d9;
}
.type-ico.task {
background: #fff7ed;
color: #c2410c;
}
.type-ico.note {
background: #eff6ff;
color: #2563eb;
.type-card h3 {
margin: 0 0 0.4rem;
display: flex;
align-items: center;
gap: 0.45rem;
font-size: 0.95rem;
color: #0f172a;
}
dl {