2026-10-06 19:54:38 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, ref, watch } from 'vue';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
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';
|
2026-10-06 19:54:38 +03:30
|
|
|
|
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';
|
2026-10-29 20:53:26 +03:30
|
|
|
|
import {
|
|
|
|
|
|
ROUTINE_TASK_STATUS_LABELS,
|
|
|
|
|
|
apiError,
|
|
|
|
|
|
formatDayNumeric,
|
|
|
|
|
|
formatTime,
|
|
|
|
|
|
hasExplicitTime,
|
|
|
|
|
|
isOpenStatus,
|
|
|
|
|
|
} from '@/lib/routines';
|
2026-10-06 19:54:38 +03:30
|
|
|
|
import { useRoutineTaskActions } from '@/lib/useRoutineTaskActions';
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth';
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const { Text: ATypographyText, Title: ATypographyTitle } = Typography;
|
|
|
|
|
|
|
2026-10-06 19:54:38 +03:30
|
|
|
|
const props = withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
entity: RoutineEntityRefDto;
|
|
|
|
|
|
title?: string;
|
2026-10-13 14:45:29 +03:30
|
|
|
|
emptyTitle?: string;
|
|
|
|
|
|
emptyHint?: string;
|
2026-10-06 19:54:38 +03:30
|
|
|
|
}>(),
|
2026-10-13 14:45:29 +03:30
|
|
|
|
{
|
|
|
|
|
|
title: 'کارهای مرتبط',
|
|
|
|
|
|
emptyTitle: 'هنوز کاری برای این پرونده ثبت نشده.',
|
|
|
|
|
|
},
|
2026-10-06 19:54:38 +03:30
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-10-29 20:53:26 +03:30
|
|
|
|
const detail = ref<RoutineTaskDto | null>(null);
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-29 20:53:26 +03:30
|
|
|
|
if (detail.value?.id === updated.id) detail.value = updated;
|
2026-10-06 19:54:38 +03:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-10-29 20:53:26 +03:30
|
|
|
|
detail.value = t;
|
2026-10-06 19:54:38 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onCreated(t: RoutineTaskDto) {
|
|
|
|
|
|
items.value = [t, ...items.value];
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<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>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
ایجاد Task
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-flex>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<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' }"
|
|
|
|
|
|
/>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<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' }">
|
2026-10-06 19:54:38 +03:30
|
|
|
|
<RoutineTaskRow
|
2026-10-18 19:39:59 +03:30
|
|
|
|
v-for="t in open"
|
2026-10-06 19:54:38 +03:30
|
|
|
|
:key="t.id"
|
|
|
|
|
|
:task="t"
|
|
|
|
|
|
:busy="actions.busyId.value === t.id"
|
|
|
|
|
|
show-date
|
|
|
|
|
|
show-assignee
|
2026-10-18 19:39:59 +03:30
|
|
|
|
show-delay
|
2026-10-06 19:54:38 +03:30
|
|
|
|
@toggle="actions.toggle"
|
|
|
|
|
|
@open="openTask"
|
|
|
|
|
|
@action="onAction"
|
|
|
|
|
|
/>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<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>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
|
|
|
|
|
<QuickTaskModal :open="quickOpen" :preset="entity" @close="quickOpen = false" @created="onCreated" />
|
|
|
|
|
|
<RoutineTaskModals :actions="actions" />
|
2026-10-29 20:53:26 +03:30
|
|
|
|
<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>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-card>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
</template>
|