Selection of multiple officials and role sub-categories
This commit is contained in:
@@ -77,7 +77,7 @@ const dueTime = ref('10:30');
|
||||
const durationMinutes = ref(30);
|
||||
const personId = ref('');
|
||||
const dealId = ref('');
|
||||
const assigneeId = ref('');
|
||||
const assigneeIds = ref<string[]>([]);
|
||||
const isDone = ref(false);
|
||||
const needsNote = ref(false);
|
||||
const reminderAt = ref('');
|
||||
@@ -109,7 +109,17 @@ const priorityOptions: Array<{ value: ActivityPriority; label: string; tone: str
|
||||
|
||||
const typeMeta = computed(() => typeOptions.find((o) => o.value === type.value));
|
||||
const selectedPerson = computed(() => people.value.find((p) => p.id === personId.value) ?? null);
|
||||
const selectedAssignee = computed(() => users.value.find((u) => u.id === assigneeId.value) ?? null);
|
||||
const selectedAssignees = computed(() =>
|
||||
assigneeIds.value
|
||||
.map((id) => users.value.find((u) => u.id === id))
|
||||
.filter((u): u is (typeof users.value)[number] => Boolean(u)),
|
||||
);
|
||||
const assigneePickerLabel = computed(() => {
|
||||
const list = selectedAssignees.value;
|
||||
if (!list.length) return 'انتخاب مسئول';
|
||||
if (list.length === 1) return list[0]!.fullName;
|
||||
return `${list[0]!.fullName} و ${toFaDigits(list.length - 1)} نفر دیگر`;
|
||||
});
|
||||
const selectedDeal = computed(() => deals.value.find((d) => d.id === dealId.value) ?? null);
|
||||
const personDeals = computed(() =>
|
||||
deals.value.filter((d) => !personId.value || d.person?.id === personId.value),
|
||||
@@ -161,7 +171,7 @@ function resetForm() {
|
||||
durationMinutes.value = 30;
|
||||
personId.value = '';
|
||||
dealId.value = '';
|
||||
assigneeId.value = auth.user?.id ?? '';
|
||||
assigneeIds.value = auth.user?.id ? [auth.user.id] : [];
|
||||
isDone.value = false;
|
||||
needsNote.value = false;
|
||||
reminderAt.value = '';
|
||||
@@ -213,7 +223,13 @@ async function load() {
|
||||
reminderNotifySms.value = data.reminderNotifySms ?? false;
|
||||
personId.value = data.person?.id ?? '';
|
||||
dealId.value = data.deal?.id ?? '';
|
||||
assigneeId.value = data.assignee?.id ?? auth.user?.id ?? '';
|
||||
assigneeIds.value = data.assignees?.length
|
||||
? data.assignees.map((a) => a.id)
|
||||
: data.assignee?.id
|
||||
? [data.assignee.id]
|
||||
: auth.user?.id
|
||||
? [auth.user.id]
|
||||
: [];
|
||||
isDone.value = data.status === 'done';
|
||||
} else {
|
||||
resetForm();
|
||||
@@ -272,15 +288,26 @@ function payload(): CreateActivityRequest {
|
||||
personId: personId.value || null,
|
||||
companyId: selectedPerson.value?.company?.id ?? null,
|
||||
dealId: dealId.value || null,
|
||||
assigneeId: assigneeId.value || null,
|
||||
assigneeIds: [...assigneeIds.value],
|
||||
assigneeId: assigneeIds.value[0] || null,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleAssignee(userId: string) {
|
||||
const idx = assigneeIds.value.indexOf(userId);
|
||||
if (idx >= 0) {
|
||||
if (assigneeIds.value.length <= 1) return;
|
||||
assigneeIds.value = assigneeIds.value.filter((id) => id !== userId);
|
||||
return;
|
||||
}
|
||||
assigneeIds.value = [...assigneeIds.value, userId];
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if (!type.value) return 'نوع فعالیت را انتخاب کنید.';
|
||||
if (title.value.trim().length < 2) return 'عنوان فعالیت را بنویسید.';
|
||||
if (!dueDate.value || !dueTime.value) return 'تاریخ و ساعت را مشخص کنید.';
|
||||
if (!assigneeId.value) return 'مسئول فعالیت را انتخاب کنید.';
|
||||
if (!assigneeIds.value.length) return 'حداقل یک مسئول فعالیت را انتخاب کنید.';
|
||||
if (isNew.value && !personId.value) return 'شخص یا مشتری مرتبط را انتخاب کنید.';
|
||||
if (needsNote.value && !description.value.trim()) return 'توضیح این فعالیت را بنویسید.';
|
||||
if (reminderAt.value && !reminderNotifyApp.value && !reminderNotifySms.value) {
|
||||
@@ -480,22 +507,56 @@ watch([id, isNew], () => void load(), { immediate: true });
|
||||
|
||||
<div class="card">
|
||||
<div class="field">
|
||||
<label>مسئول انجام این فعالیت <i>*</i></label>
|
||||
<label>مسئولان انجام این فعالیت <i>*</i></label>
|
||||
<button class="picker person-pick" type="button" @click.stop="assigneeOpen = !assigneeOpen">
|
||||
<UserAvatar :name="selectedAssignee?.fullName || 'مسئول'" :size="32" />
|
||||
<span :class="{ ghost: !selectedAssignee }">
|
||||
{{ selectedAssignee?.fullName || 'انتخاب مسئول' }}
|
||||
<div class="assignee-avatars">
|
||||
<UserAvatar
|
||||
v-for="user in selectedAssignees.slice(0, 3)"
|
||||
:key="user.id"
|
||||
:name="user.fullName"
|
||||
:size="28"
|
||||
/>
|
||||
<UserAvatar
|
||||
v-if="!selectedAssignees.length"
|
||||
name="مسئول"
|
||||
:size="28"
|
||||
/>
|
||||
</div>
|
||||
<span :class="{ ghost: !selectedAssignees.length }">
|
||||
{{ assigneePickerLabel }}
|
||||
</span>
|
||||
<UiIcon name="chevron" :size="16" />
|
||||
</button>
|
||||
<ul v-if="assigneeOpen" class="menu">
|
||||
<ul v-if="assigneeOpen" class="menu multi-menu" @click.stop>
|
||||
<li v-for="user in users" :key="user.id">
|
||||
<button type="button" @click="assigneeId = user.id; assigneeOpen = false">
|
||||
<button
|
||||
type="button"
|
||||
class="multi-option"
|
||||
:class="{ on: assigneeIds.includes(user.id) }"
|
||||
@click="toggleAssignee(user.id)"
|
||||
>
|
||||
<span class="check" :class="{ on: assigneeIds.includes(user.id) }">
|
||||
<UiIcon v-if="assigneeIds.includes(user.id)" name="check-plain" :size="12" />
|
||||
</span>
|
||||
<UserAvatar :name="user.fullName" :size="28" />
|
||||
{{ user.fullName }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<small class="hint">میتوانید چند نفر را بهعنوان مسئول انتخاب کنید.</small>
|
||||
<div v-if="selectedAssignees.length" class="assignee-chips">
|
||||
<span v-for="user in selectedAssignees" :key="user.id" class="chip">
|
||||
{{ user.fullName }}
|
||||
<button
|
||||
v-if="assigneeIds.length > 1"
|
||||
type="button"
|
||||
aria-label="حذف مسئول"
|
||||
@click="toggleAssignee(user.id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -622,8 +683,14 @@ watch([id, isNew], () => void load(), { immediate: true });
|
||||
<dd>{{ toFaDigits(durationMinutes) }} دقیقه</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>مسئول</dt>
|
||||
<dd>{{ selectedAssignee?.fullName || '—' }}</dd>
|
||||
<dt>مسئولان</dt>
|
||||
<dd>
|
||||
{{
|
||||
selectedAssignees.length
|
||||
? selectedAssignees.map((u) => u.fullName).join('، ')
|
||||
: '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>مشتری مرتبط</dt>
|
||||
@@ -825,6 +892,81 @@ watch([id, isNew], () => void load(), { immediate: true });
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.assignee-avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.assignee-avatars :deep(.avatar),
|
||||
.assignee-avatars > * {
|
||||
margin-inline-end: -0.35rem;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.assignee-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.assignee-chips .chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assignee-chips .chip button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
padding: 0 0.1rem;
|
||||
}
|
||||
|
||||
.multi-menu .multi-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-menu .check {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
border-radius: 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.multi-menu .check.on {
|
||||
background: #14b8a6;
|
||||
border-color: #14b8a6;
|
||||
}
|
||||
|
||||
.multi-menu .multi-option.on {
|
||||
background: #f0fdfa;
|
||||
}
|
||||
|
||||
.type-ico {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
Reference in New Issue
Block a user