fix(routines): keep the daily desk for internal team work
Customer-linked tasks stay on their record and no longer appear in today, calendar, score, or reminders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { type Actor, elevated } from '../common/record-scope';
|
||||
import { dayKeyToDate, tehranDayKey, addDays } from '../routines/routine-dates';
|
||||
import { internalRoutineTaskWhere } from '../routines/internal-routine';
|
||||
import { normalizeLevels, periodStart, previousPeriod, sanitizeConditions } from './gamification-engine';
|
||||
import { GamificationService, parsePeriod, toRuleDto } from './gamification.service';
|
||||
import type {
|
||||
@@ -751,6 +752,7 @@ export class GamificationAdminService {
|
||||
organizationId,
|
||||
status: { not: 'cancelled' },
|
||||
scheduledFor: { gte: dayKeyToDate(fromKey), lte: dayKeyToDate(toKey) },
|
||||
...internalRoutineTaskWhere,
|
||||
};
|
||||
const [total, done] = await Promise.all([
|
||||
this.prisma.routineTask.count({ where }),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Prisma, type RoutineTaskSource } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* کارهای روتین فقط کار داخلی تیم است.
|
||||
* پیگیری، لید، مشتری و معامله روی پرونده خودشان میمانند و وارد میز روتین نمیشوند.
|
||||
*/
|
||||
export const internalRoutineTaskWhere: Prisma.RoutineTaskWhereInput = {
|
||||
source: { in: ['routine', 'manual'] },
|
||||
leadId: null,
|
||||
personId: null,
|
||||
companyId: null,
|
||||
dealId: null,
|
||||
invoiceId: null,
|
||||
activityId: null,
|
||||
};
|
||||
|
||||
/** فیلتر میز روتین. منبع مشتری حتی اگر صریحاً خواسته شود وارد لیست نمیشود. */
|
||||
export function deskRoutineTaskWhere(
|
||||
source?: RoutineTaskSource | null,
|
||||
): Prisma.RoutineTaskWhereInput {
|
||||
if (source === 'routine' || source === 'manual') {
|
||||
return { ...internalRoutineTaskWhere, source };
|
||||
}
|
||||
if (source) return { AND: [internalRoutineTaskWhere, { source }] };
|
||||
return internalRoutineTaskWhere;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Cron } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { dayKeyToDate, tehranDayKey, tehranHour, toFaDigits } from './routine-dates';
|
||||
import { internalRoutineTaskWhere } from './internal-routine';
|
||||
|
||||
const TIME_FMT = new Intl.DateTimeFormat('fa-IR', {
|
||||
timeZone: 'Asia/Tehran',
|
||||
@@ -44,6 +45,7 @@ export class RoutineRemindersService {
|
||||
status: { in: ['todo', 'in_progress'] },
|
||||
reminderAt: { lte: now },
|
||||
reminderSentAt: null,
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
select: { id: true, organizationId: true, assigneeId: true, title: true, dueAt: true, reminderAt: true },
|
||||
take: 300,
|
||||
@@ -76,7 +78,7 @@ export class RoutineRemindersService {
|
||||
/** هشدار Task اجباری که مهلتش گذشته («… هنوز انجام نشده») */
|
||||
private async sendOverdueAlerts() {
|
||||
const rows = await this.prisma.routineTask.findMany({
|
||||
where: { status: 'overdue', isMandatory: true, overdueNotifiedAt: null },
|
||||
where: { status: 'overdue', isMandatory: true, overdueNotifiedAt: null, ...internalRoutineTaskWhere },
|
||||
select: { id: true, organizationId: true, assigneeId: true, title: true },
|
||||
take: 300,
|
||||
});
|
||||
@@ -114,7 +116,11 @@ export class RoutineRemindersService {
|
||||
|
||||
const groups = await this.prisma.routineTask.groupBy({
|
||||
by: ['organizationId', 'assigneeId'],
|
||||
where: { scheduledFor: todayDate, status: { in: ['todo', 'in_progress'] } },
|
||||
where: {
|
||||
scheduledFor: todayDate,
|
||||
status: { in: ['todo', 'in_progress'] },
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
if (!groups.length) return;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from '@mento/shared';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { deskRoutineTaskWhere, internalRoutineTaskWhere } from './internal-routine';
|
||||
import type { RoutineRangeQueryDto } from './dto';
|
||||
import { describeRecurrence } from './recurrence';
|
||||
import { routineRule } from './routine-mapper';
|
||||
@@ -118,6 +119,7 @@ export class RoutineStatsService {
|
||||
organizationId,
|
||||
assigneeId: userId,
|
||||
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(day) },
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
select: { scheduledFor: true, status: true },
|
||||
});
|
||||
@@ -155,6 +157,7 @@ export class RoutineStatsService {
|
||||
isMandatory: true,
|
||||
status: { not: 'cancelled' },
|
||||
scheduledFor: { gte: dayKeyToDate(addDays(today, -180)), lte: dayKeyToDate(today) },
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
@@ -437,8 +440,8 @@ export class RoutineStatsService {
|
||||
organizationId,
|
||||
assigneeId: { in: userIds },
|
||||
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(to) },
|
||||
...deskRoutineTaskWhere(q.source),
|
||||
...(q.categoryId ? { categoryId: q.categoryId } : {}),
|
||||
...(q.source ? { source: q.source } : {}),
|
||||
...(q.status ? { status: q.status } : {}),
|
||||
},
|
||||
select: statSelect,
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
tehranDateTime,
|
||||
tehranDayKey,
|
||||
} from './routine-dates';
|
||||
import { deskRoutineTaskWhere, internalRoutineTaskWhere } from './internal-routine';
|
||||
|
||||
const OPEN_STATUSES: RoutineTaskStatus[] = ['todo', 'in_progress', 'overdue'];
|
||||
|
||||
@@ -84,6 +85,7 @@ export class RoutineTasksService {
|
||||
assigneeId: target,
|
||||
scheduledFor: dayDate,
|
||||
status: { not: 'cancelled' },
|
||||
...internalRoutineTaskWhere,
|
||||
...deep,
|
||||
},
|
||||
include: routineTaskInclude,
|
||||
@@ -95,6 +97,7 @@ export class RoutineTasksService {
|
||||
assigneeId: target,
|
||||
status: 'overdue',
|
||||
scheduledFor: { lt: dayDate },
|
||||
...internalRoutineTaskWhere,
|
||||
...deep,
|
||||
},
|
||||
include: { ...routineTaskInclude, routine: { select: { ...routineTaskInclude.routine.select, carryOverOverdue: true } } },
|
||||
@@ -141,10 +144,16 @@ export class RoutineTasksService {
|
||||
assigneeId: actor.userId,
|
||||
scheduledFor: day,
|
||||
status: { in: ['todo', 'in_progress'] },
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
}),
|
||||
this.prisma.routineTask.count({
|
||||
where: { organizationId: actor.organizationId, assigneeId: actor.userId, status: 'overdue' },
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
assigneeId: actor.userId,
|
||||
status: 'overdue',
|
||||
...internalRoutineTaskWhere,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return { remaining, overdue };
|
||||
@@ -158,6 +167,7 @@ export class RoutineTasksService {
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
status: 'overdue',
|
||||
...internalRoutineTaskWhere,
|
||||
...filter,
|
||||
...this.deepWhere(actor),
|
||||
},
|
||||
@@ -179,9 +189,9 @@ export class RoutineTasksService {
|
||||
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(to) },
|
||||
...filter,
|
||||
...this.deepWhere(actor),
|
||||
...deskRoutineTaskWhere(q.source),
|
||||
...(q.categoryId ? { categoryId: q.categoryId } : {}),
|
||||
...(q.status ? { status: q.status } : {}),
|
||||
...(q.source ? { source: q.source } : {}),
|
||||
};
|
||||
const rows = await this.prisma.routineTask.findMany({
|
||||
where,
|
||||
@@ -311,46 +321,10 @@ export class RoutineTasksService {
|
||||
return toTaskDto(row);
|
||||
}
|
||||
|
||||
async createFromActivity(actor: Actor, activityId: string): Promise<RoutineTaskDto> {
|
||||
const activity = await this.prisma.activity.findFirst({
|
||||
where: { id: activityId, organizationId: actor.organizationId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
dueAt: true,
|
||||
priority: true,
|
||||
personId: true,
|
||||
companyId: true,
|
||||
dealId: true,
|
||||
assigneeId: true,
|
||||
},
|
||||
});
|
||||
if (!activity) throw new NotFoundException('پیگیری پیدا نشد');
|
||||
|
||||
const existing = await this.prisma.routineTask.findFirst({
|
||||
where: { organizationId: actor.organizationId, activityId, status: { not: 'cancelled' } },
|
||||
include: routineTaskInclude,
|
||||
});
|
||||
if (existing) return toTaskDto(existing);
|
||||
|
||||
const scope = await this.scope(actor);
|
||||
const assigneeId =
|
||||
activity.assigneeId && canManage(scope, activity.assigneeId)
|
||||
? activity.assigneeId
|
||||
: actor.userId;
|
||||
return this.create(actor, {
|
||||
title: activity.title,
|
||||
description: activity.description,
|
||||
dueAt: activity.dueAt?.toISOString() ?? null,
|
||||
priority: activity.priority,
|
||||
assigneeId,
|
||||
personId: activity.personId,
|
||||
companyId: activity.companyId,
|
||||
dealId: activity.dealId,
|
||||
activityId: activity.id,
|
||||
source: 'follow_up',
|
||||
});
|
||||
async createFromActivity(_actor: Actor, _activityId: string): Promise<RoutineTaskDto> {
|
||||
throw new BadRequestException(
|
||||
'پیگیری مشتری در کارهای روتین ثبت نمیشود. کارهای روتین فقط برای کار داخلی تیم است.',
|
||||
);
|
||||
}
|
||||
|
||||
async update(actor: Actor, id: string, dto: UpdateRoutineTaskDto): Promise<RoutineTaskDto> {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import EntityLinkPicker, { type PickedEntity } from '@/components/routines/EntityLinkPicker.vue';
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
ENTITY_TYPE_LABELS,
|
||||
@@ -64,7 +63,6 @@ const form = reactive({
|
||||
categoryId: '',
|
||||
assigneeId: '',
|
||||
isMandatory: false,
|
||||
entity: null as PickedEntity | null,
|
||||
});
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
@@ -110,7 +108,6 @@ watch(
|
||||
categoryId: '',
|
||||
assigneeId: props.defaultAssigneeId || auth.user?.id || '',
|
||||
isMandatory: false,
|
||||
entity: null,
|
||||
});
|
||||
error.value = '';
|
||||
void ensureContext();
|
||||
@@ -138,14 +135,14 @@ async function submit() {
|
||||
error.value = 'عنوان کار را وارد کنید.';
|
||||
return Promise.reject();
|
||||
}
|
||||
const link = props.preset ?? form.entity;
|
||||
const link = props.preset;
|
||||
const body: CreateRoutineTaskRequest = {
|
||||
title: form.title.trim(),
|
||||
description: form.description.trim() || null,
|
||||
priority: form.priority,
|
||||
categoryId: form.categoryId || null,
|
||||
assigneeId: form.assigneeId || null,
|
||||
isMandatory: form.isMandatory,
|
||||
isMandatory: link ? false : form.isMandatory,
|
||||
source: sourceFor(link?.type),
|
||||
scheduledFor: form.date || todayKey(),
|
||||
dueAt: form.time ? localInputToIso(form.date || todayKey(), form.time) : null,
|
||||
@@ -182,7 +179,7 @@ async function submit() {
|
||||
<a-form-item label="عنوان" required>
|
||||
<a-input
|
||||
v-model:value="form.title"
|
||||
placeholder="مثلاً: تماس با مشتری برای پیگیری پیشفاکتور"
|
||||
placeholder="مثلاً: آمادهسازی گزارش هفتگی تیم"
|
||||
:maxlength="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -229,16 +226,20 @@ async function submit() {
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="اتصال به پرونده">
|
||||
<a-space v-if="preset" :size="8">
|
||||
<a-col v-if="preset" :span="24">
|
||||
<a-form-item label="پرونده">
|
||||
<a-space :size="8">
|
||||
<a-tag :bordered="false">{{ ENTITY_TYPE_LABELS[preset.type] }}</a-tag>
|
||||
<a-typography-text strong>{{ preset.label }}</a-typography-text>
|
||||
</a-space>
|
||||
<EntityLinkPicker v-else v-model="form.entity" />
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="این کار روی همین پرونده میماند و در برنامه روزانه کارهای روتین دیده نمیشود."
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-col v-else :span="24">
|
||||
<a-checkbox v-model:checked="form.isMandatory">
|
||||
کار اجباری (در امتیاز روزانه و Streak حساب میشود)
|
||||
</a-checkbox>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
@@ -19,7 +18,14 @@ import RoutineTaskRow, { type RoutineTaskRowAction } from '@/components/routines
|
||||
import RoutineTaskModals from '@/components/routines/RoutineTaskModals.vue';
|
||||
import { api } from '@/lib/api';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import { apiError, isOpenStatus } from '@/lib/routines';
|
||||
import {
|
||||
ROUTINE_TASK_STATUS_LABELS,
|
||||
apiError,
|
||||
formatDayNumeric,
|
||||
formatTime,
|
||||
hasExplicitTime,
|
||||
isOpenStatus,
|
||||
} from '@/lib/routines';
|
||||
import { useRoutineTaskActions } from '@/lib/useRoutineTaskActions';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
@@ -38,7 +44,6 @@ const props = withDefaults(
|
||||
},
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const enabled = computed(() => {
|
||||
@@ -51,6 +56,7 @@ 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;
|
||||
@@ -73,6 +79,7 @@ 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) {
|
||||
@@ -88,7 +95,7 @@ const open = computed(() => items.value.filter((t) => isOpenStatus(t.status)));
|
||||
const closed = computed(() => items.value.filter((t) => !isOpenStatus(t.status)));
|
||||
|
||||
function openTask(t: RoutineTaskDto) {
|
||||
void router.push({ path: '/routines', query: { task: t.id } });
|
||||
detail.value = t;
|
||||
}
|
||||
|
||||
function onCreated(t: RoutineTaskDto) {
|
||||
@@ -182,5 +189,27 @@ function onCreated(t: RoutineTaskDto) {
|
||||
|
||||
<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>
|
||||
|
||||
@@ -97,6 +97,11 @@ const actions = useRoutineTaskActions((updated) => {
|
||||
});
|
||||
|
||||
const isOpen = computed(() => (task.value ? isOpenStatus(task.value.status) : false));
|
||||
const customerTask = computed(() => {
|
||||
const t = task.value;
|
||||
if (!t) return false;
|
||||
return (t.source !== 'routine' && t.source !== 'manual') || t.entities.length > 0;
|
||||
});
|
||||
const done = computed(() => task.value?.status === 'completed');
|
||||
const canManage = computed(() => !!shell?.context.value?.isManager);
|
||||
const priorities = Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>;
|
||||
@@ -190,10 +195,13 @@ function eventText(ev: RoutineTaskEventDto): string {
|
||||
</div>
|
||||
<h3 class="td-title" :class="{ done }">{{ task.title }}</h3>
|
||||
<p v-if="task.description" class="td-desc">{{ task.description }}</p>
|
||||
<p v-if="customerTask" class="note">
|
||||
این کار مربوط به پرونده است و در برنامه روزانه کارهای روتین نیست.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- اکشن اصلی -->
|
||||
<section v-if="!editing" class="td-actions">
|
||||
<section v-if="!editing && !customerTask" class="td-actions">
|
||||
<UiButton
|
||||
v-if="isOpen"
|
||||
variant="success"
|
||||
@@ -370,7 +378,7 @@ function eventText(ev: RoutineTaskEventDto): string {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template v-if="task && !editing" #footer>
|
||||
<template v-if="task && !editing && !customerTask" #footer>
|
||||
<UiButton variant="ghost" size="sm" @click="startEdit">
|
||||
<template #icon><UiIcon name="pencil" :size="14" /></template>
|
||||
ویرایش
|
||||
|
||||
@@ -296,29 +296,6 @@ async function remove() {
|
||||
}
|
||||
}
|
||||
|
||||
const routinesEnabled = computed(() => {
|
||||
const mods = auth.user?.enabledModules;
|
||||
return !mods || mods.includes('routines');
|
||||
});
|
||||
const converting = ref(false);
|
||||
|
||||
/** پیگیری → Task در «کارهای روتین» (تکراری نمیسازد) */
|
||||
async function convertToTask() {
|
||||
if (!id.value || converting.value) return;
|
||||
converting.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await api.post<{ id: string }>(`/routine-tasks/from-activity/${id.value}`);
|
||||
moreOpen.value = false;
|
||||
void router.push({ path: '/routines', query: { task: data.id } });
|
||||
} catch (e) {
|
||||
const msg = (e as { response?: { data?: { message?: string | string[] } } }).response?.data?.message;
|
||||
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'تبدیل به Task ممکن نشد.');
|
||||
} finally {
|
||||
converting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(id, () => {
|
||||
commentOpen.value = false;
|
||||
postponeOpen.value = false;
|
||||
@@ -344,9 +321,6 @@ onMounted(() => {
|
||||
</button>
|
||||
<div v-if="moreOpen && activity" class="menu">
|
||||
<RouterLink :to="{ name: 'activity-edit', params: { id } }">ویرایش</RouterLink>
|
||||
<button v-if="routinesEnabled" type="button" :disabled="converting" @click="convertToTask">
|
||||
تبدیل به Task
|
||||
</button>
|
||||
<button type="button" class="danger" @click="remove">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ const hideHeader = computed(() => route.name === 'routine-new' || route.name ===
|
||||
<template>
|
||||
<section class="crm-page routines-shell">
|
||||
<template v-if="!hideHeader">
|
||||
<UiPageHeader title="کارهای روتین" lead="برنامه روزانه، روتینهای تکرارشونده و پیگیری انجام کارها">
|
||||
<UiPageHeader title="کارهای روتین" lead="برنامه روزانه و کارهای داخلی تیم">
|
||||
<template #actions>
|
||||
<RouterLink to="/routines/settings" class="icon-link" title="تنظیمات یادآوری و اعلانها">
|
||||
<UiIcon name="bell" :size="17" />
|
||||
|
||||
@@ -51,7 +51,9 @@ const f = reactive({
|
||||
const users = computed(() => shell.context.value?.users ?? []);
|
||||
const teams = computed(() => shell.context.value?.teams ?? []);
|
||||
const categories = computed(() => shell.context.value?.categories ?? []);
|
||||
const sources = Object.entries(ROUTINE_TASK_SOURCE_LABELS) as Array<[RoutineTaskSource, string]>;
|
||||
const sources = (['routine', 'manual'] as const).map(
|
||||
(key) => [key, ROUTINE_TASK_SOURCE_LABELS[key]] as [RoutineTaskSource, string],
|
||||
);
|
||||
const statuses = Object.entries(ROUTINE_TASK_STATUS_LABELS) as Array<[RoutineTaskStatus, string]>;
|
||||
|
||||
const query = computed<RoutineStatsQuery>(() => {
|
||||
|
||||
Reference in New Issue
Block a user