From 6bbd8704b89b1933d6f5afc6ab346981e9bdeae4 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 24 Aug 2026 09:48:27 +0330 Subject: [PATCH] setting pushed Co-authored-by: Cursor --- src/api/automations.ts | 24 + src/components/BasicSelect.vue | 4 +- src/model/automations.ts | 100 +++ .../components/AutomationEditorFormBody.vue | 736 ++++++++++++++++++ .../automations/components/table.vue | 121 +++ .../automations/hooks/automationsColumns.tsx | 93 +++ src/pages/menuPages/automations/index.vue | 132 ++++ src/pages/menuPages/configuration/index.vue | 4 + .../menuPages/configuration/settingGroups.ts | 9 - .../menuPages/configuration/settingTypes.ts | 25 + .../configuration/tradingHoursKeys.ts | 63 ++ .../components/TradingHoursDayRow.vue | 196 +++++ .../components/TradingHoursTimeField.vue | 56 ++ src/pages/menuPages/tradingHours/index.vue | 410 ++++++++++ .../tradingHours/useTradingHoursPage.ts | 277 +++++++ src/router/routes/setting.ts | 39 +- src/utils/adminHelpCopy.ts | 63 ++ 17 files changed, 2338 insertions(+), 14 deletions(-) create mode 100644 src/api/automations.ts create mode 100644 src/model/automations.ts create mode 100644 src/pages/menuPages/automations/components/AutomationEditorFormBody.vue create mode 100644 src/pages/menuPages/automations/components/table.vue create mode 100644 src/pages/menuPages/automations/hooks/automationsColumns.tsx create mode 100644 src/pages/menuPages/automations/index.vue create mode 100644 src/pages/menuPages/configuration/tradingHoursKeys.ts create mode 100644 src/pages/menuPages/tradingHours/components/TradingHoursDayRow.vue create mode 100644 src/pages/menuPages/tradingHours/components/TradingHoursTimeField.vue create mode 100644 src/pages/menuPages/tradingHours/index.vue create mode 100644 src/pages/menuPages/tradingHours/useTradingHoursPage.ts diff --git a/src/api/automations.ts b/src/api/automations.ts new file mode 100644 index 0000000..771f0d2 --- /dev/null +++ b/src/api/automations.ts @@ -0,0 +1,24 @@ +import type { TableInput } from '@core/type/baseTable' +import useAxios from '@core/utils/useAxios' +import type { Automation, AutomationCatalog, AutomationCondition } from '@/model/automations' + +export const getAutomationsTable = async (params: TableInput) => + await useAxios.post>('/admin/automations/table', params) + +export const getAutomationCatalog = async () => + await useAxios.get('/admin/automations/catalog') + +export const previewAutomationAudience = async (conditions: AutomationCondition[]) => + await useAxios.post<{ count: number }>('/admin/automations/preview', { conditions }) + +export const createAutomation = async (payload: Partial) => + await useAxios.post('/admin/automations', payload) + +export const updateAutomation = async (id: number, payload: Partial) => + await useAxios.put(`/admin/automations/${id}`, payload) + +export const toggleAutomationActive = async (id: number) => + await useAxios.put<{ is_active: boolean }>(`/admin/automations/${id}/toggle`) + +export const deleteAutomation = async (id: number) => + await useAxios.delete(`/admin/automations/${id}`) diff --git a/src/components/BasicSelect.vue b/src/components/BasicSelect.vue index 90e4c1a..c1072c1 100644 --- a/src/components/BasicSelect.vue +++ b/src/components/BasicSelect.vue @@ -32,9 +32,9 @@ -
{{ val[props.valueName] }}
+
{{ val[props.valueName] }}
-
{{ val[props.valueName] }}
+
{{ val[props.valueName] }}
diff --git a/src/model/automations.ts b/src/model/automations.ts new file mode 100644 index 0000000..d8c050b --- /dev/null +++ b/src/model/automations.ts @@ -0,0 +1,100 @@ +export type AutomationTriggerType = 'schedule' | 'event' | 'price' +export type AutomationChannel = 'sms' | 'in_app' +export type AutomationConditionCr = 'and' | 'or' + +export interface AutomationCondition { + cr?: AutomationConditionCr + field: string + op?: string | null + value?: unknown +} + +export type AutomationAudienceMode = 'all' | 'filter' +export type AutomationPriceWindow = 'calendar_day' | 'from_last' + +export interface AutomationTriggerConfig { + event?: string + delay_hours?: number + every_days?: number + step_toman?: number | string | null + direction?: 'up' | 'down' | 'both' + product_id?: number | null + price_window?: AutomationPriceWindow +} + +export interface Automation { + id?: number + name: string + code?: string + is_active: boolean + starts_at?: string | null + ends_at?: string | null + sms_send_hour?: number | null + in_app_send_hour?: number | null + trigger_type: AutomationTriggerType + trigger_config?: AutomationTriggerConfig + audience_mode?: AutomationAudienceMode + conditions?: AutomationCondition[] + channels: AutomationChannel[] + title: string + body: string + cooldown_hours?: number + max_sends?: number | null + sent_count?: number + failed_count?: number +} + +export interface AutomationCatalogOption { + key: string | number + name: string +} + +export interface AutomationFieldInput { + key: string + type: 'number' | 'select' | 'operator' + label: string + options?: AutomationCatalogOption[] +} + +export interface AutomationField { + key: string + group: string + name: string + inputs: AutomationFieldInput[] +} + +export interface AutomationCatalog { + triggers: AutomationCatalogOption[] + events: AutomationCatalogOption[] + channels: AutomationCatalogOption[] + operators: AutomationCatalogOption[] + fields: AutomationField[] + products: Array<{ id: number; name: string }> + hours: AutomationCatalogOption[] + price_windows?: AutomationCatalogOption[] + audience_modes?: AutomationCatalogOption[] +} + +export function emptyAutomation(): Automation { + return { + name: '', + title: '', + body: '', + is_active: true, + trigger_type: 'schedule', + audience_mode: 'all', + trigger_config: { + delay_hours: 0, + every_days: 1, + direction: 'both', + price_window: 'calendar_day', + }, + conditions: [], + channels: ['sms'], + sms_send_hour: 10, + in_app_send_hour: 10, + cooldown_hours: 24, + starts_at: null, + ends_at: null, + } +} diff --git a/src/pages/menuPages/automations/components/AutomationEditorFormBody.vue b/src/pages/menuPages/automations/components/AutomationEditorFormBody.vue new file mode 100644 index 0000000..2e5816a --- /dev/null +++ b/src/pages/menuPages/automations/components/AutomationEditorFormBody.vue @@ -0,0 +1,736 @@ + + + + + diff --git a/src/pages/menuPages/automations/components/table.vue b/src/pages/menuPages/automations/components/table.vue new file mode 100644 index 0000000..681b811 --- /dev/null +++ b/src/pages/menuPages/automations/components/table.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/pages/menuPages/automations/hooks/automationsColumns.tsx b/src/pages/menuPages/automations/hooks/automationsColumns.tsx new file mode 100644 index 0000000..5970b96 --- /dev/null +++ b/src/pages/menuPages/automations/hooks/automationsColumns.tsx @@ -0,0 +1,93 @@ +import type { BaseTableColumn } from '@core/type/baseTable' + +const triggerLabel: Record = { + schedule: 'زمان‌دار', + event: 'رویداد', + price: 'حرکت قیمت', +} + +function channelText(record: any): string { + const ch = Array.isArray(record.channels) ? record.channels : [] + const parts: string[] = [] + if (ch.includes('sms')) parts.push('پیامک') + if (ch.includes('in_app')) parts.push('اعلان') + return parts.join(' / ') || '—' +} + +function previewBody(text: unknown): string { + const s = String(text || '').replace(/\s+/g, ' ').trim() + if (!s) return '—' + return s.length > 48 ? `${s.slice(0, 48)}…` : s +} + +export const cols: BaseTableColumn[] = [ + { + title: 'ردیف', + dataIndex: 'id', + key: 'id', + customRender: (record: any) => record.index + 1, + }, + { + title: 'نام', + dataIndex: 'name', + key: 'name', + type: 'Text', + }, + { + title: 'کد', + dataIndex: 'code', + key: 'code', + }, + { + title: 'عنوان', + dataIndex: 'title', + key: 'title', + type: 'Text', + }, + { + title: 'متن', + dataIndex: 'body', + key: 'body', + customRender: ({ record }) => previewBody(record.body), + }, + { + title: 'نوع', + dataIndex: 'trigger_type', + key: 'trigger_type', + customRender: ({ record }) => triggerLabel[record.trigger_type] || record.trigger_type, + }, + { + title: 'کانال', + dataIndex: 'channels', + key: 'channels', + customRender: ({ record }) => channelText(record), + }, + { + title: 'فعال', + dataIndex: 'is_active', + key: 'is_active', + slot: 'is_active', + width: 90, + }, + { + title: 'ارسال شده', + dataIndex: 'sent_count', + key: 'sent_count', + customRender: ({ record }) => Number(record.sent_count ?? 0), + }, + { + title: 'ناموفق', + dataIndex: 'failed_count', + key: 'failed_count', + customRender: ({ record }) => Number(record.failed_count ?? 0), + }, + { + title: 'عملیات', + dataIndex: 'action', + key: 'action', + slot: 'action', + fixed: 'right', + filterable: false, + width: 120, + }, +] diff --git a/src/pages/menuPages/automations/index.vue b/src/pages/menuPages/automations/index.vue new file mode 100644 index 0000000..3e36209 --- /dev/null +++ b/src/pages/menuPages/automations/index.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/pages/menuPages/configuration/index.vue b/src/pages/menuPages/configuration/index.vue index c9fd6e7..d6c12fe 100644 --- a/src/pages/menuPages/configuration/index.vue +++ b/src/pages/menuPages/configuration/index.vue @@ -215,12 +215,14 @@ import { helpText, isSettingVisible, isCodeManagedSettingKey, + isSuperAdminOwnedSettingKey, isSettingLocked, isSettingActive, isSwitchOn, normalizeSettingRow, resolveSettingFieldType } from './settingTypes' +import { TRADING_HOURS_SETTING_KEY_SET } from './tradingHoursKeys' const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), { mode: 'editable' @@ -309,6 +311,7 @@ const filteredRows = computed(() => (baseData.value || []).filter((row) => { if (row.id == null || !matchesFilter(row)) return false if (!isSettingVisible(row.key)) return false + if (!isSystemMode.value && TRADING_HOURS_SETTING_KEY_SET.has(row.key || '')) return false if (isSystemMode.value) return true return isEditable(row) }) @@ -429,6 +432,7 @@ function rowColSpan(row: Const): number { function isReadonly(row: Const): boolean { if (isCodeManagedSettingKey(row.key)) return true + if (isSuperAdminOwnedSettingKey(row.key)) return true if (isSystemMode.value) return false return isSettingLocked(row.can_edit) } diff --git a/src/pages/menuPages/configuration/settingGroups.ts b/src/pages/menuPages/configuration/settingGroups.ts index 55e17a6..fdaa33a 100644 --- a/src/pages/menuPages/configuration/settingGroups.ts +++ b/src/pages/menuPages/configuration/settingGroups.ts @@ -31,15 +31,6 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [ match: (k) => /^(banner_enabled|tracking_systems_enabled|jibit_verification|register_jibit_verify|show_product_details|show_product_trade_status|show_product_card_meta|products_site_link_enabled|products_site_link_url|products_site_link_text|has_chat)$/.test(k) }, - { - id: 'trading_hours', - title: 'ساعت کاری و نمایش قیمت', - icon: 'vuesax-linear:clock', - match: (k) => - /^(close_price|startDay|endDay|show_last_price|show_home_price_when_closed|show_gold_ounce_price)$/.test( - k - ) - }, { id: 'fees', title: 'کارمزدها', diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index edd1ff3..ccebad8 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -199,6 +199,31 @@ export function isCodeManagedSettingKey(key?: string | null): boolean { return SETTING_CODE_MANAGED_KEYS.has(String(key ?? '').trim()) } +/** + * دسترسی مشتری به قابلیت‌ها — مالک: سوپرادمین. + * در پنل خانگی دیده می‌شوند ولی ذخیره‌شان ۴۰۳ می‌گیرد، پس فقط‌خواندنی‌اند. + * هم‌تراز SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS در سرور. + */ +export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([ + 'customer_direct_sms', + 'customer_special_pricing', + 'enable_conditional_order', + 'enable_customer_level_profit', + 'enable_product_funds', + 'group_sms', + 'has_page_earn_money', + 'has_page_gift_card', + 'has_page_notification', + 'has_page_price_trigger', + 'jibit_enabled', + 'permision__enabled', + 'show_to_users_chat', +]) + +export function isSuperAdminOwnedSettingKey(key?: string | null): boolean { + return SUPERADMIN_OWNED_SETTING_KEYS.has(String(key ?? '').trim()) +} + /** کلیدهای legacy — در پنل نمایش داده نمی‌شوند */ export const HIDDEN_LEGACY_SETTING_KEYS = new Set([ 'main_product_price', diff --git a/src/pages/menuPages/configuration/tradingHoursKeys.ts b/src/pages/menuPages/configuration/tradingHoursKeys.ts new file mode 100644 index 0000000..9412105 --- /dev/null +++ b/src/pages/menuPages/configuration/tradingHoursKeys.ts @@ -0,0 +1,63 @@ +/** کلیدهای تنظیمات ساعت کاری — فقط در صفحه «ساعت کاری» ویرایش می‌شوند */ +export const TRADING_HOURS_SETTING_KEYS = [ + 'close_price', + 'startDay', + 'endDay', + 'show_last_price', + 'show_home_price_when_closed', + 'show_gold_ounce_price', + 'trading_hours_by_day', +] as const + +export type TradingHoursSettingKey = (typeof TRADING_HOURS_SETTING_KEYS)[number] + +export const TRADING_HOURS_SETTING_KEY_SET = new Set(TRADING_HOURS_SETTING_KEYS) + +export type WeekdayKey = 'sat' | 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' + +export type DaySchedule = { + enabled: boolean + start: string + end: string +} + +export type WeeklyScheduleDraft = { + mode: 'uniform' | 'per_day' + days: Record +} + +export const WEEKDAY_DEFS: { key: WeekdayKey; label: string; short: string }[] = [ + { key: 'sat', label: 'شنبه', short: 'ش' }, + { key: 'sun', label: 'یکشنبه', short: 'ی' }, + { key: 'mon', label: 'دوشنبه', short: 'د' }, + { key: 'tue', label: 'سه‌شنبه', short: 'س' }, + { key: 'wed', label: 'چهارشنبه', short: 'چ' }, + { key: 'thu', label: 'پنجشنبه', short: 'پ' }, + { key: 'fri', label: 'جمعه', short: 'ج' }, +] + +export const DEFAULT_DAY_START = '09:00:00' +export const DEFAULT_DAY_END = '22:00:00' + +export function defaultDaySchedule(start = DEFAULT_DAY_START, end = DEFAULT_DAY_END): DaySchedule { + return { enabled: true, start, end } +} + +export function defaultWeeklySchedule( + start = DEFAULT_DAY_START, + end = DEFAULT_DAY_END +): WeeklyScheduleDraft { + const day = defaultDaySchedule(start, end) + return { + mode: 'uniform', + days: { + sat: { ...day }, + sun: { ...day }, + mon: { ...day }, + tue: { ...day }, + wed: { ...day }, + thu: { ...day }, + fri: { ...day }, + }, + } +} diff --git a/src/pages/menuPages/tradingHours/components/TradingHoursDayRow.vue b/src/pages/menuPages/tradingHours/components/TradingHoursDayRow.vue new file mode 100644 index 0000000..08c10cf --- /dev/null +++ b/src/pages/menuPages/tradingHours/components/TradingHoursDayRow.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/pages/menuPages/tradingHours/components/TradingHoursTimeField.vue b/src/pages/menuPages/tradingHours/components/TradingHoursTimeField.vue new file mode 100644 index 0000000..2b2112e --- /dev/null +++ b/src/pages/menuPages/tradingHours/components/TradingHoursTimeField.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/pages/menuPages/tradingHours/index.vue b/src/pages/menuPages/tradingHours/index.vue new file mode 100644 index 0000000..89def9b --- /dev/null +++ b/src/pages/menuPages/tradingHours/index.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/src/pages/menuPages/tradingHours/useTradingHoursPage.ts b/src/pages/menuPages/tradingHours/useTradingHoursPage.ts new file mode 100644 index 0000000..19b2e89 --- /dev/null +++ b/src/pages/menuPages/tradingHours/useTradingHoursPage.ts @@ -0,0 +1,277 @@ +import { debounce } from 'lodash' +import { computed, ref } from 'vue' +import type { Const } from '@/model/configuration' +import { getEditableConfiguration, setConfiguration } from '@/api/configuration' +import { + formatValueForSave, + isSwitchOn, + normalizeTimeValue, + resolveSettingFieldType, + resolveSettingTitle, + helpText, +} from '@/pages/menuPages/configuration/settingTypes' +import { + DEFAULT_DAY_END, + DEFAULT_DAY_START, + TRADING_HOURS_SETTING_KEY_SET, + WEEKDAY_DEFS, + defaultWeeklySchedule, + type DaySchedule, + type WeekdayKey, + type WeeklyScheduleDraft, +} from '@/pages/menuPages/configuration/tradingHoursKeys' + +function parseWeeklyJson(raw?: string | null): WeeklyScheduleDraft | null { + if (!raw?.trim()) return null + try { + const parsed = JSON.parse(raw) as Partial + if (parsed.mode !== 'uniform' && parsed.mode !== 'per_day') return null + if (!parsed.days || typeof parsed.days !== 'object') return null + const base = defaultWeeklySchedule() + for (const def of WEEKDAY_DEFS) { + const row = (parsed.days as Record>)[def.key] + if (!row) continue + base.days[def.key] = { + enabled: row.enabled !== false, + start: normalizeTimeValue(row.start || base.days[def.key].start) || DEFAULT_DAY_START, + end: normalizeTimeValue(row.end || base.days[def.key].end) || DEFAULT_DAY_END, + } + } + base.mode = parsed.mode + return base + } catch { + return null + } +} + +function serializeWeekly(draft: WeeklyScheduleDraft): string { + return JSON.stringify(draft) +} + +function applyUniformTimes(draft: WeeklyScheduleDraft, start: string, end: string) { + for (const def of WEEKDAY_DEFS) { + draft.days[def.key] = { + ...draft.days[def.key], + start, + end, + } + } +} + +export function useTradingHoursPage() { + const loading = ref(true) + const rowsByKey = ref>({}) + const drafts = ref>({}) + const savingKeys = ref>(new Set()) + const savedFlashKeys = ref>(new Set()) + const weekly = ref(defaultWeeklySchedule()) + const uniformStart = ref(DEFAULT_DAY_START) + const uniformEnd = ref(DEFAULT_DAY_END) + + const debouncedSave = new Map>() + const saveTimers = new Map>() + + const closePriceEnabled = computed(() => isSwitchOn(draftValue('close_price'))) + + function draftValue(key: string): string { + const row = rowsByKey.value[key] + if (!row?.id) return '' + return drafts.value[String(row.id)] ?? row.value ?? '' + } + + function rowMeta(key: string) { + const row = rowsByKey.value[key] + return { + title: row ? resolveSettingTitle(row) : key, + help: row ? helpText(row) : '', + exists: Boolean(row?.id), + } + } + + function hydrateWeeklyFromRows() { + const start = normalizeTimeValue(draftValue('startDay') || DEFAULT_DAY_START) || DEFAULT_DAY_START + const end = normalizeTimeValue(draftValue('endDay') || DEFAULT_DAY_END) || DEFAULT_DAY_END + uniformStart.value = start + uniformEnd.value = end + + const fromJson = parseWeeklyJson(draftValue('trading_hours_by_day')) + if (fromJson) { + weekly.value = fromJson + return + } + + weekly.value = defaultWeeklySchedule(start, end) + } + + async function load() { + loading.value = true + try { + const rows = await getEditableConfiguration() + const map: Record = {} + const draftMap: Record = {} + for (const row of rows || []) { + if (!row.key || !TRADING_HOURS_SETTING_KEY_SET.has(row.key)) continue + map[row.key] = row + if (row.id != null) draftMap[String(row.id)] = row.value ?? '' + } + rowsByKey.value = map + drafts.value = draftMap + hydrateWeeklyFromRows() + } finally { + loading.value = false + } + } + + function markSavedFlash(key: string) { + savedFlashKeys.value.add(key) + if (saveTimers.has(key)) clearTimeout(saveTimers.get(key)) + saveTimers.set( + key, + setTimeout(() => { + savedFlashKeys.value.delete(key) + }, 1800) + ) + } + + async function persistKey(key: string, rawValue: string) { + const row = rowsByKey.value[key] + const type = resolveSettingFieldType(row ?? { key, type: 'text' }) + const value = formatValueForSave(rawValue, type, key) + + if (row?.id) { + const current = row.value ?? '' + if (value === current && drafts.value[String(row.id)] === value) return + } + + savingKeys.value.add(key) + savedFlashKeys.value.delete(key) + + try { + const payload: Const = row?.id + ? { + id: row.id, + key, + value, + description: row.description ?? '', + is_active: true, + title_fa: row.title_fa ?? '', + type, + } + : { + key, + value, + description: '', + is_active: true, + title_fa: key === 'trading_hours_by_day' ? 'برنامه هفتگی ساعت کاری' : '', + type: key === 'trading_hours_by_day' ? 'longtext' : 'text', + } + + const saved = await setConfiguration(payload) + rowsByKey.value[key] = { ...rowsByKey.value[key], ...saved, key } + if (saved.id != null) { + drafts.value[String(saved.id)] = value + } + markSavedFlash(key) + } finally { + savingKeys.value.delete(key) + } + } + + function schedulePersist(key: string, value: string, options?: { flush?: boolean }) { + if (!debouncedSave.has(key)) { + debouncedSave.set( + key, + debounce((nextValue: string) => { + void persistKey(key, nextValue) + }, key === 'trading_hours_by_day' ? 500 : 350) + ) + } + const fn = debouncedSave.get(key)! + if (options?.flush) { + fn.cancel() + void persistKey(key, value) + return + } + fn(value) + } + + function setDraftByKey(key: string, value: string, options?: { flush?: boolean }) { + const row = rowsByKey.value[key] + if (row?.id != null) { + drafts.value[String(row.id)] = value + } + schedulePersist(key, value, options) + } + + function setSwitch(key: string, checked: boolean) { + setDraftByKey(key, checked ? 'true' : 'false', { flush: true }) + } + + function setUniformTimes(start: string, end: string) { + const normalizedStart = normalizeTimeValue(start) || DEFAULT_DAY_START + const normalizedEnd = normalizeTimeValue(end) || DEFAULT_DAY_END + uniformStart.value = normalizedStart + uniformEnd.value = normalizedEnd + applyUniformTimes(weekly.value, normalizedStart, normalizedEnd) + weekly.value.mode = 'uniform' + setDraftByKey('startDay', normalizedStart, { flush: true }) + setDraftByKey('endDay', normalizedEnd, { flush: true }) + setDraftByKey('trading_hours_by_day', serializeWeekly(weekly.value), { flush: true }) + } + + function setScheduleMode(mode: 'uniform' | 'per_day') { + weekly.value.mode = mode + if (mode === 'uniform') { + applyUniformTimes(weekly.value, uniformStart.value, uniformEnd.value) + } + setDraftByKey('trading_hours_by_day', serializeWeekly(weekly.value), { flush: true }) + } + + function syncLegacyBoundsFromWeekly() { + const enabled = WEEKDAY_DEFS.map((def) => weekly.value.days[def.key]).filter((day) => day.enabled) + if (!enabled.length) return + const starts = [...enabled].sort((a, b) => a.start.localeCompare(b.start)) + const ends = [...enabled].sort((a, b) => a.end.localeCompare(b.end)) + uniformStart.value = starts[0].start + uniformEnd.value = ends[ends.length - 1].end + setDraftByKey('startDay', uniformStart.value, { flush: true }) + setDraftByKey('endDay', uniformEnd.value, { flush: true }) + } + + function setDaySchedule(day: WeekdayKey, patch: Partial) { + weekly.value.days[day] = { + ...weekly.value.days[day], + ...patch, + start: normalizeTimeValue(patch.start ?? weekly.value.days[day].start) || DEFAULT_DAY_START, + end: normalizeTimeValue(patch.end ?? weekly.value.days[day].end) || DEFAULT_DAY_END, + } + weekly.value.mode = 'per_day' + setDraftByKey('trading_hours_by_day', serializeWeekly(weekly.value)) + syncLegacyBoundsFromWeekly() + } + + function isSaving(key: string) { + return savingKeys.value.has(key) + } + + function isSavedFlash(key: string) { + return savedFlashKeys.value.has(key) + } + + return { + loading, + closePriceEnabled, + weekly, + uniformStart, + uniformEnd, + draftValue, + rowMeta, + load, + setSwitch, + setUniformTimes, + setScheduleMode, + setDaySchedule, + isSaving, + isSavedFlash, + } +} diff --git a/src/router/routes/setting.ts b/src/router/routes/setting.ts index 9fb3e5f..ad44ad6 100644 --- a/src/router/routes/setting.ts +++ b/src/router/routes/setting.ts @@ -1,5 +1,6 @@ import type { RouteRecordRaw } from 'vue-router' import { hasSmsPanelAccess } from '@/utils/smsPanelAccess' +import { isDeployAdmin } from '@/utils/deployAdmin' import { useUserStore } from '@/store/userStore' import { bannerEnabledSetting, trackingSystemsEnabledSetting, appNotificationEnabledSetting } from '@/utils/config' @@ -59,10 +60,22 @@ const Setting: RouteRecordRaw = { } }, { - name: 'advancedConfigurationRedirect', + name: 'advancedConfiguration', path: 'advanced-configuration', - redirect: { name: 'configuration' }, - meta: { hideMenu: true } + component: () => import('@/pages/menuPages/configuration/index.vue'), + props: { mode: 'system' }, + meta: { + deployAdminOnly: true, + icon: 'vuesax-linear:code-circle', + title: 'تنظیمات پیشرفته', + order: 2.5 + }, + beforeEnter() { + const userStore = useUserStore() + if (!isDeployAdmin(userStore.userInfo?.mobile)) { + return { name: 'configuration' } + } + } }, { name: 'smsPanelMain', @@ -81,6 +94,16 @@ const Setting: RouteRecordRaw = { } } }, + { + name: 'tradingHours', + path: 'trading-hours', + component: () => import('@/pages/menuPages/tradingHours/index.vue'), + meta: { + icon: 'vuesax-linear:clock', + title: 'ساعت کاری', + order: 3.6 + } + }, { name: 'banners', path: 'banners', @@ -111,6 +134,16 @@ const Setting: RouteRecordRaw = { } } }, + { + name: 'automations', + path: 'automations', + component: () => import('@/pages/menuPages/automations/index.vue'), + meta: { + icon: 'vuesax-linear:flash', + title: 'اتوماسیون', + order: 4.7 + } + }, { name: 'smsMain', path: 'sms', diff --git a/src/utils/adminHelpCopy.ts b/src/utils/adminHelpCopy.ts index c13c461..870cc19 100644 --- a/src/utils/adminHelpCopy.ts +++ b/src/utils/adminHelpCopy.ts @@ -107,3 +107,66 @@ export const physicalWithdrawApproveHelp = export const physicalWithdrawStatusHelp = 'پیگیری مراحل ارسال: در انتظار ارسال، ارسال شده، لغو شده، یا در انتظار تعیین مجدد قیمت (برای محصولاتی که وزن نهایی مشخص نیست).' + +export const automationAudienceHelp = + 'چه کسانی این پیام را بگیرند. «همه کاربران» یعنی همه مشتریان خانگی فعال. «با شرط» فقط کسانی که شرط‌های زیر را دارند.' + +export const automationNameHelp = + 'نام داخلی برای پیدا کردن این اتوماسیون در لیست پنل؛ به مشتری نشان داده نمی‌شود.' + +export const automationTitleHelp = + 'عنوانی که همراه پیامک یا اعلان داخل اپ برای مشتری می‌آید.' + +export const automationBodyHelp = + 'متن اصلی پیام. با دکمه‌های زیر فیلد می‌توانید %نام، %نام_کوچک و %موبایل را در محل کرسر بگذارید تا موقع ارسال با اطلاعات هر مشتری جایگزین شود.' + +export const automationTriggerTypeHelp = + 'زمان‌دار: طبق فاصلهٔ روزها. رویداد: وقتی اتفاقی مثل ثبت‌نام بیفتد. قیمت: وقتی قیمت به اندازهٔ مشخصی حرکت کند.' + +export const automationEveryDaysHelp = + 'هر چند روز یک‌بار این پیام برای مخاطبان واجد شرایط فرستاده می‌شود. مثلاً ۲۰ یعنی حدوداً هر ۲۰ روز یک‌بار برای هر نفر.' + +export const automationEventHelp = + 'کدام رویداد سیستم باعث شروع این اتوماسیون شود (مثلاً ثبت‌نام یا خرید).' + +export const automationEventTimingHelp = + 'سر ضرب: بلافاصله بعد از رویداد. با تأخیر: چند ساعت بعد از رویداد ارسال می‌شود.' + +export const automationDelayHoursHelp = + 'چند ساعت بعد از رویداد پیام برود. مثلاً ۲۴ یعنی یک روز بعد.' + +export const automationPriceWindowHelp = + 'در یک روز: تغییر قیمت از ابتدای همان روز تقویمی تا الان را می‌سنجد و در همان روز حداکثر یک‌بار می‌فرستد. از آخرین ارسال: پله‌پله از آخرین باری که برای همان نفر فرستاده شده حساب می‌کند.' + +export const automationPriceDirectionHelp = + 'بالا = فقط وقتی قیمت بالا رفته. پایین = فقط وقتی پایین آمده. هر دو = هر دو جهت.' + +export const automationPriceStepHelp = + 'حداقل رقم تغییر قیمت به تومان که باید رخ دهد تا پیام برود. مثلاً ۱۰۰۰۰۰ یعنی صد هزار تومان.' + +export const automationPriceProductHelp = + 'اگر خالی بگذارید قیمت تابلو/مرجع کلی استفاده می‌شود. اگر محصول انتخاب کنید، فقط حرکت قیمت همان محصول ملاک است.' + +export const automationCooldownHelp = + 'حداقل فاصلهٔ ساعت بین دو ارسال موفق به یک نفر برای این اتوماسیون. جلوی اسپم شدن همان مشتری را می‌گیرد.' + +export const automationMaxSendsHelp = + 'سقف تعداد دفعاتی که این اتوماسیون می‌تواند به یک نفر پیام بدهد. خالی = بدون سقف.' + +export const automationSmsHelp = + 'روشن = ارسال پیامک. اگر ساعت انتخاب کنید، ارسال تا آن ساعت نگه داشته می‌شود؛ اگر خالی باشد طبق زمان تریگر می‌رود.' + +export const automationInAppHelp = + 'روشن = اعلان داخل اپ مشتری. ساعت اختیاری مثل پیامک عمل می‌کند.' + +export const automationStartsAtHelp = + 'از این تاریخ و ساعت به بعد اتوماسیون می‌تواند اجرا شود. خالی = از همین الان.' + +export const automationEndsAtHelp = + 'بعد از این تاریخ و ساعت دیگر اجرا نمی‌شود. خالی = بدون پایان.' + +export const automationStatusHelp = + 'فعال = موتور اتوماسیون این قانون را اجرا می‌کند. غیرفعال = ذخیره می‌ماند ولی چیزی نمی‌فرستد.' + +export const tradingHoursPageIntro = + 'ساعت باز و بسته بودن فروشگاه و رفتار نمایش قیمت در اپ مشتری. می‌توانید برای همه روزها یک ساعت بگذارید یا برای هر روز هفته جداگانه تنظیم کنید.'