Files
tehran_taj/apps/user-app-v2/src/views/report/ReportDocument.vue
T
s.esmaeili f441ca8289 feat(user-apps): appearance settings (dark mode + reduced motion), product morph, filter sheet
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.
2026-10-04 16:18:56 +03:30

199 lines
5.2 KiB
Vue

<!--
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; 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">
/*
v2 styling. The template and script above are IDENTICAL to user-app's — only
this block differs. v2 draws the document as a flat, hairline-separated block
rather than an outlined card.
*/
.doc {
padding: 18px 2px;
background: transparent;
border: none;
border-bottom: 1px solid @hairline;
}
.doc:last-child {
border-bottom: none;
}
.doc__head {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
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;
}
.doc__row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed @hairlineSoft;
}
.doc__row:last-child {
border-bottom: none;
}
.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: 10px;
padding-top: 10px;
border-top: 1px dashed @hairline;
font-size: 13px;
font-weight: 700;
}
.doc__stack {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.is-in {
color: @green;
}
.is-out {
color: @red;
}
</style>