2026-09-09 17:08:46 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
|
|
|
|
|
|
import BaseModal from '@/components/ui/BaseModal.vue'
|
|
|
|
|
|
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
|
|
|
|
|
|
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
|
|
|
|
|
|
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
|
|
|
|
|
|
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
|
|
|
|
|
import AdminSelect from '@/components/ui/AdminSelect.vue'
|
2026-09-30 09:53:30 +03:30
|
|
|
|
import { customersApi, levelsApi, type CustomerLevel } from '@/services/api'
|
2026-09-19 17:23:11 +03:30
|
|
|
|
import CustomerSuggestionsPanel from './CustomerSuggestionsPanel.vue'
|
2026-09-09 17:08:46 +03:30
|
|
|
|
import { ApiError } from '@/services/http'
|
|
|
|
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
|
|
import { CUSTOMER_TYPE_LABELS } from '@/utils/adminLabels'
|
|
|
|
|
|
import { formatDateTime, formatMeterage } from '@/utils/format'
|
|
|
|
|
|
import type { Customer, CustomerType } from '@/types'
|
|
|
|
|
|
|
|
|
|
|
|
const customerList = ref<Customer[]>([])
|
|
|
|
|
|
const levels = ref<CustomerLevel[]>([])
|
|
|
|
|
|
const agents = ref<Customer[]>([])
|
|
|
|
|
|
const searchQuery = ref('')
|
2026-09-19 17:23:11 +03:30
|
|
|
|
const roleTab = ref<'customer' | 'agent'>('customer')
|
2026-09-09 17:08:46 +03:30
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const modalMode = ref<'add' | 'edit' | null>(null)
|
|
|
|
|
|
const editingId = ref<string | null>(null)
|
|
|
|
|
|
const formError = ref('')
|
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
|
|
|
|
function displayCell(value: string | number | null | undefined): string {
|
|
|
|
|
|
if (value == null) return '-'
|
|
|
|
|
|
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '-'
|
|
|
|
|
|
const text = value.trim()
|
|
|
|
|
|
if (!text || text === '—') return '-'
|
|
|
|
|
|
return text
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function displayMeterage(value: number | null | undefined): string {
|
|
|
|
|
|
if (value == null || !Number.isFinite(value)) return '-'
|
|
|
|
|
|
return formatMeterage(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const customerTypeOptions = [
|
|
|
|
|
|
{ value: 'wholesale', label: 'عمدهفروش' },
|
|
|
|
|
|
{ value: 'retail', label: 'خردهفروش' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const appRoleOptions = [
|
|
|
|
|
|
{ value: 'customer', label: 'مشتری' },
|
|
|
|
|
|
{ value: 'agent', label: 'واسطه' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-09-19 17:23:11 +03:30
|
|
|
|
const APP_ROLE_LABELS: Record<'customer' | 'agent', string> = {
|
|
|
|
|
|
customer: 'مشتری',
|
|
|
|
|
|
agent: 'واسطه',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function appRoleLabel(role?: 'customer' | 'agent' | null) {
|
|
|
|
|
|
return APP_ROLE_LABELS[role === 'agent' ? 'agent' : 'customer']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 17:08:46 +03:30
|
|
|
|
const levelOptions = computed(() => [
|
|
|
|
|
|
{ value: '', label: 'بدون سطح' },
|
|
|
|
|
|
...levels.value.map((level) => ({ value: level.id, label: level.label })),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const agentOptions = computed(() => [
|
|
|
|
|
|
{ value: '', label: 'بدون واسطه' },
|
|
|
|
|
|
...agents.value
|
|
|
|
|
|
.filter((agent) => agent.id !== editingId.value)
|
|
|
|
|
|
.map((agent) => ({
|
|
|
|
|
|
value: agent.id,
|
|
|
|
|
|
label: agent.name + (agent.agentCode ? ` (${agent.agentCode})` : ''),
|
|
|
|
|
|
})),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const emptyForm = () => ({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
phone: '',
|
|
|
|
|
|
nationalId: '',
|
|
|
|
|
|
customerType: 'wholesale' as CustomerType,
|
|
|
|
|
|
companyName: '',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
levelDefId: '' as string,
|
|
|
|
|
|
assignedAgentId: '' as string,
|
|
|
|
|
|
appRole: 'customer' as 'customer' | 'agent',
|
|
|
|
|
|
agentCode: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const form = reactive(emptyForm())
|
|
|
|
|
|
|
|
|
|
|
|
const isModalOpen = computed(() => modalMode.value !== null)
|
|
|
|
|
|
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش مشتری' : 'افزودن مشتری'))
|
|
|
|
|
|
const isFormValid = computed(
|
|
|
|
|
|
() => form.name.trim() && form.phone.trim() && form.nationalId.trim(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const filteredCustomers = computed(() => {
|
|
|
|
|
|
const q = searchQuery.value.trim()
|
|
|
|
|
|
if (!q) return customerList.value
|
|
|
|
|
|
return customerList.value.filter(
|
|
|
|
|
|
(c) =>
|
|
|
|
|
|
c.name.includes(q) ||
|
|
|
|
|
|
c.phone.includes(q) ||
|
|
|
|
|
|
c.nationalId.includes(q) ||
|
|
|
|
|
|
c.companyName.includes(q),
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function loadCustomers() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-09-19 17:23:11 +03:30
|
|
|
|
customerList.value = await customersApi.list(undefined, roleTab.value)
|
2026-09-09 17:08:46 +03:30
|
|
|
|
} catch (err) {
|
2026-09-19 17:23:11 +03:30
|
|
|
|
toast.error(
|
|
|
|
|
|
err instanceof ApiError
|
|
|
|
|
|
? err.message
|
|
|
|
|
|
: roleTab.value === 'agent'
|
|
|
|
|
|
? 'بارگذاری واسطهها انجام نشد.'
|
|
|
|
|
|
: 'بارگذاری مشتریان انجام نشد.',
|
|
|
|
|
|
)
|
2026-09-09 17:08:46 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 17:23:11 +03:30
|
|
|
|
function setRoleTab(tab: 'customer' | 'agent') {
|
|
|
|
|
|
if (roleTab.value === tab) return
|
|
|
|
|
|
roleTab.value = tab
|
|
|
|
|
|
searchQuery.value = ''
|
|
|
|
|
|
void loadCustomers()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 17:08:46 +03:30
|
|
|
|
async function loadLookups() {
|
|
|
|
|
|
try {
|
2026-09-30 09:53:30 +03:30
|
|
|
|
const [levelsData, agentsData] = await Promise.all([
|
2026-09-09 17:08:46 +03:30
|
|
|
|
levelsApi.list(),
|
|
|
|
|
|
customersApi.list(undefined, 'agent'),
|
|
|
|
|
|
])
|
|
|
|
|
|
levels.value = levelsData
|
|
|
|
|
|
agents.value = agentsData
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
toast.error(err instanceof ApiError ? err.message : 'بارگذاری اطلاعات کمکی انجام نشد.')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
void loadCustomers()
|
|
|
|
|
|
void loadLookups()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function openAddModal() {
|
|
|
|
|
|
modalMode.value = 'add'
|
|
|
|
|
|
editingId.value = null
|
|
|
|
|
|
Object.assign(form, emptyForm())
|
2026-09-19 17:23:11 +03:30
|
|
|
|
form.appRole = roleTab.value
|
2026-09-09 17:08:46 +03:30
|
|
|
|
formError.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openEditModal(customer: Customer) {
|
|
|
|
|
|
modalMode.value = 'edit'
|
|
|
|
|
|
editingId.value = customer.id
|
|
|
|
|
|
form.name = customer.name
|
|
|
|
|
|
form.phone = customer.phone
|
|
|
|
|
|
form.nationalId = customer.nationalId
|
|
|
|
|
|
form.customerType = customer.customerType
|
|
|
|
|
|
form.companyName = customer.companyName
|
|
|
|
|
|
form.isActive = customer.isActive
|
|
|
|
|
|
form.levelDefId = customer.levelDefId ?? ''
|
|
|
|
|
|
form.assignedAgentId = customer.assignedAgentId ?? ''
|
|
|
|
|
|
form.appRole = customer.appRole === 'agent' ? 'agent' : 'customer'
|
|
|
|
|
|
form.agentCode = customer.agentCode ?? ''
|
|
|
|
|
|
formError.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeModal() {
|
|
|
|
|
|
modalMode.value = null
|
|
|
|
|
|
editingId.value = null
|
|
|
|
|
|
formError.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitCustomer() {
|
|
|
|
|
|
if (!isFormValid.value) {
|
|
|
|
|
|
formError.value = 'نام، تلفن و شناسه ملی الزامی هستند.'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
phone: form.phone.trim(),
|
|
|
|
|
|
nationalId: form.nationalId.trim(),
|
|
|
|
|
|
customerType: form.customerType,
|
|
|
|
|
|
isActive: form.isActive,
|
|
|
|
|
|
levelDefId: form.levelDefId || null,
|
|
|
|
|
|
assignedAgentId: form.assignedAgentId || null,
|
|
|
|
|
|
appRole: form.appRole,
|
|
|
|
|
|
agentCode: form.appRole === 'agent' ? form.agentCode.trim() || null : null,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (modalMode.value === 'edit' && editingId.value) {
|
|
|
|
|
|
const updated = await customersApi.update(editingId.value, payload)
|
|
|
|
|
|
const index = customerList.value.findIndex((c) => c.id === editingId.value)
|
|
|
|
|
|
if (index !== -1) customerList.value[index] = { ...customerList.value[index]!, ...updated }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const created = await customersApi.create(payload)
|
|
|
|
|
|
customerList.value.unshift(created)
|
|
|
|
|
|
}
|
|
|
|
|
|
toast.success(modalMode.value === 'edit' ? 'اطلاعات مشتری بهروزرسانی شد.' : 'مشتری جدید ثبت شد.')
|
|
|
|
|
|
closeModal()
|
|
|
|
|
|
if (payload.appRole === 'agent') void loadLookups()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
toast.error(err instanceof ApiError ? err.message : 'ذخیره مشتری انجام نشد.')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persistActive(id: string, isActive: boolean) {
|
|
|
|
|
|
await customersApi.setActive(id, isActive)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearSearch() {
|
|
|
|
|
|
searchQuery.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="customers-page">
|
|
|
|
|
|
<div class="admin-page-header">
|
|
|
|
|
|
<div>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
<h1 class="admin-page-header__title">مشتریان و واسطهها</h1>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" class="admin-btn admin-btn--primary" @click="openAddModal">
|
2026-09-19 17:23:11 +03:30
|
|
|
|
{{ roleTab === 'agent' ? 'افزودن واسطه' : 'افزودن مشتری' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="customers-page__tabs" role="tablist" aria-label="نوع کاربر">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
class="customers-page__tab"
|
|
|
|
|
|
:class="{ 'is-active': roleTab === 'customer' }"
|
|
|
|
|
|
:aria-selected="roleTab === 'customer'"
|
|
|
|
|
|
@click="setRoleTab('customer')"
|
|
|
|
|
|
>
|
|
|
|
|
|
مشتریان
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
class="customers-page__tab"
|
|
|
|
|
|
:class="{ 'is-active': roleTab === 'agent' }"
|
|
|
|
|
|
:aria-selected="roleTab === 'agent'"
|
|
|
|
|
|
@click="setRoleTab('agent')"
|
|
|
|
|
|
>
|
|
|
|
|
|
واسطهها
|
2026-09-09 17:08:46 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="customers-page__search admin-card">
|
|
|
|
|
|
<AdminIcon name="search" :size="18" />
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="searchQuery"
|
|
|
|
|
|
type="search"
|
|
|
|
|
|
class="customers-page__search-input"
|
|
|
|
|
|
placeholder="جستجو در نام، تلفن، شناسه ملی..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<AdminPageSkeleton v-if="loading" variant="table" />
|
|
|
|
|
|
<BaseEmptyState
|
|
|
|
|
|
v-else-if="!filteredCustomers.length"
|
|
|
|
|
|
class="admin-card"
|
|
|
|
|
|
icon="customers"
|
2026-09-19 17:23:11 +03:30
|
|
|
|
:title="
|
|
|
|
|
|
searchQuery.trim()
|
|
|
|
|
|
? 'نتیجهای یافت نشد'
|
|
|
|
|
|
: roleTab === 'agent'
|
|
|
|
|
|
? 'واسطهای ثبت نشده'
|
|
|
|
|
|
: 'مشتریان ثبت شده'
|
|
|
|
|
|
"
|
2026-09-09 17:08:46 +03:30
|
|
|
|
:description="
|
|
|
|
|
|
searchQuery.trim()
|
|
|
|
|
|
? 'عبارت جستجو را تغییر دهید یا فیلترها را پاک کنید.'
|
2026-09-19 17:23:11 +03:30
|
|
|
|
: roleTab === 'agent'
|
|
|
|
|
|
? 'برای شروع، اولین واسطه را ثبت کنید.'
|
|
|
|
|
|
: 'برای شروع، اولین مشتری را ثبت کنید.'
|
2026-09-09 17:08:46 +03:30
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button v-if="searchQuery.trim()" type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="clearSearch">
|
|
|
|
|
|
پاک کردن جستجو
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button v-else type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAddModal">
|
2026-09-19 17:23:11 +03:30
|
|
|
|
{{ roleTab === 'agent' ? 'افزودن واسطه' : 'افزودن مشتری' }}
|
2026-09-09 17:08:46 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</BaseEmptyState>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="customers-page__table admin-card desktop-only">
|
|
|
|
|
|
<div class="admin-table-wrap">
|
|
|
|
|
|
<table class="admin-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
<th>{{ roleTab === 'agent' ? 'نام واسطه' : 'نام مشتری' }}</th>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
<th>تلفن</th>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
<th>نقش</th>
|
|
|
|
|
|
<th v-if="roleTab === 'agent'">کد واسطه</th>
|
|
|
|
|
|
<th v-if="roleTab === 'customer'">نوع</th>
|
|
|
|
|
|
<th v-if="roleTab === 'customer'">سطح</th>
|
|
|
|
|
|
<th v-if="roleTab === 'customer'">واسطه</th>
|
|
|
|
|
|
<th v-if="roleTab === 'customer'">شرکت</th>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
<th class="numeric">تعداد سفارش</th>
|
|
|
|
|
|
<th class="numeric">مجموع متراژ</th>
|
|
|
|
|
|
<th class="status">وضعیت</th>
|
|
|
|
|
|
<th>تاریخ ثبت</th>
|
|
|
|
|
|
<th class="actions">عملیات</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-for="customer in filteredCustomers" :key="customer.id">
|
|
|
|
|
|
<td>{{ displayCell(customer.name) }}</td>
|
|
|
|
|
|
<td>{{ displayCell(customer.phone) }}</td>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
<td>{{ appRoleLabel(customer.appRole) }}</td>
|
|
|
|
|
|
<td v-if="roleTab === 'agent'">{{ displayCell(customer.agentCode) }}</td>
|
|
|
|
|
|
<td v-if="roleTab === 'customer'">{{ displayCell(CUSTOMER_TYPE_LABELS[customer.customerType]) }}</td>
|
|
|
|
|
|
<td v-if="roleTab === 'customer'">{{ displayCell(customer.levelLabel) }}</td>
|
|
|
|
|
|
<td v-if="roleTab === 'customer'">{{ displayCell(customer.assignedAgentName) }}</td>
|
|
|
|
|
|
<td v-if="roleTab === 'customer'">{{ displayCell(customer.companyName) }}</td>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
<td class="numeric">{{ displayCell(customer.orderCount) }}</td>
|
|
|
|
|
|
<td class="numeric">{{ displayMeterage(customer.totalMeterage) }}</td>
|
|
|
|
|
|
<td class="status">
|
|
|
|
|
|
<ActiveSwitchCell
|
|
|
|
|
|
:id="customer.id"
|
|
|
|
|
|
v-model="customer.isActive"
|
|
|
|
|
|
:entity-name="customer.name"
|
|
|
|
|
|
entity-type="مشتری"
|
|
|
|
|
|
:persist="persistActive"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>{{ displayCell(formatDateTime(customer.registeredAt)) }}</td>
|
|
|
|
|
|
<td class="actions">
|
|
|
|
|
|
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(customer)">
|
|
|
|
|
|
ویرایش
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!loading && filteredCustomers.length" class="compact-only admin-card-list">
|
|
|
|
|
|
<article v-for="customer in filteredCustomers" :key="customer.id" class="admin-card-item">
|
|
|
|
|
|
<div class="admin-card-item__header">
|
|
|
|
|
|
<p class="admin-card-item__title">{{ displayCell(customer.name) }}</p>
|
|
|
|
|
|
<div class="admin-card-item__header-actions">
|
|
|
|
|
|
<ActiveSwitchCell
|
|
|
|
|
|
:id="customer.id"
|
|
|
|
|
|
v-model="customer.isActive"
|
|
|
|
|
|
:entity-name="customer.name"
|
|
|
|
|
|
entity-type="مشتری"
|
|
|
|
|
|
:persist="persistActive"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(customer)">
|
|
|
|
|
|
ویرایش
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin-card-item__meta">
|
|
|
|
|
|
<span>{{ displayCell(customer.phone) }}</span>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
<span v-if="roleTab === 'agent'">کد: {{ displayCell(customer.agentCode) }}</span>
|
|
|
|
|
|
<span v-if="roleTab === 'customer'">{{ displayCell(CUSTOMER_TYPE_LABELS[customer.customerType]) }}</span>
|
|
|
|
|
|
<span v-if="roleTab === 'customer'">{{ displayCell(customer.levelLabel) }}</span>
|
|
|
|
|
|
<span v-if="roleTab === 'customer'">واسطه: {{ displayCell(customer.assignedAgentName) }}</span>
|
|
|
|
|
|
<span v-if="roleTab === 'customer'">{{ displayCell(customer.companyName) }}</span>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
<span>
|
|
|
|
|
|
{{ displayCell(customer.orderCount) }} سفارش · {{ displayMeterage(customer.totalMeterage) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<BaseModal :open="isModalOpen" :title="modalTitle" size="lg" @close="closeModal">
|
|
|
|
|
|
<form class="admin-form" @submit.prevent="submitCustomer">
|
|
|
|
|
|
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label for="customer-name">نام مشتری <span class="admin-form__required">*</span></label>
|
|
|
|
|
|
<input id="customer-name" v-model="form.name" type="text" class="admin-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__row">
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label for="customer-phone">تلفن <span class="admin-form__required">*</span></label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="customer-phone"
|
|
|
|
|
|
v-model="form.phone"
|
|
|
|
|
|
type="tel"
|
|
|
|
|
|
class="admin-input"
|
|
|
|
|
|
inputmode="tel"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label for="customer-national-id">شناسه ملی <span class="admin-form__required">*</span></label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="customer-national-id"
|
|
|
|
|
|
v-model="form.nationalId"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
class="admin-input"
|
|
|
|
|
|
inputmode="numeric"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__row">
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label>نوع مشتری</label>
|
|
|
|
|
|
<AdminSelect
|
|
|
|
|
|
v-model="form.customerType"
|
|
|
|
|
|
block
|
|
|
|
|
|
aria-label="نوع مشتری"
|
|
|
|
|
|
:options="customerTypeOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label for="customer-company">شرکت مرتبط</label>
|
|
|
|
|
|
<input id="customer-company" v-model="form.companyName" type="text" class="admin-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__row">
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label>سطح</label>
|
|
|
|
|
|
<AdminSelect
|
|
|
|
|
|
v-model="form.levelDefId"
|
|
|
|
|
|
block
|
|
|
|
|
|
aria-label="سطح مشتری"
|
|
|
|
|
|
:options="levelOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label>واسطه</label>
|
|
|
|
|
|
<AdminSelect
|
|
|
|
|
|
v-model="form.assignedAgentId"
|
|
|
|
|
|
block
|
|
|
|
|
|
aria-label="واسطه اختصاصیافته"
|
|
|
|
|
|
:options="agentOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__row">
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<label>نقش در اپ</label>
|
|
|
|
|
|
<AdminSelect
|
|
|
|
|
|
v-model="form.appRole"
|
|
|
|
|
|
block
|
|
|
|
|
|
aria-label="نقش در اپ"
|
|
|
|
|
|
:options="appRoleOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="form.appRole === 'agent'" class="admin-form__field">
|
|
|
|
|
|
<label for="customer-agent-code">کد واسطه</label>
|
|
|
|
|
|
<input id="customer-agent-code" v-model="form.agentCode" type="text" class="admin-input" dir="ltr" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="admin-form__field">
|
|
|
|
|
|
<BaseSwitch v-model="form.isActive" label="وضعیت" />
|
|
|
|
|
|
</div>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
|
|
|
|
|
|
<CustomerSuggestionsPanel
|
|
|
|
|
|
v-if="modalMode === 'edit' && editingId && form.appRole === 'customer'"
|
|
|
|
|
|
:customer-id="editingId"
|
|
|
|
|
|
/>
|
2026-09-09 17:08:46 +03:30
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<button type="button" class="admin-btn admin-btn--outline" @click="closeModal">انصراف</button>
|
|
|
|
|
|
<button type="button" class="admin-btn admin-btn--primary" :disabled="!isFormValid || saving" @click="submitCustomer">
|
|
|
|
|
|
<span v-if="saving" class="admin-btn__spinner" aria-hidden="true" />
|
2026-09-19 17:23:11 +03:30
|
|
|
|
{{
|
|
|
|
|
|
modalMode === 'edit'
|
|
|
|
|
|
? 'ذخیره تغییرات'
|
|
|
|
|
|
: form.appRole === 'agent'
|
|
|
|
|
|
? 'ثبت واسطه'
|
|
|
|
|
|
: 'ثبت مشتری'
|
|
|
|
|
|
}}
|
2026-09-09 17:08:46 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</BaseModal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-19 17:23:11 +03:30
|
|
|
|
.customers-page__tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.customers-page__tab {
|
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.customers-page__tab.is-active {
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
background: var(--color-primary-soft);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 17:08:46 +03:30
|
|
|
|
.customers-page__search {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.customers-page__search-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.customers-page__table {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
</style>
|