diff --git a/package-lock.json b/package-lock.json index a55ef2e..778cb65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,12 @@ "@ant-design/icons-vue": "^7.0.1", "ant-design-vue": "^4.2.6", "axios": "^1.18.1", + "dayjs": "^1.11.21", + "jalaliday": "^3.1.1", "pinia": "^3.0.4", "vue": "^3.5.18", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "vue3-persian-datetime-picker": "^1.2.2" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", @@ -1262,6 +1265,12 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", @@ -1271,6 +1280,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1302,6 +1321,12 @@ "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1336,7 +1361,7 @@ }, "node_modules/dayjs": { "version": "1.11.21", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, @@ -1563,6 +1588,12 @@ "node": ">= 6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1624,6 +1655,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -1694,6 +1746,23 @@ "node": ">= 6" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-plain-object": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz", @@ -1715,6 +1784,21 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/jalaali-js": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", + "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", + "license": "MIT" + }, + "node_modules/jalaliday": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/jalaliday/-/jalaliday-3.1.1.tgz", + "integrity": "sha512-exZzDOfp/32Gj2YkEHNhBBMcltBCTrIe8RXLTfpCvdjYHNfbYSQiNeFVf4RR59FV+2fixUOCEFfzOzyXfgenlg==", + "license": "MIT", + "peerDependencies": { + "dayjs": "^1.11.13" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2058,12 +2142,57 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-jalaali": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.9.6.tgz", + "integrity": "sha512-v8wXjQplvk5ez+sUqgsWIrafwIf1BEXXvzTYwsg1wHcqh27nSgKPCJ6FnZRrCz03MoNyB9N31L0oms+vE8Rq7g==", + "license": "MIT", + "dependencies": { + "jalaali-js": "^1.1.0", + "moment": "^2.22.2", + "moment-timezone": "^0.5.21", + "rimraf": "^3.0.2" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -2101,6 +2230,15 @@ "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==", "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -2108,6 +2246,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -2203,6 +2350,22 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", @@ -2504,6 +2667,15 @@ "vue": "^3.0.0" } }, + "node_modules/vue3-persian-datetime-picker": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vue3-persian-datetime-picker/-/vue3-persian-datetime-picker-1.2.2.tgz", + "integrity": "sha512-d7nkj5vgtUvEXZboSdRmP1uwBfXvXgXqdvsOOMQb34jiMZU/aBDrTYWTEe1N+XKF9pvTTJn8Rws9ttJmyhK/hw==", + "license": "MIT", + "dependencies": { + "moment-jalaali": "^0.9.4" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz", @@ -2512,6 +2684,12 @@ "dependencies": { "loose-envify": "^1.0.0" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index fe4e4cc..31065a0 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,12 @@ "@ant-design/icons-vue": "^7.0.1", "ant-design-vue": "^4.2.6", "axios": "^1.18.1", + "dayjs": "^1.11.21", + "jalaliday": "^3.1.1", "pinia": "^3.0.4", "vue": "^3.5.18", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "vue3-persian-datetime-picker": "^1.2.2" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", diff --git a/src/components/JalaliDatePicker.vue b/src/components/JalaliDatePicker.vue new file mode 100644 index 0000000..b503962 --- /dev/null +++ b/src/components/JalaliDatePicker.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/config/defaultHamkarUiConfig.ts b/src/config/defaultHamkarUiConfig.ts new file mode 100644 index 0000000..e2f9e87 --- /dev/null +++ b/src/config/defaultHamkarUiConfig.ts @@ -0,0 +1,139 @@ +export type HamkarPricingBoxTheme = '1' | '2' | '3' | '4' +export type HamkarOrderUi = 'new' | 'old' +export type HamkarPriceFontSize = 'کوچک' | 'متوسط' | 'بزرگ' | 'بزرگ‌تر' + +export type HamkarUiConfig = { + kind: 'hamkar' + /** + * When false, GET /get_user will NOT emit ui_settings — + * Laravel settings stay source of truth (prevents visual jumps). + */ + 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 + } +} + +export const HAMKAR_PRICING_BOX_THEME_OPTIONS: { + value: HamkarPricingBoxTheme + label: string +}[] = [ + { value: '1', label: 'تم ۱ (پیش‌فرض)' }, + { value: '2', label: 'تم ۲' }, + { value: '3', label: 'تم ۳' }, + { value: '4', label: 'تم ۴' }, +] + +export const HAMKAR_ORDER_UI_OPTIONS: { value: HamkarOrderUi; label: string }[] = [ + { value: 'new', label: 'جدید (کارتی)' }, + { value: 'old', label: 'قدیمی (جدولی)' }, +] + +export const HAMKAR_PRICE_FONT_SIZE_OPTIONS: { + value: HamkarPriceFontSize + label: string +}[] = [ + { value: 'کوچک', label: 'کوچک' }, + { value: 'متوسط', label: 'متوسط' }, + { value: 'بزرگ', label: 'بزرگ' }, + { value: 'بزرگ‌تر', label: 'بزرگ‌تر' }, +] + +function asColor(value: unknown): string { + if (value == null) return '' + const s = String(value).trim() + if (!s || s === 'null' || s === 'undefined') return '' + return s +} + +export function createDefaultHamkarUiConfig(): HamkarUiConfig { + return { + kind: 'hamkar', + settingsManaged: false, + registryMode: 'dynamicMode', + brand: { + primaryColor: '', + secondaryColor: '', + backgroundColor: '', + }, + settings: { + pricing_box_theme: '1', + price_font_size: 'بزرگ', + orderUI: 'old', + }, + } +} + +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' + + 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, + }, + } +} + +/** Payload for API: empty color strings → null */ +export function hamkarUiToApiPayload(config: HamkarUiConfig) { + const normalized = normalizeHamkarUiConfig(config) + const emptyToNull = (v: string) => (v.trim() ? v.trim() : null) + return { + ...normalized, + brand: { + primaryColor: emptyToNull(normalized.brand.primaryColor), + secondaryColor: emptyToNull(normalized.brand.secondaryColor), + backgroundColor: emptyToNull(normalized.brand.backgroundColor), + }, + } +} + +/** Build Laravel-compatible settings overlay for Hamkar user app. */ +export function hamkarUiToSettingsList(config: HamkarUiConfig) { + if (!config.settingsManaged) return [] + return [ + { key: 'pricing_box_theme', value: config.settings.pricing_box_theme }, + { key: 'price_font_size', value: config.settings.price_font_size }, + { key: 'orderUI', value: config.settings.orderUI }, + ] +} diff --git a/src/config/defaultKhanegiUiConfig.ts b/src/config/defaultKhanegiUiConfig.ts index ec9ac95..b9c621d 100644 --- a/src/config/defaultKhanegiUiConfig.ts +++ b/src/config/defaultKhanegiUiConfig.ts @@ -1,3 +1,63 @@ +export type ThemeColors = { + primary: string + secondary: string + tertiary: string + background: string + surface: string + surfaceHover: string + textPrimary: string + textSecondary: string + textSubtle: string + border: string + borderSubtle: string + pageBg: string + pageBgMobile: string + onAccent: string + success: string + warning: string + danger: string +} + +const navyGoldDark: ThemeColors = { + primary: '#0A1B37', + secondary: '#e3b056', + tertiary: '#ce9d43', + background: '#ffffff', + surface: '#222228', + surfaceHover: '#2c2c34', + textPrimary: '#ffffff', + textSecondary: 'rgba(255, 255, 255, 0.45)', + textSubtle: 'rgba(255, 255, 255, 0.35)', + border: 'rgba(255, 255, 255, 0.16)', + borderSubtle: 'rgba(255, 255, 255, 0.12)', + pageBg: '#121216', + pageBgMobile: '#0e0e12', + onAccent: '#0a0a0a', + success: '#16A34A', + warning: '#F59E0B', + danger: '#DC2626', +} + +const navyGoldLight: ThemeColors = { + primary: '#0A1B37', + secondary: '#c9973a', + tertiary: '#b98633', + background: '#faf8f5', + surface: '#ffffff', + surfaceHover: '#fffefb', + textPrimary: '#1c1916', + textSecondary: '#5c564d', + textSubtle: '#8a8278', + border: 'rgba(184, 153, 88, 0.22)', + borderSubtle: 'rgba(0, 0, 0, 0.08)', + pageBg: '#f5f2ed', + pageBgMobile: '#faf8f5', + onAccent: '#ffffff', + success: '#16A34A', + warning: '#F59E0B', + danger: '#DC2626', +} + /** Default Khanegi app.theme + navigation shell for new UI configs */ export function createDefaultKhanegiUiConfig() { return { @@ -10,25 +70,12 @@ export function createDefaultKhanegiUiConfig() { locale: 'fa-IR', startRoute: 'home', theme: { - mode: 'dark', - colors: { - primary: '#0A1B37', - secondary: '#e3b056', - tertiary: '#ce9d43', - background: '#ffffff', - surface: '#222228', - surfaceHover: '#2c2c34', - textPrimary: '#ffffff', - textSecondary: 'rgba(255, 255, 255, 0.45)', - textSubtle: 'rgba(255, 255, 255, 0.35)', - border: 'rgba(255, 255, 255, 0.16)', - borderSubtle: 'rgba(255, 255, 255, 0.12)', - pageBg: '#121216', - pageBgMobile: '#0e0e12', - onAccent: '#0a0a0a', - success: '#16A34A', - warning: '#F59E0B', - danger: '#DC2626', + presetId: 'navy-gold' as string, + mode: 'dark' as 'dark' | 'light' | 'system', + colors: { ...navyGoldDark }, + schemes: { + dark: { ...navyGoldDark }, + light: { ...navyGoldLight }, }, borderRadius: { sm: 8, md: 12, lg: 16 }, }, @@ -65,7 +112,7 @@ export function createDefaultKhanegiUiConfig() { brandSubtitle: 'پنل کاربری', }, }, - pages: {}, + pages: {} as Record, } } diff --git a/src/config/hamkarEffectiveTheme.ts b/src/config/hamkarEffectiveTheme.ts new file mode 100644 index 0000000..e361bfb --- /dev/null +++ b/src/config/hamkarEffectiveTheme.ts @@ -0,0 +1,80 @@ +/** + * Mirrors Hamkar user `apply-theme.ts` + default CSS in `main.less` / `maindynamic.less`. + * Named modes (Montpar, limangold, …) do NOT paint registry color fields onto the chrome. + */ + +export type HamkarEffectiveTheme = { + mode: string + isDynamic: boolean + /** What paints the top header / mobile menu bar */ + headerBg: string + /** Selected menu / text_Secondary / icon accent the user actually sees */ + accent: string + /** Ant Design colorPrimary token (buttons) */ + antPrimary: string + /** Page / body background */ + pageBg: string + /** CSS vars apply-theme writes on :root */ + cssVars: { + bg: string + bg2: string + colorPrimary: string + } + note: string +} + +const NAMED_DEFAULT = { + headerBg: '#ffffff', + accent: '#2563EB', + antPrimary: '#1d4ed8', + pageBg: '#fafafa', + cssVars: { + bg: '', + bg2: '#ffffff', + colorPrimary: '#1d4ed8', + }, +} as const + +function hexOr(value: string | null | undefined, fallback: string) { + const s = (value || '').trim() + if (!s || s === 'null' || s === 'undefined') return fallback + return s +} + +export function resolveHamkarEffectiveTheme(input: { + registryMode?: string | null + primaryColor?: string | null + secondaryColor?: string | null + backgroundColor?: string | null +}): HamkarEffectiveTheme { + const mode = String(input.registryMode || 'dynamicMode').trim() || 'dynamicMode' + const isDynamic = mode === 'dynamicMode' + + if (!isDynamic) { + return { + mode, + isDynamic: false, + ...NAMED_DEFAULT, + note: `حالت «${mode}»: هدر و منو از CSS پیش‌فرض همکار می‌آیند؛ فیلدهای color / secondary / bg رجیستری روی ظاهر اعمال نمی‌شوند.`, + } + } + + const panel = hexOr(input.primaryColor, '#ffffff') + const accent = hexOr(input.secondaryColor, '#1d4ed8') + const pageBg = hexOr(input.backgroundColor, '#ffffff') + + return { + mode, + isDynamic: true, + headerBg: panel, + accent, + antPrimary: accent, + pageBg, + cssVars: { + bg: pageBg, + bg2: panel, + colorPrimary: accent, + }, + note: 'حالت dynamicMode: color → هدر (--bg2)، secondary_color → اکسنت (--colorPrimary)، background_color → پس‌زمینه (--bg).', + } +} diff --git a/src/config/khanegiThemePresets.ts b/src/config/khanegiThemePresets.ts new file mode 100644 index 0000000..3185319 --- /dev/null +++ b/src/config/khanegiThemePresets.ts @@ -0,0 +1,222 @@ +import { createDefaultKhanegiUiConfig, type ThemeColors, type UiConfig } from './defaultKhanegiUiConfig' + +export type KhanegiPresetId = 'navy-gold' | 'emerald' | 'warm-bronze' | 'custom' + +export type ThemeSchemes = { + dark: ThemeColors + light: ThemeColors +} + +export type KhanegiThemePreset = { + id: Exclude + nameFa: string + description: string + borderRadius: { sm: number; md: number; lg: number } + schemes: ThemeSchemes + /** رنگ‌های پیش‌نمایش کارت (حالت dark) */ + preview: ThemeColors +} + +const navyGoldDark: ThemeColors = { + primary: '#0A1B37', + secondary: '#e3b056', + tertiary: '#ce9d43', + background: '#ffffff', + surface: '#222228', + surfaceHover: '#2c2c34', + textPrimary: '#ffffff', + textSecondary: 'rgba(255, 255, 255, 0.45)', + textSubtle: 'rgba(255, 255, 255, 0.35)', + border: 'rgba(255, 255, 255, 0.16)', + borderSubtle: 'rgba(255, 255, 255, 0.12)', + pageBg: '#121216', + pageBgMobile: '#0e0e12', + onAccent: '#0a0a0a', + success: '#16A34A', + warning: '#F59E0B', + danger: '#DC2626', +} + +const navyGoldLight: ThemeColors = { + primary: '#0A1B37', + secondary: '#c9973a', + tertiary: '#b98633', + background: '#faf8f5', + surface: '#ffffff', + surfaceHover: '#fffefb', + textPrimary: '#1c1916', + textSecondary: '#5c564d', + textSubtle: '#8a8278', + border: 'rgba(184, 153, 88, 0.22)', + borderSubtle: 'rgba(0, 0, 0, 0.08)', + pageBg: '#f5f2ed', + pageBgMobile: '#faf8f5', + onAccent: '#ffffff', + success: '#16A34A', + warning: '#F59E0B', + danger: '#DC2626', +} + +const emeraldDark: ThemeColors = { + primary: '#064e3b', + secondary: '#fbbf24', + tertiary: '#d97706', + background: '#ffffff', + surface: '#14201c', + surfaceHover: '#1c2c26', + textPrimary: '#ffffff', + textSecondary: 'rgba(255, 255, 255, 0.5)', + textSubtle: 'rgba(255, 255, 255, 0.35)', + border: 'rgba(255, 255, 255, 0.14)', + borderSubtle: 'rgba(255, 255, 255, 0.1)', + pageBg: '#0b1411', + pageBgMobile: '#08100d', + onAccent: '#052e24', + success: '#22c55e', + warning: '#f59e0b', + danger: '#ef4444', +} + +const emeraldLight: ThemeColors = { + primary: '#065f46', + secondary: '#059669', + tertiary: '#047857', + background: '#ecfdf5', + surface: '#ffffff', + surfaceHover: '#d1fae5', + textPrimary: '#064e3b', + textSecondary: '#047857', + textSubtle: '#6b8578', + border: 'rgba(5, 150, 105, 0.28)', + borderSubtle: 'rgba(6, 95, 70, 0.12)', + pageBg: '#d1fae5', + pageBgMobile: '#ecfdf5', + onAccent: '#ffffff', + success: '#16a34a', + warning: '#f59e0b', + danger: '#dc2626', +} + +const warmBronzeDark: ThemeColors = { + primary: '#3f2a1a', + secondary: '#d4a373', + tertiary: '#b08968', + background: '#fffaf5', + surface: '#2a211c', + surfaceHover: '#352a23', + textPrimary: '#ffffff', + textSecondary: 'rgba(255, 255, 255, 0.5)', + textSubtle: 'rgba(255, 255, 255, 0.35)', + border: 'rgba(255, 255, 255, 0.14)', + borderSubtle: 'rgba(255, 255, 255, 0.1)', + pageBg: '#16110e', + pageBgMobile: '#120e0b', + onAccent: '#1c1410', + success: '#16a34a', + warning: '#f59e0b', + danger: '#dc2626', +} + +const warmBronzeLight: ThemeColors = { + primary: '#7c2d12', + secondary: '#c2410c', + tertiary: '#9a3412', + background: '#fff7ed', + surface: '#ffffff', + surfaceHover: '#ffedd5', + textPrimary: '#7c2d12', + textSecondary: '#9a3412', + textSubtle: '#c2410c', + border: 'rgba(194, 65, 12, 0.28)', + borderSubtle: 'rgba(124, 45, 18, 0.12)', + pageBg: '#ffedd5', + pageBgMobile: '#fff7ed', + onAccent: '#ffffff', + success: '#16a34a', + warning: '#f59e0b', + danger: '#dc2626', +} + +export const khanegiThemePresets: KhanegiThemePreset[] = [ + { + id: 'navy-gold', + nameFa: 'سرمه‌ای طلایی', + description: 'تم اصلی خانگی — دارک و لایت', + borderRadius: { sm: 8, md: 12, lg: 16 }, + schemes: { dark: navyGoldDark, light: navyGoldLight }, + preview: navyGoldDark, + }, + { + id: 'emerald', + nameFa: 'زمردی طلایی', + description: 'تم سبز تیره با اکسنت طلایی', + borderRadius: { sm: 8, md: 12, lg: 16 }, + schemes: { dark: emeraldDark, light: emeraldLight }, + preview: emeraldDark, + }, + { + id: 'warm-bronze', + nameFa: 'برنز گرم', + description: 'تم برنز/کرم با گوشه‌های گردتر', + borderRadius: { sm: 12, md: 16, lg: 24 }, + schemes: { dark: warmBronzeDark, light: warmBronzeLight }, + preview: warmBronzeDark, + }, +] + +export function getKhanegiPreset(id: string): KhanegiThemePreset | undefined { + return khanegiThemePresets.find((p) => p.id === id) +} + +/** اعمال preset روی config فعلی؛ pages موجود حفظ می‌شوند */ +export function applyKhanegiPreset(config: UiConfig, presetId: Exclude): UiConfig { + const preset = getKhanegiPreset(presetId) + if (!preset) return config + + const mode = config.app.theme.mode === 'light' ? 'light' : 'dark' + const activeColors = { ...preset.schemes[mode] } + + config.app.theme.presetId = presetId + config.app.theme.mode = mode + config.app.theme.colors = activeColors + config.app.theme.schemes = { + dark: { ...preset.schemes.dark }, + light: { ...preset.schemes.light }, + } + config.app.theme.borderRadius = { ...preset.borderRadius } + + return config +} + +/** ساخت config تازه از preset (برای نسخه جدید) */ +export function createConfigFromPreset(presetId: Exclude = 'navy-gold'): UiConfig { + const config = createDefaultKhanegiUiConfig() + return applyKhanegiPreset(config, presetId) +} + +export function resolvePresetId(config: UiConfig): KhanegiPresetId { + const id = config.app?.theme?.presetId + if (id === 'navy-gold' || id === 'emerald' || id === 'warm-bronze' || id === 'custom') { + return id + } + return 'custom' +} + +export function syncActiveColorsFromMode(config: UiConfig) { + const schemes = config.app.theme.schemes + if (!schemes?.dark || !schemes?.light) return + const mode = config.app.theme.mode === 'light' ? 'light' : 'dark' + Object.assign(config.app.theme.colors, schemes[mode]) +} + +export function markCustomAndSyncSchemes(config: UiConfig) { + config.app.theme.presetId = 'custom' + const mode = config.app.theme.mode === 'light' ? 'light' : 'dark' + if (!config.app.theme.schemes) { + config.app.theme.schemes = { + dark: { ...config.app.theme.colors }, + light: { ...config.app.theme.colors }, + } + } + Object.assign(config.app.theme.schemes[mode], config.app.theme.colors) +} diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue index b43c25c..9a93527 100644 --- a/src/layouts/AppLayout.vue +++ b/src/layouts/AppLayout.vue @@ -1,10 +1,15 @@ + + diff --git a/src/pages/UiConfigPage.vue b/src/pages/UiConfigPage.vue index 12a7cb2..9246275 100644 --- a/src/pages/UiConfigPage.vue +++ b/src/pages/UiConfigPage.vue @@ -1,11 +1,19 @@ @@ -112,20 +189,66 @@ onMounted(load)

