Add Khanegi uiVariant modular/newTheme with get_user payload split.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 12:58:45 +03:30
co-authored by Cursor
parent 2f0e39edbb
commit 1adaef5d18
5 changed files with 46 additions and 5 deletions
@@ -0,0 +1,14 @@
-- Khanegi appearance family: modular (default) vs newTheme.
CREATE TYPE "KhanegiUiVariant" AS ENUM ('modular', 'newTheme');
ALTER TABLE "tenants"
ADD COLUMN "ui_variant" "KhanegiUiVariant" NOT NULL DEFAULT 'modular';
-- Pilot already on new theme
UPDATE "tenants"
SET "ui_variant" = 'newTheme'
WHERE "product" = 'khanegi'
AND (
"domain" = 'app.aygingold.ir'
OR ("domains"::text ILIKE '%app.aygingold.ir%')
);
+8
View File
@@ -13,6 +13,12 @@ enum ProductType {
khanegi khanegi
} }
/// Khanegi front-end appearance family (ignored for hamkar).
enum KhanegiUiVariant {
modular
newTheme
}
enum UserRole { enum UserRole {
super_admin super_admin
viewer viewer
@@ -63,6 +69,8 @@ model Tenant {
smsSyncedAt DateTime? @map("sms_synced_at") smsSyncedAt DateTime? @map("sms_synced_at")
/// Full Khanegi UI JSON (theme, navigation, pages). Hamkar: unused for now. /// Full Khanegi UI JSON (theme, navigation, pages). Hamkar: unused for now.
appUiConfig Json? @map("app_ui_config") appUiConfig Json? @map("app_ui_config")
/// Khanegi only: which appearance editor / get_user payload shape to use.
uiVariant KhanegiUiVariant @default(modular) @map("ui_variant")
/// Client cache-bust / force-reload signal for this sold version (GET /get_version). /// Client cache-bust / force-reload signal for this sold version (GET /get_version).
appVersion String @default("1") @map("app_version") appVersion String @default("1") @map("app_version")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
+12 -4
View File
@@ -43,19 +43,23 @@ export class GetUserService {
? registryMode ? registryMode
: 'dynamicMode' : 'dynamicMode'
const isKhanegi = tenant.product === 'khanegi';
const uiVariant =
isKhanegi && tenant.uiVariant === 'newTheme' ? 'newTheme' : 'modular';
const color = const color =
hamkarBrand?.primaryColor || hamkarBrand?.primaryColor ||
khanegiColors?.primary || (uiVariant === 'newTheme' ? khanegiColors?.primary : null) ||
tenant.primaryColor || tenant.primaryColor ||
null null
const secondary = const secondary =
hamkarBrand?.secondaryColor || hamkarBrand?.secondaryColor ||
khanegiColors?.secondary || (uiVariant === 'newTheme' ? khanegiColors?.secondary : null) ||
tenant.secondaryColor || tenant.secondaryColor ||
null null
const background = const background =
hamkarBrand?.backgroundColor || hamkarBrand?.backgroundColor ||
khanegiColors?.background || (uiVariant === 'newTheme' ? khanegiColors?.background : null) ||
tenant.backgroundColor || tenant.backgroundColor ||
null null
@@ -75,8 +79,12 @@ export class GetUserService {
product: tenant.product, product: tenant.product,
theme: themeMode, theme: themeMode,
mode: themeMode, mode: themeMode,
/** Khanegi front-end family for consumers */
ui_variant: isKhanegi ? uiVariant : undefined,
app_ui_config: app_ui_config:
tenant.product === 'khanegi' ? this.stripKhanegiSaMeta(ui) : undefined, isKhanegi && uiVariant === 'newTheme'
? this.stripKhanegiSaMeta(ui)
: undefined,
/** Laravel-compatible overlay; Hamkar merges only when present */ /** Laravel-compatible overlay; Hamkar merges only when present */
ui_settings: uiSettings.length ? uiSettings : undefined, ui_settings: uiSettings.length ? uiSettings : undefined,
}; };
+6 -1
View File
@@ -8,7 +8,7 @@ import {
MaxLength, MaxLength,
MinLength, MinLength,
} from 'class-validator'; } from 'class-validator';
import { ProductType } from '@prisma/client'; import { KhanegiUiVariant, ProductType } from '@prisma/client';
export class CreateTenantDto { export class CreateTenantDto {
@IsEnum(ProductType) @IsEnum(ProductType)
@@ -80,6 +80,11 @@ export class CreateTenantDto {
@IsObject() @IsObject()
appUiConfig?: Record<string, unknown>; appUiConfig?: Record<string, unknown>;
/** Khanegi only; default modular. */
@IsOptional()
@IsEnum(KhanegiUiVariant)
uiVariant?: KhanegiUiVariant;
@IsOptional() @IsOptional()
@IsString() @IsString()
@MaxLength(64) @MaxLength(64)
+6
View File
@@ -38,6 +38,7 @@ export class TenantsService {
appUrl: true, appUrl: true,
adminUrl: true, adminUrl: true,
appUiConfig: true, appUiConfig: true,
uiVariant: true,
appVersion: true, appVersion: true,
isActive: true, isActive: true,
notes: true, notes: true,
@@ -81,6 +82,10 @@ export class TenantsService {
appUiConfig: dto.appUiConfig appUiConfig: dto.appUiConfig
? (dto.appUiConfig as Prisma.InputJsonValue) ? (dto.appUiConfig as Prisma.InputJsonValue)
: undefined, : undefined,
uiVariant:
dto.product === ProductType.khanegi
? dto.uiVariant ?? 'modular'
: 'modular',
appVersion: dto.appVersion?.trim() || '1', appVersion: dto.appVersion?.trim() || '1',
isActive: dto.isActive ?? true, isActive: dto.isActive ?? true,
notes: dto.notes, notes: dto.notes,
@@ -144,6 +149,7 @@ export class TenantsService {
...(dto.appUiConfig !== undefined && { ...(dto.appUiConfig !== undefined && {
appUiConfig: dto.appUiConfig as Prisma.InputJsonValue, appUiConfig: dto.appUiConfig as Prisma.InputJsonValue,
}), }),
...(dto.uiVariant !== undefined && { uiVariant: dto.uiVariant }),
...(dto.appVersion !== undefined && { ...(dto.appVersion !== undefined && {
appVersion: dto.appVersion.trim() || '1', appVersion: dto.appVersion.trim() || '1',
}), }),