parity(apps+api): report + report/detail on the internal ledger [ref: henza_pwa views/report]
Ports "گزارش" end to end (D24), the last missing app page.
In the reference this feature computes nothing: Customer\TahesabController
proxies an external accounting system (hesab.henzagold.com and a bare IP at
31.7.68.147:3030) with TLS verification disabled, a bearer token hardcoded in
the source and a URL-embedded secret, keyed on Customer.mcode. The target has
no such system and no mcode, so per the owner's option الف this is built on our
own ledger with no external connection.
API — customer/me/report{,/detail,/balances,/:id}
- one row per CONFIRMED ledger entry with the running gold and rial balance
after it, newest first as the reference displayed it
- only confirmed entries count, the same rule the cached
Customer.goldDebtGrams/fiatDebtRial follow, so the statement's closing
balance agrees with the figure shown everywhere else in the app
- fields the target has no source for (foreign currency, نام کارساخته,
جنس فلز, the external system's internal ids) are null/zero and the UI OMITS
the row rather than rendering a fabricated value (REP-01)
SEC-16, NOT ported: the reference's detail($id) took the factor code from the
URL and performed no ownership check at all — it never consults auth() — so any
signed-in customer could read any other customer's document by guessing a code.
Here the id is only a filter on top of a customerId scope from the token.
Tested in both directions.
Apps (both, template+script byte-identical, per-app styling only)
- ReportView with the reference's two tabs, its card rows and its «جزئیات»
modal; ReportDocument for one document
- routes /report and /report/detail, the latter opening the second tab
- dashboard entry (v1 tile, v2 row), shown to every signed-in customer since
the reference's hasMCode() gate has no target equivalent
Verified: build 5/5 · jest 45/45 · report 40/40 (new) ·
complete-profile 38/38 · apps smoke 43/43 · typecheck clean on all four apps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5ad1083aee
commit
3f549f1749
@@ -228,6 +228,72 @@ export interface ExpertChildOrder {
|
||||
items: OrderItemDetail[];
|
||||
customer: { id: string; name: string; mobile: string } | null;
|
||||
}
|
||||
/**
|
||||
* D24 — "گزارش", the account statement.
|
||||
*
|
||||
* The field names are the reference's (henza_pwa/src/type/accounting.ts), so
|
||||
* the ported components bind exactly as they did. Money and weights travel as
|
||||
* decimal STRINGS, never as JS numbers, so no figure is rounded in transit.
|
||||
*/
|
||||
export interface ReportRow {
|
||||
id: number;
|
||||
/** The ledger entry id; what the detail call is keyed on. */
|
||||
factor_Code: string;
|
||||
sh_factor: string;
|
||||
tarikh: string;
|
||||
/** Gold in / out, as separate positive columns. */
|
||||
geram_v: string;
|
||||
geram_kh: string;
|
||||
rial_v: string;
|
||||
rial_kh: string;
|
||||
/** Running balances AFTER this row. Signed: negative means in credit. */
|
||||
tahesab_T: string;
|
||||
tahesab_P: string;
|
||||
/** No foreign-currency ledger in the target — always zero (REP-01). */
|
||||
rial_V2: string;
|
||||
rial_Kh2: string;
|
||||
tahesabP2: string;
|
||||
sourceType: string;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
export interface ReportPage {
|
||||
result: ReportRow[];
|
||||
count: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/** One document's detail. Fields the target cannot know are null, not invented. */
|
||||
export interface ReportDetailRow {
|
||||
factor_Code: string;
|
||||
sh_factor: string;
|
||||
tarikh: string;
|
||||
vazn: string;
|
||||
ayar: number | null;
|
||||
no: string;
|
||||
sh_sharti: string | null;
|
||||
name_az: string | null;
|
||||
tabdil750_v: string;
|
||||
tabdil750_kh: string;
|
||||
fi: string | null;
|
||||
fiF: string | null;
|
||||
Darsad_Talayee: string | null;
|
||||
accountName: string | null;
|
||||
bankName: string | null;
|
||||
transferTo: string | null;
|
||||
receivedAt: string | null;
|
||||
note: string | null;
|
||||
nO_kar: null;
|
||||
jens_felez: null;
|
||||
Darsad: null;
|
||||
maliatDarsad: null;
|
||||
}
|
||||
|
||||
export interface ReportBalance {
|
||||
arz: string;
|
||||
mandeh: string;
|
||||
}
|
||||
export const api = {
|
||||
listProducts: () => request<ProductSummary[]>('/catalog/products'),
|
||||
|
||||
@@ -328,4 +394,23 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
// ── report / report detail (D24) ─────────────────────────────────────
|
||||
// The reference proxied an external accounting system
|
||||
// (henza_server Customer/TahesabController). These read the target's own
|
||||
// ledger instead — no external call — and the customer comes from the token.
|
||||
getReport: (page = 1) =>
|
||||
request<ReportPage>(`/customer/me/report?page=${page}`),
|
||||
getReportDetailList: (page = 1) =>
|
||||
request<{ result: ReportDetailRow[]; page: number; limit: number }>(
|
||||
`/customer/me/report/detail?page=${page}`,
|
||||
),
|
||||
getReportBalances: () =>
|
||||
request<{ status: number; message: string; result: ReportBalance[] }>(
|
||||
'/customer/me/report/balances',
|
||||
),
|
||||
getReportDocument: (id: string) =>
|
||||
request<{ result: ReportDetailRow[]; page: number; limit: number }>(
|
||||
`/customer/me/report/${encodeURIComponent(id)}`,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -62,6 +62,21 @@ export const router = createRouter({
|
||||
component: () => import('../views/user/CompleteProfileView.vue'),
|
||||
meta: { title: 'تکمیل پروفایل', layout: 'FullPage', requiresAuth: true, requiresIncompleteProfile: true },
|
||||
},
|
||||
// D24 — the account statement. Reference paths and names kept
|
||||
// (henza_pwa router/index.ts:163-165, :191-193). The detail route opens
|
||||
// the same page on its second tab, as the reference did.
|
||||
{
|
||||
path: '/report',
|
||||
name: 'report',
|
||||
component: () => import('../views/report/ReportView.vue'),
|
||||
meta: { title: 'گزارش', layout: 'FullPage', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/report/detail',
|
||||
name: 'reportDetail',
|
||||
component: () => import('../views/report/ReportView.vue'),
|
||||
meta: { title: 'جزئیات حساب', layout: 'FullPage', requiresAuth: true },
|
||||
},
|
||||
{ path: '/notifications', name: 'notifications', component: () => import('../views/user/NotificationsView.vue') },
|
||||
{ path: '/search', name: 'search', component: () => import('../views/search/SearchView.vue') },
|
||||
{ path: '/collection/:id?', name: 'collection', component: () => import('../views/search/CollectionView.vue') },
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<!--
|
||||
One document of "جزئیات حساب".
|
||||
|
||||
Ported from henza_pwa/src/views/report/components/AccountingDetail.vue,
|
||||
restyled into this app's design language. The LOGIC is the reference's and is
|
||||
identical in user-app-v2; only the styling differs.
|
||||
|
||||
Kept from the reference: شماره سند + تاریخ, وزن خالص, عیار, نوع سند,
|
||||
شماره انگ, نام آزمایشگاه, قیمت, and the طلا in/out pair normalized to 750
|
||||
(AccountingDetail.vue:6-85).
|
||||
|
||||
Rows the reference showed that the target has no equivalent for — نام کارساخته
|
||||
(nO_kar), جنس فلز (jens_felez) and the two extra percentage lines — are
|
||||
OMITTED rather than rendered blank or zero. They came from an external
|
||||
accounting system the target does not have; the API returns null for them and
|
||||
this component skips a row whose value is null, so the card never asserts
|
||||
something it does not know. Recorded as REP-01.
|
||||
|
||||
Rows the target CAN fill and the reference could not are added at the end
|
||||
(bank/account/destination), since those live on our own ledger entry.
|
||||
-->
|
||||
<template>
|
||||
<article class="doc">
|
||||
<header class="doc__head">
|
||||
<span><span class="doc__muted">شماره سند:</span> {{ item.sh_factor }}</span>
|
||||
<span><span class="doc__muted">تاریخ:</span> {{ jdate(item.tarikh) }}</span>
|
||||
</header>
|
||||
|
||||
<dl class="doc__rows">
|
||||
<div class="doc__row">
|
||||
<dt>نوع سند</dt>
|
||||
<dd>{{ item.no }}</dd>
|
||||
</div>
|
||||
<div class="doc__row">
|
||||
<dt>وزن خالص</dt>
|
||||
<dd dir="ltr">{{ item.vazn }}</dd>
|
||||
</div>
|
||||
<div v-if="item.ayar !== null" class="doc__row">
|
||||
<dt>عیار</dt>
|
||||
<dd dir="ltr">{{ item.ayar }}</dd>
|
||||
</div>
|
||||
<div v-if="item.sh_sharti" class="doc__row">
|
||||
<dt>شماره انگ</dt>
|
||||
<dd dir="ltr">{{ item.sh_sharti }}</dd>
|
||||
</div>
|
||||
<div v-if="item.name_az" class="doc__row">
|
||||
<dt>نام آزمایشگاه</dt>
|
||||
<dd>{{ item.name_az }}</dd>
|
||||
</div>
|
||||
<div v-if="item.Darsad_Talayee" class="doc__row">
|
||||
<dt>اجرت (گرم)</dt>
|
||||
<dd dir="ltr">{{ item.Darsad_Talayee }}</dd>
|
||||
</div>
|
||||
<div v-if="item.fi" class="doc__row">
|
||||
<dt>قیمت هر گرم</dt>
|
||||
<dd dir="ltr">{{ rial(item.fi) }}</dd>
|
||||
</div>
|
||||
<div v-if="item.fiF" class="doc__row">
|
||||
<dt>مبلغ سند</dt>
|
||||
<dd dir="ltr">{{ rial(item.fiF) }}</dd>
|
||||
</div>
|
||||
<div v-if="item.bankName" class="doc__row">
|
||||
<dt>بانک</dt>
|
||||
<dd>{{ item.bankName }}</dd>
|
||||
</div>
|
||||
<div v-if="item.accountName" class="doc__row">
|
||||
<dt>نام حساب</dt>
|
||||
<dd>{{ item.accountName }}</dd>
|
||||
</div>
|
||||
<div v-if="item.transferTo" class="doc__row">
|
||||
<dt>مقصد حواله</dt>
|
||||
<dd>{{ item.transferTo }}</dd>
|
||||
</div>
|
||||
<div v-if="item.receivedAt" class="doc__row">
|
||||
<dt>تاریخ دریافت</dt>
|
||||
<dd>{{ jdate(item.receivedAt) }}</dd>
|
||||
</div>
|
||||
<div v-if="item.note" class="doc__row">
|
||||
<dt>توضیحات</dt>
|
||||
<dd>{{ item.note }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="doc__gold">
|
||||
<span>طلا</span>
|
||||
<div class="doc__stack">
|
||||
<span dir="ltr" class="is-in">{{ item.tabdil750_v }}</span>
|
||||
<span dir="ltr" class="is-out">{{ item.tabdil750_kh }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { jdate, price } from '../../utils/filters'
|
||||
import type { ReportDetailRow } from '../../api/client'
|
||||
|
||||
defineProps<{ item: ReportDetailRow }>()
|
||||
|
||||
/** Same rule as the statement: group the whole part only. */
|
||||
function rial(value: string): string {
|
||||
const n = parseFloat(value)
|
||||
if (!Number.isFinite(n)) return value
|
||||
const whole = Math.trunc(Math.abs(n)).toString()
|
||||
return (n < 0 ? '-' : '') + price(whole)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.doc {
|
||||
padding: 16px;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.doc__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.doc__muted {
|
||||
color: @inkMuted;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.doc__rows {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.doc__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: @canvasSoft;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.doc__row dt {
|
||||
font-size: 12.5px;
|
||||
color: @inkMuted;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.doc__row dd {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.doc__gold {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
background: @hairlineSoft;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.doc__stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.is-in {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.is-out {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,437 @@
|
||||
<!--
|
||||
"گزارش" — the customer's account statement.
|
||||
|
||||
Ported from henza_pwa/src/views/report/index.vue (route /report, name
|
||||
report) plus its two card components (components/Accounting.vue,
|
||||
components/AccountingDetail.vue), restyled into this app's design language.
|
||||
The LOGIC is the reference's and is identical in user-app-v2; only the
|
||||
styling differs.
|
||||
|
||||
Kept from the reference:
|
||||
- the title "گزارش" and the back arrow to the dashboard (index.vue:7-10)
|
||||
- the two tabs «کلیات حساب» / «جزئیات حساب» (:15-16), switching clears the
|
||||
list and re-queries from page 1 (:178-181)
|
||||
- the summary card's rows, in the reference's order: شماره سند + تاریخ,
|
||||
طلا (in/out), مانده طلا, ریال (in/out), مانده ریال (Accounting.vue:5-48)
|
||||
- balances coloured by sign — red when negative (Accounting.vue:29, :46)
|
||||
- the «جزئیات» link opening the document in a modal (:66, index.vue:44-52)
|
||||
- paging by scroll on the detail tab (:231-241)
|
||||
|
||||
Deliberately NOT ported: the ارز (foreign-currency) rows. The reference read
|
||||
them from an external accounting system; the target has no foreign-currency
|
||||
ledger, so showing a hardcoded "0 $" would assert something untrue. Recorded
|
||||
as REP-01.
|
||||
|
||||
The reference gated this page on `myStore.hasMCode()` (index.vue:174-177) —
|
||||
the customer's id in that external system. The target has no mcode, so the
|
||||
gate is simply a session, and the server scopes every row to the token.
|
||||
-->
|
||||
<template>
|
||||
<div class="rep">
|
||||
<PageBar title="گزارش" back-to="userPage::hasSubRoute">
|
||||
<template #below>
|
||||
<div class="rep__tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="rep__tab"
|
||||
:class="{ 'is-on': activeKey === '1' }"
|
||||
@click="changeTab('1')"
|
||||
>
|
||||
کلیات حساب
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rep__tab"
|
||||
:class="{ 'is-on': activeKey === '2' }"
|
||||
@click="changeTab('2')"
|
||||
>
|
||||
جزئیات حساب
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</PageBar>
|
||||
|
||||
<Spinner :spinning="loading && !rows.length && !details.length" tip="درحال دریافت اطلاعات">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<container class="rep__body">
|
||||
<!-- ── کلیات حساب ─────────────────────────────────────────── -->
|
||||
<template v-if="activeKey === '1'">
|
||||
<div v-if="rows.length" class="rep__list">
|
||||
<article v-for="item in rows" :key="item.factor_Code" class="rep__card">
|
||||
<header class="rep__head">
|
||||
<span><span class="rep__muted">شماره سند:</span> {{ item.sh_factor }}</span>
|
||||
<span><span class="rep__muted">تاریخ:</span> {{ jdate(item.tarikh) }}</span>
|
||||
</header>
|
||||
|
||||
<div class="rep__pair">
|
||||
<span>طلا</span>
|
||||
<div class="rep__stack">
|
||||
<span dir="ltr" class="is-in">{{ item.geram_v }}</span>
|
||||
<span dir="ltr" class="is-out">{{ item.geram_kh }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rep__pair is-balance">
|
||||
<span>مانده طلا:</span>
|
||||
<span dir="ltr" :class="sign(item.tahesab_T)">{{ item.tahesab_T }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rep__pair">
|
||||
<span>ریال</span>
|
||||
<div class="rep__stack">
|
||||
<span dir="ltr" class="is-in">{{ rial(item.rial_v) }}</span>
|
||||
<span dir="ltr" class="is-out">{{ rial(item.rial_kh) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rep__pair is-balance">
|
||||
<span>مانده ریال:</span>
|
||||
<span dir="ltr" :class="sign(item.tahesab_P)">{{ rial(item.tahesab_P) }}</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="rep__more" @click="openDetail(item)">جزئیات</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<section v-else-if="!loading && !failed" class="rep__empty">
|
||||
<Icon icon="vuesax-linear:receipt-text" size="26" />
|
||||
<p>هنوز سندی برای حساب شما ثبت نشده است.</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- ── جزئیات حساب ────────────────────────────────────────── -->
|
||||
<template v-else>
|
||||
<div v-if="details.length" class="rep__list">
|
||||
<ReportDocument v-for="d in details" :key="d.factor_Code" :item="d" />
|
||||
</div>
|
||||
|
||||
<section v-else-if="!loading && !failed" class="rep__empty">
|
||||
<Icon icon="vuesax-linear:receipt-text" size="26" />
|
||||
<p>هنوز سندی برای حساب شما ثبت نشده است.</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<section v-if="failed" class="rep__empty">
|
||||
<Icon icon="vuesax-linear:danger" size="26" />
|
||||
<p>دریافت اطلاعات با خطا روبهرو شد.</p>
|
||||
<button type="button" class="rep__retry" @click="reload">تلاش دوباره</button>
|
||||
</section>
|
||||
|
||||
<div v-if="loading && (rows.length || details.length)" class="rep__more">در حال دریافت…</div>
|
||||
<div ref="sentinel" class="rep__sentinel" />
|
||||
</container>
|
||||
</PullRefresh>
|
||||
</Spinner>
|
||||
|
||||
<Drawer
|
||||
v-model:open="modalOpen"
|
||||
placement="bottom"
|
||||
height="auto"
|
||||
:closable="false"
|
||||
:body-style="{ padding: '0' }"
|
||||
>
|
||||
<div class="rep__modal">
|
||||
<Spinner :spinning="modalLoading" tip="درحال دریافت اطلاعات">
|
||||
<ReportDocument v-for="d in modalRows" :key="d.factor_Code" :item="d" />
|
||||
<p v-if="!modalLoading && !modalRows.length" class="rep__modalEmpty">
|
||||
جزئیاتی برای این سند ثبت نشده است.
|
||||
</p>
|
||||
</Spinner>
|
||||
<button type="button" class="rep__close" @click="modalOpen = false">بازگشت</button>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import ReportDocument from './ReportDocument.vue'
|
||||
import { jdate, price } from '../../utils/filters'
|
||||
import { api, type ReportRow, type ReportDetailRow } from '../../api/client'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
/**
|
||||
* The reference had /report and /report/detail as two routes. The detail one
|
||||
* lands on the same screen with the second tab already selected, which is what
|
||||
* its 'جزئیات حساب' tab showed.
|
||||
*/
|
||||
const activeKey = ref<'1' | '2'>(route.name === 'reportDetail' ? '2' : '1')
|
||||
|
||||
const rows = ref<ReportRow[]>([])
|
||||
const details = ref<ReportDetailRow[]>([])
|
||||
const page = ref(1)
|
||||
const hasMore = ref(true)
|
||||
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const failed = ref(false)
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const modalRows = ref<ReportDetailRow[]>([])
|
||||
|
||||
/**
|
||||
* price() groups digits from the right, so a decimal tail would be grouped
|
||||
* too. Rial figures are whole here, so the .00 is dropped before grouping and
|
||||
* a negative sign is preserved outside the grouping.
|
||||
*/
|
||||
function rial(value: string): string {
|
||||
const n = parseFloat(value)
|
||||
if (!Number.isFinite(n)) return value
|
||||
const whole = Math.trunc(Math.abs(n)).toString()
|
||||
return (n < 0 ? '-' : '') + price(whole)
|
||||
}
|
||||
|
||||
/** The reference coloured a balance by its sign (Accounting.vue:29). */
|
||||
function sign(value: string): string {
|
||||
return parseFloat(value) < 0 ? 'is-out' : 'is-in'
|
||||
}
|
||||
|
||||
async function fetchPage() {
|
||||
if (loading.value || !hasMore.value) return
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
if (activeKey.value === '1') {
|
||||
const data = await api.getReport(page.value)
|
||||
rows.value = [...rows.value, ...(data.result ?? [])]
|
||||
if ((data.result?.length ?? 0) < (data.limit ?? 50)) hasMore.value = false
|
||||
else page.value += 1
|
||||
} else {
|
||||
const data = await api.getReportDetailList(page.value)
|
||||
details.value = [...details.value, ...(data.result ?? [])]
|
||||
if ((data.result?.length ?? 0) < (data.limit ?? 50)) hasMore.value = false
|
||||
else page.value += 1
|
||||
}
|
||||
} catch {
|
||||
failed.value = true
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
rows.value = []
|
||||
details.value = []
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
failed.value = false
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
reset()
|
||||
await fetchPage()
|
||||
}
|
||||
|
||||
/** The reference cleared the list and re-queried on tab change (index.vue:178). */
|
||||
async function changeTab(key: '1' | '2') {
|
||||
if (activeKey.value === key) return
|
||||
activeKey.value = key
|
||||
await reload()
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
await reload()
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
/** The «جزئیات» link — the reference opened the same card in a modal. */
|
||||
async function openDetail(item: ReportRow) {
|
||||
modalOpen.value = true
|
||||
modalLoading.value = true
|
||||
modalRows.value = []
|
||||
try {
|
||||
const data = await api.getReportDocument(item.factor_Code)
|
||||
modalRows.value = data.result ?? []
|
||||
} catch {
|
||||
modalRows.value = []
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// The reference paged by scroll on the detail tab (index.vue:231-241).
|
||||
const sentinel = ref<HTMLElement | null>(null)
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchPage()
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) void fetchPage()
|
||||
})
|
||||
if (sentinel.value) observer.observe(sentinel.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => observer?.disconnect())
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rep__tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.rep__tab {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 999px;
|
||||
background: @canvas;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: @inkMuted;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rep__tab.is-on {
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rep__body {
|
||||
padding-top: 132px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.rep__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 16px;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.rep__card {
|
||||
padding: 16px;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.rep__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rep__muted {
|
||||
color: @inkMuted;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.rep__pair {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 9px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: @canvasSoft;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rep__pair.is-balance {
|
||||
background: @hairlineSoft;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rep__stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.is-in {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.is-out {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.rep__more,
|
||||
.rep__sentinel {
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.rep__more {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rep__empty {
|
||||
margin-top: 8px;
|
||||
padding: 34px 20px;
|
||||
border: 1px dashed @hairline;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: @inkMuted;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rep__retry {
|
||||
padding: 8px 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid @hairline;
|
||||
background: @canvas;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rep__modal {
|
||||
padding: 20px 16px 24px;
|
||||
}
|
||||
|
||||
.rep__modalEmpty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.rep__close {
|
||||
margin-top: 18px;
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -186,6 +186,10 @@ const BASE_TILES = [
|
||||
{ id: 1, icon: 'vuesax-linear:receipt-text', title: 'اسناد', disabled: false },
|
||||
{ id: 2, icon: 'vuesax-linear:user', title: 'پروفایل', disabled: false },
|
||||
{ id: 3, icon: 'vuesax-linear:heart', title: 'علاقمندی', disabled: false },
|
||||
// D24 — the account statement. The reference put it on the dashboard too,
|
||||
// gated on hasMCode() (Dashboard.vue:179, :449). The target has no mcode, so
|
||||
// every signed-in customer sees it and the server scopes it to their token.
|
||||
{ id: 6, icon: 'vuesax-linear:chart-2', title: 'گزارش', disabled: false },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -214,6 +218,7 @@ function goToDetails(id: number) {
|
||||
if (id === 3) router.push({ name: 'favoritesList' })
|
||||
if (id === 4) router.push({ name: 'ChildOrder' })
|
||||
if (id === 5) router.push({ name: 'customerChoice' })
|
||||
if (id === 6) router.push({ name: 'report' })
|
||||
}
|
||||
|
||||
function logOut() {
|
||||
|
||||
Reference in New Issue
Block a user