Files
CRM_Montzar/apps/web/src/components/routines/QuickTaskModal.vue
T

254 lines
7.0 KiB
Vue
Raw Normal View History

<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>