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,253 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, reactive, ref, watch } from 'vue';
|
||||
import type {
|
||||
ActivityPriority,
|
||||
CreateRoutineTaskRequest,
|
||||
RoutineContextDto,
|
||||
RoutineEntityRefDto,
|
||||
RoutineTaskDto,
|
||||
RoutineTaskSource,
|
||||
} from '@mento/shared';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import EntityLinkPicker, { type PickedEntity } from '@/components/routines/EntityLinkPicker.vue';
|
||||
import { UiButton, UiField, UiInput, UiModal, UiSelect, UiTextarea } from '@/components/ui';
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
ENTITY_TYPE_LABELS,
|
||||
PRIORITY_LABELS,
|
||||
ROUTINES_CTX,
|
||||
apiError,
|
||||
localInputToIso,
|
||||
todayKey,
|
||||
} from '@/lib/routines';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
/** پروندهای که Task از صفحه آن ساخته میشود */
|
||||
preset?: RoutineEntityRefDto | null;
|
||||
defaultDate?: string | null;
|
||||
defaultAssigneeId?: string | null;
|
||||
}>(),
|
||||
{ preset: null, defaultDate: null, defaultAssigneeId: null },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ close: []; created: [task: RoutineTaskDto] }>();
|
||||
|
||||
const auth = useAuthStore();
|
||||
const shell = inject(ROUTINES_CTX, null);
|
||||
const localCtx = ref<RoutineContextDto | null>(null);
|
||||
const ctx = computed(() => shell?.context.value ?? localCtx.value);
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
date: todayKey(),
|
||||
time: '',
|
||||
priority: 'medium' as ActivityPriority,
|
||||
categoryId: '',
|
||||
assigneeId: '',
|
||||
isMandatory: false,
|
||||
entity: null as PickedEntity | null,
|
||||
});
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
const priorities = Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>;
|
||||
|
||||
async function ensureContext() {
|
||||
if (ctx.value) return;
|
||||
try {
|
||||
const { data } = await api.get<RoutineContextDto>('/routines/context');
|
||||
localCtx.value = data;
|
||||
} catch {
|
||||
/* بدون زمینه هم فرم کار میکند */
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!open) return;
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
description: '',
|
||||
date: props.defaultDate || todayKey(),
|
||||
time: '',
|
||||
priority: 'medium',
|
||||
categoryId: '',
|
||||
assigneeId: props.defaultAssigneeId || auth.user?.id || '',
|
||||
isMandatory: false,
|
||||
entity: null,
|
||||
});
|
||||
error.value = '';
|
||||
void ensureContext();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const assignees = computed(() => {
|
||||
const c = ctx.value;
|
||||
if (!c?.isManager) return [];
|
||||
return c.users;
|
||||
});
|
||||
|
||||
function sourceFor(type: RoutineEntityRefDto['type'] | undefined): RoutineTaskSource {
|
||||
if (type === 'lead') return 'lead';
|
||||
if (type === 'person' || type === 'company') return 'customer';
|
||||
if (type === 'deal') return 'deal';
|
||||
if (type === 'activity') return 'follow_up';
|
||||
return 'manual';
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error.value = '';
|
||||
if (form.title.trim().length < 2) {
|
||||
error.value = 'عنوان کار را وارد کنید.';
|
||||
return;
|
||||
}
|
||||
const link = props.preset ?? form.entity;
|
||||
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,
|
||||
source: sourceFor(link?.type),
|
||||
scheduledFor: form.date || todayKey(),
|
||||
dueAt: form.time ? localInputToIso(form.date || todayKey(), form.time) : null,
|
||||
};
|
||||
if (link) (body as unknown as Record<string, unknown>)[`${link.type}Id`] = link.id;
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data } = await api.post<RoutineTaskDto>('/routine-tasks', body);
|
||||
emit('created', data);
|
||||
shell?.notifyChanged();
|
||||
emit('close');
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ثبت کار ممکن نشد.');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiModal :open="open" title="کار جدید" @close="emit('close')">
|
||||
<form class="qt-form" @submit.prevent="submit">
|
||||
<UiField label="عنوان" required>
|
||||
<UiInput v-model="form.title" placeholder="مثلاً: تماس با مشتری برای پیگیری پیشفاکتور" :maxlength="200" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="توضیحات">
|
||||
<UiTextarea v-model="form.description" :rows="2" :maxlength="2000" />
|
||||
</UiField>
|
||||
|
||||
<div class="grid-2">
|
||||
<UiField label="تاریخ" required>
|
||||
<JalaliDateInput v-model="form.date" mode="date" :clearable="false" />
|
||||
</UiField>
|
||||
<UiField label="ساعت" hint="خالی = تا پایان روز">
|
||||
<JalaliDateInput v-model="form.time" mode="time" />
|
||||
</UiField>
|
||||
<UiField label="اولویت">
|
||||
<UiSelect v-model="form.priority">
|
||||
<option v-for="[k, label] in priorities" :key="k" :value="k">{{ label }}</option>
|
||||
</UiSelect>
|
||||
</UiField>
|
||||
<UiField label="دستهبندی">
|
||||
<UiSelect v-model="form.categoryId">
|
||||
<option value="">بدون دسته</option>
|
||||
<option v-for="c in ctx?.categories ?? []" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</UiSelect>
|
||||
</UiField>
|
||||
<UiField v-if="assignees.length > 1" label="مسئول" class="span-2">
|
||||
<UiSelect v-model="form.assigneeId">
|
||||
<option v-for="u in assignees" :key="u.id" :value="u.id">
|
||||
{{ u.fullName }}{{ u.id === auth.user?.id ? ' (خودم)' : '' }}
|
||||
</option>
|
||||
</UiSelect>
|
||||
</UiField>
|
||||
</div>
|
||||
|
||||
<UiField label="اتصال به پرونده">
|
||||
<div v-if="preset" class="preset">
|
||||
<span>{{ ENTITY_TYPE_LABELS[preset.type] }}</span>
|
||||
<strong>{{ preset.label }}</strong>
|
||||
</div>
|
||||
<EntityLinkPicker v-else v-model="form.entity" />
|
||||
</UiField>
|
||||
|
||||
<label class="check">
|
||||
<input v-model="form.isMandatory" type="checkbox" />
|
||||
<span>کار اجباری (در امتیاز روزانه و Streak حساب میشود)</span>
|
||||
</label>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<UiButton :loading="saving" @click="submit">ثبت کار</UiButton>
|
||||
<UiButton variant="ghost" @click="emit('close')">انصراف</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qt-form {
|
||||
display: grid;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.preset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.preset span {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--ink-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user