Initial Mento CRM: portal OTP, reports, SMS, and deploy for crm.montzar.ir.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { api } from '@/lib/api';
|
||||
import type {
|
||||
CustomFieldDefinitionDto,
|
||||
CustomFieldEntity,
|
||||
} from '@mento/shared';
|
||||
|
||||
const props = defineProps<{
|
||||
entityType: CustomFieldEntity;
|
||||
modelValue: Record<string, unknown>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Record<string, unknown>];
|
||||
}>();
|
||||
|
||||
const fields = ref<CustomFieldDefinitionDto[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
fields.value = (
|
||||
await api.get<CustomFieldDefinitionDto[]>('/custom-fields', {
|
||||
params: { entityType: props.entityType, activeOnly: true },
|
||||
})
|
||||
).data;
|
||||
} catch {
|
||||
fields.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(key: string, value: unknown) {
|
||||
emit('update:modelValue', { ...props.modelValue, [key]: value });
|
||||
}
|
||||
|
||||
function displayValue(key: string) {
|
||||
const v = props.modelValue[key];
|
||||
if (v == null) return '';
|
||||
if (typeof v === 'boolean') return v;
|
||||
return String(v);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.entityType,
|
||||
() => {
|
||||
void load();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="muted">بارگذاری فیلدهای سفارشی…</div>
|
||||
<div v-else-if="fields.length" class="custom-block">
|
||||
<h3>فیلدهای سفارشی</h3>
|
||||
<div class="grid">
|
||||
<div v-for="field in fields" :key="field.id" class="field">
|
||||
<label :for="`cf-${field.key}`">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="req">*</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
v-if="field.fieldType === 'select'"
|
||||
:id="`cf-${field.key}`"
|
||||
:value="String(displayValue(field.key))"
|
||||
@change="setValue(field.key, ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="">انتخاب کنید</option>
|
||||
<option v-for="opt in field.options" :key="opt" :value="opt">
|
||||
{{ opt }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<label v-else-if="field.fieldType === 'boolean'" class="check">
|
||||
<input
|
||||
:id="`cf-${field.key}`"
|
||||
type="checkbox"
|
||||
:checked="Boolean(props.modelValue[field.key])"
|
||||
@change="
|
||||
setValue(field.key, ($event.target as HTMLInputElement).checked)
|
||||
"
|
||||
/>
|
||||
فعال
|
||||
</label>
|
||||
|
||||
<input
|
||||
v-else-if="field.fieldType === 'number'"
|
||||
:id="`cf-${field.key}`"
|
||||
type="number"
|
||||
:value="String(displayValue(field.key))"
|
||||
@input="
|
||||
setValue(
|
||||
field.key,
|
||||
($event.target as HTMLInputElement).value === ''
|
||||
? null
|
||||
: Number(($event.target as HTMLInputElement).value),
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else-if="field.fieldType === 'date'"
|
||||
:id="`cf-${field.key}`"
|
||||
type="date"
|
||||
:value="String(displayValue(field.key))"
|
||||
@input="setValue(field.key, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else
|
||||
:id="`cf-${field.key}`"
|
||||
type="text"
|
||||
:value="String(displayValue(field.key))"
|
||||
@input="setValue(field.key, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-block {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.4rem;
|
||||
border-top: 1px dashed var(--line);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.req {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type='text'],
|
||||
input[type='number'],
|
||||
input[type='date'] {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 0.95rem;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user