feat: routines module; super admin subscription badge, 7-day trial and lifetime plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import type { RoutineTaskDto } from '@mento/shared';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import UserAvatar from '@/components/UserAvatar.vue';
|
||||
import { UiBadge, UiDropdownMenu } from '@/components/ui';
|
||||
import {
|
||||
ENTITY_TYPE_LABELS,
|
||||
PRIORITY_LABELS,
|
||||
PRIORITY_VARIANTS,
|
||||
ROUTINE_TASK_SOURCE_LABELS,
|
||||
ROUTINE_TASK_STATUS_LABELS,
|
||||
STATUS_VARIANTS,
|
||||
entityRoute,
|
||||
formatDayNumeric,
|
||||
formatDelay,
|
||||
formatTime,
|
||||
hasExplicitTime,
|
||||
isOpenStatus,
|
||||
} from '@/lib/routines';
|
||||
|
||||
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 menuOpen = ref(false);
|
||||
|
||||
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(() =>
|
||||
done.value ? `برگرداندن «${props.task.title}» به حالت انجامنشده` : `علامتزدن «${props.task.title}» بهعنوان انجامشده`,
|
||||
);
|
||||
|
||||
function act(kind: RoutineTaskRowAction) {
|
||||
emit('action', kind, props.task);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="rt-row"
|
||||
:class="[`st-${task.status}`, { done, closed, busy }]"
|
||||
@click="emit('open', task)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rt-check"
|
||||
:class="{ on: done }"
|
||||
:disabled="busy || closed"
|
||||
:aria-pressed="done"
|
||||
:aria-label="checkLabel"
|
||||
@click.stop="emit('toggle', task)"
|
||||
>
|
||||
<span class="box">
|
||||
<UiIcon v-if="done" name="check-plain" :size="16" />
|
||||
<span v-else-if="busy" class="spin" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="rt-main">
|
||||
<div class="rt-title-line">
|
||||
<h3 class="rt-title">{{ task.title }}</h3>
|
||||
<UiIcon v-if="task.isMandatory" class="mand" name="flag" :size="13" title="اجباری" />
|
||||
</div>
|
||||
<p v-if="task.description" class="rt-desc">{{ task.description }}</p>
|
||||
|
||||
<div class="rt-meta">
|
||||
<span v-if="showDate" class="meta-item">
|
||||
<UiIcon name="calendar" :size="13" />
|
||||
{{ formatDayNumeric(task.scheduledFor) }}
|
||||
</span>
|
||||
<span v-if="showTime" class="meta-item">
|
||||
<UiIcon name="clock" :size="13" />
|
||||
{{ formatTime(task.dueAt) }}
|
||||
</span>
|
||||
<span v-if="showDelay && task.status === 'overdue'" class="meta-item late">
|
||||
<UiIcon name="warning" :size="13" />
|
||||
{{ formatDelay(task.dueAt) }} تأخیر
|
||||
</span>
|
||||
<span v-if="done && task.completedAt" class="meta-item ok">
|
||||
<UiIcon name="check" :size="13" />
|
||||
{{ formatTime(task.completedAt) }}
|
||||
</span>
|
||||
<span v-if="task.category" class="cat-chip">
|
||||
<i :style="{ background: task.category.color }" />
|
||||
{{ task.category.name }}
|
||||
</span>
|
||||
<UiBadge size="sm" :variant="PRIORITY_VARIANTS[task.priority]">
|
||||
{{ PRIORITY_LABELS[task.priority] }}
|
||||
</UiBadge>
|
||||
<UiBadge v-if="task.status !== 'todo'" size="sm" :variant="STATUS_VARIANTS[task.status]">
|
||||
{{ ROUTINE_TASK_STATUS_LABELS[task.status] }}
|
||||
</UiBadge>
|
||||
<span class="src-chip" :title="`منبع: ${ROUTINE_TASK_SOURCE_LABELS[task.source]}`">
|
||||
<UiIcon :name="task.source === 'routine' ? 'refresh' : 'pin'" :size="12" />
|
||||
{{ ROUTINE_TASK_SOURCE_LABELS[task.source] }}
|
||||
</span>
|
||||
<RouterLink
|
||||
v-for="ent in task.entities"
|
||||
:key="`${ent.type}-${ent.id}`"
|
||||
class="ent-link"
|
||||
:to="entityRoute(ent)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="ent-type">{{ ENTITY_TYPE_LABELS[ent.type] }}</span>
|
||||
{{ ent.label }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rt-side" @click.stop>
|
||||
<span v-if="showAssignee && task.assignee" class="assignee" :title="task.assignee.fullName">
|
||||
<UserAvatar :name="task.assignee.fullName" :user-id="task.assignee.id" :size="26" />
|
||||
<span class="assignee-name">{{ task.assignee.fullName }}</span>
|
||||
</span>
|
||||
<UiDropdownMenu v-model:open="menuOpen" align="end">
|
||||
<template #trigger="{ toggle }">
|
||||
<button type="button" class="rt-more" aria-label="عملیات بیشتر" @click="toggle">
|
||||
<UiIcon name="dots" :size="18" />
|
||||
</button>
|
||||
</template>
|
||||
<button type="button" @click="emit('open', task)">جزئیات</button>
|
||||
<button v-if="task.status === 'todo'" type="button" @click="act('start')">شروع کار</button>
|
||||
<button v-if="open || done" type="button" @click="act('complete-note')">
|
||||
{{ done ? 'ویرایش گزارش نتیجه' : 'انجام با ثبت گزارش' }}
|
||||
</button>
|
||||
<button v-if="open" type="button" @click="act('reschedule')">تغییر زمان</button>
|
||||
<button v-if="task.status === 'overdue'" type="button" @click="act('move-today')">انتقال به امروز</button>
|
||||
<button v-if="open && task.rules.allowSkip" type="button" @click="act('skip')">رد کردن</button>
|
||||
<button v-if="open" type="button" class="danger" @click="act('cancel')">لغو کار</button>
|
||||
</UiDropdownMenu>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rt-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.rt-row:hover {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.rt-row.st-overdue {
|
||||
border-inline-start: 3px solid var(--danger);
|
||||
}
|
||||
|
||||
.rt-row.done {
|
||||
background: color-mix(in srgb, var(--success-soft) 28%, var(--surface));
|
||||
}
|
||||
|
||||
.rt-row.closed {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.rt-check {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: -6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.rt-check:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rt-check:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.rt-check .box {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 7px;
|
||||
border: 2px solid var(--line-strong);
|
||||
background: var(--surface);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast), transform 0.12s ease;
|
||||
}
|
||||
|
||||
.rt-check:not(:disabled):hover .box {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.rt-check:active .box {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.rt-check.on .box {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.st-overdue .rt-check .box {
|
||||
border-color: color-mix(in srgb, var(--danger) 55%, white);
|
||||
}
|
||||
|
||||
.spin {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--line);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: rt-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rt-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rt-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rt-title-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rt-title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-base);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--ink);
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.done .rt-title {
|
||||
color: var(--muted);
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: color-mix(in srgb, var(--muted) 60%, transparent);
|
||||
}
|
||||
|
||||
.mand {
|
||||
color: var(--warning-deep);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rt-desc {
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rt-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
margin-top: 2px;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-item.late {
|
||||
color: var(--danger);
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
|
||||
.meta-item.ok {
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.cat-chip,
|
||||
.src-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cat-chip i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.src-chip {
|
||||
color: var(--muted-light);
|
||||
}
|
||||
|
||||
.ent-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 220px;
|
||||
padding: 1px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent-deep);
|
||||
font-weight: var(--fw-medium);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ent-link:hover {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.ent-type {
|
||||
color: var(--muted);
|
||||
font-weight: var(--fw-normal);
|
||||
}
|
||||
|
||||
.rt-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.assignee {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--ink-secondary);
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.assignee-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rt-more {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rt-more:hover {
|
||||
background: var(--surface-subtle);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
:global(.ui-dd-panel button.danger) {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.rt-row {
|
||||
padding: var(--sp-3);
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.rt-check {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: -8px -6px;
|
||||
}
|
||||
|
||||
.rt-check .box {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.rt-title {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.assignee-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user