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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user