SearchView: Removed the dynamic collection-tile grid (e.g. "پرفروش‌های این هفته") and the "نتایج / N محصول" section header from the UI. Underlying data/filtering logic untouched.

DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):

Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.

Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.

LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
This commit is contained in:
s.esmaeili
2026-09-18 12:54:45 +03:30
parent 05fe780c89
commit 53a304b5d5
355 changed files with 22865 additions and 14 deletions
@@ -0,0 +1,568 @@
<!--
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
v-if="session.customerId"
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>
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
</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>
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
</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>
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
</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>
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
</button>
</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>
<DialogVant :timeOut="-1" v-model:show="data.notify" :closeOnClickOverlay="true">
<template v-slot:img>
<img :src="exitImage" alt="" class="opacity-70 mb-8" />
</template>
<template v-slot:title>آیا می خواهید از برنامه خارج شوید؟</template>
<template v-slot:button>
<div class="text-center text-[#1C2D56]" @click="logOut">بله</div>
<div class="text-center text-[#6b7280]" @click="data.notify = false">بی خیال</div>
</template>
</DialogVant>
<DialogVant :timeOut="-1" v-model:show="data.loginNotify" :closeOnClickOverlay="true">
<template v-slot:img>
<img :src="loginDialogImage" alt="" class="opacity-80" />
</template>
<template v-slot:title>
<div class="p-8 text-justify">
برای دسترسی به تمامی امکانات نرم افزار منتزر، ابتدا گزینه ورود/ثبت نام را لمس کنید و در
صورت نداشتن حساب کاربری، ثبت نام نمایید.
</div>
</template>
<template v-slot:button>
<div class="text-center text-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="data.loginNotify = false">بیخیال</div>
</template>
</DialogVant>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Loading, PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
import RateStrip from '../../components/home/RateStrip.vue'
import DialogVant from '../../components/DialogVant.vue'
import exitImage from '../../assets/img/empty/dialogEmpty5.svg'
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
import { price } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
const router = useRouter()
const session = useSessionStore()
const customer = ref<CustomerDetail | null>(null)
const goldPrice = ref<GoldPriceQuote | null>(null)
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) {
if (!session.customerId) {
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' })
}
function logOut() {
session.logout()
data.notify = false
router.push({ name: 'home' })
}
async function loadDashboard() {
if (session.customerId) {
customer.value = await api.getCustomer(session.customerId)
}
try {
goldPrice.value = await api.getLatestGoldPrice()
} catch {
goldPrice.value = null
}
}
async function onRefresh() {
await loadDashboard()
refreshing.value = false
}
// 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 {
background: @canvas;
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;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(18px) saturate(140%);
border-bottom: 1px solid fade(@primary, 5%);
box-shadow: 0 8px 20px -18px fade(@primary, 55%);
}
.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;
background: linear-gradient(155deg, @ink 0%, @ink 55%, @wineDeep 130%);
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 {
color: @ink;
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;
background: @ink;
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;
color: @ink;
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);
border-color: fade(@wine, 20%);
}
.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>