Improve person assets UI and fix Tahesab asnad date/list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 11:08:46 +03:30
co-authored by Cursor
parent 43526cb980
commit 1ac23e13c1
4 changed files with 117 additions and 32 deletions
@@ -3,7 +3,13 @@ import { computed, onMounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/money';
import {
dateToJalali,
formatLocalDateInput,
parseLocalInput,
} from '@/lib/jalali';
import MoneyInput from '@/components/MoneyInput.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import type {
PersonAccountingLinkDto,
PersonAssetsDto,
@@ -16,6 +22,10 @@ const props = defineProps<{
personId: string;
}>();
const emit = defineEmits<{
linkChanged: [moshtariCode: number | null];
}>();
const loading = ref(false);
const error = ref('');
const ok = ref('');
@@ -26,6 +36,7 @@ const assets = ref<PersonAssetsDto | null>(null);
const activeTab = ref<'مانده حساب' | 'جزئیات حساب' | 'ثبت سند'>('مانده حساب');
const asnadLoading = ref(false);
const asnadItems = ref<Record<string, unknown>[]>([]);
/** مدل JalaliDateInput = تاریخ محلی میلادی YYYY-MM-DD */
const fromDate = ref('');
const toDate = ref('');
const manualCode = ref('');
@@ -120,18 +131,28 @@ function formatMandehAmount(amount: string | number, unit: string) {
return abs.toLocaleString('fa-IR', { maximumFractionDigits: 3 });
}
/** تبدیل تاریخ محلی میلادی به شمسی YYYY-MM-DD برای API ته‌حساب */
function toShamsiYmd(localIso: string): string | null {
const d = parseLocalInput(localIso);
if (!d) return null;
const { jy, jm, jd } = dateToJalali(d);
return `${jy}-${String(jm).padStart(2, '0')}-${String(jd).padStart(2, '0')}`;
}
function defaultJalaliRange() {
// تقریبی: کاربر می‌تواند عوض کند؛ پیش‌فرض بازه یک‌ماهه متنی
const now = new Date();
// بدون وابستگی سنگین: مقادیر نمونه قابل ویرایش
toDate.value = `${now.getFullYear() - 621}-01-01`;
// بهتر: از API تاریخ نخواهیم؛ فیلدها را خالی‌تر با placeholder بگذاریم
const y = 1405;
fromDate.value = `${y}-01-01`;
toDate.value = `${y}-12-29`;
sanad.value.year = y;
sanad.value.month = 1;
sanad.value.day = 1;
const from = new Date(now);
from.setMonth(from.getMonth() - 6);
fromDate.value = formatLocalDateInput(from);
toDate.value = formatLocalDateInput(now);
const j = dateToJalali(now);
sanad.value.year = j.jy;
sanad.value.month = j.jm;
sanad.value.day = j.jd;
}
function emitLinkCode() {
emit('linkChanged', currentLink.value?.moshtariCode ?? null);
}
async function loadConnectionsAndLinks() {
@@ -155,6 +176,12 @@ async function loadConnectionsAndLinks() {
) {
connectionId.value = def?.id ?? '';
}
if (!currentLink.value && links.value.length) {
const preferred =
links.value.find((l) => l.isDefaultConnection) ?? links.value[0];
if (preferred) connectionId.value = preferred.connectionId;
}
emitLinkCode();
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
@@ -162,6 +189,7 @@ async function loadConnectionsAndLinks() {
'بارگذاری اتصال ته‌حساب ناموفق بود.',
)
: 'بارگذاری اتصال ته‌حساب ناموفق بود.';
emit('linkChanged', null);
} finally {
loading.value = false;
}
@@ -195,8 +223,10 @@ async function refreshAssets() {
async function loadAsnad() {
if (!connectionId.value || !currentLink.value) return;
if (!fromDate.value || !toDate.value) {
error.value = 'تاریخ از و تا را وارد کنید (شمسی YYYY-MM-DD).';
const from = toShamsiYmd(fromDate.value);
const to = toShamsiYmd(toDate.value);
if (!from || !to) {
error.value = 'تاریخ از و تا را از تقویم انتخاب کنید.';
return;
}
asnadLoading.value = true;
@@ -207,12 +237,16 @@ async function loadAsnad() {
{
params: {
connectionId: connectionId.value,
from: fromDate.value,
to: toDate.value,
from,
to,
},
timeout: 60_000,
},
);
asnadItems.value = data.items ?? [];
ok.value = asnadItems.value.length
? `${asnadItems.value.length} سند نمایش داده شد.`
: 'در این بازه سندی یافت نشد.';
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
@@ -315,6 +349,7 @@ async function unlink() {
`/integrations/tahesab/people/${props.personId}/links/${connectionId.value}`,
);
assets.value = null;
asnadItems.value = [];
await loadConnectionsAndLinks();
}
@@ -355,10 +390,17 @@ async function pushSanad() {
watch(connectionId, async () => {
matchCandidates.value = [];
asnadItems.value = [];
emitLinkCode();
if (currentLink.value) await refreshAssets();
else assets.value = null;
});
watch(activeTab, (t) => {
if (t === 'جزئیات حساب' && currentLink.value && !asnadItems.value.length) {
void loadAsnad();
}
});
onMounted(async () => {
defaultJalaliRange();
await loadConnectionsAndLinks();
@@ -374,8 +416,8 @@ onMounted(async () => {
</p>
<template v-else>
<div class="toolbar">
<label v-if="hasMultipleConnections" class="conn-pick">
<div v-if="hasMultipleConnections" class="toolbar">
<label class="conn-pick">
<span>اتصال</span>
<select v-model="connectionId">
<option v-for="c in connections" :key="c.id" :value="c.id">
@@ -383,19 +425,12 @@ onMounted(async () => {
</option>
</select>
</label>
<div v-else-if="connections[0]" class="muted">
اتصال: {{ connections[0].label }} · DB {{ connections[0].dbName }}
</div>
</div>
<div class="link-bar card">
<template v-if="currentLink">
<div>
<strong>لینک‌شده</strong>
<span class="muted">
کد ته‌حساب: {{ currentLink.moshtariCode }} · روش:
{{ currentLink.matchMethod }}
</span>
<strong>لینک‌شده به ته‌حساب</strong>
</div>
<div class="actions">
<button type="button" class="btn ghost" @click="refreshAssets">بروزرسانی مانده</button>
@@ -506,11 +541,11 @@ onMounted(async () => {
<div class="date-row">
<label>
از
<input v-model="fromDate" dir="ltr" placeholder="1405-01-01" />
<JalaliDateInput v-model="fromDate" placeholder="از تاریخ" />
</label>
<label>
تا
<input v-model="toDate" dir="ltr" placeholder="1405-12-29" />
<JalaliDateInput v-model="toDate" placeholder="تا تاریخ" />
</label>
<button type="button" class="btn" :disabled="asnadLoading" @click="loadAsnad">
{{ asnadLoading ? '…' : 'نمایش اسناد' }}
@@ -790,6 +825,10 @@ onMounted(async () => {
align-items: end;
margin-bottom: 0.75rem;
}
.date-row label {
min-width: 160px;
flex: 1;
}
.date-row label,
.sanad-form label {
display: grid;