admin-panel: convert wholesale-customer add form into a modal

bulkCustomer.vue (reached today only via /customer/add's AddCustomer.vue tab
wrapper, and via /bulkCustomer/edit/:id from 5 different list variants) gets
open/id modal support; both routes stay as a fallback.

OmdeCustomerList.vue is shared by 5 list pages (OmdeList, BankerList,
ShopkeeperList, OmdeNonVerifiedList, OmdeArchivedList). Per user decision, the
add button and modal-based edit are wired only on OmdeList.vue (the one this
form conceptually belongs to) via a new showAddButton prop, default false —
the other four keep navigating to the edit route unchanged.

singleCustomer.vue and ReagentCustomer.vue were also planned as add-form
candidates but have no create entry point anywhere in the app (edit-only,
reachable solely via /singleCustomer/edit/:id and /reagent/edit/:id) — left
untouched, nothing to convert. offersIgnore-add-edit.vue is a standalone
settings page with no list counterpart, not a create form — left as a page.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-14 17:05:42 +03:30
co-authored by Claude Sonnet 5
parent 1bb9802d1f
commit 0e1dd0c36a
3 changed files with 169 additions and 7 deletions
@@ -5,6 +5,11 @@
wrapper around it (the rest of AddCustomer's markup is commented out in
legacy). Field names, Farsi labels and validation messages are preserved.
Converted to a modal (BaseFormModal): /customer/add (via AddCustomer.vue)
and /bulkCustomer/edit/:id (via EditBulkCustomer.vue) still exist and still
render this component directly as a route-based fallback, but OmdeList.vue
now opens it as a modal via open/id props.
Dropped: the MyUpload avatar widget and the Jalali DateInput, which belong to
fields the redesigned Customer schema has no column for. Flagged in
docs/ADMIN-PORT-PLAN.md rather than faked.
@@ -12,7 +17,108 @@
The نوع کاربری selector is live again now that Customer.type exists; its four
options are legacy's own list, byte for byte.
-->
<a-form layout="vertical" :model="form" @submit.prevent="handleSubmitOmde">
<BaseFormModal
v-if="asModal"
:open="Boolean(open)"
:id="id"
add-title="افزودن مشتری عمده"
edit-title="ویرایش مشتری عمده"
:loading="loading"
:submitting="loading"
width="lg"
@update:open="$emit('update:open', $event)"
@submit="handleSubmitOmde"
>
<a-card title="اطلاعات پایه" class="mb-base">
<div class="grid-4">
<a-form-item label="نام" :validate-status="err.fname ? 'error' : ''" :help="err.fname">
<a-input v-model:value="form.fname" />
</a-form-item>
<a-form-item label="نام خانوادگی" :validate-status="err.lname ? 'error' : ''" :help="err.lname">
<a-input v-model:value="form.lname" />
</a-form-item>
<a-form-item label="کد ملی">
<a-input v-model:value="form.ncode" />
</a-form-item>
<a-form-item
label="شماره تلفن همراه"
:validate-status="err.mobile ? 'error' : ''"
:help="err.mobile"
>
<a-input v-model:value="form.mobile" />
</a-form-item>
<a-form-item label="رمز عبور">
<a-input-password v-model:value="form.password" />
</a-form-item>
<a-form-item label="تلفن ثابت">
<a-input v-model:value="form.phone" />
</a-form-item>
</div>
</a-card>
<a-card title="اطلاعات تجاری" class="mb-base">
<div class="grid-4">
<a-form-item label="نوع کاربری">
<a-select
v-model:value="form.type"
show-search
:filter-option="filterUserType"
style="width: 100%; direction: rtl !important"
>
<a-select-option v-for="item in userTypes" :key="item.id" :value="item.id">
{{ item.title }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="نام مغازه">
<a-input v-model:value="form.store_name" />
</a-form-item>
<a-form-item label="نام پاساژ">
<a-input v-model:value="form.pasazh_name" />
</a-form-item>
<a-form-item label="کد مشتری">
<a-input v-model:value="form.mcode" />
</a-form-item>
<a-form-item label="کارشناس فروش">
<a-select v-model:value="form.reagent_id" allow-clear placeholder="انتخاب کنید">
<a-select-option v-for="r in reagent" :key="r.id" :value="r.id">
{{ r.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="درصد">
<a-input-number v-model:value="form.percent" class="w-full" />
</a-form-item>
<a-form-item label="سقف اعتبار (برحسب گرم)">
<a-input-number v-model:value="form.gram_level" class="w-full" />
</a-form-item>
</div>
</a-card>
<a-card title="اطلاعات تکمیلی" class="mb-base">
<div class="grid-2">
<a-form-item label="شهر">
<a-select
v-model:value="form.city_id"
show-search
allow-clear
placeholder="انتخاب کنید"
:filter-option="filterCity"
>
<a-select-option v-for="c in cities" :key="c.id" :value="c.id">
{{ c.title }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="آدرس">
<a-textarea v-model:value="form.address" :auto-size="{ minRows: 2, maxRows: 4 }" />
</a-form-item>
</div>
</a-card>
</BaseFormModal>
<!-- route-based fallback for /customer/add and /bulkCustomer/edit/:id -->
<a-form v-else layout="vertical" :model="form" @submit.prevent="handleSubmitOmde">
<a-spin :spinning="loading">
<a-card title="اطلاعات پایه" class="mb-base">
<div class="grid-4">
@@ -112,12 +218,24 @@
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import BaseFormModal from '../../../components/common/BaseFormModal.vue';
import api from '../../../api/client';
const props = defineProps<{
open?: boolean;
id?: string;
}>();
const emit = defineEmits<{
(e: 'update:open', value: boolean): void;
(e: 'success'): void;
}>();
const asModal = props.open !== undefined;
const route = useRoute();
const router = useRouter();
const id = route.params.id as string | undefined;
const id = asModal ? props.id : (route.params.id as string | undefined);
const loading = ref(false);
const cities = ref<{ id: string; title: string }[]>([]);
const reagent = ref<{ id: string; name: string }[]>([]);
@@ -210,7 +328,12 @@ async function handleSubmitOmde() {
await api.post('customer/create', payload);
}
message.info('اطلاعات با موفقیت به ثبت رسید.');
router.go(-1);
if (asModal) {
emit('success');
emit('update:open', false);
} else {
router.go(-1);
}
} catch (e: any) {
// legacy surfaced the server's mobile-uniqueness error specifically
const mobileErr = e?.response?.data?.errors?.mobile?.[0];
@@ -13,6 +13,13 @@
-->
<div id="page-user-list">
<a-card :title="title">
<ListPageHeader
v-if="showAddButton"
rid="persons.create.client"
add-label="افزودن مشتری عمده"
@add="openAdd"
/>
<div v-if="query && query.count && query.total">
{{ query.total }} رکورد یافت شد -
<span class="text-danger">{{ query.count }} رکورد غیر فعال</span>
@@ -205,6 +212,15 @@
</div>
</template>
</XTable>
<BulkCustomer
v-if="showAddButton"
:key="editingId ?? 'new'"
:open="modalOpen"
:id="editingId"
@update:open="modalOpen = $event"
@success="tbl?.fetch()"
/>
</a-card>
</div>
</template>
@@ -226,6 +242,8 @@ import {
UnorderedListOutlined,
} from '@ant-design/icons-vue';
import XTable from '../../../components/x-table/XTable.vue';
import ListPageHeader from '../../../components/common/ListPageHeader.vue';
import BulkCustomer from '../../apps/customer/bulkCustomer.vue';
import { useUsersStore } from '../../../stores/users';
import api from '../../../api/client';
import type { XTbl } from '../../../components/x-table/model';
@@ -243,8 +261,10 @@ const props = withDefaults(
* silently "fixed" — see docs/ADMIN-PORT-PLAN.md.
*/
rejectOnlyStepOne?: boolean;
/** Only OmdeList.vue passes this — see the comment on its call site. */
showAddButton?: boolean;
}>(),
{ rejectOnlyStepOne: false },
{ rejectOnlyStepOne: false, showAddButton: false },
);
const router = useRouter();
@@ -252,6 +272,8 @@ const users = useUsersStore();
const hasPermission = (rid: string) => Boolean(users.hasPermission(rid));
const tbl = ref<{ fetch: () => Promise<void> } | null>(null);
const modalOpen = ref(false);
const editingId = ref<string | undefined>(undefined);
const query = ref<{ count?: number; total?: number } | undefined>(undefined);
/** legacy setCount(): the "N رکورد غیر فعال" banner above the grid */
@@ -399,7 +421,20 @@ async function toggleArchive(id: string) {
}
function toEdit(id: string) {
router.push(`/bulkCustomer/edit/${id}`);
// Only the OmdeList variant (showAddButton) has the modal wired up; the
// other four (BankerList, ShopkeeperList, OmdeNonVerifiedList,
// OmdeArchivedList) keep navigating to the route, unchanged.
if (props.showAddButton) {
editingId.value = id;
modalOpen.value = true;
} else {
router.push(`/bulkCustomer/edit/${id}`);
}
}
function openAdd() {
editingId.value = undefined;
modalOpen.value = true;
}
function toView(id: string) {
router.push(`/bulkCustomer/view/${id}`);
@@ -1,6 +1,10 @@
<template>
<!-- Ported from legacy views/customer/omde/OmdeList.vue (615 lines). -->
<OmdeCustomerList :title="title" :model="model" />
<!--
Ported from legacy views/customer/omde/OmdeList.vue (615 lines).
The only one of OmdeCustomerList's five callers with an add button — see
OmdeCustomerList.vue's showAddButton prop for why it isn't on all five.
-->
<OmdeCustomerList :title="title" :model="model" show-add-button />
</template>
<script setup lang="ts">