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>
259 lines
7.2 KiB
Vue
259 lines
7.2 KiB
Vue
<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>
|