diff --git a/apps/web/src/views/ActivityFormView.vue b/apps/web/src/views/ActivityFormView.vue index b8db497..735f886 100644 --- a/apps/web/src/views/ActivityFormView.vue +++ b/apps/web/src/views/ActivityFormView.vue @@ -46,6 +46,7 @@ import type { PaginatedResponse, PersonDto, PipelineBoardDto, + TeamDto, } from '@mento/shared'; const props = withDefaults( @@ -80,6 +81,7 @@ const error = ref(''); const people = ref([]); const deals = ref([]); const users = ref([]); +const teams = ref([]); const boards = ref([]); const boardId = ref(''); const defaultBoardId = computed(() => boards.value.find((b) => b.isDefault)?.id ?? ''); @@ -264,6 +266,7 @@ const selectedAssignees = computed(() => .filter((u): u is (typeof users.value)[number] => Boolean(u)), ); const pickedRoleIds = ref([]); +const pickedTeamIds = ref([]); const roleGroups = computed(() => { const map = new Map(); for (const user of users.value) { @@ -278,6 +281,17 @@ const roleGroups = computed(() => { } return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'fa')); }); +const teamGroups = computed(() => { + const active = new Set(users.value.filter((user) => user.isActive !== false).map((user) => user.id)); + return teams.value + .map((team) => ({ + id: team.id, + name: team.name, + userIds: [...new Set(team.members.map((member) => member.userId).filter((id) => active.has(id)))], + })) + .filter((team) => team.userIds.length > 0) + .sort((a, b) => a.name.localeCompare(b.name, 'fa')); +}); function normalizeSearch(value: string) { return value.replace(/ي/g, 'ی').replace(/ك/g, 'ک').replace(/\u200c/g, ' ').trim().toLowerCase(); } @@ -287,6 +301,11 @@ const filteredRoleGroups = computed(() => { if (!q) return roleGroups.value; return roleGroups.value.filter((role) => normalizeSearch(role.name).includes(q)); }); +const filteredTeamGroups = computed(() => { + const q = assigneeSearch.value; + if (!q) return teamGroups.value; + return teamGroups.value.filter((team) => normalizeSearch(team.name).includes(q)); +}); const filteredUsers = computed(() => { const q = assigneeSearch.value; if (!q) return users.value; @@ -296,28 +315,41 @@ const filteredUsers = computed(() => { normalizeSearch(user.orgRoleName ?? '').includes(q), ); }); -function isRoleSelected(userIds: string[]) { +function coversAll(userIds: string[]) { return userIds.length > 0 && userIds.every((id) => assigneeIds.value.includes(id)); } const selectedRoles = computed(() => roleGroups.value.filter((role) => pickedRoleIds.value.includes(role.id)), ); +const selectedTeams = computed(() => + teamGroups.value.filter((team) => pickedTeamIds.value.includes(team.id)), +); const extraAssignees = computed(() => { - const covered = new Set(selectedRoles.value.flatMap((role) => role.userIds)); + const covered = new Set([ + ...selectedRoles.value.flatMap((role) => role.userIds), + ...selectedTeams.value.flatMap((team) => team.userIds), + ]); return selectedAssignees.value.filter((user) => !covered.has(user.id)); }); const assigneePickerLabel = computed(() => { - const roles = selectedRoles.value; + const groups = [...selectedRoles.value, ...selectedTeams.value]; const extras = extraAssignees.value; if (!assigneeIds.value.length) return 'انتخاب مسئول'; - if (roles.length && !extras.length) { - if (roles.length === 1) return `${roles[0]!.name} (${toFaDigits(roles[0]!.userIds.length)} نفر)`; - if (roles.length === 2) return `${roles[0]!.name} و ${roles[1]!.name}`; - return `${roles[0]!.name} و ${toFaDigits(roles.length - 1)} نقش دیگر`; + if (groups.length && !extras.length) { + if (groups.length === 1) { + return `${groups[0]!.name} (${toFaDigits(groups[0]!.userIds.length)} نفر)`; + } + if (groups.length === 2) return `${groups[0]!.name} و ${groups[1]!.name}`; + const moreWord = + selectedTeams.value.length && !selectedRoles.value.length + ? 'تیم' + : selectedRoles.value.length && !selectedTeams.value.length + ? 'نقش' + : 'مورد'; + return `${groups[0]!.name} و ${toFaDigits(groups.length - 1)} ${moreWord} دیگر`; } - if (roles.length && extras.length) { - const roleLabel = roles.map((role) => role.name).join('، '); - return `${roleLabel} و ${toFaDigits(extras.length)} نفر`; + if (groups.length && extras.length) { + return `${groups.map((group) => group.name).join('، ')} و ${toFaDigits(extras.length)} نفر`; } if (selectedAssignees.value.length === 1) return selectedAssignees.value[0]!.fullName; return `${selectedAssignees.value[0]!.fullName} و ${toFaDigits(selectedAssignees.value.length - 1)} نفر دیگر`; @@ -325,9 +357,8 @@ const assigneePickerLabel = computed(() => { const assigneeSummaryLabel = computed(() => { if (!assigneeIds.value.length) return '—'; const parts = [ - ...selectedRoles.value.map( - (role) => `${role.name} (${toFaDigits(role.userIds.length)} نفر)`, - ), + ...selectedRoles.value.map((role) => `${role.name} (${toFaDigits(role.userIds.length)} نفر)`), + ...selectedTeams.value.map((team) => `${team.name} (${toFaDigits(team.userIds.length)} نفر)`), ...extraAssignees.value.map((user) => user.fullName), ]; return parts.join('، '); @@ -390,6 +421,7 @@ function resetForm() { dealId.value = ''; assigneeIds.value = auth.user?.id ? [auth.user.id] : []; pickedRoleIds.value = []; + pickedTeamIds.value = []; isDone.value = false; needsNote.value = false; reminderAt.value = ''; @@ -426,18 +458,20 @@ async function load() { loading.value = true; error.value = ''; try { - const [peopleRes, boardRes, usersRes, activityBoardsRes] = await Promise.all([ + const [peopleRes, boardRes, usersRes, activityBoardsRes, teamsRes] = await Promise.all([ api.get>('/people', { params: { limit: 100 } }), api.get('/pipelines/board'), api.get('/users').catch(() => ({ data: [] as OrgUserDto[] })), api .get('/activities/boards') .catch(() => ({ data: [] as ActivityBoardDto[] })), + api.get('/teams').catch(() => ({ data: [] as TeamDto[] })), activityTypes.load(true), ]); people.value = peopleRes.data.items; deals.value = boardRes.data.stages.flatMap((s) => s.deals); users.value = usersRes.data; + teams.value = teamsRes.data; boards.value = activityBoardsRes.data; boardId.value = props.initialBoardId || defaultBoardId.value; @@ -473,7 +507,7 @@ async function load() { : auth.user?.id ? [auth.user.id] : []; - refreshPickedRoles(); + refreshPickedGroups(); isDone.value = data.status === 'done'; await loadSavedPhotos(data); } else { @@ -551,14 +585,32 @@ function payload(): CreateActivityRequest { }; } -function refreshPickedRoles() { +function refreshPickedGroups() { pickedRoleIds.value = roleGroups.value .filter((role) => { - if (!isRoleSelected(role.userIds)) return false; + if (!coversAll(role.userIds)) return false; if (role.userIds.length >= 2) return true; return pickedRoleIds.value.includes(role.id); }) .map((role) => role.id); + pickedTeamIds.value = teamGroups.value + .filter((team) => { + if (!coversAll(team.userIds)) return false; + if (team.userIds.length >= 2) return true; + return pickedTeamIds.value.includes(team.id); + }) + .map((team) => team.id); +} + +function coveredByPickedGroups() { + return new Set([ + ...roleGroups.value + .filter((role) => pickedRoleIds.value.includes(role.id)) + .flatMap((role) => role.userIds), + ...teamGroups.value + .filter((team) => pickedTeamIds.value.includes(team.id)) + .flatMap((team) => team.userIds), + ]); } function toggleAssignee(userId: string) { @@ -569,31 +621,38 @@ function toggleAssignee(userId: string) { } else { assigneeIds.value = [...assigneeIds.value, userId]; } - refreshPickedRoles(); + refreshPickedGroups(); } -function toggleRole(roleId: string, userIds: string[]) { - if (pickedRoleIds.value.includes(roleId)) { - pickedRoleIds.value = pickedRoleIds.value.filter((id) => id !== roleId); - const still = new Set( - roleGroups.value - .filter((role) => pickedRoleIds.value.includes(role.id)) - .flatMap((role) => role.userIds), - ); +function toggleGroup(kind: 'role' | 'team', groupId: string, userIds: string[]) { + const picked = kind === 'role' ? pickedRoleIds : pickedTeamIds; + if (picked.value.includes(groupId)) { + picked.value = picked.value.filter((id) => id !== groupId); + const still = coveredByPickedGroups(); assigneeIds.value = assigneeIds.value.filter((id) => !userIds.includes(id) || still.has(id)); - refreshPickedRoles(); + if (!assigneeIds.value.length && auth.user?.id) assigneeIds.value = [auth.user.id]; + refreshPickedGroups(); return; } const onlyDefaultUser = pickedRoleIds.value.length === 0 && + pickedTeamIds.value.length === 0 && assigneeIds.value.length === 1 && assigneeIds.value[0] === auth.user?.id && !userIds.includes(auth.user?.id ?? ''); - pickedRoleIds.value = [...pickedRoleIds.value, roleId]; + picked.value = [...picked.value, groupId]; assigneeIds.value = onlyDefaultUser ? [...userIds] : [...new Set([...assigneeIds.value, ...userIds])]; - refreshPickedRoles(); + refreshPickedGroups(); +} + +function toggleRole(roleId: string, userIds: string[]) { + toggleGroup('role', roleId, userIds); +} + +function toggleTeam(teamId: string, userIds: string[]) { + toggleGroup('team', teamId, userIds); } function clearPhotos() { @@ -963,7 +1022,7 @@ watch([id, isNew], () => void load(), { immediate: true }); ref="assigneeSearchInput" v-model="assigneeQuery" type="search" - placeholder="جستجوی نقش یا نام عضو…" + :placeholder="teamGroups.length ? 'جستجوی تیم، نقش یا نام عضو…' : 'جستجوی نقش یا نام عضو…'" @keydown.esc="assigneeOpen = false" /> @@ -985,7 +1044,24 @@ watch([id, isNew], () => void load(), { immediate: true }); - + +
  • - نقشی یا عضوی پیدا نشد. + موردی پیدا نشد.
  • - یک نقش را انتخاب کنید تا همهٔ اعضای آن مسئول شوند، یا چند نفر را جدا انتخاب کنید. -
    + + {{ + teamGroups.length + ? 'یک تیم یا نقش را انتخاب کنید تا همهٔ اعضایش مسئول شوند، یا چند نفر را جدا انتخاب کنید.' + : 'یک نقش را انتخاب کنید تا همهٔ اعضای آن مسئول شوند، یا چند نفر را جدا انتخاب کنید.' + }} + +
    {{ role.name }} + + {{ team.name }} + + {{ user.fullName }}