Files
CRM_Montzar/apps/web/src/components/routines/QuickTaskModal.vue
T
MahdiandCursor 534dae45b2 feat(roles): let each section decide who may assign work to others
Section access now only creates a record for yourself. Assigning it to a colleague requires a separate permission on that section.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-03 13:15:21 +03:30

259 lines
7.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, inject, reactive, ref, watch } from 'vue';
import type {
ActivityPriority,
CreateRoutineTaskRequest,
RoutineContextDto,
RoutineEntityRefDto,
RoutineTaskDto,
RoutineTaskSource,
} from '@mento/shared';
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';
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';
import { canAssignToOthers } from '@/lib/assign-access';
const { Text: ATypographyText } = Typography;
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('');
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 ? ' (خودم)' : ''}`,
})),
);
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;
if (!c?.isManager || !canAssignToOthers(auth.user, 'routines')) 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 Promise.reject();
}
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: link ? false : 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, 'ثبت کار ممکن نشد.');
return Promise.reject();
} finally {
saving.value = false;
}
}
</script>
<template>
<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"
placeholder="مثلاً: آماده‌سازی گزارش هفتگی تیم"
: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>
<JalaliDateInput v-model="form.date" mode="date" :clearable="false" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="ساعت" extra="خالی = تا پایان روز">
<JalaliDateInput v-model="form.time" mode="time" />
</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>
<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>
</a-form-item>
<a-alert
type="info"
show-icon
message="این کار روی همین پرونده می‌ماند و در برنامه روزانه کارهای روتین دیده نمی‌شود."
/>
</a-col>
<a-col v-else :span="24">
<a-checkbox v-model:checked="form.isMandatory">
کار اجباری (در امتیاز روزانه و Streak حساب می‌شود)
</a-checkbox>
</a-col>
</a-row>
<a-alert
v-if="error"
type="error"
show-icon
:message="error"
:style="{ marginTop: '12px' }"
/>
</a-modal>
</template>