Files
CRM_Montzar/apps/web/src/views/CompanyFormView.vue
T

435 lines
10 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.
This file contains Unicode characters that might be confused with other characters. 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 { api } from '@/lib/api';
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
import EntityTimeline from '@/components/EntityTimeline.vue';
import UiIcon from '@/components/UiIcon.vue';
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 nationalId = ref('');
const phone = ref('');
const email = ref('');
const website = ref('');
const address = ref('');
const notes = ref('');
const customFields = ref<Record<string, unknown>>({});
function fill(c: CompanyDto) {
name.value = c.name;
nationalId.value = c.nationalId ?? '';
phone.value = c.phone ?? '';
email.value = c.email ?? '';
website.value = c.website ?? '';
address.value = c.address ?? '';
notes.value = c.notes ?? '';
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,
nationalId: nationalId.value || undefined,
phone: phone.value || undefined,
email: email.value || undefined,
website: website.value || undefined,
address: address.value || undefined,
notes: notes.value || undefined,
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 || !confirm('این شرکت حذف شود؟ افراد متصل بدون شرکت می‌مانند.')) return;
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>
<section class="page" :class="{ embedded }">
<div v-if="embedded" class="embed-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
</div>
<header v-if="!embedded" class="desktop-head">
<div class="title-block">
<span class="doc-ico"><UiIcon name="building" :size="22" /></span>
<div>
<h1>{{ pageTitle }}</h1>
<p class="crumb">
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }">شرکت‌ها</RouterLink>
<span>›</span>
<strong>{{ pageTitle }}</strong>
</p>
</div>
</div>
<div class="desk-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit">
<UiIcon name="check-plain" :size="16" />
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
حذف
</button>
</div>
</header>
<header v-if="!embedded" class="mobile-head">
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
<UiIcon name="check-plain" :size="22" />
</button>
<h1>{{ pageTitle }}</h1>
<button class="icon-btn" type="button" aria-label="بستن" @click="cancel">
<UiIcon name="x" :size="20" />
</button>
</header>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<form v-else class="form" @submit.prevent="submit">
<section class="card">
<div class="grid">
<div class="field wide">
<label for="name">نام شرکت</label>
<input id="name" v-model="name" required minlength="2" />
</div>
<div class="field">
<label for="nationalId">شناسه ملی</label>
<input id="nationalId" v-model="nationalId" />
</div>
<div class="field">
<label for="phone">تلفن</label>
<input id="phone" v-model="phone" inputmode="tel" />
</div>
<div class="field">
<label for="email">ایمیل</label>
<input id="email" v-model="email" type="email" />
</div>
<div class="field">
<label for="website">وب‌سایت</label>
<input id="website" v-model="website" />
</div>
<div class="field wide">
<label for="address">آدرس</label>
<input id="address" v-model="address" />
</div>
<div class="field wide">
<label for="notes">یادداشت</label>
<textarea id="notes" v-model="notes" rows="4" />
</div>
</div>
</section>
<CustomFieldsSection entity-type="company" v-model="customFields" />
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
<p v-if="error" class="error">{{ error }}</p>
<div class="mobile-actions">
<button class="btn primary" type="submit" :disabled="saving">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
حذف
</button>
</div>
</form>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 0.85rem;
max-width: 820px;
min-width: 0;
padding-bottom: 1.5rem;
}
.page.embedded {
max-width: none;
padding-bottom: 0;
}
.embed-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.desktop-head {
display: none;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.title-block {
display: flex;
align-items: center;
gap: 0.75rem;
}
.doc-ico {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
background: #ecfdf5;
color: #0f766e;
}
h1 {
margin: 0;
font-size: 1.35rem;
}
.crumb {
margin: 0.2rem 0 0;
display: flex;
align-items: center;
gap: 0.35rem;
color: #94a3b8;
font-size: 0.82rem;
}
.crumb a {
color: #0f766e;
font-weight: 700;
}
.desk-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.mobile-head {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
}
.mobile-head h1 {
text-align: center;
font-size: 1.05rem;
}
.icon-btn {
width: var(--control-h);
height: var(--control-h);
border: 0;
background: none;
color: #0f766e;
display: grid;
place-items: center;
cursor: pointer;
}
.form {
display: grid;
gap: 0.75rem;
}
.card {
background: #fff;
border-radius: 18px;
padding: 0.95rem;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.field {
display: grid;
gap: 0.35rem;
}
.field.wide {
grid-column: 1 / -1;
}
.field label {
color: #64748b;
font-size: 0.88rem;
font-weight: 700;
}
.field input,
.field select,
.field textarea {
width: 100%;
border: 1px solid #e2e8f0;
border-radius: var(--radius);
min-height: var(--control-h);
padding: 0 0.85rem;
font: inherit;
background: #fff;
box-sizing: border-box;
}
.field textarea {
min-height: 5.5rem;
padding: 0.65rem 0.85rem;
}
.muted {
color: #64748b;
margin: 0;
}
.error {
color: #b91c1c;
margin: 0;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: var(--control-h);
padding: 0 0.95rem;
border-radius: var(--radius);
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
font: inherit;
}
.btn.primary {
background: #14b8a6;
color: #fff;
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
}
.btn.cancel {
background: #fff;
color: #64748b;
border-color: #e2e8f0;
}
.btn.danger {
background: #fff;
color: #b91c1c;
border-color: #fecaca;
}
.mobile-actions {
display: grid;
gap: 0.5rem;
}
@media (min-width: 960px) {
.desktop-head {
display: flex;
}
.mobile-head,
.mobile-actions {
display: none;
}
}
@media (max-width: 700px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>