2026-09-18 12:54:45 +03:30
|
|
|
|
<!--
|
|
|
|
|
|
Ported from legacy henza_pwa/src/views/user/src/components/Dashboard.vue.
|
|
|
|
|
|
|
|
|
|
|
|
Legacy's original (820 lines) is a multi-role workbench mixing
|
|
|
|
|
|
plain-customer, sales-agent ("reagent") and "banker" (type=1, sub-account
|
|
|
|
|
|
switching) views. Kept: the real balance card, the nav grid to
|
|
|
|
|
|
Orders/Documents/Profile/Favorites, and the logout/login-nudge dialogs —
|
|
|
|
|
|
all backed by real data.
|
|
|
|
|
|
|
|
|
|
|
|
DROPPED as architecturally incompatible, not merely deferred: everything
|
|
|
|
|
|
tied to legacy's `Customer.type`/multi-account model — "انتخاب مشتری"
|
|
|
|
|
|
(toggleUser/hasActiveChild), "سفارش مشتریان" (ChildOrders) and
|
|
|
|
|
|
customerChoice. Sales agents are AdminUser records with no customer-facing
|
|
|
|
|
|
session at all, and there is no `type` field on our Customer model, so the
|
|
|
|
|
|
data model this UI assumed no longer exists.
|
|
|
|
|
|
|
|
|
|
|
|
DROPPED as no backend exists: "سفارش ساخت" (custom/pre-order manufacturing)
|
|
|
|
|
|
— PreOrder has a DB table but no controller anywhere in apps/api.
|
|
|
|
|
|
|
|
|
|
|
|
REPLACED, not dropped: legacy's "گزارش حساب" called out to the external
|
|
|
|
|
|
Tahesab accounting service, which the first migration audit retired in
|
|
|
|
|
|
favour of this system's own internal ledger — the balance card below shows
|
|
|
|
|
|
that real internal position directly.
|
|
|
|
|
|
|
|
|
|
|
|
DROPPED: the multi-row world gold/currency ticker (legacy's GET /price, ~14
|
|
|
|
|
|
rows). The new API has one single-karat quote endpoint, folded into the
|
|
|
|
|
|
bottom rate row rather than fabricating rows that don't exist.
|
|
|
|
|
|
|
|
|
|
|
|
Second visual pass — a deliberately different structure from the previous
|
|
|
|
|
|
"card + 4-cell grid" layout, while staying in the same visual language as
|
|
|
|
|
|
HomeView (same bar treatment, same reveal choreography, same @ink/@wine
|
|
|
|
|
|
palette — no gold/bronze; this app's identity is black + burgundy only):
|
|
|
|
|
|
|
|
|
|
|
|
1. a bare, logo-height bar (no title chrome), matching Home's bar rhythm
|
|
|
|
|
|
2. one full hero card: balance figure + identity, with quick actions
|
|
|
|
|
|
built INTO the card itself as an icon row on its own dark surface —
|
|
|
|
|
|
replacing the old "card, then a separate white 4-cell grid below it"
|
|
|
|
|
|
split
|
|
|
|
|
|
3. "اسناد" / "علاقمندی" as wide settings-style rows with a chevron,
|
|
|
|
|
|
replacing the grid tiles those two also occupied
|
|
|
|
|
|
4. the live rate folded into a single slim inline row at the foot of the
|
|
|
|
|
|
page instead of a titled section
|
|
|
|
|
|
|
|
|
|
|
|
Data, routes, dialogs and auth behaviour are all unchanged from before —
|
|
|
|
|
|
only markup structure and styling moved.
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="dash">
|
|
|
|
|
|
<header class="dash__bar">
|
|
|
|
|
|
<div class="dash__barInner">
|
|
|
|
|
|
<span class="dash__brand">میز کار</span>
|
|
|
|
|
|
<button
|
2026-09-22 01:20:00 +03:30
|
|
|
|
v-if="session.isLoggedIn"
|
2026-09-18 12:54:45 +03:30
|
|
|
|
type="button"
|
|
|
|
|
|
class="dash__exit"
|
|
|
|
|
|
aria-label="خروج"
|
|
|
|
|
|
@click="data.notify = true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon icon="vuesax-linear:logout" size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
|
|
|
|
|
<template #pulling><Loading size="18" /></template>
|
|
|
|
|
|
<template #loosing><Loading size="18" /></template>
|
|
|
|
|
|
<template #loading><Loading size="18" /></template>
|
|
|
|
|
|
<template #success><span /></template>
|
|
|
|
|
|
|
|
|
|
|
|
<container class="dash__body" :class="{ 'is-revealed': revealed }">
|
|
|
|
|
|
<!-- Signed in: real position from GET /customers/:id, quick actions
|
|
|
|
|
|
built into the same dark surface as the balance itself. -->
|
|
|
|
|
|
<section v-if="customer" class="dash__hero dash__reveal" :style="revealDelay(0)">
|
|
|
|
|
|
<div class="dash__heroTop">
|
|
|
|
|
|
<div class="dash__heroIdentity">
|
|
|
|
|
|
<span class="dash__heroName">{{ customer.fullName }}</span>
|
|
|
|
|
|
<span class="dash__heroMobile">{{ session.mobile }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="dash__heroSeal" :class="isDebtor ? 'is-debtor' : 'is-creditor'">
|
|
|
|
|
|
{{ isDebtor ? 'بدهکار' : 'بستانکار' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="dash__heroFigure">
|
|
|
|
|
|
<span class="dash__heroNum">{{ grams }}</span>
|
|
|
|
|
|
<span class="dash__heroUnit">گرم {{ remainText }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="dash__heroDebt">
|
|
|
|
|
|
<span>بدهی ریالی</span>
|
|
|
|
|
|
<b>{{ price(customer.fiatDebtRial) }} <i>ریال</i></b>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Signed out: a quiet invitation, no fabricated numbers -->
|
|
|
|
|
|
<section v-else class="dash__invite dash__reveal" :style="revealDelay(0)">
|
|
|
|
|
|
<div class="dash__inviteIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:security-user" size="22" color="#fff" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3>حساب گرمی شما</h3>
|
|
|
|
|
|
<p>برای دیدن موجودی، سفارشها و اسناد خود وارد شوید.</p>
|
|
|
|
|
|
<button type="button" class="dash__inviteCta" @click="router.push({ name: 'login' })">
|
|
|
|
|
|
ورود / ثبتنام
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Settings-style rows: every destination the hero no longer carries
|
|
|
|
|
|
as its own quick-action row. -->
|
|
|
|
|
|
<section class="dash__rows dash__reveal" :style="revealDelay(1)">
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(0)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:box" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>سفارشات</b>
|
|
|
|
|
|
<small>پیگیری سفارشهای شما</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(1)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:receipt-text" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>اسناد</b>
|
|
|
|
|
|
<small>تراکنشها و رسیدهای حساب</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(2)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:user" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>پروفایل</b>
|
|
|
|
|
|
<small>اطلاعات و تنظیمات حساب</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(3)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:heart" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>علاقمندی</b>
|
|
|
|
|
|
<small>محصولات ذخیرهشده شما</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</button>
|
2026-09-23 01:49:49 +03:30
|
|
|
|
<!--
|
|
|
|
|
|
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.
|
|
|
|
|
|
-->
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(6)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:chart-2" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>گزارش</b>
|
|
|
|
|
|
<small>گردش حساب طلایی و ریالی شما</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Appearance (theme + reduced motion). Device-level preferences, not
|
|
|
|
|
|
account data, so — unlike every row above — it needs no login. -->
|
|
|
|
|
|
<button type="button" class="dash__row" @click="router.push({ name: 'appearanceSettings' })">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:brush" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>تنظیمات ظاهری</b>
|
|
|
|
|
|
<small>پوسته روشن و تیره، انیمیشنها</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-23 01:49:49 +03:30
|
|
|
|
</button>
|
2026-09-23 01:12:53 +03:30
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
|
D21 — the two sales-expert entries, rendered only for an expert, in
|
|
|
|
|
|
v2's own row style. The reference showed them the same way, gated on
|
|
|
|
|
|
the agent's own account (Dashboard.vue:117-125). An ordinary customer
|
|
|
|
|
|
never sees them, and the server refuses both routes for a non-expert
|
|
|
|
|
|
token regardless of what the app renders.
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template v-if="session.isSalesExpert">
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(4)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:box-tick" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>سفارش مشتریان</b>
|
|
|
|
|
|
<small>سفارشهای مشتریان معرفیشده شما</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-23 01:12:53 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="dash__row" @click="goToDetails(5)">
|
|
|
|
|
|
<span class="dash__rowIcon" aria-hidden="true">
|
|
|
|
|
|
<Icon icon="vuesax-linear:profile-2user" size="18" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="dash__rowText">
|
|
|
|
|
|
<b>مشتریان من</b>
|
|
|
|
|
|
<small>فهرست مشتریان معرفیشده شما</small>
|
|
|
|
|
|
</span>
|
2026-10-04 16:18:56 +03:30
|
|
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
2026-09-23 01:12:53 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Live 18k rate — a single inline row, not a titled section. -->
|
|
|
|
|
|
<RateStrip :quote="goldPrice" class="dash__rate dash__reveal" :style="revealDelay(2)" />
|
|
|
|
|
|
</container>
|
|
|
|
|
|
</PullRefresh>
|
|
|
|
|
|
|
2026-10-04 13:57:25 +03:30
|
|
|
|
<IslandSheet
|
|
|
|
|
|
v-model:show="data.notify"
|
|
|
|
|
|
art="logout"
|
|
|
|
|
|
title="آیا می خواهید از برنامه خارج شوید؟"
|
|
|
|
|
|
description="برای استفاده دوباره از امکانات منتزر، باید دوباره وارد حساب کاربری خود شوید."
|
|
|
|
|
|
confirm-text="بله"
|
|
|
|
|
|
cancel-text="بی خیال"
|
|
|
|
|
|
@confirm="logOut"
|
|
|
|
|
|
/>
|
2026-09-18 12:54:45 +03:30
|
|
|
|
|
2026-10-04 13:57:25 +03:30
|
|
|
|
<IslandSheet
|
|
|
|
|
|
v-model:show="data.loginNotify"
|
|
|
|
|
|
art="login"
|
|
|
|
|
|
title="ابتدا وارد حساب کاربری شوید"
|
|
|
|
|
|
description="برای دسترسی به تمامی امکانات نرم افزار منتزر، گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید."
|
|
|
|
|
|
confirm-text="ورود/ثبت نام"
|
|
|
|
|
|
cancel-text="بیخیال"
|
|
|
|
|
|
@confirm="router.push({ name: 'login' })"
|
|
|
|
|
|
/>
|
2026-09-18 12:54:45 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-10-11 14:31:32 +03:30
|
|
|
|
import { computed, nextTick, onActivated, onMounted, reactive, ref } from 'vue'
|
2026-09-18 12:54:45 +03:30
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
import { Loading, PullRefresh } from 'vant'
|
|
|
|
|
|
import Icon from '../../components/icon/Icon.vue'
|
|
|
|
|
|
import RateStrip from '../../components/home/RateStrip.vue'
|
2026-10-04 13:57:25 +03:30
|
|
|
|
import IslandSheet from '../../components/ui/IslandSheet.vue'
|
2026-09-18 12:54:45 +03:30
|
|
|
|
import { price } from '../../utils/filters'
|
|
|
|
|
|
import { useSessionStore } from '../../stores/session'
|
|
|
|
|
|
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
|
2026-10-11 14:31:32 +03:30
|
|
|
|
import { useCachedReads } from '../../api/cachedReads'
|
|
|
|
|
|
import { KEYS } from '../../stores/queryCache'
|
2026-09-18 12:54:45 +03:30
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const session = useSessionStore()
|
2026-10-11 14:31:32 +03:30
|
|
|
|
const reads = useCachedReads()
|
2026-09-18 12:54:45 +03:30
|
|
|
|
|
2026-10-11 14:31:32 +03:30
|
|
|
|
const customer = ref<CustomerDetail | null>(reads.peek<CustomerDetail>(KEYS.profile) ?? null)
|
|
|
|
|
|
const goldPrice = ref<GoldPriceQuote | null>(reads.peek<GoldPriceQuote>(KEYS.goldPrice) ?? null)
|
2026-09-18 12:54:45 +03:30
|
|
|
|
const refreshing = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// The ledger stores the gram position as a signed figure — positive means the
|
|
|
|
|
|
// customer owes gold. Labelled the same way AccountStrip does on the home
|
|
|
|
|
|
// screen so the two never disagree.
|
|
|
|
|
|
const isDebtor = computed(() => (customer.value ? parseFloat(customer.value.goldDebtGrams) > 0 : false))
|
|
|
|
|
|
const remainText = computed(() => (isDebtor.value ? 'بدهکار' : 'بستانکار'))
|
|
|
|
|
|
const grams = computed(() =>
|
|
|
|
|
|
customer.value ? Math.abs(parseFloat(customer.value.goldDebtGrams)) : 0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const data = reactive({ notify: false, loginNotify: false })
|
|
|
|
|
|
|
|
|
|
|
|
function goToDetails(id: number) {
|
2026-09-22 01:20:00 +03:30
|
|
|
|
if (!session.isLoggedIn) {
|
2026-09-18 12:54:45 +03:30
|
|
|
|
data.loginNotify = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (id === 0) router.push({ name: 'ordersList' })
|
|
|
|
|
|
if (id === 1) router.push({ name: 'transactionsList' })
|
|
|
|
|
|
if (id === 2) router.push({ name: 'profileData' })
|
|
|
|
|
|
if (id === 3) router.push({ name: 'favoritesList' })
|
2026-09-23 01:12:53 +03:30
|
|
|
|
if (id === 4) router.push({ name: 'ChildOrder' })
|
|
|
|
|
|
if (id === 5) router.push({ name: 'customerChoice' })
|
2026-09-23 01:49:49 +03:30
|
|
|
|
if (id === 6) router.push({ name: 'report' })
|
2026-09-18 12:54:45 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 16:45:36 +03:30
|
|
|
|
async function logOut() {
|
2026-09-18 12:54:45 +03:30
|
|
|
|
data.notify = false
|
2026-10-11 16:01:15 +03:30
|
|
|
|
try {
|
|
|
|
|
|
await api.endSharedSession()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// خروج محلی ادامه پیدا میکند
|
|
|
|
|
|
}
|
2026-10-07 16:45:36 +03:30
|
|
|
|
let central: string | null = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
central = (await api.ssoLogoutUrl()).logout_url
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
central = null
|
|
|
|
|
|
}
|
|
|
|
|
|
session.logout()
|
|
|
|
|
|
sessionStorage.removeItem('gb_iam_sso_checked')
|
|
|
|
|
|
if (central) {
|
|
|
|
|
|
window.location.assign(central)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-09-18 12:54:45 +03:30
|
|
|
|
router.push({ name: 'home' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 14:31:32 +03:30
|
|
|
|
async function loadDashboard(force = false) {
|
|
|
|
|
|
// Not wrapped before: a failure here (expired token, network blip) left
|
|
|
|
|
|
// the exception uncaught, so onRefresh's refreshing.value = false never
|
|
|
|
|
|
// ran and PullRefresh stayed stuck in its loading state — a working pull
|
|
|
|
|
|
// gesture never dismissed it again until a full page reload.
|
|
|
|
|
|
const [me, price] = await Promise.allSettled([
|
|
|
|
|
|
// The balance on this tab is the one figure read fresh on every visit;
|
|
|
|
|
|
// the cached copy only fills the screen until it arrives.
|
|
|
|
|
|
session.isLoggedIn ? reads.me({ force: true }) : Promise.resolve(null),
|
|
|
|
|
|
reads.goldPrice({ force }),
|
|
|
|
|
|
])
|
|
|
|
|
|
if (me.status === 'fulfilled') customer.value = me.value
|
|
|
|
|
|
else console.log(me.reason)
|
|
|
|
|
|
goldPrice.value = price.status === 'fulfilled' ? price.value : null
|
2026-09-18 12:54:45 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onRefresh() {
|
2026-10-11 14:31:32 +03:30
|
|
|
|
await loadDashboard(true)
|
2026-09-18 12:54:45 +03:30
|
|
|
|
refreshing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 14:31:32 +03:30
|
|
|
|
let activatedOnce = false
|
|
|
|
|
|
onActivated(() => {
|
|
|
|
|
|
// Kept alive by MasterLayout, so every later visit lands here, not in onMounted.
|
|
|
|
|
|
if (activatedOnce) void loadDashboard()
|
|
|
|
|
|
activatedOnce = true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-18 12:54:45 +03:30
|
|
|
|
// First-paint entrance, mirroring HomeView's choreography exactly (module-scope
|
|
|
|
|
|
// flag so it only replays on a full app reload, not on every tab switch).
|
|
|
|
|
|
let hasRevealed = false
|
|
|
|
|
|
const revealed = ref(hasRevealed)
|
|
|
|
|
|
const REVEAL_STEP_MS = 70
|
|
|
|
|
|
|
|
|
|
|
|
function revealDelay(i: number) {
|
|
|
|
|
|
return { '--reveal-delay': `${i * REVEAL_STEP_MS}ms` }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadDashboard()
|
|
|
|
|
|
if (revealed.value) return
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
revealed.value = true
|
|
|
|
|
|
hasRevealed = true
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.dash {
|
2026-10-04 16:18:56 +03:30
|
|
|
|
background: @page;
|
2026-09-18 12:54:45 +03:30
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// A bare bar, sized like Home's, but with no border/title-bar chrome — the
|
|
|
|
|
|
// hero card below carries the page's identity instead of a header label.
|
|
|
|
|
|
.dash__bar {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 20;
|
2026-10-04 16:18:56 +03:30
|
|
|
|
background: alpha(@canvas, 82%);
|
2026-09-18 12:54:45 +03:30
|
|
|
|
backdrop-filter: blur(18px) saturate(140%);
|
2026-10-04 16:18:56 +03:30
|
|
|
|
border-bottom: 1px solid alpha(@primary, 5%);
|
|
|
|
|
|
box-shadow: 0 8px 20px -18px alpha(@primary, 55%);
|
2026-09-18 12:54:45 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__barInner {
|
|
|
|
|
|
max-width: 768px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__brand {
|
|
|
|
|
|
font-size: 14.5px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: @ink;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__exit {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
width: 34px;
|
|
|
|
|
|
height: 34px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 11px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
color: @wine;
|
|
|
|
|
|
background: @wineWash;
|
|
|
|
|
|
transition: transform 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__exit:active {
|
|
|
|
|
|
transform: scale(0.92);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__body {
|
|
|
|
|
|
padding-top: 58px;
|
|
|
|
|
|
padding-bottom: 90px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__reveal {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translate3d(0, 14px, 0) scale(0.98);
|
|
|
|
|
|
will-change: transform, opacity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-revealed .dash__reveal {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translate3d(0, 0, 0) scale(1);
|
|
|
|
|
|
transition:
|
|
|
|
|
|
opacity 0.62s cubic-bezier(0.32, 0.72, 0, 1) var(--reveal-delay, 0ms),
|
|
|
|
|
|
transform 0.62s cubic-bezier(0.32, 0.72, 0, 1) var(--reveal-delay, 0ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.dash__reveal {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.is-revealed .dash__reveal {
|
|
|
|
|
|
transition: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Hero ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// One full surface: identity, balance and quick actions all on the same
|
|
|
|
|
|
// ink → wine gradient, instead of a dark card handing off to a separate
|
|
|
|
|
|
// white grid underneath it.
|
|
|
|
|
|
.dash__hero {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
margin-bottom: 28px;
|
|
|
|
|
|
padding: 20px 18px 16px;
|
|
|
|
|
|
border-radius: 26px;
|
2026-10-04 16:18:56 +03:30
|
|
|
|
background: linear-gradient(155deg, @heroSurface 0%, @heroSurface 55%, #5E1427 130%);
|
2026-09-18 12:54:45 +03:30
|
|
|
|
color: #fff;
|
|
|
|
|
|
box-shadow: 0 22px 40px -26px rgba(20, 22, 26, 0.32);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroTop {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroIdentity {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroName {
|
|
|
|
|
|
font-size: 13.5px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroMobile {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
|
direction: ltr;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroSeal {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 5px 12px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__heroSeal.is-debtor {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: @wineSoft;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__heroSeal.is-creditor {
|
2026-10-04 16:18:56 +03:30
|
|
|
|
color: #14161A;
|
2026-09-18 12:54:45 +03:30
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroFigure {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroNum {
|
|
|
|
|
|
font-size: 34px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
letter-spacing: -0.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroUnit {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.62);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroDebt {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
padding-top: 13px;
|
|
|
|
|
|
border-top: 1px solid rgba(255, 255, 255, 0.14);
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.62);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__heroDebt b {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__heroDebt i {
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.62);
|
|
|
|
|
|
padding-inline-start: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Signed-out invite ──────────────────────────────────────────────────
|
|
|
|
|
|
.dash__invite {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding: 26px 20px;
|
|
|
|
|
|
border-radius: 26px;
|
2026-10-04 16:18:56 +03:30
|
|
|
|
background: @heroSurface;
|
2026-09-18 12:54:45 +03:30
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__inviteIcon {
|
|
|
|
|
|
width: 46px;
|
|
|
|
|
|
height: 46px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__invite h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__invite p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
|
max-width: 240px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__inviteCta {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
padding: 10px 22px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
2026-10-04 16:18:56 +03:30
|
|
|
|
color: #14161A;
|
2026-09-18 12:54:45 +03:30
|
|
|
|
background: #fff;
|
|
|
|
|
|
transition: transform 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__inviteCta:active {
|
|
|
|
|
|
transform: scale(0.96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Settings-style rows ────────────────────────────────────────────────
|
|
|
|
|
|
.dash__rows {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 13px 14px;
|
|
|
|
|
|
border: 1px solid @hairline;
|
|
|
|
|
|
border-radius: 17px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: @canvas;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
transition: transform 0.18s ease, border-color 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__row:active {
|
|
|
|
|
|
transform: scale(0.985);
|
2026-10-04 16:18:56 +03:30
|
|
|
|
border-color: alpha(@wine, 20%);
|
2026-09-18 12:54:45 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__rowIcon {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
color: @wine;
|
|
|
|
|
|
background: @wineWash;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dash__rowText {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__rowText b {
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: @ink;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dash__rowText small {
|
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
|
color: @inkFaint;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Rate row ───────────────────────────────────────────────────────────
|
|
|
|
|
|
.dash__rate {
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|