Files
CRM_Montzar/apps/web/src/components/routines/RelatedTasksPanel.vue
T

216 lines
6.8 KiB
Vue
Raw Normal View History

<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>