Files
CRM_Montzar/apps/web/src/components/routines/RelatedTasksPanel.vue
T
MahdiandCursor a17c7dbabc 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>
2026-10-29 20:53:26 +03:30

216 lines
6.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Empty as AEmpty,
Flex as AFlex,
Space as ASpace,
Spin as ASpin,
Tag as ATag,
Typography,
} from 'ant-design-vue';
import { CheckOutlined, PlusOutlined } from '@ant-design/icons-vue';
import type { RoutineEntityRefDto, RoutineTaskDto } from '@mento/shared';
import QuickTaskModal from '@/components/routines/QuickTaskModal.vue';
import RoutineTaskRow, { type RoutineTaskRowAction } from '@/components/routines/RoutineTaskRow.vue';
import RoutineTaskModals from '@/components/routines/RoutineTaskModals.vue';
import { api } from '@/lib/api';
import { toFaDigits } from '@/lib/jalali';
import {
ROUTINE_TASK_STATUS_LABELS,
apiError,
formatDayNumeric,
formatTime,
hasExplicitTime,
isOpenStatus,
} from '@/lib/routines';
import { useRoutineTaskActions } from '@/lib/useRoutineTaskActions';
import { useAuthStore } from '@/stores/auth';
const { Text: ATypographyText, Title: ATypographyTitle } = Typography;
const props = withDefaults(
defineProps<{
entity: RoutineEntityRefDto;
title?: string;
emptyTitle?: string;
emptyHint?: string;
}>(),
{
title: 'کارهای مرتبط',
emptyTitle: 'هنوز کاری برای این پرونده ثبت نشده.',
},
);
const auth = useAuthStore();
const enabled = computed(() => {
const mods = auth.user?.enabledModules;
return !mods || mods.includes('routines');
});
const items = ref<RoutineTaskDto[]>([]);
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;
loading.value = true;
error.value = '';
try {
const { data } = await api.get<RoutineTaskDto[]>('/routine-tasks/by-entity', {
params: { [`${props.entity.type}Id`]: props.entity.id },
});
items.value = data;
} catch (e) {
error.value = apiError(e, 'بارگذاری کارهای مرتبط ممکن نشد.');
} finally {
loading.value = false;
}
}
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) {
if (kind === 'start') void actions.start(task);
else if (kind === 'complete-note') actions.openComplete(task);
else if (kind === 'reschedule') actions.openReschedule(task);
else if (kind === 'move-today') void actions.moveToday(task);
else if (kind === 'skip') actions.openSkip(task);
else actions.openCancel(task);
}
const open = computed(() => items.value.filter((t) => isOpenStatus(t.status)));
const closed = computed(() => items.value.filter((t) => !isOpenStatus(t.status)));
function openTask(t: RoutineTaskDto) {
detail.value = t;
}
function onCreated(t: RoutineTaskDto) {
items.value = [t, ...items.value];
}
</script>
<template>
<a-card v-if="enabled" :bordered="false" :style="{ marginTop: '16px' }">
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
<a-space :size="8">
<CheckOutlined />
<a-typography-title :level="5" :style="{ margin: 0 }">{{ title }}</a-typography-title>
<a-tag v-if="open.length" :bordered="false">{{ toFaDigits(open.length) }} باز</a-tag>
</a-space>
<a-button @click="quickOpen = true">
<template #icon><PlusOutlined /></template>
ایجاد Task
</a-button>
</a-flex>
<a-alert
v-if="error"
type="error"
show-icon
:message="error"
:style="{ marginTop: '12px' }"
/>
<a-alert
v-if="actions.error.value"
type="error"
show-icon
:message="actions.error.value"
:style="{ marginTop: '12px' }"
/>
<a-spin :spinning="loading && !items.length">
<a-empty
v-if="!loading && !items.length"
:description="emptyTitle"
:style="{ margin: '16px 0' }"
>
<template v-if="emptyHint" #description>
<div>
<div>{{ emptyTitle }}</div>
<a-typography-text type="secondary">{{ emptyHint }}</a-typography-text>
</div>
</template>
<a-button type="primary" @click="quickOpen = true">
<template #icon><PlusOutlined /></template>
ایجاد Task
</a-button>
</a-empty>
<a-flex v-else vertical :gap="8" :style="{ marginTop: '12px' }">
<RoutineTaskRow
v-for="t in open"
:key="t.id"
:task="t"
:busy="actions.busyId.value === t.id"
show-date
show-assignee
show-delay
@toggle="actions.toggle"
@open="openTask"
@action="onAction"
/>
<a-button
v-if="closed.length"
type="link"
@click="showClosed = !showClosed"
>
{{ showClosed ? 'پنهان کردن' : 'نمایش' }}
{{ toFaDigits(closed.length) }} کار انجام‌شده / بسته
</a-button>
<template v-if="showClosed">
<RoutineTaskRow
v-for="t in closed"
:key="t.id"
:task="t"
:busy="actions.busyId.value === t.id"
show-date
show-assignee
@toggle="actions.toggle"
@open="openTask"
@action="onAction"
/>
</template>
</a-flex>
</a-spin>
<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>