ظاهر اپ خانگی

-

{{ tenantName }} · app_ui_config

+

{{ tenantName }} · ۳ تم آماده + سفارشی

ذخیره ظاهر +
+

انتخاب تم

+
+ + +
+
+
{{ tenantName || 'پیش‌نمایش' }} +
+ + +
اکسنت
-
+
surface
نمونه کارت اپ
-
+
متن فرعی با رنگ theme
@@ -133,29 +256,16 @@ onMounted(load) -
-

پالت‌های آماده

-
- -
-
+
-

حالت تم

+

حالت پیش‌فرض اپ

تیره روشن @@ -163,7 +273,7 @@ onMounted(load)
-

رنگ‌ها

+

رنگ‌ها {{ isCustom ? '(سفارشی)' : '(فقط‌خواندنی تا انتخاب سفارشی)' }}

@@ -189,17 +306,38 @@ onMounted(load) - + - + - + @@ -257,7 +395,7 @@ onMounted(load)

- برای pages و جزئیات کامل می‌توانید JSON را مستقیم ویرایش کنید. اگر از قبل در دیتابیس مشتری pages دارید، اینجا پیست کنید. + برای pages و جزئیات کامل می‌توانید JSON را مستقیم ویرایش کنید.

{{ jsonError }}
@@ -285,6 +423,74 @@ onMounted(load) margin-bottom: 16px; } +.preset-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; +} + +.preset-card { + border: 2px solid var(--line); + border-radius: 16px; + padding: 12px; + background: #fff; + cursor: pointer; + text-align: right; + font-family: inherit; + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; +} + +.preset-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); +} + +.preset-card.active { + border-color: var(--sa-primary, #20c997); + box-shadow: 0 0 0 3px rgba(32, 201, 151, 0.18); +} + +.preset-swatches { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; + margin-bottom: 8px; +} + +.preset-swatches span { + height: 22px; + border-radius: 6px; +} + +.custom-swatches span { + background: linear-gradient(135deg, #e2e8f0, #94a3b8); +} + +.custom-swatches span:nth-child(2) { + background: linear-gradient(135deg, #fbbf24, #f59e0b); +} + +.custom-swatches span:nth-child(3) { + background: linear-gradient(135deg, #cbd5e1, #64748b); +} + +.custom-swatches span:nth-child(4) { + background: linear-gradient(135deg, #1e293b, #0f172a); +} + +.preset-name { + font-size: 0.9rem; + font-weight: 700; + color: var(--ink); +} + +.preset-desc { + margin-top: 4px; + font-size: 0.75rem; + color: var(--ink-soft); + line-height: 1.4; +} + .preview-phone { padding: 16px; margin-bottom: 18px; @@ -296,8 +502,36 @@ onMounted(load) display: flex; justify-content: space-between; align-items: center; + gap: 8px; margin-bottom: 12px; font-weight: 700; + flex-wrap: wrap; +} + +.preview-modes { + display: inline-flex; + gap: 4px; + background: rgba(0, 0, 0, 0.2); + border-radius: 10px; + padding: 2px; +} + +.preview-modes button { + border: none; + background: transparent; + color: inherit; + opacity: 0.65; + font-family: inherit; + font-size: 12px; + padding: 4px 10px; + border-radius: 8px; + cursor: pointer; +} + +.preview-modes button.on { + opacity: 1; + background: rgba(255, 255, 255, 0.16); + font-weight: 700; } .preview-chip { @@ -310,46 +544,6 @@ onMounted(load) padding: 14px; } -.palette-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 10px; -} - -.palette-card { - border: 1px solid var(--line); - border-radius: 14px; - padding: 10px; - background: #fff; - cursor: pointer; - text-align: right; - font-family: inherit; - transition: transform 0.15s ease, box-shadow 0.15s ease; -} - -.palette-card:hover { - transform: translateY(-2px); - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); -} - -.palette-swatches { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 4px; - margin-bottom: 8px; -} - -.palette-swatches span { - height: 22px; - border-radius: 6px; -} - -.palette-name { - font-size: 0.85rem; - font-weight: 600; - color: var(--ink); -} - .color-grid { display: grid; gap: 12px; @@ -383,6 +577,11 @@ onMounted(load) cursor: pointer; } +.color-native:disabled { + cursor: not-allowed; + opacity: 0.5; +} + .color-swatch { width: 44px; height: 44px; diff --git a/src/router/index.ts b/src/router/index.ts index caacc9f..40b93e7 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -34,6 +34,26 @@ const router = createRouter({ name: 'server-panel', component: () => import('@/pages/ServerPanelPage.vue'), }, + { + path: 'servers/:tenantId', + name: 'server-tenant', + component: () => import('@/pages/ServerTenantPage.vue'), + }, + { + path: 'plugins', + name: 'plugin-panel', + component: () => import('@/pages/PluginPanelPage.vue'), + }, + { + path: 'plugins/:tenantId', + name: 'plugin-tenant', + component: () => import('@/pages/PluginTenantPage.vue'), + }, + { + path: 'admins', + name: 'admins', + component: () => import('@/pages/AdminsPage.vue'), + }, { path: 'products/:product', name: 'product-versions', @@ -54,6 +74,11 @@ const router = createRouter({ name: 'version-ui', component: () => import('@/pages/UiConfigPage.vue'), }, + { + path: 'versions/:id/ui-hamkar', + name: 'version-ui-hamkar', + component: () => import('@/pages/HamkarUiConfigPage.vue'), + }, { path: 'audit', name: 'audit', diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 9667127..927dec1 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -4,7 +4,9 @@ import api from '@/api/http' type User = { id: string - email: string + mobile: string + firstName?: string + lastName?: string name: string role: string } @@ -15,8 +17,8 @@ export const useAuthStore = defineStore('auth', () => { const isAuthenticated = computed(() => Boolean(token.value)) - async function login(email: string, password: string) { - const { data } = await api.post('/auth/login', { email, password }) + async function login(mobile: string, password: string) { + const { data } = await api.post('/auth/login', { mobile, password }) token.value = data.accessToken user.value = data.user localStorage.setItem('sa_token', data.accessToken) diff --git a/src/styles.css b/src/styles.css index abc519a..42d396a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -194,6 +194,8 @@ button, input, textarea, select, .module-card.sms .icon-wrap { background: linear-gradient(145deg, #9b7cff, #7c5cfc); } .module-card.server .icon-wrap { background: linear-gradient(145deg, #ff8a8a, #ff6b6b); } +.module-card.plugin .icon-wrap { background: linear-gradient(145deg, #5ec8e8, #2aa8cc); } +.module-card.admins .icon-wrap { background: linear-gradient(145deg, #5b8def, #3b6fd4); } .module-card.hamkar .icon-wrap { background: linear-gradient(145deg, #3ad4b0, #20c997); } .module-card.khanegi .icon-wrap { background: linear-gradient(145deg, #ffc86b, #ffb84d); } @@ -238,6 +240,8 @@ button, input, textarea, select, .module-card.sms .progress > span { background: #7c5cfc; width: 72%; } .module-card.server .progress > span { background: #ff6b6b; width: 54%; } +.module-card.plugin .progress > span { background: #2aa8cc; width: 58%; } +.module-card.admins .progress > span { background: #3b6fd4; width: 66%; } .module-card.hamkar .progress > span { background: #20c997; width: 80%; } .module-card.khanegi .progress > span { background: #ffb84d; width: 68%; } diff --git a/src/types/vue3-persian-datetime-picker.d.ts b/src/types/vue3-persian-datetime-picker.d.ts new file mode 100644 index 0000000..f3f1064 --- /dev/null +++ b/src/types/vue3-persian-datetime-picker.d.ts @@ -0,0 +1,5 @@ +declare module 'vue3-persian-datetime-picker' { + import type { DefineComponent } from 'vue' + const component: DefineComponent, Record, any> + export default component +} diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts new file mode 100644 index 0000000..a06660c --- /dev/null +++ b/src/utils/datetime.ts @@ -0,0 +1,63 @@ +/** Iran timezone + Persian (Jalali) calendar helpers for SuperAdmin web. */ + +export const IRAN_TZ = 'Asia/Tehran' + +function toDate(value?: string | Date | number | null): Date | null { + if (value == null || value === '') return null + const d = value instanceof Date ? value : new Date(value) + return Number.isNaN(d.getTime()) ? null : d +} + +/** تاریخ شمسی به وقت ایران — مثلاً ۱۴۰۵/۰۵/۰۵ */ +export function formatIranDate(value?: string | Date | number | null): string { + const d = toDate(value) + if (!d) return '—' + return new Intl.DateTimeFormat('fa-IR-u-ca-persian', { + timeZone: IRAN_TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(d) +} + +/** تاریخ و ساعت شمسی به وقت ایران */ +export function formatIranDateTime(value?: string | Date | number | null): string { + const d = toDate(value) + if (!d) return '—' + return new Intl.DateTimeFormat('fa-IR-u-ca-persian', { + timeZone: IRAN_TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }).format(d) +} + +/** YYYY-MM-DD (میلادی) برای روز تقویمی ایران */ +export function iranYmd(value?: string | Date | number | null): string { + const d = toDate(value) ?? new Date() + return new Intl.DateTimeFormat('en-CA', { + timeZone: IRAN_TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(d) +} + +/** ISO از تاریخ روز ایران (ظهر محلی تا جابه‌جایی DST کم‌اثر باشد) */ +export function iranDateInputToIso(ymd: string | null | undefined): string | null { + if (!ymd || !/^\d{4}-\d{2}-\d{2}$/.test(ymd)) return null + // Interpret as Iran calendar day noon via fixed offset +03:30 (Iran standard; DST abolished) + const d = new Date(`${ymd}T12:00:00+03:30`) + return Number.isNaN(d.getTime()) ? null : d.toISOString() +} + +export function isoToIranDateInput(iso?: string | null): string { + if (!iso) return '' + return iranYmd(iso) +} + +export function todayIranDateInput(): string { + return iranYmd(new Date()) +} diff --git a/vite.config.ts b/vite.config.ts index d8a6a09..5151154 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,14 @@ export default defineConfig({ target: 'http://localhost:3001', changeOrigin: true, }, + '/get_version': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + '/uploads': { + target: 'http://localhost:3001', + changeOrigin: true, + }, }, }, })