fix some bugs from accounting 7

This commit is contained in:
2026-07-28 21:02:44 +03:30
parent 7cd3fac28b
commit a49b20dfd4
20 changed files with 1734 additions and 93 deletions
+216 -37
View File
@@ -4,39 +4,86 @@
class="app-header px-4 pb-2 top-0 wrap-fix background-color-transition"
:class="{ 'app-header--scrolled': isHeaderScrolled }"
>
<div class="flex justify-between my-2 items-center">
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
<div class="z-100000 rounded-full border-1 text-primary border-primary">
<div class="flex justify-between my-2 items-center gap-2">
<div
class="app-header__identity flex items-center gap-2 min-w-0"
@click.stop="router.push({ name: 'user' })"
>
<div class="z-100000 shrink-0 rounded-full border-1 text-primary border-primary">
<Icon icon="vuesax-linear:user" size="20" />
</div>
<div class="text-primary color-transition font-bold">
<div class="app-header__name text-primary color-transition font-bold">
{{ userStore.userInfo?.name }}
</div>
</div>
<div class="flex items-center gap-2">
<div class="app-header__actions flex items-center gap-1.5 shrink-0">
<button
v-if="notificationEnabled"
type="button"
class="mhome-header-btn mhome-header-btn--notify"
aria-label="اعلان"
@click.stop="goNotifications"
class="mhome-header-btn mhome-header-btn--icon"
aria-label="بروزرسانی"
@click="reloadApp"
>
<span class="mhome-header-btn__icon-wrap">
<Icon icon="vuesax-linear:notification-bing" size="18" />
<span v-if="unreadCount > 0" class="mhome-header-btn__badge">
<Icon icon="vuesax-linear:refresh-2" size="18" />
</button>
<div ref="menuRoot" class="mhome-header-menu">
<button
type="button"
class="mhome-header-btn mhome-header-btn--icon mhome-header-btn--menu"
:class="{ 'mhome-header-btn--active': menuOpen }"
aria-label="منو"
:aria-expanded="menuOpen"
@click.stop="menuOpen = !menuOpen"
>
<Icon icon="vuesax-linear:menu" size="18" />
<span
v-if="notificationEnabled && unreadCount > 0"
class="mhome-header-menu__badge"
>
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</span>
</button>
<button
type="button"
class="mhome-header-btn mhome-header-btn--logout"
@click="$emit('logout')"
>
<Icon icon="vuesax-linear:logout" size="18" />
<span>خروج</span>
</button>
</button>
<div v-if="menuOpen" class="mhome-header-menu__panel" role="menu">
<button
v-if="notificationEnabled"
type="button"
class="mhome-header-menu__item"
role="menuitem"
@click="goNotifications"
>
<span class="mhome-header-btn__icon-wrap">
<Icon icon="vuesax-linear:notification-bing" size="16" />
<span v-if="unreadCount > 0" class="mhome-header-btn__badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</span>
<span>اعلان‌ها</span>
</button>
<button
v-if="showInstallEntry"
type="button"
class="mhome-header-menu__item"
role="menuitem"
@click="onInstall"
>
<Icon icon="vuesax-linear:mobile" size="16" />
<span>{{ canNativeInstall ? 'نصب اپلیکیشن' : 'آموزش نصب' }}</span>
</button>
<button
type="button"
class="mhome-header-menu__item mhome-header-menu__item--danger"
role="menuitem"
@click="onLogout"
>
<Icon icon="vuesax-linear:logout" size="16" />
<span>خروج</span>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -47,11 +94,28 @@ import { nextTick, onActivated, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/store/userStore'
import { useAppNotificationBadge } from '@/composables/useAppNotificationBadge'
import { usePwaInstall } from '@/composables/usePwaInstall'
import { PWA_MANIFEST_READY_EVENT } from '@/modules/pwa/manifest'
import {
hasNativeInstallPrompt,
initPwaInstallPromptListener,
} from '@/modules/pwa/pwaInstall'
import {
canRequestBrowserNotification,
requestBrowserNotificationPermission,
} from '@/utils/browserNotification'
import router from '@/router'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
const emit = defineEmits<{
logout: []
}>()
const headerRef = ref<HTMLElement | null>(null)
const menuRoot = ref<HTMLElement | null>(null)
const menuOpen = ref(false)
const isHeaderScrolled = ref(false)
const canNativeInstall = ref(false)
const route = useRoute()
const userStore = useUserStore()
@@ -60,12 +124,48 @@ const {
unreadCount,
clearUnreadLocally,
} = useAppNotificationBadge()
const { tryInstallOrOpenGuide, showInstallEntry } = usePwaInstall()
function goNotifications() {
function refreshCanInstall() {
canNativeInstall.value = hasNativeInstallPrompt()
}
function closeMenu() {
menuOpen.value = false
}
async function goNotifications() {
closeMenu()
clearUnreadLocally()
if (canRequestBrowserNotification()) {
void requestBrowserNotificationPermission()
}
void router.push({ name: 'appNotifications' })
}
async function onInstall() {
closeMenu()
await tryInstallOrOpenGuide()
refreshCanInstall()
}
function onLogout() {
closeMenu()
emit('logout')
}
function reloadApp() {
window.location.reload()
}
function onDocPointerDown(event: PointerEvent) {
if (!menuOpen.value) return
const root = menuRoot.value
if (root && !root.contains(event.target as Node)) {
closeMenu()
}
}
let intersectionObserver: IntersectionObserver | null = null
let scrollTarget: HTMLElement | Window | null = null
@@ -166,6 +266,12 @@ function setupScrollTracking() {
}
onMounted(async () => {
initPwaInstallPromptListener()
refreshCanInstall()
window.addEventListener(PWA_MANIFEST_READY_EVENT, refreshCanInstall)
window.addEventListener('appinstalled', refreshCanInstall)
document.addEventListener('pointerdown', onDocPointerDown)
setupScrollTracking()
await nextTick()
setupScrollTracking()
@@ -189,11 +295,10 @@ watch(
onUnmounted(() => {
teardownScrollTracking()
window.removeEventListener(PWA_MANIFEST_READY_EVENT, refreshCanInstall)
window.removeEventListener('appinstalled', refreshCanInstall)
document.removeEventListener('pointerdown', onDocPointerDown)
})
defineEmits<{
logout: []
}>()
</script>
<style scoped>
@@ -202,6 +307,7 @@ defineEmits<{
box-sizing: border-box;
padding-top: calc(0.5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
background-color: transparent;
overflow: visible;
}
.app-header--scrolled {
@@ -211,6 +317,11 @@ defineEmits<{
0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.app-header__name {
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
max-width: 42vw;
}
.mhome-header-btn {
display: inline-flex;
align-items: center;
@@ -227,21 +338,23 @@ defineEmits<{
cursor: pointer;
}
.mhome-header-btn--logout {
color: #dc2626;
border-color: #fecaca;
background: #fef2f2;
.mhome-header-btn--icon {
width: 36px;
height: 36px;
padding: 0;
}
.mhome-header-btn--notify {
color: #0a1b37;
border-color: #e5e7eb;
background: #fff;
.mhome-header-btn--menu {
position: relative;
overflow: visible;
}
.mhome-header-btn--active {
background: #f3f4f6;
}
.mhome-header-btn__icon-wrap {
position: relative;
display: inline-flex;
@apply relative inline-flex;
}
.mhome-header-btn__badge {
@@ -259,4 +372,70 @@ defineEmits<{
line-height: 16px;
text-align: center;
}
.mhome-header-menu {
position: relative;
overflow: visible;
}
.mhome-header-menu__badge {
position: absolute;
top: -6px;
left: -6px;
z-index: 2;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: #dc2626;
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
pointer-events: none;
box-shadow: 0 0 0 2px #fff;
}
.mhome-header-menu__panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 40;
min-width: 190px;
padding: 6px;
border-radius: 12px;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
}
.mhome-header-menu__item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: transparent;
color: #374151;
font-size: 13px;
font-weight: 600;
font-family: inherit;
text-align: right;
cursor: pointer;
}
.mhome-header-menu__item:hover {
background: #f3f4f6;
}
.mhome-header-menu__item--danger {
color: #dc2626;
}
.mhome-header-menu__item--danger:hover {
background: #fef2f2;
}
</style>
@@ -13,7 +13,7 @@
<button
v-if="notificationEnabled"
type="button"
class="dhome-btn-ghost"
class="dhome-btn-ghost dhome-btn-ghost--notify"
aria-label="اعلان"
@click="goNotifications"
>
@@ -25,8 +25,17 @@
</span>
<span>اعلان</span>
</button>
<button type="button" class="dhome-btn-ghost" @click="$emit('logout')">
<BasicIcon icon="vuesax-linear:logout" size="16" />
<button type="button" class="dhome-btn-ghost" aria-label="به‌روزرسانی" @click="reloadApp">
<BasicIcon icon="vuesax-linear:refresh-2" size="16" />
<span>به‌روزرسانی</span>
</button>
<button
v-if="showLogout"
type="button"
class="dhome-btn-ghost dhome-btn-ghost--danger"
@click="$emit('logout')"
>
<BasicIcon icon="vuesax-linear:logout" size="16" color="#dc2626" />
<span>خروج</span>
</button>
</div>
@@ -37,8 +46,16 @@
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useUserStore } from '@/store/userStore'
import { useAppNotificationBadge } from '@/composables/useAppNotificationBadge'
import { canRequestBrowserNotification, requestBrowserNotificationPermission } from '@/utils/browserNotification'
import router from '@/router'
withDefaults(
defineProps<{
showLogout?: boolean
}>(),
{ showLogout: true },
)
defineEmits<{
logout: []
}>()
@@ -50,10 +67,17 @@ const {
clearUnreadLocally,
} = useAppNotificationBadge()
function goNotifications() {
async function goNotifications() {
clearUnreadLocally()
if (canRequestBrowserNotification()) {
await requestBrowserNotificationPermission()
}
void router.push({ name: 'appNotifications' })
}
function reloadApp() {
window.location.reload()
}
</script>
<style scoped>
@@ -0,0 +1,41 @@
<template>
<DialogVant v-model:show="visible" :closeOnClickOverlay="false" :timeOut="-1">
<template v-slot:title>
<div class="flex flex-col items-center">
<DotLottieVue
class="w-11rem h-11rem"
autoplay
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
/>
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
</div>
</template>
<template v-slot:button>
<div class="text-center text-red flex items-center justify-center" @click="$emit('confirm')">
<a-spin :spinning="false" size="small">خروج</a-spin>
</div>
<div class="text-center" @click="visible = false">انصراف</div>
</template>
</DialogVant>
</template>
<script setup lang="ts">
import DialogVant from '@/components/DialogVant.vue'
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
import {computed} from "vue";
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
confirm: []
}>()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
</script>
@@ -13,7 +13,7 @@
<button
v-if="notificationEnabled"
type="button"
class="dhome-btn-ghost"
class="dhome-btn-ghost dhome-btn-ghost--notify"
aria-label="اعلان"
@click="goNotifications"
>
@@ -25,13 +25,17 @@
</span>
<span>اعلان</span>
</button>
<button type="button" class="dhome-btn-ghost" aria-label="به‌روزرسانی" @click="reloadApp">
<BasicIcon icon="vuesax-linear:refresh-2" size="16" />
<span>به‌روزرسانی</span>
</button>
<button
v-if="showLogout"
type="button"
class="dhome-btn-ghost"
class="dhome-btn-ghost dhome-btn-ghost--danger"
@click="$emit('logout')"
>
<BasicIcon icon="vuesax-linear:logout" size="16" />
<BasicIcon icon="vuesax-linear:logout" size="16" color="#dc2626" />
<span>خروج</span>
</button>
</div>
@@ -42,6 +46,7 @@
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useUserStore } from '@/store/userStore'
import { useAppNotificationBadge } from '@/composables/useAppNotificationBadge'
import { canRequestBrowserNotification, requestBrowserNotificationPermission } from '@/utils/browserNotification'
import router from '@/router'
withDefaults(
@@ -62,10 +67,17 @@ const {
clearUnreadLocally,
} = useAppNotificationBadge()
function goNotifications() {
async function goNotifications() {
clearUnreadLocally()
if (canRequestBrowserNotification()) {
await requestBrowserNotificationPermission()
}
void router.push({ name: 'appNotifications' })
}
function reloadApp() {
window.location.reload()
}
</script>
<style scoped>
@@ -101,6 +101,15 @@
color: #111827;
background: rgba(15, 23, 42, 0.04);
}
&--danger {
color: #dc2626;
&:hover {
color: #b91c1c;
background: #fef2f2;
}
}
}
/* Alerts */
+23 -1
View File
@@ -1,5 +1,5 @@
import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue'
import { loading } from '@/utils'
import { loading, removeToken } from '@/utils'
import {
guideTypePassword,
isValidPersian,
@@ -12,6 +12,7 @@ import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { updateMe, updatePass } from '@/api/me'
import { useUserStore } from '@/store/userStore'
import { useWalletStore } from '@/store/walletStore'
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
import { useSettingsStore } from '@/store/settingsStore'
@@ -34,6 +35,7 @@ export interface UserPageContext {
isLegal: ReturnType<typeof computed<{ title: string; color: string; icon: string }>>
shareButtonLabel: ReturnType<typeof computed<string>>
menuActions: ReturnType<typeof computed<UserMenuAction[]>>
showLogoutConfirm: ReturnType<typeof ref<boolean>>
loading: typeof loading
guideTypePassword: typeof guideTypePassword
isValidPersian: typeof isValidPersian
@@ -43,6 +45,8 @@ export interface UserPageContext {
shareCode: () => void
ok: () => void
submitPassForm: () => void
requestLogout: () => void
logout: () => Promise<void>
}
export const USER_PAGE_CONTEXT_KEY: InjectionKey<UserPageContext> = Symbol('userPageContext')
@@ -60,8 +64,10 @@ export function useUserPage() {
const formRef = ref<FormInstance>()
const formPassRef = ref<FormInstance>()
const userStore = useUserStore()
const walletStore = useWalletStore()
const editPassVisible = ref(false)
const editPasswordVisible = ref(false)
const showLogoutConfirm = ref(false)
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const settingsStore = useSettingsStore()
@@ -183,6 +189,19 @@ kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
})
}
function requestLogout() {
showLogoutConfirm.value = true
}
async function logout() {
userStore.userInfo = undefined
walletStore.walletInfo = undefined
removeToken()
showLogoutConfirm.value = false
await router.push({ name: 'login' })
window.location.reload()
}
onMounted(() => {
setForm()
if (userStore.userInfo?.is_legal === 0) {
@@ -201,6 +220,7 @@ kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
isLegal,
shareButtonLabel,
menuActions,
showLogoutConfirm,
loading,
guideTypePassword,
isValidPersian,
@@ -210,6 +230,8 @@ kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
shareCode,
ok,
submitPassForm,
requestLogout,
logout,
}
provide(USER_PAGE_CONTEXT_KEY, context)
@@ -58,6 +58,7 @@
<BasicIcon icon="vuesax-linear:edit-2" size="16" />
<span>ویرایش اطلاعات کاربری</span>
</button>
<InstallButton />
</section>
<section class="duser-surface duser-actions">
@@ -77,6 +78,16 @@
<BasicIcon class="duser-actions__chevron" icon="vuesax-linear:arrow-left" size="16" />
</button>
</div>
<button
type="button"
class="duser-actions__item duser-actions__item--logout"
@click="requestLogout"
>
<span class="duser-actions__icon duser-actions__icon--logout">
<BasicIcon icon="vuesax-linear:logout" size="18" />
</span>
<span class="duser-actions__label duser-actions__label--logout">خروج</span>
</button>
</section>
</div>
</div>
@@ -85,6 +96,7 @@
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import InstallButton from '@/components/InstallButton.vue'
import router from '@/router'
import { useUserPageContext } from '../composables/useUserPage'
@@ -95,6 +107,7 @@ const {
menuActions,
copyCode,
shareCode,
requestLogout,
} = useUserPageContext()
</script>
@@ -218,6 +218,16 @@
background: #f9fafb;
border-color: #e5e7eb;
}
&--logout {
margin-top: 8px;
border-color: rgba(239, 68, 68, 0.25);
&:hover {
background: #fef2f2;
border-color: rgba(239, 68, 68, 0.4);
}
}
}
&__icon {
@@ -231,6 +241,11 @@
color: #374151;
flex-shrink: 0;
overflow: visible;
&--logout {
background: #fef2f2;
color: #ef4444;
}
}
&__icon-image {
@@ -245,6 +260,10 @@
font-size: 13px;
font-weight: 600;
color: #111827;
&--logout {
color: #ef4444;
}
}
&__chevron {
+5
View File
@@ -52,6 +52,8 @@
>
<UserPasswordForm />
</BasicModal>
<LogoutConfirmDialog v-model:show="showLogoutConfirm" @confirm="logout" />
</div>
</template>
@@ -60,6 +62,7 @@ import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import LogoutConfirmDialog from '@/views/base/home/components/LogoutConfirmDialog.vue'
import { useUserPage } from './composables/useUserPage'
import MobileUserView from './mobile/MobileUserView.vue'
import DesktopUserView from './desktop/DesktopUserView.vue'
@@ -73,6 +76,8 @@ const {
editPasswordVisible,
isLegal,
loading,
showLogoutConfirm,
logout,
ok,
submitPassForm,
} = useUserPage()
+6 -47
View File
@@ -63,6 +63,7 @@
<div>ویرایش اطلاعات کاربری</div>
</div>
</BasicButton>
<InstallButton />
<div class="h-40vh overflow-y-auto mt-4">
<BasicButton
class="mt-2 w-full"
@@ -175,14 +176,12 @@
icon=""
iconSize="18"
size="large"
@click="showLogoutConfirm=!showLogoutConfirm"
@click="requestLogout"
>
<div class="flex">
<Icon
:color="y > 50 ? '' : ''"
class="color-transition absolute right-8"
icon="vuesax-linear:logout"
@click="showLogoutConfirm = true"
/>
<div>خروج از حساب کاربری</div>
</div>
@@ -190,44 +189,16 @@
</div>
</div>
</div>
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="false" :timeOut="-1">
<!-- <template v-slot:img>
<img :src="registerDelayImage" alt="" class="opacity-80">
</template>-->
<template v-slot:title>
<div class="flex flex-col items-center">
<DotLottieVue
class="w-11rem h-11rem"
autoplay
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
/>
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
</div>
</template>
<template v-slot:button>
<div class="text-center text-red flex items-center justify-center" @click="logout">
<a-spin :spinning="logoutLoading" size="small"> خروج</a-spin>
</div>
<div class="text-center" @click="showLogoutConfirm = false">انصراف</div>
</template>
</DialogVant>
</div>
</template>
<script lang="ts" setup>
import img from '@/assets/img/user_ly1.svg'
import { loading, removeToken } from '@/utils'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import router from '@/router'
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
import DialogVant from '@/components/DialogVant.vue'
import { useWalletStore } from '@/store/walletStore'
import InstallButton from '@/components/InstallButton.vue'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { useWindowScroll } from '@vueuse/core/index'
import { onMounted, computed, ref } from 'vue'
import { computed } from 'vue'
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
import { useSettingsStore } from '@/store/settingsStore'
import { useUserStore } from '@/store/userStore'
@@ -235,26 +206,14 @@ import { useUserPageContext } from '../composables/useUserPage'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
const userStore = useUserStore()
const walletStore = useWalletStore()
const showLogoutConfirm = ref<boolean>()
const logoutLoading = ref<boolean>(false)
const { y } = useWindowScroll()
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const settingsStore = useSettingsStore()
const priceTriggerEnabled = computed(() =>
settingsStore.isSettingTruthy('has_page_price_trigger'),
)
const { editPassVisible, editPasswordVisible, copyCode, shareCode } = useUserPageContext()
async function logout() {
logoutLoading.value = true
userStore.userInfo = undefined
walletStore.walletInfo = undefined
removeToken()
await router.push({ name: 'login' })
window.location.reload()
}
const { editPassVisible, editPasswordVisible, copyCode, shareCode, requestLogout } =
useUserPageContext()
</script>
<style scoped>
+2
View File
@@ -107,6 +107,7 @@
<div v-else>{{ !isSMS ? 'ورود' : stepTwoVerify ? 'ورود' : 'ارسال کد' }}</div>
</div>
</BasicButton>
<InstallButton />
<BasicButton
v-if="getToken() && userStore.userInfo?.finger_print"
:disabled="loading"
@@ -139,6 +140,7 @@ import { mobileValidation, required } from '@/utils/inputRules'
// import type {UserMobile} from "@/models/login";
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import InstallButton from '@/components/InstallButton.vue'
import { authenticateWithFingerprint, getToken, loading, removeToken, setToken, URLUserType } from '@/utils'
import { reactive, ref } from 'vue'
import OtpInput from 'vue3-otp-input'