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

379 lines
9.9 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 axios from 'axios';
import { api } from '@/lib/api';
import {
UiBadge,
UiButton,
UiCard,
UiEmptyState,
UiField,
UiInput,
UiPageHeader,
UiSelect,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
import type {
CreateCustomFieldRequest,
CustomFieldDefinitionDto,
CustomFieldEntity,
CustomFieldType,
} from '@mento/shared';
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const fields = ref<CustomFieldDefinitionDto[]>([]);
const filter = ref<string>('all');
const label = ref('');
const entityType = ref<CustomFieldEntity>('person');
const fieldType = ref<CustomFieldType>('text');
const optionsRaw = ref('');
const required = ref(false);
const entityLabels: Record<CustomFieldEntity, string> = {
person: 'شخص',
company: 'شرکت',
deal: 'معامله',
};
const typeLabels: Record<CustomFieldType, string> = {
text: 'متن',
number: 'عدد',
date: 'تاریخ',
boolean: 'بله/خیر',
select: 'لیست انتخابی',
};
const visible = computed(() =>
filter.value === 'all'
? fields.value
: fields.value.filter((f) => f.entityType === filter.value),
);
async function load() {
loading.value = true;
error.value = '';
try {
fields.value = (await api.get<CustomFieldDefinitionDto[]>('/custom-fields')).data;
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری فیلدها ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function createField() {
if (!label.value.trim()) {
error.value = 'عنوان فیلد را وارد کنید.';
return;
}
saving.value = true;
error.value = '';
try {
const payload: CreateCustomFieldRequest = {
entityType: entityType.value,
label: label.value.trim(),
fieldType: fieldType.value,
required: required.value,
options:
fieldType.value === 'select'
? optionsRaw.value
.split(/[,،\n]/)
.map((o) => o.trim())
.filter(Boolean)
: undefined,
};
await api.post('/custom-fields', payload);
label.value = '';
optionsRaw.value = '';
required.value = false;
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
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 toggleActive(field: CustomFieldDefinitionDto) {
await api.patch(`/custom-fields/${field.id}`, { isActive: !field.isActive });
await load();
}
async function removeField(field: CustomFieldDefinitionDto) {
if (!confirm(`فیلد «${field.label}» حذف شود؟`)) return;
await api.delete(`/custom-fields/${field.id}`);
await load();
}
onMounted(load);
</script>
<template>
<section class="crm-page narrow">
<UiPageHeader
title="فیلدهای سفارشی"
lead="فیلدهای اختصاصی سازمان را برای شخص، شرکت یا معامله تعریف کنید."
/>
<form class="crm-form" @submit.prevent="createField">
<UiCard>
<template #header>
<div class="card-title">
<UiIcon name="settings" :size="18" />
<h3>فیلد جدید</h3>
</div>
</template>
<div class="crm-form-grid-3">
<UiField label="عنوان" required for-id="cf-label">
<UiInput id="cf-label" v-model="label" required placeholder="مثلاً صنعت" />
</UiField>
<UiField label="روی چه چیزی؟" for-id="cf-entity">
<UiSelect id="cf-entity" v-model="entityType">
<option value="person">شخص</option>
<option value="company">شرکت</option>
<option value="deal">معامله</option>
</UiSelect>
</UiField>
<UiField label="نوع فیلد" for-id="cf-type">
<UiSelect id="cf-type" v-model="fieldType">
<option value="text">متن</option>
<option value="number">عدد</option>
<option value="date">تاریخ</option>
<option value="boolean">بله/خیر</option>
<option value="select">لیست انتخابی</option>
</UiSelect>
</UiField>
<div v-if="fieldType === 'select'" class="crm-col-span-full">
<UiField label="گزینه‌ها (با کاما جدا کنید)" for-id="cf-options">
<UiInput
id="cf-options"
v-model="optionsRaw"
placeholder="مثلاً فناوری، تولید، خدمات"
/>
</UiField>
</div>
<div class="crm-col-span-full check-row">
<label class="check">
<input v-model="required" type="checkbox" />
<span>فیلد اجباری است</span>
</label>
</div>
</div>
<template #footer>
<UiButton
variant="primary"
type="submit"
:disabled="saving"
:loading="saving"
>
<UiIcon name="plus" :size="16" />
{{ saving ? 'در حال ثبت…' : 'افزودن فیلد' }}
</UiButton>
</template>
</UiCard>
</form>
<div class="crm-toolbar">
<UiSelect v-model="filter" class="filter-select">
<option value="all">همه موجودیت‌ها</option>
<option value="person">شخص</option>
<option value="company">شرکت</option>
<option value="deal">معامله</option>
</UiSelect>
<UiButton variant="ghost" type="button" @click="load">
<UiIcon name="refresh" :size="16" />
بروزرسانی
</UiButton>
</div>
<p v-if="error" class="error-msg">{{ error }}</p>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<div v-else class="list">
<UiCard v-for="field in visible" :key="field.id" padding="sm" class="field-item-card">
<div class="field-item">
<div class="field-meta">
<div class="field-title-line">
<strong>{{ field.label }}</strong>
<UiBadge :variant="field.entityType === 'deal' ? 'warning' : field.entityType === 'company' ? 'info' : 'primary'" size="sm">
{{ entityLabels[field.entityType] }}
</UiBadge>
<UiBadge variant="neutral" size="sm">
{{ typeLabels[field.fieldType] }}
</UiBadge>
<UiBadge v-if="field.required" variant="danger" size="sm">
اجباری
</UiBadge>
<UiBadge :variant="field.isActive ? 'success' : 'neutral'" size="sm" dot>
{{ field.isActive ? 'فعال' : 'غیرفعال' }}
</UiBadge>
</div>
<p class="meta-code">
کلید سیستمی: <code>{{ field.key }}</code>
<span v-if="field.options?.length" class="options-preview">
· گزینه‌ها: {{ field.options.join('، ') }}
</span>
</p>
</div>
<div class="actions">
<UiButton
variant="outline"
size="sm"
@click="toggleActive(field)"
>
{{ field.isActive ? 'غیرفعال‌سازی' : 'فعال‌سازی' }}
</UiButton>
<UiButton
variant="danger"
size="sm"
@click="removeField(field)"
>
حذف
</UiButton>
</div>
</div>
</UiCard>
<UiEmptyState
v-if="!visible.length"
icon="settings"
title="فیلدی تعریف نشده است"
description="هیچ فیلد سفارشی برای این موجودیت وجود ندارد."
/>
</div>
</section>
</template>
<style scoped>
.card-title {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.card-title h3 {
margin: 0;
font-size: var(--fs-base);
font-weight: 700;
}
.check-row {
display: flex;
align-items: center;
}
.check {
display: flex;
align-items: center;
gap: var(--sp-2);
cursor: pointer;
font-size: var(--fs-sm);
color: var(--color-text);
}
.filter-select {
min-width: 180px;
}
.error-msg {
color: var(--color-danger);
font-size: var(--fs-sm);
font-weight: 600;
}
.list {
display: grid;
gap: var(--sp-3);
}
.field-item-card {
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.field-item-card:hover {
box-shadow: var(--shadow-md);
}
.field-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--sp-4);
flex-wrap: wrap;
}
.field-meta {
display: grid;
gap: var(--sp-1);
}
.field-title-line {
display: flex;
align-items: center;
gap: var(--sp-2);
flex-wrap: wrap;
}
.field-title-line strong {
font-size: var(--fs-base);
color: var(--color-text-emphasis);
}
.meta-code {
margin: 0;
font-size: var(--fs-xs);
color: var(--color-text-muted);
}
.meta-code code {
font-family: Consolas, monospace;
background: var(--color-surface-sunken);
padding: 1px var(--sp-2);
border-radius: var(--radius-sm);
color: var(--color-text);
}
.options-preview {
color: var(--color-text-muted);
}
.actions {
display: flex;
gap: var(--sp-2);
align-items: center;
}
.muted {
color: var(--color-text-muted);
}
@media (max-width: 640px) {
.field-item {
flex-direction: column;
align-items: stretch;
}
.actions {
justify-content: flex-end;
}
}
</style>