diff --git a/src/App.vue b/src/App.vue index 7798e16..87971a4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -198,10 +198,11 @@ const { handlePointerMove: onSupportPointerMove, handlePointerUp: supportPointerUp, handlePointerCancel: onSupportPointerCancel -} = useDraggableFloatingButton('khaneghi_support_button_position', { +} = useDraggableFloatingButton('khaneghi_support_button_position_v2', { width: 118, height: 42, - defaultBottomOffset: 120 + defaultBottomGap: 12, + defaultRightOffset: 16 }) function openSupportLink() { @@ -329,7 +330,7 @@ function update() { #support-button { position: fixed; - z-index: 9999; + z-index: 5100; cursor: grab; user-select: none; border-radius: 9999px; diff --git a/src/utils/categoryTabs.ts b/src/utils/categoryTabs.ts index 1e6ad1d..6fe6b4d 100644 --- a/src/utils/categoryTabs.ts +++ b/src/utils/categoryTabs.ts @@ -1,6 +1,6 @@ import { getStoreCategories } from '@/api/categories' -/** تب‌های فیلتر محصول — فقط از API دسته‌بندی (هماهنگ با پنل ادمین) */ +/** تب‌های فیلتر محصول — دسته‌های فعال که محصول دارند (خودکار از API) */ export async function loadProductCategoryTabNames(): Promise { const categories = await getStoreCategories() return categories.map((item) => item.name) diff --git a/src/utils/storeHours.ts b/src/utils/storeHours.ts new file mode 100644 index 0000000..7173084 --- /dev/null +++ b/src/utils/storeHours.ts @@ -0,0 +1,107 @@ +import { useSettingsStore } from '@/store/settingsStore' +import { lastPrice } from '@/utils/price' + +export interface StoreTradingHours { + start: string + end: string + closePriceEnabled: boolean +} + +export interface EffectiveTradingHours { + start: string + end: string + enforced: boolean +} + +let productTradingHours: { start: string; end: string } | null = null + +function parseTimeToSeconds(time: string): number { + const parts = time.trim().split(':').map(Number) + const [hours = 0, minutes = 0, seconds = 0] = parts + return hours * 3600 + minutes * 60 + seconds +} + +export function formatTradingTimeDisplay(time: string): string { + const parts = time.trim().split(':') + if (parts.length >= 3) { + return `${parts[0]}:${parts[1]}:${parts[2]}` + } + if (parts.length >= 2) { + return `${parts[0]}:${parts[1]}:00` + } + return time +} + +export function setProductTradingHours(start?: string | null, end?: string | null) { + if (start && end) { + productTradingHours = { start, end } + } +} + +export function getStoreTradingHours(): StoreTradingHours { + const settings = useSettingsStore() + const closeRaw = settings.getSettingByKey('close_price') + const closePriceEnabled = + closeRaw === true || + closeRaw === 1 || + String(closeRaw ?? 'false').trim().toLowerCase() === 'true' + + return { + start: String(settings.getSettingByKey('startDay') ?? '00:00'), + end: String(settings.getSettingByKey('endDay') ?? '23:59'), + closePriceEnabled, + } +} + +export function getEffectiveTradingHours(): EffectiveTradingHours { + const settings = getStoreTradingHours() + if (settings.closePriceEnabled) { + return { + start: settings.start, + end: settings.end, + enforced: true, + } + } + + if (productTradingHours?.start && productTradingHours?.end) { + return { + start: productTradingHours.start, + end: productTradingHours.end, + enforced: true, + } + } + + return { + start: settings.start, + end: settings.end, + enforced: false, + } +} + +export function isStoreClosedBySchedule(now = new Date()): boolean { + const { start, end, enforced } = getEffectiveTradingHours() + if (!enforced) { + return false + } + + const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + const openSeconds = parseTimeToSeconds(start) + const closeSeconds = parseTimeToSeconds(end) + + return currentSeconds < openSeconds || currentSeconds > closeSeconds +} + +export function isStoreClosed(): boolean { + if (lastPrice.value?.closed === true) { + return true + } + + return isStoreClosedBySchedule() +} + +export function getStoreClosedMessage(): string { + const { start, end } = getEffectiveTradingHours() + const open = formatTradingTimeDisplay(start) + const close = formatTradingTimeDisplay(end) + return `فروشگاه در حال حاضر بسته است. ساعت کاری از ${open} تا ${close} می‌باشد.` +} diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index b6f648e..9516c3f 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -17,9 +17,6 @@ export interface SupportMessengerSettings { export const supportMessengerSettings = ref({}) -export const DEFAULT_SUPPORT_MESSENGER_TEXT = - 'جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.' - function getSupportSettingValue( settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>, key: string @@ -39,9 +36,11 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin if (/^https?:\/\//i.test(id)) return id - const type = (messenger ?? '').trim().toLowerCase() || 'bale' + const type = (messenger ?? '').trim().toLowerCase() const username = id.replace(/^@/, '') + if (!type) return '' + switch (type) { case 'bale': if (id.includes('ble.ir')) { @@ -59,7 +58,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin case 'soroush': return `https://splus.ir/splus/${username}` default: - return `https://ble.ir/${username}` + return '' } } @@ -69,7 +68,7 @@ export async function loadSupportMessengerFromSettings() { if (!settings || !Array.isArray(settings)) return supportMessengerSettings.value = { - messenger: getSupportSettingValue(settings, 'support_messenger') || 'bale', + messenger: getSupportSettingValue(settings, 'support_messenger'), messengerId: getSupportSettingValue(settings, 'support_messenger_id'), messengerText: getSupportSettingValue(settings, 'support_messenger_text'), } diff --git a/src/utils/useDraggableFloatingButton.ts b/src/utils/useDraggableFloatingButton.ts index 1bb61e8..075ceda 100644 --- a/src/utils/useDraggableFloatingButton.ts +++ b/src/utils/useDraggableFloatingButton.ts @@ -1,24 +1,54 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' const DRAG_THRESHOLD = 6 +const POSITION_VERSION = 1 type FabPosition = { x: number; y: number } +type SavedFabOffset = { + version: number + dx: number + dy: number +} + type DraggableFloatingOptions = { width?: number height?: number - defaultBottomOffset?: number + /** فاصله از پایینِ نوار منو + montzar (px) — مثل تصویر */ + defaultBottomGap?: number defaultRightOffset?: number } +function readBottomChromeHeight(): number { + const styles = getComputedStyle(document.documentElement) + const direct = parseFloat(styles.getPropertyValue('--app-bottom-chrome-height')) + if (!Number.isNaN(direct) && direct > 0) { + return direct + } + + const nav = parseFloat(styles.getPropertyValue('--app-nav-height')) || 80 + const creditOffset = parseFloat(styles.getPropertyValue('--app-credit-footer-offset')) + if (!Number.isNaN(creditOffset) && creditOffset > 0) { + return nav + creditOffset + } + + const probe = document.createElement('div') + probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;height:var(--app-bottom-chrome-height)' + document.body.appendChild(probe) + const measured = probe.getBoundingClientRect().height + document.body.removeChild(probe) + + return measured > 0 ? measured : nav + 28 +} + export function useDraggableFloatingButton( storageKey: string, options: DraggableFloatingOptions = {} ) { const width = options.width ?? 96 const height = options.height ?? 44 - const defaultBottomOffset = options.defaultBottomOffset ?? 120 - const defaultRightOffset = options.defaultRightOffset ?? 20 + const defaultBottomGap = options.defaultBottomGap ?? 12 + const defaultRightOffset = options.defaultRightOffset ?? 16 const pos = ref({ x: 0, y: 0 }) const dragging = ref(false) @@ -44,26 +74,40 @@ export function useDraggableFloatingButton( function getHorizontalOffset() { const vw = window.innerWidth const contentWidth = getViewportWidth() + return Math.max(0, (vw - contentWidth) / 2) } - function resetToDefault() { + function getDefaultPosition(): FabPosition { const vh = window.innerHeight const offsetX = getHorizontalOffset() const contentWidth = getViewportWidth() + const bottomChrome = readBottomChromeHeight() - pos.value = { + return { x: offsetX + contentWidth - width - defaultRightOffset, - y: vh - defaultBottomOffset - height + y: vh - bottomChrome - defaultBottomGap - height } } + function applySavedOffset(dx: number, dy: number) { + const base = getDefaultPosition() + pos.value = { + x: base.x + dx, + y: base.y + dy + } + clampToViewport() + } + function clampToViewport() { - const vw = window.innerWidth const vh = window.innerHeight + const offsetX = getHorizontalOffset() + const contentWidth = getViewportWidth() + const minX = offsetX + const maxX = offsetX + contentWidth - width pos.value = { - x: Math.max(0, Math.min(vw - width, pos.value.x)), + x: Math.max(minX, Math.min(maxX, pos.value.x)), y: Math.max(0, Math.min(vh - height, pos.value.y)) } } @@ -72,25 +116,56 @@ export function useDraggableFloatingButton( try { const raw = localStorage.getItem(storageKey) if (!raw) return false - const parsed = JSON.parse(raw) as FabPosition - if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') { - pos.value = parsed - clampToViewport() + + const parsed = JSON.parse(raw) as SavedFabOffset + if ( + parsed?.version === POSITION_VERSION + && typeof parsed.dx === 'number' + && typeof parsed.dy === 'number' + ) { + applySavedOffset(parsed.dx, parsed.dy) return true } } catch { // ignore } + return false } function savePosition() { - localStorage.setItem(storageKey, JSON.stringify(pos.value)) + const base = getDefaultPosition() + const payload: SavedFabOffset = { + version: POSITION_VERSION, + dx: pos.value.x - base.x, + dy: pos.value.y - base.y + } + localStorage.setItem(storageKey, JSON.stringify(payload)) + } + + function resetToDefault() { + pos.value = getDefaultPosition() } function handleResize() { - clampToViewport() - savePosition() + try { + const raw = localStorage.getItem(storageKey) + if (raw) { + const parsed = JSON.parse(raw) as SavedFabOffset + if ( + parsed?.version === POSITION_VERSION + && typeof parsed.dx === 'number' + && typeof parsed.dy === 'number' + ) { + applySavedOffset(parsed.dx, parsed.dy) + return + } + } + } catch { + // ignore + } + + resetToDefault() } function handlePointerDown(event: PointerEvent) { diff --git a/src/utils/usePrice.ts b/src/utils/usePrice.ts index f69748d..0bf0c34 100644 --- a/src/utils/usePrice.ts +++ b/src/utils/usePrice.ts @@ -24,3 +24,58 @@ export function useTowDecimal(num: string | number): string | number { export function useFormat(num: string | number, unit = '') { return usePrice(Math.abs(+useTowDecimal(num)), true, unit) } + +type TransactionPriceSource = { + cash?: number | null + gold?: number | null + order?: { + price?: number | null + total_price?: number | null + amount?: number | null + } | null +} + +export function getTransactionTradePrice(item: TransactionPriceSource): number { + const orderPrice = Number(item.order?.price) + if (Number.isFinite(orderPrice) && orderPrice > 0) { + return Math.round(orderPrice) + } + + const cashAmount = Math.abs(Number(item.cash) || 0) + const goldAmount = + Math.abs(Number(item.gold) || 0) || Math.abs(Number(item.order?.amount) || 0) + + if (goldAmount > 0 && cashAmount > 0) { + return Math.round(cashAmount / goldAmount) + } + + const totalPrice = Number(item.order?.total_price) + if (Number.isFinite(totalPrice) && totalPrice > 0) { + return Math.round(totalPrice) + } + + return 0 +} + +export function getTransactionGoldAmount(item: TransactionPriceSource): number { + const gold = Number(item.gold) || 0 + if (Math.abs(gold) > 0) { + return gold + } + + const orderAmount = Number(item.order?.amount) || 0 + if (orderAmount > 0) { + const cash = Number(item.cash) || 0 + if (cash < 0) return orderAmount + if (cash > 0) return -orderAmount + return orderAmount + } + + return gold +} + +export function formatGoldAmount(amount?: number | null): string { + const num = Number(amount) + if (!Number.isFinite(num)) return '0' + return String(Number(num.toFixed(3))) +} diff --git a/src/views/base/copper&silver/index.vue b/src/views/base/copper&silver/index.vue index 65369b3..b28432f 100644 --- a/src/views/base/copper&silver/index.vue +++ b/src/views/base/copper&silver/index.vue @@ -239,6 +239,7 @@ import {useConfigStore} from "@/store/configStore"; import { calcFeeInGrams } from '@/utils/serviceFee' import { formatCashWalletBalanceText, formatGoldWalletBalanceText } from '@/utils/productDealMethod' import { getCashWalletBalance } from '@/utils/productAssets' +import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours' const infoVisible = ref(false) const walletStore = useWalletStore() @@ -325,6 +326,7 @@ const equivalentPrice = computed(() => usePriceWithoutType(feeInRial.value.toFix const disableTransaction = computed(() => loading.value || + isStoreClosed() || form.value?.goldValue === undefined || form.value?.goldValue === '' || form.value?.goldCash === '' || @@ -640,10 +642,18 @@ async function initPrice() { } function okConfirm() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } confirmAction.value = true } async function ok() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } try { loading.value = true infoVisible.value = true diff --git a/src/views/base/gold/index.vue b/src/views/base/gold/index.vue index 3c2d54b..2d538ca 100644 --- a/src/views/base/gold/index.vue +++ b/src/views/base/gold/index.vue @@ -296,6 +296,7 @@ import type { Product } from '@/models/products' import { useFundUserStore } from '@/store/fundStore' import {useConfigStore} from "@/store/configStore"; import { calcFeeInGrams, loadServiceFeeFromSettings } from '@/utils/serviceFee' +import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours' import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod' import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' import { useWalletAssets } from '@/composables/useWalletAssets' @@ -406,6 +407,7 @@ const priceUnavailable = computed(() => { const disableTransaction = computed(() => loading.value || + isStoreClosed() || form.value?.goldValue === undefined || form.value?.goldValue === '' || form.value?.goldCash === '' || @@ -695,6 +697,10 @@ async function initPrice() { } function okConfirm() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } if (cashValidationMessage.value) { message.warning(cashValidationMessage.value) return @@ -703,6 +709,10 @@ function okConfirm() { } async function ok() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } try { loading.value = true infoVisible.value = true diff --git a/src/views/base/products/components/DealProduct.vue b/src/views/base/products/components/DealProduct.vue index d41776c..f6275fc 100644 --- a/src/views/base/products/components/DealProduct.vue +++ b/src/views/base/products/components/DealProduct.vue @@ -297,6 +297,7 @@ import { } from '@/utils/productOrderLimit' import { useWalletAssets } from '@/composables/useWalletAssets' import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets' +import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours' const typeDeal = computed(() => getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value)) @@ -438,6 +439,7 @@ const cashValidationMessage = computed(() => const disableTransaction = computed(() => loading.value || + isStoreClosed() || form.value?.goldValue === undefined || form.value?.goldValue === '' || form.value?.goldCash === '' || @@ -795,11 +797,19 @@ async function initPrice() { // }) function okConfirm() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } confirmAction.value = true refInput.value?.validateFields() } async function ok() { + if (isStoreClosed()) { + message.warning(getStoreClosedMessage()) + return + } try { loading.value = true infoVisible.value = true diff --git a/src/views/base/transactions/components/Item.vue b/src/views/base/transactions/components/Item.vue index 7c8af65..c4aba0f 100644 --- a/src/views/base/transactions/components/Item.vue +++ b/src/views/base/transactions/components/Item.vue @@ -15,12 +15,12 @@

{{ props.item.title }}

{{ toJalaliOutOfUTC(props.item.expire_at) }} - - {{ props.item.gold }} {{ props.item.unit }} + + {{ formatGoldAmount(goldAmount) }} {{ props.item.unit }} قیمت معامله: - {{ usePriceWithoutType(parseInt((Math.abs(props.item.cash) / Math.abs(props.item.gold)).toFixed(0))) }} + {{ usePriceWithoutType(tradePrice) }} ریال
@@ -56,10 +56,10 @@
- {{ props.item.gold }} + {{ formatGoldAmount(goldAmount) }}
{{ props.item.unit }}
@@ -75,7 +75,7 @@
قیمت معامله:
- {{ usePriceWithoutType(parseInt((Math.abs(props.item.cash) / Math.abs(props.item.gold)).toFixed(0))) }} + {{ usePriceWithoutType(tradePrice) }}
ریال
@@ -114,7 +114,12 @@