Files
tehran_tala/apps/admin-panel/src/views/customer/CustomerDetail.vue
T
s.esmaeiliandClaude Opus 5.5 29084904e9 feat(orders): credit each order to the expert who placed it, as legacy did
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>
2026-10-08 10:40:21 +03:30

217 lines
7.6 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.
<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>