feat(activities): let every assignee finish their own share
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,6 +41,7 @@ const emit = defineEmits<{
|
||||
stagesChanged: [];
|
||||
add: [stageId: string];
|
||||
error: [message: string];
|
||||
notice: [message: string];
|
||||
menuDeal: [activity: ActivityDto, stageId?: string];
|
||||
toggleSelect: [activityId: string];
|
||||
bulkDelete: [];
|
||||
@@ -292,6 +293,21 @@ async function moveCard(activityId: string, toStageId: string, index?: number) {
|
||||
stageId: toStageId,
|
||||
position: insertAt,
|
||||
});
|
||||
if (!same && data.status !== toStage.kind) {
|
||||
const hideFromMine = props.params.desk === 'mine';
|
||||
from.items = hideFromMine
|
||||
? prevFrom.filter((row) => row.id !== activityId)
|
||||
: prevFrom.map((row) => (row.id === activityId ? data : row));
|
||||
from.total = hideFromMine ? Math.max(0, prevFromTotal - 1) : prevFromTotal;
|
||||
to.items = prevTo;
|
||||
to.total = prevToTotal;
|
||||
emit(
|
||||
'notice',
|
||||
'سهم شما ثبت شد. فعالیت وقتی انجامشده میشود که همهٔ مسئولها سهم خود را بزنند.',
|
||||
);
|
||||
emit('changed');
|
||||
return;
|
||||
}
|
||||
const column = same ? from : to;
|
||||
column.items = column.items.map((i) => (i.id === activityId ? data : i));
|
||||
emit('changed');
|
||||
|
||||
@@ -201,6 +201,7 @@ const page = computed(() => {
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const notice = ref('');
|
||||
const data = ref<PaginatedResponse<ActivityDto> | null>(null);
|
||||
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
|
||||
const users = ref<OrgUserDto[]>([]);
|
||||
@@ -837,6 +838,30 @@ function onFacetDone() {
|
||||
openCol.value = null;
|
||||
}
|
||||
|
||||
function assigneePeopleOf(item: ActivityDto) {
|
||||
if (item.assignees?.length) return item.assignees;
|
||||
return item.assignee ? [item.assignee] : [];
|
||||
}
|
||||
|
||||
function needsEveryone(item: ActivityDto) {
|
||||
return item.completionMode === 'all' && assigneePeopleOf(item).length > 1;
|
||||
}
|
||||
|
||||
function canMarkShare(item: ActivityDto) {
|
||||
if (item.status !== 'open') return false;
|
||||
if (!needsEveryone(item)) return true;
|
||||
const mine = auth.user?.id;
|
||||
if (!mine) return false;
|
||||
return (
|
||||
assigneePeopleOf(item).some((person) => person.id === mine) &&
|
||||
!item.completedByIds?.includes(mine)
|
||||
);
|
||||
}
|
||||
|
||||
function completeLabel(item: ActivityDto) {
|
||||
return needsEveryone(item) ? 'انجام سهم من' : 'انجام شد';
|
||||
}
|
||||
|
||||
async function complete(id: string) {
|
||||
menuId.value = null;
|
||||
boardMenuId.value = null;
|
||||
@@ -1503,6 +1528,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="notice" class="ok">{{ notice }}</p>
|
||||
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
|
||||
|
||||
<div v-if="selectMode" class="select-bar">
|
||||
@@ -1532,6 +1558,7 @@ onUnmounted(() => {
|
||||
@stages-changed="onStagesChanged"
|
||||
@add="openAddInStage"
|
||||
@error="(msg: string) => (error = msg)"
|
||||
@notice="(msg: string) => (notice = msg)"
|
||||
@menu-deal="onBoardMenu"
|
||||
@toggle-select="toggleSelect"
|
||||
@bulk-delete="askBulkDelete"
|
||||
@@ -1755,8 +1782,8 @@ onUnmounted(() => {
|
||||
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
|
||||
انجام شد
|
||||
<button v-if="canMarkShare(item)" type="button" @click="complete(item.id)">
|
||||
{{ completeLabel(item) }}
|
||||
</button>
|
||||
<button type="button" class="danger" @click="askDeleteActivity(item.id, item.title)">
|
||||
حذف
|
||||
@@ -1845,8 +1872,8 @@ onUnmounted(() => {
|
||||
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
|
||||
انجام شد
|
||||
<button v-if="canMarkShare(item)" type="button" @click="complete(item.id)">
|
||||
{{ completeLabel(item) }}
|
||||
</button>
|
||||
<button type="button" class="danger" @click="askDeleteActivity(item.id, item.title)">
|
||||
حذف
|
||||
|
||||
@@ -173,16 +173,33 @@ watch(
|
||||
const users = ref<OrgUserDto[]>([]);
|
||||
const assignUserIds = ref<string[]>([]);
|
||||
|
||||
const assigneeNames = computed(() => {
|
||||
const assigneePeople = computed(() => {
|
||||
const a = activity.value;
|
||||
if (!a) return '—';
|
||||
const list = a.assignees?.length
|
||||
? a.assignees.map((u) => u.fullName)
|
||||
: a.assignee
|
||||
? [a.assignee.fullName]
|
||||
: [];
|
||||
if (!a) return [];
|
||||
if (a.assignees?.length) return a.assignees;
|
||||
return a.assignee ? [a.assignee] : [];
|
||||
});
|
||||
const assigneeNames = computed(() => {
|
||||
const list = assigneePeople.value.map((u) => u.fullName);
|
||||
return list.length ? list.join('، ') : '—';
|
||||
});
|
||||
const needsEveryone = computed(
|
||||
() =>
|
||||
activity.value?.completionMode === 'all' && assigneePeople.value.length > 1,
|
||||
);
|
||||
const completedIds = computed(() => new Set(activity.value?.completedByIds ?? []));
|
||||
const myShareDone = computed(() => {
|
||||
const mine = auth.user?.id;
|
||||
return Boolean(mine && completedIds.value.has(mine));
|
||||
});
|
||||
const canForceClose = computed(() => {
|
||||
const role = auth.user?.role;
|
||||
return (
|
||||
role === 'owner' ||
|
||||
role === 'admin' ||
|
||||
activity.value?.createdBy?.id === auth.user?.id
|
||||
);
|
||||
});
|
||||
|
||||
const activityTypes = useActivityTypes();
|
||||
const typeView = computed(() => (activity.value ? activityTypeView(activity.value) : null));
|
||||
@@ -316,15 +333,43 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
async function complete() {
|
||||
if (!id.value || isDone.value) return;
|
||||
async function complete(kind: 'share' | 'force' | 'undo' = 'share') {
|
||||
if (!id.value) return;
|
||||
if (kind !== 'undo' && isDone.value) return;
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.patch(`/activities/${id.value}/complete`);
|
||||
await router.push({ name: 'activities' });
|
||||
const params =
|
||||
kind === 'force' ? { force: '1' } : kind === 'undo' ? { undo: '1' } : undefined;
|
||||
const { data } = await api.patch<ActivityDto>(
|
||||
`/activities/${id.value}/complete`,
|
||||
undefined,
|
||||
{ params },
|
||||
);
|
||||
if (data.status === 'done') {
|
||||
await router.push({ name: 'activities' });
|
||||
return;
|
||||
}
|
||||
activity.value = data;
|
||||
} catch {
|
||||
error.value = 'ثبت انجامشدن ناموفق بود.';
|
||||
error.value =
|
||||
kind === 'undo' ? 'لغو سهم ناموفق بود.' : 'ثبت انجامشدن ناموفق بود.';
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setCompletionMode(mode: 'any' | 'all') {
|
||||
if (!id.value || activity.value?.completionMode === mode) return;
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await api.patch<ActivityDto>(`/activities/${id.value}`, {
|
||||
completionMode: mode,
|
||||
});
|
||||
activity.value = data;
|
||||
} catch {
|
||||
error.value = 'تغییر نحوهٔ انجام ناموفق بود.';
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -597,6 +642,43 @@ onMounted(() => {
|
||||
<template v-else>
|
||||
{{ assigneeNames }}
|
||||
</template>
|
||||
<div v-if="canReassign && assigneePeople.length > 1" class="share-mode">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ on: activity.completionMode !== 'all' }"
|
||||
:disabled="saving"
|
||||
@click="setCompletionMode('any')"
|
||||
>
|
||||
با انجام یکی کافی است
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ on: activity.completionMode === 'all' }"
|
||||
:disabled="saving"
|
||||
@click="setCompletionMode('all')"
|
||||
>
|
||||
همه باید انجام دهند
|
||||
</button>
|
||||
</div>
|
||||
<ul v-if="needsEveryone && !isDone" class="share-list">
|
||||
<li
|
||||
v-for="person in assigneePeople"
|
||||
:key="person.id"
|
||||
:class="{ done: completedIds.has(person.id) }"
|
||||
>
|
||||
<span>{{ person.fullName }}</span>
|
||||
<em>{{ completedIds.has(person.id) ? 'انجام شد' : 'مانده' }}</em>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
v-if="needsEveryone && canForceClose && !isDone"
|
||||
type="button"
|
||||
class="share-force"
|
||||
:disabled="saving"
|
||||
@click="complete('force')"
|
||||
>
|
||||
بستن برای همه
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
@@ -855,15 +937,25 @@ onMounted(() => {
|
||||
<UiIcon name="clock" :size="16" />
|
||||
بازگشایی
|
||||
</button>
|
||||
<button
|
||||
v-else-if="needsEveryone && myShareDone"
|
||||
class="act primary"
|
||||
type="button"
|
||||
:disabled="saving"
|
||||
@click="complete('undo')"
|
||||
>
|
||||
<UiIcon name="refresh" :size="16" />
|
||||
لغو سهم من
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="act primary"
|
||||
type="button"
|
||||
:disabled="saving || isDone"
|
||||
@click="complete"
|
||||
@click="complete('share')"
|
||||
>
|
||||
<UiIcon name="check" :size="16" />
|
||||
انجام شد
|
||||
{{ needsEveryone ? 'انجام سهم من' : 'انجام شد' }}
|
||||
</button>
|
||||
<button class="act ghost" type="button" :disabled="saving || isDone" @click="postponeOpen = true">
|
||||
<UiIcon name="refresh" :size="16" />
|
||||
@@ -1160,6 +1252,69 @@ dd {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.share-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
margin-top: 0.55rem;
|
||||
}
|
||||
|
||||
.share-mode button {
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem 0.7rem;
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.share-mode button.on {
|
||||
border-color: #14b8a6;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.share-list {
|
||||
list-style: none;
|
||||
margin: 0.55rem 0 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.share-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.86rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.share-list em {
|
||||
font-style: normal;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.share-list li.done em {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.share-force {
|
||||
margin-top: 0.45rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #0f766e;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.flag.low,
|
||||
.priority.low {
|
||||
color: #16a34a;
|
||||
|
||||
@@ -129,6 +129,7 @@ const personId = ref('');
|
||||
const leadId = ref('');
|
||||
const dealId = ref('');
|
||||
const assigneeIds = ref<string[]>([]);
|
||||
const completionMode = ref<'any' | 'all'>('any');
|
||||
const isDone = ref(false);
|
||||
const needsNote = ref(false);
|
||||
const reminderAt = ref('');
|
||||
@@ -440,6 +441,7 @@ function resetForm() {
|
||||
personId.value = '';
|
||||
dealId.value = '';
|
||||
assigneeIds.value = auth.user?.id ? [auth.user.id] : [];
|
||||
completionMode.value = 'any';
|
||||
pickedRoleIds.value = [];
|
||||
pickedTeamIds.value = [];
|
||||
isDone.value = false;
|
||||
@@ -528,6 +530,7 @@ async function load() {
|
||||
: auth.user?.id
|
||||
? [auth.user.id]
|
||||
: [];
|
||||
completionMode.value = data.completionMode === 'all' ? 'all' : 'any';
|
||||
refreshPickedGroups();
|
||||
isDone.value = data.status === 'done';
|
||||
await loadSavedPhotos(data);
|
||||
@@ -605,6 +608,7 @@ function payload(): CreateActivityRequest {
|
||||
leadId: leadId.value || null,
|
||||
assigneeIds: [...assigneeIds.value],
|
||||
assigneeId: assigneeIds.value[0] || null,
|
||||
completionMode: assigneeIds.value.length > 1 ? completionMode.value : 'any',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -978,7 +982,9 @@ async function submit(next: 'detail' | 'new' = 'detail') {
|
||||
savedId = data.id;
|
||||
createdActivityId.value = data.id;
|
||||
if (isDone.value) {
|
||||
await api.patch(`/activities/${savedId}/complete`);
|
||||
await api.patch(`/activities/${savedId}/complete`, undefined, {
|
||||
params: { force: '1' },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await api.patch(`/activities/${savedId}`, {
|
||||
@@ -1269,6 +1275,17 @@ watch([id, isNew], () => void load(), { immediate: true });
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="assigneeIds.length > 1" class="completion-mode">
|
||||
<span>نحوهٔ انجام</span>
|
||||
<label>
|
||||
<input v-model="completionMode" type="radio" value="any" />
|
||||
با انجام یکی کافی است
|
||||
</label>
|
||||
<label>
|
||||
<input v-model="completionMode" type="radio" value="all" />
|
||||
همهٔ مسئولها باید انجام دهند
|
||||
</label>
|
||||
</div>
|
||||
<small class="hint">
|
||||
{{
|
||||
canAssignOthers
|
||||
@@ -1799,6 +1816,27 @@ watch([id, isNew], () => void load(), { immediate: true });
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.completion-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem 1rem;
|
||||
margin-top: 0.75rem;
|
||||
color: #334155;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.completion-mode span {
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.completion-mode label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assignee-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user