Files
tehran_tala/apps/admin-panel/src/views/customer/CustomerDetail.vue
T
s.esmaeiliandClaude Haiku 5.5 532ceee189 feat: move brand name to one config and redesign the accounting card
اپ: نام برند در همه‌ی صفحه‌ها و پیام‌ها یکسان شده و از یک تنظیم مرکزی خوانده می‌شود.
اپ: کارت حسابداری در صفحه‌ی مشتری بازطراحی شد؛ مانده‌ها و وضعیت لینک واضح‌تر نمایش داده می‌شوند.
پنل: کارت حسابداری در پرونده‌ی مشتری بازطراحی شد؛ مانده‌ها، آخرین ارسال و فرم لینک دستی واضح‌تر شده‌اند.
پنل: ستون «کد» در جدول‌ها به‌طور پیش‌فرض پنهان است و از انتخاب ستون‌ها قابل نمایش است.
پنل: تغییر نمایش ستون‌ها در جدول‌ها بلافاصله اعمال می‌شود.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-14 13:31:20 +03:30

761 lines
25 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.
This file contains Unicode characters that might be confused with other characters. 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 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})
Layout: profile header → financial KPI tiles → main column (record tabs)
beside a side column (profile details, accounting).
-->
<a-spin :spinning="loading">
<div class="customer-detail" :style="themeVars">
<!-- Profile header -->
<section class="panel hero">
<div class="hero-main">
<a-avatar :size="64" :src="customer?.image?.image" class="hero-avatar">
{{ initials }}
</a-avatar>
<div class="hero-text">
<div class="hero-eyebrow">{{ title }}</div>
<h1 class="hero-name">{{ customer?.name || '—' }}</h1>
<div class="hero-tags">
<a-tag v-if="customer" :color="customer.active === 1 ? 'success' : 'error'">
{{ customer.active === 1 ? 'فعال' : 'غیرفعال' }}
</a-tag>
<a-tag v-if="customer" :color="customer.sts === 1 ? 'processing' : 'warning'">
{{ customer.sts === 1 ? 'مجاز به سفارش' : 'عدم سفارش' }}
</a-tag>
<a-tag v-if="customer?.type_str">{{ customer.type_str }}</a-tag>
<a-tag v-if="customer?.is_archive === 1" color="default">بایگانی‌شده</a-tag>
</div>
<div class="hero-meta">
<span v-if="customer?.mobile" class="meta-item">
<PhoneOutlined />
<span dir="ltr">{{ customer.mobile }}</span>
</span>
<span v-if="customer?.city?.title" class="meta-item">
<EnvironmentOutlined />
{{ customer.city.title }}
</span>
<span v-if="customer?.reagent?.name" class="meta-item">
<UserOutlined />
کارشناس فروش: {{ customer.reagent.name }}
</span>
<span v-if="customer?.caj" class="meta-item">
<CalendarOutlined />
عضویت از {{ jdate(customer.caj) }}
</span>
</div>
</div>
</div>
<div class="hero-actions">
<a-button @click="router.back()">
<template #icon><ArrowRightOutlined /></template>
بازگشت
</a-button>
<a-button v-if="canEdit" type="primary" @click="router.push({ name: editRoute, params: { id } })">
<template #icon><EditOutlined /></template>
ویرایش مشتری
</a-button>
</div>
</section>
<!-- Financial KPIs -->
<section class="kpis">
<div class="panel kpi">
<div class="kpi-label">بدهی طلایی</div>
<div class="kpi-value" :class="sign(customer?.remain)">
{{ num(customer?.remain, 3) }}<small>گرم</small>
</div>
<div class="kpi-foot">{{ balanceWord(customer?.remain) }}</div>
</div>
<div class="panel kpi">
<div class="kpi-label">بدهی ریالی</div>
<div class="kpi-value" :class="sign(customer?.rial_remain)">
{{ num(customer?.rial_remain, 0) }}<small>ریال</small>
</div>
<div class="kpi-foot">{{ balanceWord(customer?.rial_remain) }}</div>
</div>
<div class="panel kpi">
<div class="kpi-label">سقف اعتبار</div>
<div class="kpi-value">{{ num(customer?.gram_level, 3) }}<small>گرم</small></div>
<template v-if="creditUsage !== null">
<a-progress
:percent="creditUsage"
:stroke-color="creditColor"
:show-info="false"
size="small"
class="kpi-progress"
/>
<div class="kpi-foot">{{ num(creditUsage, 0) }}٪ از سقف استفاده شده</div>
</template>
<div v-else class="kpi-foot">سقفی تعیین نشده</div>
</div>
<div class="panel kpi">
<div class="kpi-label">میزان فروش</div>
<div class="kpi-value">{{ num(dashboard?.grams_sell, 3) }}<small>گرم</small></div>
<div class="kpi-foot">مجموع وزن سفارش‌ها</div>
</div>
<div class="panel kpi">
<div class="kpi-label">سفارش‌ها</div>
<div class="kpi-value">{{ num(dashboard?.total_order ?? orders.length, 0) }}</div>
<div class="kpi-foot">
<span :class="{ 'text-warning': Number(dashboard?.open_order) > 0 }">
{{ num(dashboard?.open_order, 0) }} باز
</span>
· {{ num(dashboard?.ledger_entries ?? documents.length, 0) }} سند
</div>
</div>
</section>
<div class="layout">
<!-- Main column: the customer's records -->
<section class="panel layout-main">
<a-tabs v-model:active-key="activeTab" class="detail-tabs">
<a-tab-pane key="orders">
<template #tab>
سفارش‌ها <span class="tab-count">{{ num(orders.length, 0) }}</span>
</template>
<a-table
size="middle"
:data-source="orders"
:columns="orderColumns"
:pagination="pagination(10, orders.length)"
:locale="{ emptyText: 'سفارشی برای این مشتری ثبت نشده است' }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'name'">
<span class="mono" dir="ltr">{{ record.name }}</span>
</template>
<template v-else-if="column.dataIndex === 'w'">
{{ num(record.w, 3) }} <span class="unit">گرم</span>
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="ORDER_STATUS[record.status]?.color">
{{ ORDER_STATUS[record.status]?.label ?? record.status ?? '—' }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'branch'">{{ record.branch ?? '—' }}</template>
<template v-else-if="column.dataIndex === 'caj'">{{ jdate(record.caj) }}</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="documents">
<template #tab>
سندهای مالی <span class="tab-count">{{ num(documents.length, 0) }}</span>
</template>
<a-table
size="middle"
:data-source="documents"
:columns="documentColumns"
:pagination="pagination(10, documents.length)"
:locale="{ emptyText: 'سندی برای این مشتری ثبت نشده است' }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'type'">
{{ SOURCE_TYPE[record.type] ?? record.type }}
</template>
<template v-else-if="column.dataIndex === 'value'">
<span :class="record.direction === 'DEBIT' ? 'debt' : record.direction === 'CREDIT' ? 'credit' : ''">
{{ record.direction === 'CREDIT' ? '−' : record.direction === 'DEBIT' ? '+' : '' }}{{
num(record.value, record.currency === 'RIAL' ? 0 : 3)
}}
</span>
<span class="unit">{{ record.currency === 'RIAL' ? 'ریال' : 'گرم' }}</span>
</template>
<template v-else-if="column.dataIndex === 'confirmed'">
<a-badge
:status="record.confirmed === 1 ? 'success' : 'default'"
:text="record.confirmed === 1 ? 'تایید شده' : 'تایید نشده'"
/>
</template>
<template v-else-if="column.dataIndex === 'note'">
<span class="note">{{ record.note || '—' }}</span>
</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">
<template #tab>
کیف پول <span class="tab-count">{{ num(wallet.length, 0) }}</span>
</template>
<a-table
size="middle"
:data-source="wallet"
:columns="walletColumns"
:pagination="pagination(10, wallet.length)"
:locale="{ emptyText: 'اعتباری در کیف پول ثبت نشده است' }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'value'">
{{ num(record.value, 3) }} <span class="unit">گرم</span>
</template>
<template v-else-if="column.dataIndex === 'caj'">{{ jdate(record.caj) }}</template>
<template v-else-if="column.dataIndex === 'expires_at'">
{{ record.expires_at ? jdate(record.expires_at) : '—' }}
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane v-if="showActivity" key="activity">
<template #tab>
فعالیت‌ها <span class="tab-count">{{ num(activity.length, 0) }}</span>
</template>
<a-table
size="middle"
:data-source="activity"
:columns="activityColumns"
:pagination="pagination(15, activity.length)"
:locale="{ emptyText: 'فعالیتی ثبت نشده است' }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'caj'">
<span dir="ltr">{{ jdateTime(record.caj) }}</span>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</section>
<!-- Side column: who the customer is -->
<aside class="layout-side">
<section class="panel">
<h2 class="panel-title">اطلاعات هویتی و تماس</h2>
<dl class="facts">
<div v-for="f in identityFacts" :key="f.label" class="fact">
<dt>{{ f.label }}</dt>
<dd :dir="f.ltr ? 'ltr' : undefined">{{ f.value || '—' }}</dd>
</div>
</dl>
</section>
<section v-if="businessFacts.length" class="panel">
<h2 class="panel-title">اطلاعات کسب‌وکار</h2>
<dl class="facts">
<div v-for="f in businessFacts" :key="f.label" class="fact">
<dt>{{ f.label }}</dt>
<dd>{{ f.value }}</dd>
</div>
</dl>
</section>
<!-- D27 — link and balance in the accounting software. -->
<CustomerAccountingCard :customer-id="id" />
</aside>
</div>
</div>
</a-spin>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { theme } from 'ant-design-vue';
import {
ArrowRightOutlined,
CalendarOutlined,
EditOutlined,
EnvironmentOutlined,
PhoneOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { jdate, jdateTime } from '../../utils/filters';
import api from '../../api/client';
import { useUsersStore } from '../../stores/users';
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;
/** Named route of this customer kind's edit form; the button hides if absent or not permitted. */
editRoute?: string;
}>(),
{
title: 'مشاهده اطلاعات مشتری',
showWallet: false,
showActivity: false,
defaultTab: 'orders',
editRoute: undefined,
},
);
const route = useRoute();
const router = useRouter();
const users = useUsersStore();
const { token } = theme.useToken();
const id = route.params.id as string;
const loading = ref(false);
const activeTab = ref(props.defaultTab);
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>[]>([]);
// Colours come from the antd theme so the page follows the panel's palette.
const themeVars = computed(() => ({
'--cd-bg': token.value.colorBgContainer,
'--cd-border': token.value.colorBorderSecondary,
'--cd-text': token.value.colorText,
'--cd-text-2': token.value.colorTextSecondary,
'--cd-text-3': token.value.colorTextTertiary,
'--cd-fill': token.value.colorFillQuaternary,
'--cd-primary': token.value.colorPrimary,
'--cd-primary-bg': token.value.colorPrimaryBg,
'--cd-error': token.value.colorError,
'--cd-success': token.value.colorSuccess,
'--cd-warning': token.value.colorWarning,
}));
// OrderStatus enum (schema.prisma); labels as on the accounting settings page.
const ORDER_STATUS: Record<string, { label: string; color?: string }> = {
SUSPENDED: { label: 'معلق (عبور از سقف)', color: 'error' },
PLACED: { label: 'ثبت سفارش', color: 'default' },
ADMIN_APPROVED: { label: 'تأیید مدیر', color: 'processing' },
SOURCING: { label: 'در حال تأمین', color: 'processing' },
PREPARING: { label: 'در حال آماده‌سازی', color: 'processing' },
DELIVERED_TO_AGENT: { label: 'تحویل مسئول فروش', color: 'cyan' },
DELIVERED_TO_CUSTOMER: { label: 'تحویل مشتری', color: 'success' },
CANCELLED: { label: 'لغو شده', color: 'default' },
};
// LedgerSourceType enum (schema.prisma).
const SOURCE_TYPE: Record<string, string> = {
ORDER_STOCK: 'سفارش کالای موجود',
ORDER_PRE_ORDER: 'پیش‌سفارش',
REPAYMENT_MELTED_GOLD: 'تسویه با آبشده',
REPAYMENT_TRANSFER: 'تسویه با واریز',
REPAYMENT_CHEQUE: 'تسویه با چک',
REPAYMENT_CASH: 'تسویه نقدی',
WALLET_BONUS: 'پاداش کیف پول',
ADJUSTMENT: 'اصلاحیه',
};
const orderColumns = [
{ title: 'شماره سفارش', dataIndex: 'name' },
{ title: 'وضعیت', dataIndex: 'status' },
{ title: 'وزن', dataIndex: 'w' },
{ title: 'تعداد آیتم', dataIndex: 'items_count', align: 'center' as const },
{ title: 'شعبه', dataIndex: 'branch' },
{ title: 'تاریخ', dataIndex: 'caj' },
];
const documentColumns = [
{ title: 'نوع', dataIndex: 'type' },
{ title: 'مقدار', dataIndex: 'value' },
{ title: 'وضعیت', dataIndex: 'confirmed' },
{ title: 'توضیحات', dataIndex: 'note', ellipsis: true },
{ 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' },
];
const num = (v: unknown, places: number) =>
v === null || v === undefined || v === '' || Number.isNaN(Number(v))
? '—'
: Number(v).toLocaleString('fa-IR', { maximumFractionDigits: places });
// Positive balance = the customer owes (same convention as the accounting card).
const sign = (v: unknown) => (Number(v) > 0 ? 'debt' : Number(v) < 0 ? 'credit' : '');
const balanceWord = (v: unknown) =>
v === null || v === undefined ? '' : Number(v) > 0 ? 'بدهکار' : Number(v) < 0 ? 'بستانکار' : 'تسویه';
const pagination = (pageSize: number, total: number) =>
total > pageSize ? { pageSize, size: 'small' as const, showSizeChanger: false } : false;
const initials = computed(() => (customer.value?.name ?? '').trim().charAt(0) || '؟');
const creditUsage = computed<number | null>(() => {
const limit = Number(customer.value?.gram_level);
if (!limit || limit <= 0) return null;
const used = Math.max(Number(customer.value?.remain) || 0, 0);
return Math.min(Math.round((used / limit) * 100), 100);
});
const creditColor = computed(() => {
const u = creditUsage.value ?? 0;
return u >= 90 ? token.value.colorError : u >= 70 ? token.value.colorWarning : token.value.colorPrimary;
});
const canEdit = computed(() => {
if (!props.editRoute || !router.hasRoute(props.editRoute)) return false;
const meta = router.resolve({ name: props.editRoute, params: { id } }).meta as Record<string, any>;
if (meta.superAdminOnly && !users.isSuperAdmin) return false;
return !meta.role || Boolean(users.hasPermission(meta.role));
});
const identityFacts = computed(() => {
const c = customer.value;
return [
{ label: 'موبایل', value: c?.mobile, ltr: true },
{ label: 'تلفن ثابت', value: c?.phone, ltr: true },
{ label: 'کد ملی', value: c?.national_code, ltr: true },
{ label: 'تاریخ تولد', value: c?.birth_date ? jdate(c.birth_date) : null },
{ label: 'شهر', value: c?.city?.title },
{ label: 'آدرس', value: c?.address },
];
});
// Only the fields this customer actually has — retail customers usually have none.
const businessFacts = computed(() => {
const c = customer.value;
return [
{ label: 'نام فروشگاه', value: c?.store_name },
{ label: 'پاساژ', value: c?.pasazh_name },
{ label: 'منطقه', value: c?.zone },
{ label: 'کد عضویت', value: c?.mcode },
{ label: 'کارشناس فروش', value: c?.reagent?.name },
{
label: 'درصد اجرت',
value: c?.percent !== null && c?.percent !== undefined && c?.percent !== '' ? `${num(c.percent, 2)}٪` : null,
},
].filter((f) => f.value);
});
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>
.customer-detail {
display: flex;
flex-direction: column;
gap: 16px;
color: var(--cd-text);
}
.panel {
background: var(--cd-bg);
border: 1px solid var(--cd-border);
border-radius: 12px;
}
.panel-title {
margin: 0;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
border-bottom: 1px solid var(--cd-border);
}
/* ── Header ─────────────────────────────────────────── */
.hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 20px 24px;
}
.hero-main {
display: flex;
align-items: flex-start;
gap: 16px;
min-width: 0;
}
.hero-avatar {
flex-shrink: 0;
background: var(--cd-primary);
font-size: 26px;
font-weight: 700;
}
.hero-text {
min-width: 0;
}
.hero-eyebrow {
font-size: 12px;
color: var(--cd-text-3);
}
.hero-name {
margin: 2px 0 8px;
font-size: 22px;
font-weight: 800;
line-height: 1.4;
color: var(--cd-text);
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.hero-tags :deep(.ant-tag) {
margin: 0;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 6px 20px;
margin-top: 12px;
font-size: 13px;
color: var(--cd-text-2);
}
.meta-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.meta-item :deep(.anticon) {
color: var(--cd-text-3);
}
.hero-actions {
display: flex;
flex-shrink: 0;
gap: 8px;
}
/* ── KPI tiles ──────────────────────────────────────── */
.kpis {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 16px;
}
.kpi {
display: flex;
flex-direction: column;
padding: 16px 18px;
}
.kpi-label {
font-size: 13px;
color: var(--cd-text-2);
}
.kpi-value {
margin-top: 6px;
font-size: 22px;
font-weight: 800;
line-height: 1.3;
font-variant-numeric: tabular-nums;
color: var(--cd-text);
}
.kpi-value small {
margin-inline-start: 4px;
font-size: 12px;
font-weight: 500;
color: var(--cd-text-3);
}
.kpi-progress {
margin: 6px 0 0;
}
.kpi-foot {
margin-top: auto;
padding-top: 6px;
font-size: 12px;
color: var(--cd-text-3);
}
/* ── Two-column body ────────────────────────────────── */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 16px;
align-items: start;
}
.layout-main {
padding: 4px 20px 16px;
}
.layout-side {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-tabs :deep(.ant-tabs-nav) {
margin-bottom: 12px;
}
.detail-tabs :deep(.ant-tabs-tab) {
padding: 14px 0;
font-weight: 600;
}
.tab-count {
display: inline-block;
min-width: 22px;
margin-inline-start: 6px;
padding: 0 7px;
border-radius: 10px;
background: var(--cd-fill);
color: var(--cd-text-2);
font-size: 12px;
font-weight: 500;
line-height: 20px;
text-align: center;
}
.detail-tabs :deep(.ant-tabs-tab-active) .tab-count {
background: var(--cd-primary-bg);
color: var(--cd-primary);
}
.detail-tabs :deep(.ant-table-thead > tr > th) {
font-weight: 600;
color: var(--cd-text-2);
}
.facts {
margin: 0;
padding: 6px 18px 10px;
}
.fact {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 9px 0;
border-bottom: 1px dashed var(--cd-border);
}
.fact:last-child {
border-bottom: 0;
}
.fact dt {
flex-shrink: 0;
font-size: 13px;
color: var(--cd-text-2);
}
.fact dd {
margin: 0;
font-weight: 600;
text-align: end;
word-break: break-word;
}
/* ── Shared bits ────────────────────────────────────── */
.debt {
color: var(--cd-error);
}
.credit {
color: var(--cd-success);
}
.text-warning {
color: var(--cd-warning);
font-weight: 600;
}
.unit {
font-size: 12px;
color: var(--cd-text-3);
}
.mono {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.note {
color: var(--cd-text-2);
}
/* ── Responsive ─────────────────────────────────────── */
@media (max-width: 1399px) {
.kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1199px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
/* details first on narrow screens — they frame the records below */
.layout-side {
order: -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
align-items: start;
}
}
@media (max-width: 767px) {
.hero {
flex-direction: column;
padding: 16px;
}
.hero-actions {
width: 100%;
}
.hero-actions :deep(.ant-btn) {
flex: 1;
}
.kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
}
@media (max-width: 575px) {
.customer-detail {
gap: 12px;
}
.hero-avatar {
width: 48px !important;
height: 48px !important;
line-height: 48px !important;
font-size: 20px;
}
.hero-name {
font-size: 18px;
}
.kpi {
padding: 12px 14px;
}
.kpi-value {
font-size: 18px;
}
.layout-main {
padding: 0 12px 12px;
}
.layout-side {
grid-template-columns: minmax(0, 1fr);
}
.detail-tabs :deep(.ant-table-content) {
overflow-x: auto;
}
.detail-tabs :deep(.ant-table-content > table) {
min-width: max-content;
}
}
</style>