From d5f0bf84efc7436d835d195b62312b1d33bf79a9 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 3 Aug 2026 20:51:33 +0330 Subject: [PATCH] Add Hamkar UI reset-to-default snapshot and restore button. Co-authored-by: Cursor --- src/config/defaultHamkarUiConfig.ts | 148 ++++++++++++++++++++-------- src/pages/HamkarUiConfigPage.vue | 140 +++++++++++++++++++++++--- 2 files changed, 236 insertions(+), 52 deletions(-) diff --git a/src/config/defaultHamkarUiConfig.ts b/src/config/defaultHamkarUiConfig.ts index e2f9e87..f9c10a9 100644 --- a/src/config/defaultHamkarUiConfig.ts +++ b/src/config/defaultHamkarUiConfig.ts @@ -2,6 +2,28 @@ export type HamkarPricingBoxTheme = '1' | '2' | '3' | '4' export type HamkarOrderUi = 'new' | 'old' export type HamkarPriceFontSize = 'کوچک' | 'متوسط' | 'بزرگ' | 'بزرگ‌تر' +export type HamkarBrandColors = { + /** Registry `color` → CSS `--bg2` (only when mode=dynamicMode) */ + primaryColor: string + /** Registry `secondary_color` → CSS `--colorPrimary` (dynamicMode) */ + secondaryColor: string + /** Registry `background_color` → CSS `--bg` (dynamicMode) */ + backgroundColor: string +} + +export type HamkarUiSettings = { + pricing_box_theme: HamkarPricingBoxTheme + price_font_size: HamkarPriceFontSize + orderUI: HamkarOrderUi +} + +/** Immutable factory look — set once, never overwritten on normal save */ +export type HamkarAppearanceSnapshot = { + registryMode: string + brand: HamkarBrandColors + settings: HamkarUiSettings +} + export type HamkarUiConfig = { kind: 'hamkar' /** @@ -11,19 +33,10 @@ export type HamkarUiConfig = { settingsManaged: boolean /** Named CSS mode from live registry (Montpar, limangold, …) or dynamicMode */ registryMode: string - brand: { - /** Registry `color` → CSS `--bg2` (only when mode=dynamicMode) */ - primaryColor: string - /** Registry `secondary_color` → CSS `--colorPrimary` (dynamicMode) */ - secondaryColor: string - /** Registry `background_color` → CSS `--bg` (dynamicMode) */ - backgroundColor: string - } - settings: { - pricing_box_theme: HamkarPricingBoxTheme - price_font_size: HamkarPriceFontSize - orderUI: HamkarOrderUi - } + brand: HamkarBrandColors + settings: HamkarUiSettings + /** Original appearance before SuperAdmin color edits (reset target) */ + defaultSnapshot?: HamkarAppearanceSnapshot } export const HAMKAR_PRICING_BOX_THEME_OPTIONS: { @@ -58,6 +71,34 @@ function asColor(value: unknown): string { return s } +function normalizeSettings(raw: unknown): HamkarUiSettings { + const base = createDefaultHamkarUiConfig().settings + const settings = raw && typeof raw === 'object' ? (raw as Record) : {} + const theme = String(settings.pricing_box_theme ?? base.pricing_box_theme) + const orderUI = String(settings.orderUI ?? base.orderUI) + const font = String(settings.price_font_size ?? base.price_font_size) + return { + pricing_box_theme: (['1', '2', '3', '4'].includes(theme) + ? theme + : base.pricing_box_theme) as HamkarPricingBoxTheme, + orderUI: (orderUI === 'old' || orderUI === 'new' + ? orderUI + : base.orderUI) as HamkarOrderUi, + price_font_size: (HAMKAR_PRICE_FONT_SIZE_OPTIONS.some((o) => o.value === font) + ? font + : base.price_font_size) as HamkarPriceFontSize, + } +} + +function normalizeBrand(raw: unknown): HamkarBrandColors { + const brand = raw && typeof raw === 'object' ? (raw as Record) : {} + return { + primaryColor: asColor(brand.primaryColor), + secondaryColor: asColor(brand.secondaryColor), + backgroundColor: asColor(brand.backgroundColor), + } +} + export function createDefaultHamkarUiConfig(): HamkarUiConfig { return { kind: 'hamkar', @@ -76,55 +117,74 @@ export function createDefaultHamkarUiConfig(): HamkarUiConfig { } } +export function cloneHamkarSnapshot(config: { + registryMode: string + brand: HamkarBrandColors + settings: HamkarUiSettings +}): HamkarAppearanceSnapshot { + return { + registryMode: String(config.registryMode || 'dynamicMode').trim() || 'dynamicMode', + brand: { ...config.brand }, + settings: { ...config.settings }, + } +} + export function normalizeHamkarUiConfig(raw: unknown): HamkarUiConfig { const base = createDefaultHamkarUiConfig() if (!raw || typeof raw !== 'object') return base const input = raw as Record - const brand = input.brand && typeof input.brand === 'object' ? input.brand : {} - const settings = - input.settings && typeof input.settings === 'object' ? input.settings : {} - - const theme = String(settings.pricing_box_theme ?? base.settings.pricing_box_theme) - const orderUI = String(settings.orderUI ?? base.settings.orderUI) - const font = String(settings.price_font_size ?? base.settings.price_font_size) const registryMode = String(input.registryMode || base.registryMode).trim() || 'dynamicMode' + const settings = normalizeSettings(input.settings) + const brand = normalizeBrand(input.brand) + + let defaultSnapshot: HamkarAppearanceSnapshot | undefined + if (input.defaultSnapshot && typeof input.defaultSnapshot === 'object') { + const snap = input.defaultSnapshot as Record + defaultSnapshot = { + registryMode: String(snap.registryMode || registryMode).trim() || 'dynamicMode', + brand: normalizeBrand(snap.brand), + settings: normalizeSettings(snap.settings ?? settings), + } + } return { kind: 'hamkar', settingsManaged: Boolean(input.settingsManaged), registryMode, - brand: { - // Do not invent colors — empty means null in registry/get_user - primaryColor: asColor(brand.primaryColor), - secondaryColor: asColor(brand.secondaryColor), - backgroundColor: asColor(brand.backgroundColor), - }, - settings: { - pricing_box_theme: (['1', '2', '3', '4'].includes(theme) - ? theme - : base.settings.pricing_box_theme) as HamkarPricingBoxTheme, - orderUI: (orderUI === 'old' || orderUI === 'new' - ? orderUI - : base.settings.orderUI) as HamkarOrderUi, - price_font_size: (HAMKAR_PRICE_FONT_SIZE_OPTIONS.some((o) => o.value === font) - ? font - : base.settings.price_font_size) as HamkarPriceFontSize, - }, + brand, + settings, + defaultSnapshot, } } -/** Payload for API: empty color strings → null */ +/** Payload for API: empty color strings → null; always keep defaultSnapshot if present */ export function hamkarUiToApiPayload(config: HamkarUiConfig) { const normalized = normalizeHamkarUiConfig(config) const emptyToNull = (v: string) => (v.trim() ? v.trim() : null) + const snap = normalized.defaultSnapshot + ? { + registryMode: normalized.defaultSnapshot.registryMode, + brand: { + primaryColor: emptyToNull(normalized.defaultSnapshot.brand.primaryColor), + secondaryColor: emptyToNull(normalized.defaultSnapshot.brand.secondaryColor), + backgroundColor: emptyToNull(normalized.defaultSnapshot.brand.backgroundColor), + }, + settings: { ...normalized.defaultSnapshot.settings }, + } + : undefined + return { - ...normalized, + kind: 'hamkar' as const, + settingsManaged: normalized.settingsManaged, + registryMode: normalized.registryMode, brand: { primaryColor: emptyToNull(normalized.brand.primaryColor), secondaryColor: emptyToNull(normalized.brand.secondaryColor), backgroundColor: emptyToNull(normalized.brand.backgroundColor), }, + settings: { ...normalized.settings }, + ...(snap ? { defaultSnapshot: snap } : {}), } } @@ -137,3 +197,13 @@ export function hamkarUiToSettingsList(config: HamkarUiConfig) { { key: 'orderUI', value: config.settings.orderUI }, ] } + +/** Resolve original mode from tenant notes (`registryMode:X` or `mode:X`). */ +export function registryModeFromNotes(notes?: string | null): string | null { + if (!notes) return null + return ( + notes.match(/registryMode:([^\s|]+)/)?.[1] || + notes.match(/(?:^|\|)\s*mode:([^\s|]+)/)?.[1] || + null + ) +} diff --git a/src/pages/HamkarUiConfigPage.vue b/src/pages/HamkarUiConfigPage.vue index ee31c13..80a8e6c 100644 --- a/src/pages/HamkarUiConfigPage.vue +++ b/src/pages/HamkarUiConfigPage.vue @@ -1,16 +1,18 @@ @@ -232,6 +323,9 @@ onMounted(load)

رنگ‌ها

+

+ ظاهر پیش‌فرض ثبت‌شده: {{ defaultModeLabel }} +

+ + بازگشت به ظاهر اولیه + ذخیره ظاهر همکار @@ -340,6 +443,12 @@ onMounted(load) gap: 14px; } +.hint { + margin: 0 0 10px; + color: #64748b; + font-size: 13px; +} + .preview { padding: 16px; } @@ -466,4 +575,9 @@ onMounted(load) .palette-hex { font-family: ui-monospace, monospace; } + +.sticky-actions { + display: grid; + gap: 8px; +}