fix(routines): keep the daily desk for internal team work

Customer-linked tasks stay on their record and no longer appear in today, calendar, score, or reminders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-29 20:53:26 +03:30
co-authored by Cursor
parent 99d686fefb
commit a17c7dbabc
11 changed files with 115 additions and 90 deletions
@@ -23,7 +23,6 @@ import {
Typography,
} from 'ant-design-vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import EntityLinkPicker, { type PickedEntity } from '@/components/routines/EntityLinkPicker.vue';
import { api } from '@/lib/api';
import {
ENTITY_TYPE_LABELS,
@@ -64,7 +63,6 @@ const form = reactive({
categoryId: '',
assigneeId: '',
isMandatory: false,
entity: null as PickedEntity | null,
});
const saving = ref(false);
const error = ref('');
@@ -110,7 +108,6 @@ watch(
categoryId: '',
assigneeId: props.defaultAssigneeId || auth.user?.id || '',
isMandatory: false,
entity: null,
});
error.value = '';
void ensureContext();
@@ -138,14 +135,14 @@ async function submit() {
error.value = 'عنوان کار را وارد کنید.';
return Promise.reject();
}
const link = props.preset ?? form.entity;
const link = props.preset;
const body: CreateRoutineTaskRequest = {
title: form.title.trim(),
description: form.description.trim() || null,
priority: form.priority,
categoryId: form.categoryId || null,
assigneeId: form.assigneeId || null,
isMandatory: form.isMandatory,
isMandatory: link ? false : form.isMandatory,
source: sourceFor(link?.type),
scheduledFor: form.date || todayKey(),
dueAt: form.time ? localInputToIso(form.date || todayKey(), form.time) : null,
@@ -182,7 +179,7 @@ async function submit() {
<a-form-item label="عنوان" required>
<a-input
v-model:value="form.title"
placeholder="مثلاً: تماس با مشتری برای پیگیری پیش‌فاکتور"
placeholder="مثلاً: آماده‌سازی گزارش هفتگی تیم"
:maxlength="200"
/>
</a-form-item>
@@ -229,16 +226,20 @@ async function submit() {
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="اتصال به پرونده">
<a-space v-if="preset" :size="8">
<a-col v-if="preset" :span="24">
<a-form-item label="پرونده">
<a-space :size="8">
<a-tag :bordered="false">{{ ENTITY_TYPE_LABELS[preset.type] }}</a-tag>
<a-typography-text strong>{{ preset.label }}</a-typography-text>
</a-space>
<EntityLinkPicker v-else v-model="form.entity" />
</a-form-item>
<a-alert
type="info"
show-icon
message="این کار روی همین پرونده می‌ماند و در برنامه روزانه کارهای روتین دیده نمی‌شود."
/>
</a-col>
<a-col :span="24">
<a-col v-else :span="24">
<a-checkbox v-model:checked="form.isMandatory">
کار اجباری (در امتیاز روزانه و Streak حساب می‌شود)
</a-checkbox>
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
Alert as AAlert,
Button as AButton,
@@ -19,7 +18,14 @@ import RoutineTaskRow, { type RoutineTaskRowAction } from '@/components/routines
import RoutineTaskModals from '@/components/routines/RoutineTaskModals.vue';
import { api } from '@/lib/api';
import { toFaDigits } from '@/lib/jalali';
import { apiError, isOpenStatus } from '@/lib/routines';
import {
ROUTINE_TASK_STATUS_LABELS,
apiError,
formatDayNumeric,
formatTime,
hasExplicitTime,
isOpenStatus,
} from '@/lib/routines';
import { useRoutineTaskActions } from '@/lib/useRoutineTaskActions';
import { useAuthStore } from '@/stores/auth';
@@ -38,7 +44,6 @@ const props = withDefaults(
},
);
const router = useRouter();
const auth = useAuthStore();
const enabled = computed(() => {
@@ -51,6 +56,7 @@ const loading = ref(false);
const error = ref('');
const showClosed = ref(false);
const quickOpen = ref(false);
const detail = ref<RoutineTaskDto | null>(null);
async function load() {
if (!enabled.value || !props.entity.id) return;
@@ -73,6 +79,7 @@ watch(() => `${props.entity.type}:${props.entity.id}`, load, { immediate: true }
const actions = useRoutineTaskActions((updated) => {
const i = items.value.findIndex((t) => t.id === updated.id);
if (i >= 0) items.value[i] = updated;
if (detail.value?.id === updated.id) detail.value = updated;
});
function onAction(kind: RoutineTaskRowAction, task: RoutineTaskDto) {
@@ -88,7 +95,7 @@ const open = computed(() => items.value.filter((t) => isOpenStatus(t.status)));
const closed = computed(() => items.value.filter((t) => !isOpenStatus(t.status)));
function openTask(t: RoutineTaskDto) {
void router.push({ path: '/routines', query: { task: t.id } });
detail.value = t;
}
function onCreated(t: RoutineTaskDto) {
@@ -182,5 +189,27 @@ function onCreated(t: RoutineTaskDto) {
<QuickTaskModal :open="quickOpen" :preset="entity" @close="quickOpen = false" @created="onCreated" />
<RoutineTaskModals :actions="actions" />
<a-drawer :open="detail != null" title="جزئیات کار" @close="detail = null">
<template v-if="detail">
<a-typography-paragraph v-if="detail.description">{{ detail.description }}</a-typography-paragraph>
<a-descriptions :column="1">
<a-descriptions-item label="وضعیت">
{{ ROUTINE_TASK_STATUS_LABELS[detail.status] }}
</a-descriptions-item>
<a-descriptions-item label="مسئول">
{{ detail.assignee?.fullName || '—' }}
</a-descriptions-item>
<a-descriptions-item label="روز برنامه">
{{ formatDayNumeric(detail.scheduledFor) }}
</a-descriptions-item>
<a-descriptions-item label="مهلت">
{{ detail.dueAt && hasExplicitTime(detail) ? formatTime(detail.dueAt) : 'تا پایان روز' }}
</a-descriptions-item>
</a-descriptions>
<a-typography-paragraph v-if="detail.resultNote" :style="{ marginTop: '12px' }">
{{ detail.resultNote }}
</a-typography-paragraph>
</template>
</a-drawer>
</a-card>
</template>
@@ -97,6 +97,11 @@ const actions = useRoutineTaskActions((updated) => {
});
const isOpen = computed(() => (task.value ? isOpenStatus(task.value.status) : false));
const customerTask = computed(() => {
const t = task.value;
if (!t) return false;
return (t.source !== 'routine' && t.source !== 'manual') || t.entities.length > 0;
});
const done = computed(() => task.value?.status === 'completed');
const canManage = computed(() => !!shell?.context.value?.isManager);
const priorities = Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>;
@@ -190,10 +195,13 @@ function eventText(ev: RoutineTaskEventDto): string {
</div>
<h3 class="td-title" :class="{ done }">{{ task.title }}</h3>
<p v-if="task.description" class="td-desc">{{ task.description }}</p>
<p v-if="customerTask" class="note">
این کار مربوط به پرونده است و در برنامه روزانه کارهای روتین نیست.
</p>
</section>
<!-- اکشن اصلی -->
<section v-if="!editing" class="td-actions">
<section v-if="!editing && !customerTask" class="td-actions">
<UiButton
v-if="isOpen"
variant="success"
@@ -370,7 +378,7 @@ function eventText(ev: RoutineTaskEventDto): string {
</section>
</div>
<template v-if="task && !editing" #footer>
<template v-if="task && !editing && !customerTask" #footer>
<UiButton variant="ghost" size="sm" @click="startEdit">
<template #icon><UiIcon name="pencil" :size="14" /></template>
ویرایش
-26
View File
@@ -296,29 +296,6 @@ async function remove() {
}
}
const routinesEnabled = computed(() => {
const mods = auth.user?.enabledModules;
return !mods || mods.includes('routines');
});
const converting = ref(false);
/** پیگیری → Task در «کارهای روتین» (تکراری نمی‌سازد) */
async function convertToTask() {
if (!id.value || converting.value) return;
converting.value = true;
error.value = '';
try {
const { data } = await api.post<{ id: string }>(`/routine-tasks/from-activity/${id.value}`);
moreOpen.value = false;
void router.push({ path: '/routines', query: { task: data.id } });
} catch (e) {
const msg = (e as { response?: { data?: { message?: string | string[] } } }).response?.data?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'تبدیل به Task ممکن نشد.');
} finally {
converting.value = false;
}
}
watch(id, () => {
commentOpen.value = false;
postponeOpen.value = false;
@@ -344,9 +321,6 @@ onMounted(() => {
</button>
<div v-if="moreOpen && activity" class="menu">
<RouterLink :to="{ name: 'activity-edit', params: { id } }">ویرایش</RouterLink>
<button v-if="routinesEnabled" type="button" :disabled="converting" @click="convertToTask">
تبدیل به Task
</button>
<button type="button" class="danger" @click="remove">حذف</button>
</div>
</div>
@@ -101,7 +101,7 @@ const hideHeader = computed(() => route.name === 'routine-new' || route.name ===
<template>
<section class="crm-page routines-shell">
<template v-if="!hideHeader">
<UiPageHeader title="کارهای روتین" lead="برنامه روزانه، روتین‌های تکرارشونده و پیگیری انجام کارها">
<UiPageHeader title="کارهای روتین" lead="برنامه روزانه و کارهای داخلی تیم">
<template #actions>
<RouterLink to="/routines/settings" class="icon-link" title="تنظیمات یادآوری و اعلان‌ها">
<UiIcon name="bell" :size="17" />
@@ -51,7 +51,9 @@ const f = reactive({
const users = computed(() => shell.context.value?.users ?? []);
const teams = computed(() => shell.context.value?.teams ?? []);
const categories = computed(() => shell.context.value?.categories ?? []);
const sources = Object.entries(ROUTINE_TASK_SOURCE_LABELS) as Array<[RoutineTaskSource, string]>;
const sources = (['routine', 'manual'] as const).map(
(key) => [key, ROUTINE_TASK_SOURCE_LABELS[key]] as [RoutineTaskSource, string],
);
const statuses = Object.entries(ROUTINE_TASK_STATUS_LABELS) as Array<[RoutineTaskStatus, string]>;
const query = computed<RoutineStatsQuery>(() => {