feat: move brand name to one config and redesign the accounting card

اپ: نام برند در همه‌ی صفحه‌ها و پیام‌ها یکسان شده و از یک تنظیم مرکزی خوانده می‌شود.
اپ: کارت حسابداری در صفحه‌ی مشتری بازطراحی شد؛ مانده‌ها و وضعیت لینک واضح‌تر نمایش داده می‌شوند.
پنل: کارت حسابداری در پرونده‌ی مشتری بازطراحی شد؛ مانده‌ها، آخرین ارسال و فرم لینک دستی واضح‌تر شده‌اند.
پنل: ستون «کد» در جدول‌ها به‌طور پیش‌فرض پنهان است و از انتخاب ستون‌ها قابل نمایش است.
پنل: تغییر نمایش ستون‌ها در جدول‌ها بلافاصله اعمال می‌شود.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-14 13:31:20 +03:30
co-authored by Claude Haiku 5.5
parent 5d2a1efb0c
commit 532ceee189
100 changed files with 1870 additions and 579 deletions
@@ -4,50 +4,86 @@
linked to, and both balances side by side (the customer sees the
accounting one; the credit limit is checked against the app's own).
-->
<a-card v-if="visible" size="small" title="حسابداری" class="mt-4">
<section v-if="visible" class="acc-card" :style="themeVars">
<header class="acc-head">
<h2>حسابداری</h2>
<a-tag v-if="info?.link" color="success">لینک شده</a-tag>
<a-tag v-else color="warning">لینک نشده</a-tag>
</header>
<a-spin :spinning="loading">
<a-descriptions bordered size="small" :column="3">
<a-descriptions-item label="کد حساب">
<div class="acc-body">
<div class="acc-code">
<span class="label">کد حساب</span>
<template v-if="info?.link">
{{ info.link.externalCode }}
<a-tag class="ms-2">{{ ORIGIN[info.link.origin] ?? info.link.origin }}</a-tag>
</template>
<span v-else class="muted">لینک نشده</span>
</a-descriptions-item>
<a-descriptions-item label="مانده‌ی حسابداری (گرم / ریال)">
<template v-if="info?.accounting">
<span :class="sign(info.accounting.gold)">{{ num(info.accounting.gold, 3) }}</span> /
<span :class="sign(info.accounting.rial)">{{ num(info.accounting.rial, 0) }}</span>
<div class="muted small">{{ jdt(info.accounting.at) }}</div>
<span class="code" dir="ltr">{{ info.link.externalCode }}</span>
<span class="origin">{{ ORIGIN[info.link.origin] ?? info.link.origin }}</span>
</template>
<span v-else class="muted">—</span>
</a-descriptions-item>
<a-descriptions-item label="مانده‌ی دفتر اپ (گرم / ریال)">
<span :class="sign(info?.internal?.gold)">{{ num(info?.internal?.gold, 3) }}</span> /
<span :class="sign(info?.internal?.rial)">{{ num(info?.internal?.rial, 0) }}</span>
<div class="muted small">مبنای سقف اعتبار</div>
</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="lastJob" class="muted small mt-2">
آخرین ارسال: {{ STATUS[lastJob.status] ?? lastJob.status }} — {{ jdt(lastJob.updatedAt) }}
<span v-if="lastJob.lastError" class="warn"> — {{ lastJob.lastError }}</span>
<div class="balances">
<div class="balance">
<div class="label">مانده‌ی حسابداری</div>
<template v-if="info?.accounting">
<div class="line">
<span :class="sign(info.accounting.gold)">{{ num(info.accounting.gold, 3) }}</span>
<small>گرم</small>
</div>
<div class="line">
<span :class="sign(info.accounting.rial)">{{ num(info.accounting.rial, 0) }}</span>
<small>ریال</small>
</div>
<div class="hint">{{ jdt(info.accounting.at) }}</div>
</template>
<template v-else>
<div class="line muted">—</div>
<div class="hint">هنوز دریافت نشده</div>
</template>
</div>
<div class="balance">
<div class="label">مانده‌ی دفتر اپ</div>
<div class="line">
<span :class="sign(info?.internal?.gold)">{{ num(info?.internal?.gold, 3) }}</span>
<small>گرم</small>
</div>
<div class="line">
<span :class="sign(info?.internal?.rial)">{{ num(info?.internal?.rial, 0) }}</span>
<small>ریال</small>
</div>
<div class="hint">مبنای سقف اعتبار</div>
</div>
</div>
<div v-if="lastJob" class="last-job">
<a-badge :status="JOB_BADGE[lastJob.status] ?? 'default'" />
آخرین ارسال: {{ STATUS[lastJob.status] ?? lastJob.status }} — {{ jdt(lastJob.updatedAt) }}
<div v-if="lastJob.lastError" class="warn">{{ lastJob.lastError }}</div>
</div>
<div class="link-form">
<div class="label">{{ info?.link ? 'تغییر لینک' : 'لینک دستی' }}</div>
<div class="link-row">
<a-input
v-model:value="code"
dir="ltr"
placeholder="کد حساب در حسابداری"
@press-enter="code.trim() && link()"
/>
<a-button type="primary" ghost :disabled="!code.trim()" @click="link">لینک</a-button>
</div>
<a-popconfirm v-if="info?.link" title="لینک حذف شود؟" @confirm="unlink">
<a-button type="link" danger size="small" class="unlink">حذف لینک فعلی</a-button>
</a-popconfirm>
</div>
</div>
<a-space class="mt-2">
<a-input v-model:value="code" dir="ltr" placeholder="کد حساب در حسابداری" style="width: 200px" />
<a-button size="small" :disabled="!code.trim()" @click="link">لینک دستی</a-button>
<a-popconfirm v-if="info?.link" title="لینک حذف شود؟" @confirm="unlink">
<a-button size="small" danger>حذف لینک</a-button>
</a-popconfirm>
</a-space>
</a-spin>
</a-card>
</section>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import { message, theme } from 'ant-design-vue';
import moment from 'moment-jalaali';
import api from '../../api/client';
import { useUsersStore } from '../../stores/users';
@@ -68,12 +104,32 @@ const STATUS: Record<string, string> = {
NEEDS_REVIEW: 'نیاز به بررسی',
RESOLVED: 'حل‌شده',
};
const JOB_BADGE: Record<string, 'success' | 'processing' | 'error' | 'warning' | 'default'> = {
PENDING: 'default',
SENDING: 'processing',
SENT: 'success',
FAILED: 'error',
NEEDS_REVIEW: 'warning',
RESOLVED: 'success',
};
const users = useUsersStore();
const { token } = theme.useToken();
const info = ref<any>(null);
const loading = ref(false);
const code = ref('');
const themeVars = computed(() => ({
'--acc-bg': token.value.colorBgContainer,
'--acc-border': token.value.colorBorderSecondary,
'--acc-fill': token.value.colorFillQuaternary,
'--acc-text': token.value.colorText,
'--acc-text-2': token.value.colorTextSecondary,
'--acc-text-3': token.value.colorTextTertiary,
'--acc-error': token.value.colorError,
'--acc-success': token.value.colorSuccess,
}));
const visible = computed(() => Boolean(users.hasPermission('config.accounting')) && info.value && info.value.provider !== 'none');
const lastJob = computed(() => info.value?.jobs?.[0]);
@@ -115,28 +171,116 @@ onMounted(load);
</script>
<style scoped>
.mt-4 {
margin-top: 1rem;
.acc-card {
background: var(--acc-bg);
border: 1px solid var(--acc-border);
border-radius: 12px;
color: var(--acc-text);
}
.mt-2 {
margin-top: 0.5rem;
.acc-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--acc-border);
}
.ms-2 {
margin-inline-start: 0.5rem;
.acc-head h2 {
margin: 0;
font-size: 14px;
font-weight: 700;
}
.muted {
color: #888;
.acc-head :deep(.ant-tag) {
margin: 0;
}
.small {
.acc-body {
display: flex;
flex-direction: column;
gap: 14px;
padding: 14px 18px 16px;
}
.label {
font-size: 13px;
color: var(--acc-text-2);
}
.acc-code {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 10px;
}
.acc-code .label {
margin-inline-end: auto;
}
.code {
font-weight: 700;
letter-spacing: 0.5px;
}
.origin {
font-size: 12px;
color: var(--acc-text-3);
}
.balances {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.balance {
padding: 10px 12px;
border-radius: 10px;
background: var(--acc-fill);
}
.balance .label {
margin-bottom: 4px;
font-size: 12px;
}
.line {
font-size: 15px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.7;
}
.line small {
margin-inline-start: 4px;
font-size: 11px;
font-weight: 500;
color: var(--acc-text-3);
}
.hint {
margin-top: 2px;
font-size: 11px;
color: var(--acc-text-3);
}
.last-job {
font-size: 12px;
color: var(--acc-text-2);
}
.link-form {
padding-top: 12px;
border-top: 1px dashed var(--acc-border);
}
.link-form .label {
margin-bottom: 6px;
font-size: 12px;
}
.link-row {
display: flex;
gap: 8px;
}
.unlink {
margin-top: 4px;
padding-inline: 0;
}
.muted {
color: var(--acc-text-3);
}
.warn {
color: #d4380d;
margin-top: 2px;
color: var(--acc-error);
}
.debt {
color: #cf1322;
color: var(--acc-error);
}
.credit {
color: #389e0d;
color: var(--acc-success);
}
</style>