Customer-linked tasks stay on their record and no longer appear in today, calendar, score, or reminders. Co-authored-by: Cursor <cursoragent@cursor.com>
216 lines
6.8 KiB
Vue
216 lines
6.8 KiB
Vue
<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>
|