Improve person assets UI and fix Tahesab asnad date/list.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,8 +16,17 @@ function asAsnadList(raw: unknown): Array<Record<string, unknown>> {
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw as Array<Record<string, unknown>>;
|
||||
if (typeof raw === 'object') {
|
||||
return Object.values(raw as Record<string, unknown>).filter(
|
||||
(v) => v && typeof v === 'object',
|
||||
const obj = raw as Record<string, unknown>;
|
||||
for (const key of ['DoListAsnad', 'Asnad', 'asnad', 'Items', 'items', 'Data']) {
|
||||
const nested = obj[key];
|
||||
if (Array.isArray(nested)) {
|
||||
return nested.filter((v) => v && typeof v === 'object') as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
}
|
||||
}
|
||||
return Object.values(obj).filter(
|
||||
(v) => v && typeof v === 'object' && !Array.isArray(v),
|
||||
) as Array<Record<string, unknown>>;
|
||||
}
|
||||
return [];
|
||||
@@ -68,8 +77,8 @@ export class TahesabDocumentsService {
|
||||
);
|
||||
const link = await this.requireLink(organizationId, personId, resolvedId);
|
||||
const { api } = await this.connections.getApi(organizationId, resolvedId);
|
||||
// Count_Last = -1 برای مانده ردیفبهردیف مثل همکار
|
||||
const raw = await api.doListAsnad(-1, link.moshtariCode, from, to);
|
||||
// Count_Last = سقف تعداد سند برگشتی (۰ در نمونه گاهی خالی برمیگرداند)
|
||||
const raw = await api.doListAsnad(500, link.moshtariCode, from, to);
|
||||
const items = asAsnadList(raw);
|
||||
return {
|
||||
connectionId: resolvedId,
|
||||
|
||||
@@ -222,11 +222,14 @@ export class TahesabApi {
|
||||
fromShamsi: string,
|
||||
toShamsi: string,
|
||||
) {
|
||||
// OpenAPI: Count_Last, Moshtari_Code, AzTarikhShamsi, TaTarikhShamsi, FilterNoSanad, Mande_Jens_Felez
|
||||
return this.call('DoListAsnad', 'DoListAsnad', [
|
||||
countLast,
|
||||
moshtariCode,
|
||||
fromShamsi,
|
||||
toShamsi,
|
||||
'',
|
||||
0,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
PaginatedResponse,
|
||||
PersonDto,
|
||||
PersonAiSummaryDto,
|
||||
PersonAccountingLinkDto,
|
||||
PipelineBoardDto,
|
||||
ProjectBoardBoardDto,
|
||||
ProjectCardDto,
|
||||
@@ -94,6 +95,7 @@ const tickets = ref<TicketDto[]>([]);
|
||||
const memories = ref<MemoryItemDto[]>([]);
|
||||
const deals = ref<DealDto[]>([]);
|
||||
const cards = ref<ProjectCardDto[]>([]);
|
||||
const tahesabMoshtariCode = ref<number | null>(null);
|
||||
|
||||
const moreOpen = ref(false);
|
||||
const filterOpen = ref(false);
|
||||
@@ -372,6 +374,7 @@ async function load() {
|
||||
moreOpen.value = false;
|
||||
filterOpen.value = false;
|
||||
aiSummary.value = null;
|
||||
tahesabMoshtariCode.value = null;
|
||||
try {
|
||||
const [personRes, actRes, noteRes, quoteRes, memRes, boardRes] = await Promise.all([
|
||||
api.get<PersonDto>(`/people/${id.value}`),
|
||||
@@ -407,6 +410,9 @@ async function load() {
|
||||
params: { personId: id.value, limit: 50 },
|
||||
}),
|
||||
api.get<ProjectBoardBoardDto>('/project-boards/board'),
|
||||
api.get<PersonAccountingLinkDto[]>(
|
||||
`/integrations/tahesab/people/${id.value}/links`,
|
||||
),
|
||||
]);
|
||||
invoices.value =
|
||||
extras[0].status === 'fulfilled' ? extras[0].value.data.items : [];
|
||||
@@ -418,6 +424,12 @@ async function load() {
|
||||
.flatMap((s) => s.cards)
|
||||
.filter((c) => c.person?.id === id.value)
|
||||
: [];
|
||||
if (extras[3].status === 'fulfilled' && extras[3].value.data.length) {
|
||||
const preferred =
|
||||
extras[3].value.data.find((l) => l.isDefaultConnection) ??
|
||||
extras[3].value.data[0];
|
||||
tahesabMoshtariCode.value = preferred?.moshtariCode ?? null;
|
||||
}
|
||||
} catch {
|
||||
error.value = 'بارگذاری پرونده مشتری ناموفق بود.';
|
||||
person.value = null;
|
||||
@@ -488,6 +500,10 @@ async function completeNext() {
|
||||
}
|
||||
}
|
||||
|
||||
function onTahesabLinkChanged(code: number | null) {
|
||||
tahesabMoshtariCode.value = code;
|
||||
}
|
||||
|
||||
async function downloadAttachment(noteId: string, att: NoteAttachmentDto) {
|
||||
const { data } = await api.get<Blob>(
|
||||
`/notes/${noteId}/attachments/${att.id}/download`,
|
||||
@@ -568,6 +584,9 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
||||
<div class="who">
|
||||
<h2>
|
||||
{{ fullName }}
|
||||
<span v-if="tahesabMoshtariCode != null" class="tahesab-code" dir="ltr">{{
|
||||
tahesabMoshtariCode
|
||||
}}</span>
|
||||
<span v-if="vip" class="vip-mark">VIP</span>
|
||||
</h2>
|
||||
<p v-if="subtitle" class="sub">{{ subtitle }}</p>
|
||||
@@ -601,7 +620,12 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
||||
<div class="mob-id">
|
||||
<CustomerMoodFace :mood="mood" :size="72" />
|
||||
<div class="who">
|
||||
<h2>{{ fullName }}</h2>
|
||||
<h2>
|
||||
{{ fullName }}
|
||||
<span v-if="tahesabMoshtariCode != null" class="tahesab-code" dir="ltr">{{
|
||||
tahesabMoshtariCode
|
||||
}}</span>
|
||||
</h2>
|
||||
<p v-if="subtitle" class="sub">{{ subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -812,7 +836,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
||||
</div>
|
||||
|
||||
<div v-show="tab === 'assets'" class="panel assets-panel">
|
||||
<PersonAssetsPanel :person-id="id" />
|
||||
<PersonAssetsPanel :person-id="id" @link-changed="onTahesabLinkChanged" />
|
||||
</div>
|
||||
|
||||
<div v-show="tab === 'info'" class="panel info-panel" :class="{ 'is-empty': infoEmpty }">
|
||||
@@ -1071,6 +1095,16 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
||||
padding: 0.12rem 0.45rem;
|
||||
}
|
||||
|
||||
.tahesab-code {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
background: color-mix(in srgb, #0f766e 12%, transparent);
|
||||
border-radius: 8px;
|
||||
padding: 0.1rem 0.45rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0.25rem 0 0.7rem;
|
||||
color: #64748b;
|
||||
|
||||
Reference in New Issue
Block a user