Treat missing quotes as closed across home and products, show only the admin closed_message as hint, and block trading with consistent notices. Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
6.1 KiB
TypeScript
233 lines
6.1 KiB
TypeScript
import { ref } from 'vue'
|
|
import { useSettingsStore } from '@/store/settingsStore'
|
|
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
|
|
|
export interface StoreTradingHours {
|
|
start: string
|
|
end: string
|
|
closePriceEnabled: boolean
|
|
}
|
|
|
|
export interface EffectiveTradingHours {
|
|
start: string
|
|
end: string
|
|
enforced: boolean
|
|
}
|
|
|
|
const productTradingHours = ref<{ start: string; end: string } | null>(null)
|
|
const productTradingHoursVersion = ref(0)
|
|
|
|
function normalizeTradingTimeSetting(value: unknown, fallback: string): string {
|
|
const raw = String(value ?? '').trim()
|
|
return raw || fallback
|
|
}
|
|
|
|
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 normalized = normalizeTradingTimeSetting(time, '')
|
|
if (!normalized) {
|
|
return ''
|
|
}
|
|
|
|
const parts = normalized.split(':')
|
|
if (parts.length >= 3) {
|
|
return `${parts[0]}:${parts[1]}:${parts[2]}`
|
|
}
|
|
if (parts.length >= 2) {
|
|
return `${parts[0]}:${parts[1]}:00`
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
function hasProductTradingWindow(start?: string | null, end?: string | null): boolean {
|
|
const open = formatTradingTimeDisplay(normalizeTradingTimeSetting(start, '00:00:00'))
|
|
const close = formatTradingTimeDisplay(normalizeTradingTimeSetting(end, '00:00:00'))
|
|
if (!open || !close) {
|
|
return false
|
|
}
|
|
|
|
return open !== close
|
|
}
|
|
|
|
export function setProductTradingHours(start?: string | null, end?: string | null) {
|
|
if (!hasProductTradingWindow(start, end)) {
|
|
productTradingHours.value = null
|
|
productTradingHoursVersion.value += 1
|
|
return
|
|
}
|
|
|
|
productTradingHours.value = {
|
|
start: normalizeTradingTimeSetting(start, '00:00'),
|
|
end: normalizeTradingTimeSetting(end, '23:59'),
|
|
}
|
|
productTradingHoursVersion.value += 1
|
|
}
|
|
|
|
export function syncProductTradingHoursFromProducts(
|
|
products: Array<{ auto_open_at?: string | null; auto_close_at?: string | null }>,
|
|
) {
|
|
for (const product of products) {
|
|
if (hasProductTradingWindow(product.auto_open_at, product.auto_close_at)) {
|
|
setProductTradingHours(product.auto_open_at, product.auto_close_at)
|
|
return
|
|
}
|
|
}
|
|
|
|
setProductTradingHours(null, null)
|
|
}
|
|
|
|
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: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
|
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
|
closePriceEnabled,
|
|
}
|
|
}
|
|
|
|
export function getEffectiveTradingHours(): EffectiveTradingHours {
|
|
void productTradingHoursVersion.value
|
|
const settings = getStoreTradingHours()
|
|
if (settings.closePriceEnabled) {
|
|
return {
|
|
start: settings.start,
|
|
end: settings.end,
|
|
enforced: true,
|
|
}
|
|
}
|
|
|
|
if (productTradingHours.value?.start && productTradingHours.value?.end) {
|
|
return {
|
|
start: productTradingHours.value.start,
|
|
end: productTradingHours.value.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
|
|
}
|
|
|
|
return isOutsideStoreTradingHours(now, start, end)
|
|
}
|
|
|
|
export function isOutsideStoreTradingHours(
|
|
now = new Date(),
|
|
start?: string,
|
|
end?: string,
|
|
): boolean {
|
|
const hours = start != null && end != null
|
|
? { start, end }
|
|
: getEffectiveTradingHours()
|
|
|
|
const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()
|
|
const openSeconds = parseTimeToSeconds(hours.start)
|
|
const closeSeconds = parseTimeToSeconds(hours.end)
|
|
|
|
return currentSeconds < openSeconds || currentSeconds > closeSeconds
|
|
}
|
|
|
|
export function hasLivePriceQuote(): boolean {
|
|
const p = lastPrice.value
|
|
return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0
|
|
}
|
|
|
|
export function isStoreClosed(): boolean {
|
|
void productTradingHoursVersion.value
|
|
|
|
if (lastPrice.value?.closed === true) {
|
|
return true
|
|
}
|
|
|
|
return isStoreClosedBySchedule()
|
|
}
|
|
|
|
export const STORE_CLOSED_TITLE = 'فروشگاه بسته است'
|
|
|
|
export function stripHtmlMessage(message: string): string {
|
|
return message.replace(/<[^>]*>/g, '').trim()
|
|
}
|
|
|
|
export function getStoreClosedHint(customMessage?: unknown): string {
|
|
const custom = typeof customMessage === 'string' ? customMessage.trim() : ''
|
|
if (custom) {
|
|
return stripHtmlMessage(custom)
|
|
}
|
|
return ''
|
|
}
|
|
|
|
export function getStoreClosedMessage(): string {
|
|
return ''
|
|
}
|
|
|
|
/** برای toast/اعلان: پیام ادمین یا عنوان پیشفرض */
|
|
export function resolveStoreClosedNotice(customMessage?: unknown): string {
|
|
return getStoreClosedHint(customMessage) || STORE_CLOSED_TITLE
|
|
}
|
|
|
|
export function isMarketClosedForDisplay(): boolean {
|
|
return getPriceDisplayState() === 'closed'
|
|
}
|
|
|
|
export function shouldBlockStoreTrading(): boolean {
|
|
return isMarketClosedForDisplay()
|
|
}
|
|
|
|
export type PriceDisplayState = 'open' | 'closed' | 'no_quote'
|
|
|
|
export const NO_QUOTE_MESSAGE = 'مظنه در دسترس نیست'
|
|
|
|
/**
|
|
* وضعیت نمایش قیمت:
|
|
* - closed → فروشگاه بسته است (ساعت کاری، سیگنال سرور، یا نبود مظنه)
|
|
* - open → نمایش عادی قیمت
|
|
*/
|
|
export function getPriceDisplayState(): PriceDisplayState {
|
|
void productTradingHoursVersion.value
|
|
const p = lastPrice.value
|
|
|
|
if (p?.status === 'closed' || p?.closed === true) {
|
|
return 'closed'
|
|
}
|
|
|
|
if (isStoreClosed()) {
|
|
return 'closed'
|
|
}
|
|
|
|
if (!hasLivePriceQuote()) {
|
|
return 'closed'
|
|
}
|
|
|
|
return 'open'
|
|
}
|
|
|
|
export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string {
|
|
if (state === 'closed') {
|
|
return getStoreClosedMessage()
|
|
}
|
|
if (state === 'no_quote') {
|
|
return NO_QUOTE_MESSAGE
|
|
}
|
|
return ''
|
|
}
|