2026-10-06 19:54:38 +03:30
|
|
|
<script setup lang="ts">
|
2026-10-18 19:39:59 +03:30
|
|
|
import { computed } from 'vue';
|
2026-10-06 19:54:38 +03:30
|
|
|
import { RouterLink } from 'vue-router';
|
|
|
|
|
import type { RoutineTaskDto } from '@mento/shared';
|
2026-10-18 19:39:59 +03:30
|
|
|
import {
|
|
|
|
|
Button as AButton,
|
|
|
|
|
Checkbox as ACheckbox,
|
|
|
|
|
Dropdown as ADropdown,
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
Menu as AMenu,
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
Tag as ATag,
|
|
|
|
|
Typography,
|
|
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
import {
|
|
|
|
|
CalendarOutlined,
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
FlagOutlined,
|
|
|
|
|
MoreOutlined,
|
|
|
|
|
WarningOutlined,
|
|
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
|
|
|
|
|
const AMenuItem = AMenu.Item;
|
2026-10-06 19:54:38 +03:30
|
|
|
import UserAvatar from '@/components/UserAvatar.vue';
|
|
|
|
|
import {
|
|
|
|
|
ENTITY_TYPE_LABELS,
|
|
|
|
|
PRIORITY_LABELS,
|
|
|
|
|
ROUTINE_TASK_SOURCE_LABELS,
|
|
|
|
|
ROUTINE_TASK_STATUS_LABELS,
|
|
|
|
|
entityRoute,
|
|
|
|
|
formatDayNumeric,
|
|
|
|
|
formatDelay,
|
|
|
|
|
formatTime,
|
|
|
|
|
hasExplicitTime,
|
|
|
|
|
isOpenStatus,
|
|
|
|
|
} from '@/lib/routines';
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
const { Text: ATypographyText, Title: ATypographyTitle, Paragraph: ATypographyParagraph } =
|
|
|
|
|
Typography;
|
|
|
|
|
|
2026-10-06 19:54:38 +03:30
|
|
|
export type RoutineTaskRowAction =
|
|
|
|
|
| 'start'
|
|
|
|
|
| 'complete-note'
|
|
|
|
|
| 'reschedule'
|
|
|
|
|
| 'move-today'
|
|
|
|
|
| 'skip'
|
|
|
|
|
| 'cancel';
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
|
defineProps<{
|
|
|
|
|
task: RoutineTaskDto;
|
|
|
|
|
busy?: boolean;
|
|
|
|
|
showAssignee?: boolean;
|
|
|
|
|
showDate?: boolean;
|
|
|
|
|
showDelay?: boolean;
|
|
|
|
|
}>(),
|
|
|
|
|
{ busy: false, showAssignee: false, showDate: false, showDelay: false },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
toggle: [task: RoutineTaskDto];
|
|
|
|
|
open: [task: RoutineTaskDto];
|
|
|
|
|
action: [kind: RoutineTaskRowAction, task: RoutineTaskDto];
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const done = computed(() => props.task.status === 'completed');
|
|
|
|
|
const closed = computed(() => props.task.status === 'skipped' || props.task.status === 'cancelled');
|
|
|
|
|
const open = computed(() => isOpenStatus(props.task.status));
|
|
|
|
|
const showTime = computed(() => hasExplicitTime(props.task));
|
|
|
|
|
const checkLabel = computed(() =>
|
2026-10-18 19:39:59 +03:30
|
|
|
done.value
|
|
|
|
|
? `برگرداندن «${props.task.title}» به حالت انجامنشده`
|
|
|
|
|
: `علامتزدن «${props.task.title}» بهعنوان انجامشده`,
|
2026-10-06 19:54:38 +03:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function act(kind: RoutineTaskRowAction) {
|
|
|
|
|
emit('action', kind, props.task);
|
|
|
|
|
}
|
2026-10-18 19:39:59 +03:30
|
|
|
|
|
|
|
|
function onMenuClick({ key }: { key: string | number }) {
|
|
|
|
|
const k = String(key);
|
|
|
|
|
if (k === 'open') {
|
|
|
|
|
emit('open', props.task);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
act(k as RoutineTaskRowAction);
|
|
|
|
|
}
|
2026-10-06 19:54:38 +03:30
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-flex
|
|
|
|
|
align="flex-start"
|
|
|
|
|
:gap="12"
|
|
|
|
|
:style="{
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '12px 16px',
|
|
|
|
|
border: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
opacity: closed ? 0.62 : 1,
|
|
|
|
|
}"
|
2026-10-06 19:54:38 +03:30
|
|
|
@click="emit('open', task)"
|
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-checkbox
|
|
|
|
|
:checked="done"
|
2026-10-06 19:54:38 +03:30
|
|
|
:disabled="busy || closed"
|
|
|
|
|
:aria-label="checkLabel"
|
2026-10-18 19:39:59 +03:30
|
|
|
@click.stop
|
|
|
|
|
@update:checked="emit('toggle', task)"
|
|
|
|
|
/>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-flex vertical :gap="4" style="flex: 1; min-width: 0">
|
|
|
|
|
<a-space :size="6">
|
|
|
|
|
<a-typography-title
|
|
|
|
|
:level="5"
|
|
|
|
|
:style="{
|
|
|
|
|
margin: 0,
|
|
|
|
|
textDecoration: done ? 'line-through' : undefined,
|
|
|
|
|
}"
|
|
|
|
|
>
|
|
|
|
|
{{ task.title }}
|
|
|
|
|
</a-typography-title>
|
|
|
|
|
<FlagOutlined v-if="task.isMandatory" />
|
|
|
|
|
</a-space>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-typography-paragraph
|
|
|
|
|
v-if="task.description"
|
|
|
|
|
type="secondary"
|
|
|
|
|
:ellipsis="{ rows: 1 }"
|
|
|
|
|
:style="{ marginBottom: 0 }"
|
|
|
|
|
>
|
|
|
|
|
{{ task.description }}
|
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
|
|
|
|
|
<a-space wrap :size="[8, 4]">
|
|
|
|
|
<a-typography-text v-if="showDate" type="secondary">
|
|
|
|
|
<CalendarOutlined /> {{ formatDayNumeric(task.scheduledFor) }}
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-typography-text v-if="showTime" type="secondary">
|
|
|
|
|
<ClockCircleOutlined /> {{ formatTime(task.dueAt) }}
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-typography-text
|
|
|
|
|
v-if="showDelay && task.status === 'overdue'"
|
|
|
|
|
type="danger"
|
|
|
|
|
>
|
|
|
|
|
<WarningOutlined /> {{ formatDelay(task.dueAt) }} تأخیر
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-typography-text v-if="done && task.completedAt" type="success">
|
|
|
|
|
<CheckOutlined /> {{ formatTime(task.completedAt) }}
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
<a-tag v-if="task.category" :bordered="false">{{ task.category.name }}</a-tag>
|
|
|
|
|
<a-tag :bordered="false">{{ PRIORITY_LABELS[task.priority] }}</a-tag>
|
|
|
|
|
<a-tag v-if="task.status !== 'todo'" :bordered="false">
|
2026-10-06 19:54:38 +03:30
|
|
|
{{ ROUTINE_TASK_STATUS_LABELS[task.status] }}
|
2026-10-18 19:39:59 +03:30
|
|
|
</a-tag>
|
|
|
|
|
<a-tag :bordered="false">{{ ROUTINE_TASK_SOURCE_LABELS[task.source] }}</a-tag>
|
2026-10-06 19:54:38 +03:30
|
|
|
<RouterLink
|
|
|
|
|
v-for="ent in task.entities"
|
|
|
|
|
:key="`${ent.type}-${ent.id}`"
|
|
|
|
|
:to="entityRoute(ent)"
|
|
|
|
|
@click.stop
|
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-tag color="processing" :bordered="false">
|
|
|
|
|
{{ ENTITY_TYPE_LABELS[ent.type] }} · {{ ent.label }}
|
|
|
|
|
</a-tag>
|
2026-10-06 19:54:38 +03:30
|
|
|
</RouterLink>
|
2026-10-18 19:39:59 +03:30
|
|
|
</a-space>
|
|
|
|
|
</a-flex>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-space :size="8" @click.stop>
|
|
|
|
|
<a-space v-if="showAssignee && task.assignee" :size="6">
|
2026-10-06 19:54:38 +03:30
|
|
|
<UserAvatar :name="task.assignee.fullName" :user-id="task.assignee.id" :size="26" />
|
2026-10-18 19:39:59 +03:30
|
|
|
<a-typography-text type="secondary">{{ task.assignee.fullName }}</a-typography-text>
|
|
|
|
|
</a-space>
|
|
|
|
|
<a-dropdown>
|
|
|
|
|
<a-button type="text" aria-label="عملیات بیشتر">
|
|
|
|
|
<template #icon><MoreOutlined /></template>
|
|
|
|
|
</a-button>
|
|
|
|
|
<template #overlay>
|
|
|
|
|
<a-menu @click="onMenuClick">
|
|
|
|
|
<a-menu-item key="open">جزئیات</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="task.status === 'todo'" key="start">شروع کار</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="open || done" key="complete-note">
|
|
|
|
|
{{ done ? 'ویرایش گزارش نتیجه' : 'انجام با ثبت گزارش' }}
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="open" key="reschedule">تغییر زمان</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="task.status === 'overdue'" key="move-today">
|
|
|
|
|
انتقال به امروز
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="open && task.rules.allowSkip" key="skip">رد کردن</a-menu-item>
|
|
|
|
|
<a-menu-item v-if="open" key="cancel" danger>لغو کار</a-menu-item>
|
|
|
|
|
</a-menu>
|
2026-10-06 19:54:38 +03:30
|
|
|
</template>
|
2026-10-18 19:39:59 +03:30
|
|
|
</a-dropdown>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-flex>
|
2026-10-06 19:54:38 +03:30
|
|
|
</template>
|