Applies to both user-app and user-app-v2. Logic and structure are shared; each app keeps its own look. Appearance settings - New "تنظیمات ظاهری" page (/settings/appearance), opened from the dashboard and available without login. It offers a light/dark/system theme and a "fast & light" motion switch. - appearanceStore saves the choice to localStorage. A small inline boot script in index.html applies it before first paint so there is no white flash. The "system" option follows OS changes live. - New ToggleSwitch component. Dark mode - Theme LESS tokens now compile to CSS custom properties. Light/dark values come from a generated virtual:palette.css (themeConfig/ appearance.ts, with colors.json + new colors.dark.json). - New LESS alpha() plugin replaces fade() on theme tokens. - New semantic tokens: page, onInk, heroSurface, hairlineStrong, photoMat, photoStage. - The dark palette is layered (page < card < panel < border) so surfaces separate without shadows. - Hard-coded colors were replaced by tokens according to their role. - antd inputs/text get dark overrides; vant uses its own .van-theme-dark. - Light mode looks the same as before. Reduced motion - With html.motion-reduced, transitions are capped at 120ms, keyframe loops stop, and backdrop blurs and permanent will-change are dropped. - Spinners and the toast progress bar (which closes the toast) are exempt via .keep-motion. - Carousel autoplay, the v1 Lottie and the product morph honor the setting. Product detail - Opening a product is now a container-transform morph from the tapped card (ProductMorph). It replaces the bottom Drawer and uses composited transform/opacity only. - The detail view is seeded with the card's data, so it no longer opens blank and jumps. Filters and sheets - Filters are now a full-width bottom sheet. Selections stay a draft until applied, so the result list no longer re-renders while dragging the slider. - DropDown collapse animates grid-template-rows instead of a capped max-height. - v2 IslandSheet is restyled to the v2 design language (API unchanged). Fixes - filtersStore.reset() shallow-copied shared arrays, so clearing the filters never cleared the selected brands.
343 lines
8.2 KiB
Vue
343 lines
8.2 KiB
Vue
<!--
|
|
Ported from legacy henza_pwa/src/views/user/src/components/transactions.vue
|
|
("اسناد" — financial documents), restyled into the redesigned design
|
|
language (see components/home/* and HomeView).
|
|
|
|
Data source: legacy listed `Cash` rows from POST /get-my-findoc. The new
|
|
system's equivalent is LedgerEntry, already exposed by
|
|
GET /customers/:id/ledger-entries, so the list is real data.
|
|
|
|
Legacy's colour/icon logic is preserved in meaning: `hasFactor(item)` was
|
|
`item.type != 9` (type 9 = سفارش, the debt-creating order entry) → a green
|
|
"in" treatment for everything else, red "out" for orders. That maps 1:1 onto
|
|
our CREDIT/DEBIT direction, since an ORDER_STOCK entry is exactly the DEBIT
|
|
case. The palette is now the design system's own rather than hardcoded hexes.
|
|
|
|
DROPPED, unchanged from the previous version: the "دریافت فایل" per-document
|
|
download — legacy's getFile() hit the old Laravel host; there is no
|
|
document-file endpoint in the new API.
|
|
-->
|
|
<template>
|
|
<div class="tx">
|
|
<PageBar title="اسناد" back-to="userPage::hasSubRoute" />
|
|
|
|
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات">
|
|
<container class="tx__body">
|
|
<template v-if="entries.length">
|
|
<SectionHead title="گردش حساب" :subtitle="`${entries.length} سند`" />
|
|
|
|
<div class="tx__list">
|
|
<article v-for="item in entries" :key="item.id" class="tx__row" @click="openEntry(item)">
|
|
<span class="tx__mark" :class="isCredit(item) ? 'is-in' : 'is-out'" aria-hidden="true">
|
|
<Icon :icon="isCredit(item) ? 'vuesax-linear:arrow-down' : 'vuesax-linear:arrow-up'" size="16" />
|
|
</span>
|
|
|
|
<div class="tx__main">
|
|
<div class="tx__head">
|
|
<h3 class="tx__kind">{{ SOURCE_LABELS[item.sourceType] ?? item.sourceType }}</h3>
|
|
<span class="tx__amount" :class="isCredit(item) ? 'is-in' : 'is-out'">
|
|
{{ formatAmount(item) }}
|
|
</span>
|
|
</div>
|
|
|
|
<p v-if="item.note" class="tx__note">{{ item.note }}</p>
|
|
|
|
<div class="tx__foot">
|
|
<span class="tx__date">{{ jdate(item.createdAt) }}</span>
|
|
<span class="tx__state" :class="{ 'is-pending': !item.confirmed }">
|
|
{{ item.confirmed ? 'تایید شده' : 'در انتظار تایید' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</template>
|
|
|
|
<section v-else-if="!loading" class="tx__empty">
|
|
<Icon icon="vuesax-linear:receipt-text" size="26" />
|
|
<p>سندی برای حساب شما ثبت نشده است.</p>
|
|
</section>
|
|
</container>
|
|
</Spinner>
|
|
|
|
<Drawer
|
|
v-model:visible="showPopUp"
|
|
placement="bottom"
|
|
v-if="activeEntry"
|
|
:closable="false"
|
|
height="420px"
|
|
class="rtl"
|
|
:bodyStyle="{ padding: 0 }"
|
|
>
|
|
<div class="sheet">
|
|
<h3 class="sheet__title">جزئیات سند</h3>
|
|
<dl class="sheet__specs">
|
|
<div class="sheet__spec">
|
|
<dt>نوع سند</dt>
|
|
<dd>{{ SOURCE_LABELS[activeEntry.sourceType] ?? activeEntry.sourceType }}</dd>
|
|
</div>
|
|
<div class="sheet__spec">
|
|
<dt>مبلغ</dt>
|
|
<dd :class="isCredit(activeEntry) ? 'is-in' : 'is-out'">{{ formatAmount(activeEntry) }}</dd>
|
|
</div>
|
|
<div class="sheet__spec">
|
|
<dt>وضعیت</dt>
|
|
<dd>{{ activeEntry.confirmed ? 'تایید شده' : 'در انتظار تایید' }}</dd>
|
|
</div>
|
|
<div class="sheet__spec">
|
|
<dt>تاریخ</dt>
|
|
<dd>{{ jdate(activeEntry.createdAt) }}</dd>
|
|
</div>
|
|
<div class="sheet__spec">
|
|
<dt>توضیحات</dt>
|
|
<dd>{{ activeEntry.note || '—' }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</Drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { Drawer } from 'ant-design-vue'
|
|
import Icon from '../../components/icon/Icon.vue'
|
|
import PageBar from '../../components/ui/PageBar.vue'
|
|
import Spinner from '../../components/Spinner.vue'
|
|
import SectionHead from '../../components/home/SectionHead.vue'
|
|
import { jdate, price } from '../../utils/filters'
|
|
import { useSessionStore } from '../../stores/session'
|
|
import { api, type LedgerEntrySummary } from '../../api/client'
|
|
|
|
const SOURCE_LABELS: Record<string, string> = {
|
|
ORDER_STOCK: 'سفارش',
|
|
ORDER_PRE_ORDER: 'سفارش ساخت',
|
|
REPAYMENT_MELTED_GOLD: 'طلای آبشده',
|
|
REPAYMENT_TRANSFER: 'حواله',
|
|
REPAYMENT_CHEQUE: 'چک',
|
|
REPAYMENT_CASH: 'پولی',
|
|
WALLET_BONUS: 'پاداش',
|
|
ADJUSTMENT: 'اصلاحیه',
|
|
}
|
|
|
|
const session = useSessionStore()
|
|
|
|
const loading = ref(false)
|
|
const entries = ref<LedgerEntrySummary[]>([])
|
|
const showPopUp = ref(false)
|
|
const activeEntry = ref<LedgerEntrySummary | null>(null)
|
|
|
|
// legacy hasFactor(item) === item.type != 9 (9 = سفارش/order). Our DEBIT
|
|
// entries are exactly that case.
|
|
const isCredit = (item: LedgerEntrySummary) => item.direction === 'CREDIT'
|
|
|
|
const formatAmount = (item: LedgerEntrySummary) =>
|
|
item.currency === 'GOLD_GRAM' ? `${item.amount} گرم` : `${price(item.amount)} ریال`
|
|
|
|
const openEntry = (item: LedgerEntrySummary) => {
|
|
activeEntry.value = item
|
|
showPopUp.value = true
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!session.isLoggedIn) return
|
|
loading.value = true
|
|
try {
|
|
entries.value = await api.getMyLedgerEntries()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.tx {
|
|
background: @page;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.tx__body {
|
|
padding-top: 76px;
|
|
padding-bottom: 90px;
|
|
}
|
|
|
|
.tx__list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.tx__row {
|
|
display: flex;
|
|
gap: 11px;
|
|
padding: 12px;
|
|
cursor: pointer;
|
|
background: @canvas;
|
|
border: 1px solid @hairline;
|
|
border-radius: 18px;
|
|
transition: transform 0.18s ease;
|
|
}
|
|
|
|
.tx__row:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.tx__mark {
|
|
flex: 0 0 36px;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.tx__mark.is-in {
|
|
color: @green;
|
|
background: alpha(@green, 10%);
|
|
}
|
|
|
|
.tx__mark.is-out {
|
|
color: @wine;
|
|
background: @wineWash;
|
|
}
|
|
|
|
.tx__main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.tx__head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.tx__kind {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: @ink;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.tx__amount {
|
|
font-size: 12.5px;
|
|
font-weight: 800;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tx__amount.is-in {
|
|
color: @green;
|
|
}
|
|
|
|
.tx__amount.is-out {
|
|
color: @wine;
|
|
}
|
|
|
|
.tx__note {
|
|
margin: 6px 0 0;
|
|
font-size: 11.5px;
|
|
line-height: 1.7;
|
|
color: @inkMuted;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tx__foot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.tx__date {
|
|
font-size: 11px;
|
|
color: @inkFaint;
|
|
}
|
|
|
|
.tx__state {
|
|
font-size: 10.5px;
|
|
font-weight: 700;
|
|
color: @inkMuted;
|
|
}
|
|
|
|
.tx__state.is-pending {
|
|
color: @notify2;
|
|
}
|
|
|
|
.tx__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: @inkFaint;
|
|
}
|
|
.tx__empty p {
|
|
margin: 0;
|
|
font-size: 12.5px;
|
|
}
|
|
|
|
.sheet {
|
|
padding: 18px 16px 22px;
|
|
}
|
|
|
|
.sheet__title {
|
|
margin: 0 0 14px;
|
|
font-size: 13.5px;
|
|
font-weight: 800;
|
|
color: @ink;
|
|
}
|
|
|
|
.sheet__specs {
|
|
margin: 0;
|
|
border: 1px solid @hairline;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sheet__spec {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid @hairlineSoft;
|
|
}
|
|
|
|
.sheet__spec:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.sheet__spec dt {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: @inkFaint;
|
|
}
|
|
|
|
.sheet__spec dd {
|
|
margin: 0;
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
color: @ink;
|
|
text-align: left;
|
|
}
|
|
|
|
.sheet__spec dd.is-in {
|
|
color: @green;
|
|
}
|
|
|
|
.sheet__spec dd.is-out {
|
|
color: @wine;
|
|
}
|
|
</style>
|