اپ: نام برند در همهی صفحهها و پیامها یکسان شده و از یک تنظیم مرکزی خوانده میشود. اپ: کارت حسابداری در صفحهی مشتری بازطراحی شد؛ ماندهها و وضعیت لینک واضحتر نمایش داده میشوند. پنل: کارت حسابداری در پروندهی مشتری بازطراحی شد؛ ماندهها، آخرین ارسال و فرم لینک دستی واضحتر شدهاند. پنل: ستون «کد» در جدولها بهطور پیشفرض پنهان است و از انتخاب ستونها قابل نمایش است. پنل: تغییر نمایش ستونها در جدولها بلافاصله اعمال میشود. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
761 lines
25 KiB
Vue
761 lines
25 KiB
Vue
<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>
|