feat(activities): let every assignee finish their own share

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-09 20:48:43 +03:30
co-authored by Cursor
parent dae3ae3f7e
commit da851bd9e5
10 changed files with 479 additions and 51 deletions
+169 -14
View File
@@ -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;