2026-10-06 19:54:38 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, inject, reactive, ref, watch } from 'vue';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
ActivityPriority,
|
|
|
|
|
|
CreateRoutineTaskRequest,
|
|
|
|
|
|
RoutineContextDto,
|
|
|
|
|
|
RoutineEntityRefDto,
|
|
|
|
|
|
RoutineTaskDto,
|
|
|
|
|
|
RoutineTaskSource,
|
|
|
|
|
|
} from '@mento/shared';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Checkbox as ACheckbox,
|
|
|
|
|
|
Col as ACol,
|
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
|
Input as AInput,
|
|
|
|
|
|
Modal as AModal,
|
|
|
|
|
|
Row as ARow,
|
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
|
Tag as ATag,
|
|
|
|
|
|
Textarea as ATextarea,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'ant-design-vue';
|
2026-10-06 19:54:38 +03:30
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ENTITY_TYPE_LABELS,
|
|
|
|
|
|
PRIORITY_LABELS,
|
|
|
|
|
|
ROUTINES_CTX,
|
|
|
|
|
|
apiError,
|
|
|
|
|
|
localInputToIso,
|
|
|
|
|
|
todayKey,
|
|
|
|
|
|
} from '@/lib/routines';
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth';
|
2026-11-03 13:15:21 +03:30
|
|
|
|
import { canAssignToOthers } from '@/lib/assign-access';
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const { Text: ATypographyText } = Typography;
|
|
|
|
|
|
|
2026-10-06 19:54:38 +03:30
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const priorityOptions = computed(() =>
|
|
|
|
|
|
(Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>).map(
|
|
|
|
|
|
([value, label]) => ({ value, label }),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const categoryOptions = computed(() => [
|
|
|
|
|
|
{ value: '', label: 'بدون دسته' },
|
|
|
|
|
|
...(ctx.value?.categories ?? []).map((c) => ({ value: c.id, label: c.name })),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const assigneeOptions = computed(() =>
|
|
|
|
|
|
assignees.value.map((u) => ({
|
|
|
|
|
|
value: u.id,
|
|
|
|
|
|
label: `${u.fullName}${u.id === auth.user?.id ? ' (خودم)' : ''}`,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
2026-10-06 19:54:38 +03:30
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
void ensureContext();
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const assignees = computed(() => {
|
|
|
|
|
|
const c = ctx.value;
|
2026-11-03 13:15:21 +03:30
|
|
|
|
if (!c?.isManager || !canAssignToOthers(auth.user, 'routines')) return [];
|
2026-10-06 19:54:38 +03:30
|
|
|
|
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 = 'عنوان کار را وارد کنید.';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
return Promise.reject();
|
2026-10-06 19:54:38 +03:30
|
|
|
|
}
|
2026-10-29 20:53:26 +03:30
|
|
|
|
const link = props.preset;
|
2026-10-06 19:54:38 +03:30
|
|
|
|
const body: CreateRoutineTaskRequest = {
|
|
|
|
|
|
title: form.title.trim(),
|
|
|
|
|
|
description: form.description.trim() || null,
|
|
|
|
|
|
priority: form.priority,
|
|
|
|
|
|
categoryId: form.categoryId || null,
|
|
|
|
|
|
assigneeId: form.assigneeId || null,
|
2026-10-29 20:53:26 +03:30
|
|
|
|
isMandatory: link ? false : form.isMandatory,
|
2026-10-06 19:54:38 +03:30
|
|
|
|
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, 'ثبت کار ممکن نشد.');
|
2026-10-18 19:39:59 +03:30
|
|
|
|
return Promise.reject();
|
2026-10-06 19:54:38 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-modal
|
|
|
|
|
|
:open="open"
|
|
|
|
|
|
title="کار جدید"
|
|
|
|
|
|
:confirm-loading="saving"
|
|
|
|
|
|
ok-text="ثبت کار"
|
|
|
|
|
|
cancel-text="انصراف"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
@ok="submit"
|
|
|
|
|
|
@cancel="emit('close')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="عنوان" required>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="form.title"
|
2026-10-29 20:53:26 +03:30
|
|
|
|
placeholder="مثلاً: آمادهسازی گزارش هفتگی تیم"
|
2026-10-18 19:39:59 +03:30
|
|
|
|
:maxlength="200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="توضیحات">
|
|
|
|
|
|
<a-textarea v-model:value="form.description" :rows="2" :maxlength="2000" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="تاریخ" required>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
<JalaliDateInput v-model="form.date" mode="date" :clearable="false" />
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="ساعت" extra="خالی = تا پایان روز">
|
2026-10-06 19:54:38 +03:30
|
|
|
|
<JalaliDateInput v-model="form.time" mode="time" />
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="اولویت">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.priority"
|
|
|
|
|
|
:options="priorityOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="دستهبندی">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.categoryId"
|
|
|
|
|
|
:options="categoryOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col v-if="assignees.length > 1" :span="24">
|
|
|
|
|
|
<a-form-item label="مسئول">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.assigneeId"
|
|
|
|
|
|
:options="assigneeOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
2026-10-29 20:53:26 +03:30
|
|
|
|
<a-col v-if="preset" :span="24">
|
|
|
|
|
|
<a-form-item label="پرونده">
|
|
|
|
|
|
<a-space :size="8">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-tag :bordered="false">{{ ENTITY_TYPE_LABELS[preset.type] }}</a-tag>
|
|
|
|
|
|
<a-typography-text strong>{{ preset.label }}</a-typography-text>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-form-item>
|
2026-10-29 20:53:26 +03:30
|
|
|
|
<a-alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
message="این کار روی همین پرونده میماند و در برنامه روزانه کارهای روتین دیده نمیشود."
|
|
|
|
|
|
/>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-col>
|
2026-10-29 20:53:26 +03:30
|
|
|
|
<a-col v-else :span="24">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-checkbox v-model:checked="form.isMandatory">
|
|
|
|
|
|
کار اجباری (در امتیاز روزانه و Streak حساب میشود)
|
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
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-modal>
|
2026-10-06 19:54:38 +03:30
|
|
|
|
</template>
|