Files
CRM_Montzar/apps/web/src/views/CompanyFormView.vue
T
MahdiandCursor 6c6e32e099 feat(web): adopt vuesax icons and shared skeleton loading
Replace the inline SVG icon set with the vuesax linear pack, add skeleton primitives for loading states, and keep page scroll inside the app shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-05 14:46:35 +03:30

480 lines
16 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, onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import {
Alert as AAlert,
Breadcrumb as ABreadcrumb,
BreadcrumbItem as ABreadcrumbItem,
Button as AButton,
Card as ACard,
Col as ACol,
Divider as ADivider,
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Modal,
Row as ARow,
Space as ASpace,
Textarea as ATextarea,
Typography,
} from 'ant-design-vue';
import { CheckOutlined, DeleteOutlined } from '@/icons/ant-bridge';
import { CrmSkeleton, CrmSkeletonCard } from '@/components/skeleton';
import { api } from '@/lib/api';
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
import EntityTimeline from '@/components/EntityTimeline.vue';
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import CustomerGroupPicker from '@/components/CustomerGroupPicker.vue';
import { formatLocalDateInput, parseLocalInput } from '@/lib/jalali';
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
const props = withDefaults(
defineProps<{
embedded?: boolean;
entityId?: string;
}>(),
{ embedded: false },
);
const emit = defineEmits<{ saved: []; cancel: [] }>();
const route = useRoute();
const router = useRouter();
const id = computed(() => {
if (props.embedded) return props.entityId ?? '';
return typeof route.params.id === 'string' ? route.params.id : '';
});
const isNew = computed(() => (props.embedded ? !props.entityId : route.name === 'company-new'));
const pageTitle = computed(() => (isNew.value ? 'شرکت جدید' : 'ویرایش شرکت'));
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const name = ref('');
const code = ref('');
const sourceRef = ref('');
const importSource = ref('manual');
const nationalId = ref('');
const economicCode = ref('');
const registrationNumber = ref('');
const phone = ref('');
const otherPhones = ref('');
const fax = ref('');
const email = ref('');
const otherEmails = ref('');
const website = ref('');
const province = ref('');
const city = ref('');
const postalCode = ref('');
const address = ref('');
const groupName = ref('');
const groupIds = ref<string[]>([]);
const registeredAt = ref('');
const notes = ref('');
const tagsRaw = ref('');
const customFields = ref<Record<string, unknown>>({});
function parseTags(raw: string): string[] {
return [
...new Set(
raw
.split(/[,،#\n]/)
.map((t) => t.trim())
.filter(Boolean),
),
].slice(0, 40);
}
function fill(c: CompanyDto) {
name.value = c.name;
code.value = c.code ?? '';
sourceRef.value = c.sourceRef ?? '';
importSource.value = c.importSource || 'manual';
nationalId.value = c.nationalId ?? '';
economicCode.value = c.economicCode ?? '';
registrationNumber.value = c.registrationNumber ?? '';
phone.value = c.phone ?? '';
otherPhones.value = c.otherPhones ?? '';
fax.value = c.fax ?? '';
email.value = c.email ?? '';
otherEmails.value = c.otherEmails ?? '';
website.value = c.website ?? '';
province.value = c.province ?? '';
city.value = c.city ?? '';
postalCode.value = c.postalCode ?? '';
address.value = c.address ?? '';
groupName.value = c.groupName ?? '';
groupIds.value = (c.groups ?? []).map((g) => g.id);
registeredAt.value = c.registeredAt ? formatLocalDateInput(new Date(c.registeredAt)) : '';
notes.value = c.notes ?? '';
tagsRaw.value = (c.tags ?? []).join('، ');
customFields.value = { ...(c.customFields ?? {}) };
}
async function load() {
if (isNew.value) return;
loading.value = true;
error.value = '';
try {
const { data } = await api.get<CompanyDto>(`/companies/${id.value}`);
fill(data);
} catch {
error.value = 'بارگذاری شرکت ناموفق بود.';
} finally {
loading.value = false;
}
}
function payload(): CreateCompanyRequest {
return {
name: name.value,
code: code.value || null,
sourceRef: sourceRef.value || null,
...(sourceRef.value && importSource.value === 'manual' ? { importSource: 'didar' } : {}),
nationalId: nationalId.value || undefined,
economicCode: economicCode.value || null,
registrationNumber: registrationNumber.value || null,
phone: phone.value || undefined,
otherPhones: otherPhones.value || null,
fax: fax.value || null,
email: email.value || undefined,
otherEmails: otherEmails.value || null,
website: website.value || undefined,
province: province.value || null,
city: city.value || null,
postalCode: postalCode.value || null,
address: address.value || undefined,
groupName: groupName.value || null,
groupIds: groupIds.value,
registeredAt: parseLocalInput(registeredAt.value)?.toISOString() ?? null,
notes: notes.value || undefined,
tags: parseTags(tagsRaw.value),
customFields: customFields.value,
};
}
async function submit() {
saving.value = true;
error.value = '';
try {
if (isNew.value) {
const { data } = await api.post<CompanyDto>('/companies', payload());
if (props.embedded) {
emit('saved');
return;
}
await router.replace({ name: 'company-detail', params: { id: data.id } });
} else {
await api.patch(`/companies/${id.value}`, payload());
if (props.embedded) {
emit('saved');
return;
}
await router.push({ name: 'contacts', query: { tab: 'companies' } });
}
} catch (e) {
if (axios.isAxiosError(e)) {
if (!e.response) {
error.value = 'اتصال به سرور برقرار نشد.';
} else {
const msg = (e.response.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ذخیره ناموفق بود');
}
} else {
error.value = 'ذخیره ناموفق بود';
}
} finally {
saving.value = false;
}
}
async function remove() {
if (!id.value) return;
Modal.confirm({
title: 'حذف شرکت',
content: 'این شرکت حذف شود؟ افراد متصل بدون شرکت می‌مانند.',
okText: 'حذف',
okType: 'danger',
cancelText: 'انصراف',
async onOk() {
try {
await api.delete(`/companies/${id.value}`);
await router.push({ name: 'contacts', query: { tab: 'companies' } });
} catch {
error.value = 'حذف ناموفق بود';
}
},
});
}
function cancel() {
if (props.embedded) {
emit('cancel');
return;
}
void router.push({ name: 'contacts', query: { tab: 'companies' } });
}
onMounted(load);
</script>
<template>
<a-flex vertical :gap="16" dir="rtl">
<a-flex
v-if="!embedded"
justify="space-between"
align="center"
wrap="wrap"
:gap="12"
>
<a-flex vertical :gap="4">
<a-breadcrumb>
<a-breadcrumb-item>
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }">
شرکت‌ها
</RouterLink>
</a-breadcrumb-item>
<a-breadcrumb-item>{{ pageTitle }}</a-breadcrumb-item>
</a-breadcrumb>
<a-typography-title :level="3" :style="{ margin: 0 }">
{{ pageTitle }}
</a-typography-title>
<a-typography-text type="secondary">
ثبت و ویرایش مشخصات حقوقی، اطلاعات تماس و فیلدهای سفارشی شرکت
</a-typography-text>
</a-flex>
<a-space :size="8" wrap>
<a-button type="primary" :loading="saving" @click="submit">
<template #icon><CheckOutlined /></template>
ذخیره
</a-button>
<a-button :disabled="saving" @click="cancel">انصراف</a-button>
<a-button
v-if="!isNew"
danger
:disabled="saving"
@click="remove"
>
<template #icon><DeleteOutlined /></template>
حذف
</a-button>
</a-space>
</a-flex>
<a-alert v-if="error" type="error" show-icon :message="error" />
<CrmSkeleton :loading="loading">
<CrmSkeletonCard :rows="8">
<a-form layout="vertical" @finish="submit">
<a-card :bordered="false">
<a-divider orientation="right">اطلاعات اصلی شرکت</a-divider>
<a-row :gutter="[16, 0]">
<a-col :span="24">
<a-form-item label="نام شرکت" required>
<a-input
id="name"
v-model:value="name"
:minlength="2"
placeholder="مثال: داده‌پردازی سپهر"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد شرکت">
<a-input id="code" v-model:value="code" placeholder="کد داخلی" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد دیدار شرکت">
<a-input
id="sourceRef"
v-model:value="sourceRef"
placeholder="مثال: 5277"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="گروه‌های شرکت">
<CustomerGroupPicker v-model="groupIds" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="تاریخ ثبت شرکت">
<JalaliDateInput id="registeredAt" v-model="registeredAt" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="برچسب‌ها">
<a-input
id="company-tags"
v-model:value="tagsRaw"
placeholder="شرکتی، VIP (با کاما جدا کنید)"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="توضیحات شرکت">
<a-textarea
id="notes"
v-model:value="notes"
:rows="4"
placeholder="توضیحات مهم، حوزه‌های همکاری..."
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" :style="{ marginTop: '16px' }">
<a-divider orientation="right">اطلاعات تماس و آدرس</a-divider>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="تلفن شرکت">
<a-input
id="phone"
v-model:value="phone"
type="tel"
placeholder="۰۲۱..."
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="تلفن های دیگر شرکت">
<a-input
id="otherPhones"
v-model:value="otherPhones"
placeholder="با کاما جدا کنید"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="ایمیل شرکت">
<a-input
id="email"
v-model:value="email"
type="email"
placeholder="info@company.ir"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="ایمیل های دیگر شرکت">
<a-input
id="otherEmails"
v-model:value="otherEmails"
placeholder="با کاما جدا کنید"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره فکس">
<a-input id="fax" v-model:value="fax" type="tel" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="سایت های شرکت">
<a-input
id="website"
v-model:value="website"
placeholder="https://..."
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="استان">
<a-input id="province" v-model:value="province" placeholder="مثال: تهران" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شهرستان">
<a-input id="city" v-model:value="city" placeholder="مثال: تهران" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد پستی">
<a-input id="postalCode" v-model:value="postalCode" dir="ltr" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="آدرس">
<a-input
id="address"
v-model:value="address"
placeholder="نشانی، طبقه، پلاک..."
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" :style="{ marginTop: '16px' }">
<a-divider orientation="right">اطلاعات حقوقی و ثبتی</a-divider>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="شناسه ملی">
<a-input
id="nationalId"
v-model:value="nationalId"
placeholder="شناسه ملی ۱۱ رقمی"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره اقتصادی">
<a-input id="economicCode" v-model:value="economicCode" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره ثبت">
<a-input
id="registrationNumber"
v-model:value="registrationNumber"
dir="ltr"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<CustomFieldsSection entity-type="company" v-model="customFields" />
<RelatedTasksPanel
v-if="!isNew && id"
:entity="{ type: 'company', id, label: name }"
/>
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
<a-flex justify="flex-end" :style="{ marginTop: '16px' }">
<a-space :size="8" wrap>
<a-button type="primary" html-type="submit" :loading="saving">
<template #icon><CheckOutlined /></template>
ذخیره اطلاعات
</a-button>
<a-button :disabled="saving" @click="cancel">انصراف</a-button>
<a-button
v-if="!isNew"
danger
:disabled="saving"
@click="remove"
>
حذف شرکت
</a-button>
</a-space>
</a-flex>
</a-form>
</CrmSkeletonCard>
</CrmSkeleton>
</a-flex>
</template>