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
+111
View File
@@ -0,0 +1,111 @@
import { computed, ref } from 'vue';
import type {
ActivityDto,
ActivityStageColor,
ActivityType,
ActivityTypeDefDto,
ActivityTypeIcon,
} from '@mento/shared';
import { ACTIVITY_TYPE_LABELS } from '@mento/shared';
import { api } from '@/lib/api';
import { PIPELINE_ACCENT } from '@/lib/pipeline-board';
export const BASE_TYPE_ICON: Record<ActivityType, ActivityTypeIcon> = {
call: 'phone',
meeting: 'users',
task: 'check',
note: 'chat',
};
const BASE_TYPE_COLOR: Record<ActivityType, ActivityStageColor> = {
call: 'green',
meeting: 'purple',
task: 'orange',
note: 'blue',
};
export const ACTIVITY_TYPE_ICON_LABELS: Record<ActivityTypeIcon, string> = {
phone: 'تلفن',
users: 'جمع',
check: 'تیک',
chat: 'گفتگو',
mail: 'ایمیل',
building: 'ساختمان',
calendar: 'تقویم',
star: 'ستاره',
flag: 'پرچم',
briefcase: 'کیف',
document: 'سند',
target: 'هدف',
truck: 'ارسال',
send: 'ارسال پیام',
megaphone: 'بلندگو',
pin: 'مکان',
bell: 'زنگ',
sparkle: 'درخشش',
};
const types = ref<ActivityTypeDefDto[]>([]);
const loaded = ref(false);
let pending: Promise<void> | null = null;
/** انواع فعالیت سازمان (کش مشترک بین صفحات) */
export function useActivityTypes() {
async function load(force = false): Promise<void> {
if (loaded.value && !force) return;
if (pending && !force) return pending;
pending = api
.get<ActivityTypeDefDto[]>('/activities/types')
.then(({ data }) => {
types.value = data;
loaded.value = true;
})
.catch(() => {
if (!loaded.value) types.value = [];
})
.finally(() => {
pending = null;
});
return pending;
}
const active = computed(() => types.value.filter((t) => t.isActive));
const byId = computed(() => new Map(types.value.map((t) => [t.id, t])));
/** نوع اصلی هر نوع پایه */
const systemOf = computed(() => {
const out: Partial<Record<ActivityType, ActivityTypeDefDto>> = {};
for (const t of types.value) if (t.isSystem && !out[t.baseType]) out[t.baseType] = t;
return out;
});
/** تعریف کامل نوع مؤثر یک فعالیت */
function defOf(a: Pick<ActivityDto, 'type' | 'typeDef'>): ActivityTypeDefDto | null {
if (a.typeDef?.id) return byId.value.get(a.typeDef.id) ?? null;
return systemOf.value[a.type] ?? null;
}
return { types, active, byId, systemOf, loaded, load, defOf };
}
export type TypeView = { name: string; icon: ActivityTypeIcon; color: ActivityStageColor };
/** برچسب/آیکون/رنگ قابل نمایش برای یک فعالیت */
export function activityTypeView(a: Pick<ActivityDto, 'type' | 'typeDef'>): TypeView {
if (a.typeDef) {
return { name: a.typeDef.name, icon: a.typeDef.icon, color: a.typeDef.color };
}
return {
name: ACTIVITY_TYPE_LABELS[a.type],
icon: BASE_TYPE_ICON[a.type],
color: BASE_TYPE_COLOR[a.type],
};
}
export function typeColorStyle(color: ActivityStageColor) {
const c = PIPELINE_ACCENT[color] ?? PIPELINE_ACCENT.green;
return { background: c.soft, color: c.text };
}
export function newFieldKey(): string {
return `f_${Math.random().toString(36).slice(2, 10)}`;
}