Legacy fixed `orders.reagent_id` once, when the order was placed, and its «میزان فروش» summed `customer_id = X OR reagent_id = X`. The app credited orders to the customer's CURRENT referrer and left out an expert's own orders, so 9 of 49 imported experts showed a different figure. - Order.salesAgentId (+ referrerCustomerId for 17 legacy orders whose referrer was not an expert); additive migration, recent app orders backfilled from the customer's referrer - createOrder records the expert at placement time - one shared filter (agent-orders.ts) for the experts list, expert detail, dashboard and the expert app; child orders follow salesAgentId - customer page gets its «میزان فروش» back - import carries orders.reagent_id as is; ex-experts kept as admins keep their profile so their own orders stay linked - reconcile-sales: 1,557 of 1,558 match legacy's own formula; balances unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
217 lines
7.6 KiB
Vue
217 lines
7.6 KiB
Vue
<template>
|
||
<!--
|
||
Shared implementation of legacy ViewOmdeCustomer.vue (376 lines) and
|
||
ViewSingleCustomer.vue (425 lines) — the customer detail screens.
|
||
|
||
Both show the same four panels (profile, orders, financial documents, wallet)
|
||
against the same customer record; legacy duplicated the whole page per
|
||
customer kind. The differing bits are parameterised:
|
||
- the wholesale screen adds an activity-log panel (legacy log/ago/{id})
|
||
- the retail screen adds a wallet panel (legacy single/wallet/{id})
|
||
-->
|
||
<a-spin :spinning="loading">
|
||
<a-card :title="title">
|
||
<a-descriptions bordered size="small" :column="3">
|
||
<a-descriptions-item label="نام">{{ customer?.name ?? '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="موبایل">{{ customer?.mobile ?? '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="کد ملی">
|
||
{{ customer?.national_code ?? '-' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="شهر">{{ customer?.city?.title ?? '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="کارشناس فروش">
|
||
{{ customer?.reagent?.name ?? '-' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="وضعیت">
|
||
<a-tag :color="customer?.active === 1 ? 'green' : 'red'">
|
||
{{ customer?.active === 1 ? 'فعال' : 'غیر فعال' }}
|
||
</a-tag>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="سطح اعتبار">
|
||
{{ customer?.gram_level ?? '-' }} گرم
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="بدهی گرمی">{{ customer?.remain ?? '-' }}</a-descriptions-item>
|
||
<a-descriptions-item label="بدهی ریالی">
|
||
{{ customer?.rial_remain ?? '-' }}
|
||
</a-descriptions-item>
|
||
<!-- legacy ViewOmdeCustomer.vue:28-30 -->
|
||
<a-descriptions-item label="میزان فروش">{{ dashboard?.grams_sell ?? '-' }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
|
||
<div v-if="dashboard" class="stats mt-4">
|
||
<a-tag color="blue">کل سفارشها: {{ dashboard.total_order }}</a-tag>
|
||
<a-tag color="orange">سفارش باز: {{ dashboard.open_order }}</a-tag>
|
||
<a-tag color="green">اسناد: {{ dashboard.ledger_entries }}</a-tag>
|
||
</div>
|
||
</a-card>
|
||
|
||
<!-- D27 — link and balance in the accounting software. -->
|
||
<CustomerAccountingCard :customer-id="id" />
|
||
|
||
<a-tabs class="mt-4" :default-active-key="defaultTab">
|
||
<a-tab-pane key="orders" tab="سفارش ها">
|
||
<a-table
|
||
size="small"
|
||
:data-source="orders"
|
||
:columns="orderColumns"
|
||
:pagination="{ pageSize: 10 }"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.dataIndex === 'caj'">{{ jdate(record.caj) }}</template>
|
||
</template>
|
||
</a-table>
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane key="documents" tab="سند های مالی">
|
||
<a-table
|
||
size="small"
|
||
:data-source="documents"
|
||
:columns="documentColumns"
|
||
:pagination="{ pageSize: 10 }"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.dataIndex === 'confirmed'">
|
||
<a-tag :color="record.confirmed === 1 ? 'green' : ''">
|
||
{{ record.confirmed === 1 ? 'تایید شده' : 'تایید نشده' }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.dataIndex === 'caj'">{{ jdate(record.caj) }}</template>
|
||
</template>
|
||
</a-table>
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane v-if="showWallet" key="wallet" tab="کیف پول">
|
||
<a-table
|
||
size="small"
|
||
:data-source="wallet"
|
||
:columns="walletColumns"
|
||
:pagination="false"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.dataIndex === 'caj'">{{ jdate(record.caj) }}</template>
|
||
<template v-else-if="column.dataIndex === 'expires_at'">
|
||
{{ jdate(record.expires_at) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</a-tab-pane>
|
||
|
||
<a-tab-pane v-if="showActivity" key="activity" tab="فعالیت ها">
|
||
<a-table
|
||
size="small"
|
||
:data-source="activity"
|
||
:columns="activityColumns"
|
||
:pagination="{ pageSize: 15 }"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.dataIndex === 'caj'">{{ jdateTime(record.caj) }}</template>
|
||
</template>
|
||
</a-table>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</a-spin>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue';
|
||
import { useRoute } from 'vue-router';
|
||
import { jdate, jdateTime } from '../../utils/filters';
|
||
import api from '../../api/client';
|
||
import CustomerAccountingCard from '../../components/accounting/CustomerAccountingCard.vue';
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
title?: string;
|
||
/** The retail screen shows the wallet panel. */
|
||
showWallet?: boolean;
|
||
/** The wholesale screen shows the activity log. */
|
||
showActivity?: boolean;
|
||
defaultTab?: string;
|
||
}>(),
|
||
{ title: 'مشاهده اطلاعات مشتری', showWallet: false, showActivity: false, defaultTab: 'orders' },
|
||
);
|
||
|
||
const route = useRoute();
|
||
const id = route.params.id as string;
|
||
|
||
const loading = ref(false);
|
||
const customer = ref<Record<string, any> | undefined>(undefined);
|
||
const dashboard = ref<Record<string, any> | undefined>(undefined);
|
||
const orders = ref<Record<string, any>[]>([]);
|
||
const documents = ref<Record<string, any>[]>([]);
|
||
const wallet = ref<Record<string, any>[]>([]);
|
||
const activity = ref<Record<string, any>[]>([]);
|
||
|
||
const orderColumns = [
|
||
{ title: 'شماره سفارش', dataIndex: 'name' },
|
||
{ title: 'وزن', dataIndex: 'w' },
|
||
{ title: 'تعداد آیتم', dataIndex: 'items_count' },
|
||
{ title: 'شعبه', dataIndex: 'branch' },
|
||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||
];
|
||
|
||
const documentColumns = [
|
||
{ title: 'نوع', dataIndex: 'type' },
|
||
{ title: 'مقدار', dataIndex: 'value' },
|
||
{ title: 'وضعیت', dataIndex: 'confirmed' },
|
||
{ title: 'توضیحات', dataIndex: 'note' },
|
||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||
];
|
||
|
||
const walletColumns = [
|
||
{ title: 'مقدار (گرم)', dataIndex: 'value' },
|
||
{ title: 'تاریخ اعطا', dataIndex: 'caj' },
|
||
{ title: 'انقضا', dataIndex: 'expires_at' },
|
||
];
|
||
|
||
const activityColumns = [
|
||
{ title: 'عملیات', dataIndex: 'action' },
|
||
{ title: 'جزئیات', dataIndex: 'detail' },
|
||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||
];
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
loading.value = true;
|
||
const [c, d, o, docs] = await Promise.all([
|
||
api.get(`customer/${id}`),
|
||
api.get(`major/dashboard/${id}`).catch(() => ({ data: undefined })),
|
||
api.get(`admin/customers-omde/${id}/orders`).catch(() => ({ data: [] })),
|
||
api.get(`admin/customers-omde/${id}/documents`).catch(() => ({ data: [] })),
|
||
]);
|
||
customer.value = c.data;
|
||
dashboard.value = d.data;
|
||
orders.value = o.data;
|
||
documents.value = docs.data;
|
||
|
||
if (props.showWallet) {
|
||
const w = await api
|
||
.get(`admin/customers-omde/${id}/wallet`)
|
||
.catch(() => ({ data: [] }));
|
||
wallet.value = w.data;
|
||
}
|
||
if (props.showActivity) {
|
||
const a = await api.get(`log/ago/${id}`).catch(() => ({ data: [] }));
|
||
activity.value = a.data;
|
||
}
|
||
} catch (e) {
|
||
console.log(e);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.stats {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
.mt-4 {
|
||
margin-top: 1rem;
|
||
}
|
||
</style>
|