mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Add per-business modules and home chart slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
0632f67bda
commit
9ca6e2306f
@@ -25,8 +25,18 @@ import { normalizeBusinessPrimaryColorId } from '../business-settings/business-p
|
|||||||
import type { BusinessPrimaryColorId } from '../business-settings/business-primary-colors';
|
import type { BusinessPrimaryColorId } from '../business-settings/business-primary-colors';
|
||||||
import {
|
import {
|
||||||
normalizeDashboardLocale,
|
normalizeDashboardLocale,
|
||||||
|
normalizeEnabledBusinessModules,
|
||||||
|
normalizeHomeCharts,
|
||||||
} from '../business-settings/business-settings.util';
|
} from '../business-settings/business-settings.util';
|
||||||
import type { DashboardLocale } from '../business-settings/business-settings.types';
|
import type {
|
||||||
|
BusinessModuleId,
|
||||||
|
DashboardLocale,
|
||||||
|
HomeChartId,
|
||||||
|
} from '../business-settings/business-settings.types';
|
||||||
|
import {
|
||||||
|
DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||||
|
DEFAULT_HOME_CHARTS,
|
||||||
|
} from '../business-settings/business-settings.types';
|
||||||
|
|
||||||
type BusinessRow = {
|
type BusinessRow = {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
@@ -46,6 +56,8 @@ type BusinessRow = {
|
|||||||
ownerCellNumber: string | null;
|
ownerCellNumber: string | null;
|
||||||
primaryColor: string | null;
|
primaryColor: string | null;
|
||||||
defaultLocale: string | null;
|
defaultLocale: string | null;
|
||||||
|
enabledModules: unknown;
|
||||||
|
homeCharts: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
function slugify(value: string) {
|
function slugify(value: string) {
|
||||||
@@ -121,7 +133,9 @@ export class BusinessAdminService {
|
|||||||
own."ownerNameEn" AS "ownerNameEn",
|
own."ownerNameEn" AS "ownerNameEn",
|
||||||
own."ownerCellNumber" AS "ownerCellNumber",
|
own."ownerCellNumber" AS "ownerCellNumber",
|
||||||
b.settings->'branding'->>'primaryColor' AS "primaryColor",
|
b.settings->'branding'->>'primaryColor' AS "primaryColor",
|
||||||
b.settings->'branding'->>'defaultLocale' AS "defaultLocale"
|
b.settings->'branding'->>'defaultLocale' AS "defaultLocale",
|
||||||
|
b.settings->'modules'->'enabled' AS "enabledModules",
|
||||||
|
b.settings->'modules'->'charts' AS "homeCharts"
|
||||||
FROM businesses b
|
FROM businesses b
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT d.id, d.host, d.ssl_enabled
|
SELECT d.id, d.host, d.ssl_enabled
|
||||||
@@ -153,15 +167,34 @@ export class BusinessAdminService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map((item) => ({
|
items: items.map((item) => {
|
||||||
...item,
|
const {
|
||||||
|
enabledModules: rawEnabledModules,
|
||||||
|
homeCharts: rawHomeCharts,
|
||||||
|
...rest
|
||||||
|
} = item;
|
||||||
|
const enabledModules: BusinessModuleId[] =
|
||||||
|
rawEnabledModules == null
|
||||||
|
? [...DEFAULT_ENABLED_BUSINESS_MODULES]
|
||||||
|
: normalizeEnabledBusinessModules(rawEnabledModules);
|
||||||
|
const homeCharts: [HomeChartId, HomeChartId] =
|
||||||
|
rawHomeCharts == null
|
||||||
|
? [...DEFAULT_HOME_CHARTS]
|
||||||
|
: normalizeHomeCharts(rawHomeCharts);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
primaryColor: normalizeBusinessPrimaryColorId(
|
primaryColor: normalizeBusinessPrimaryColorId(
|
||||||
item.primaryColor,
|
item.primaryColor,
|
||||||
) as BusinessPrimaryColorId,
|
) as BusinessPrimaryColorId,
|
||||||
defaultLocale: normalizeDashboardLocale(
|
defaultLocale: normalizeDashboardLocale(
|
||||||
item.defaultLocale,
|
item.defaultLocale,
|
||||||
) as DashboardLocale,
|
) as DashboardLocale,
|
||||||
})),
|
enabledModules,
|
||||||
|
moduleCount: enabledModules.length,
|
||||||
|
homeCharts,
|
||||||
|
};
|
||||||
|
}),
|
||||||
total: totalRow[0]?.total ?? 0,
|
total: totalRow[0]?.total ?? 0,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { BusinessSettings } from './business-settings.types';
|
|||||||
import {
|
import {
|
||||||
mergeBusinessSettings,
|
mergeBusinessSettings,
|
||||||
normalizeBusinessSettings,
|
normalizeBusinessSettings,
|
||||||
|
normalizeEnabledBusinessModules,
|
||||||
|
normalizeHomeCharts,
|
||||||
toPrismaJson,
|
toPrismaJson,
|
||||||
} from './business-settings.util';
|
} from './business-settings.util';
|
||||||
import { UpdateBusinessSettingsDto } from './dto/update-business-settings.dto';
|
import { UpdateBusinessSettingsDto } from './dto/update-business-settings.dto';
|
||||||
@@ -49,6 +51,14 @@ export class BusinessSettingsService {
|
|||||||
const businessId = BigInt(businessIdRaw);
|
const businessId = BigInt(businessIdRaw);
|
||||||
await this.assertPermission(businessId, actor.id, 'business.update');
|
await this.assertPermission(businessId, actor.id, 'business.update');
|
||||||
|
|
||||||
|
if (dto.modules !== undefined) {
|
||||||
|
if (!(await this.permissions.isSuperAdmin(actor.id))) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Only super admins can change business modules',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const business = await this.prisma.business.findUnique({
|
const business = await this.prisma.business.findUnique({
|
||||||
where: { id: businessId },
|
where: { id: businessId },
|
||||||
select: { id: true, settings: true },
|
select: { id: true, settings: true },
|
||||||
@@ -98,6 +108,19 @@ export class BusinessSettingsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.modules) {
|
||||||
|
patch.modules = {
|
||||||
|
enabled:
|
||||||
|
dto.modules.enabled !== undefined
|
||||||
|
? normalizeEnabledBusinessModules(dto.modules.enabled)
|
||||||
|
: current.modules.enabled,
|
||||||
|
charts:
|
||||||
|
dto.modules.charts !== undefined
|
||||||
|
? normalizeHomeCharts(dto.modules.charts)
|
||||||
|
: current.modules.charts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const next = mergeBusinessSettings(current, patch);
|
const next = mergeBusinessSettings(current, patch);
|
||||||
|
|
||||||
const updated = await this.prisma.business.update({
|
const updated = await this.prisma.business.update({
|
||||||
|
|||||||
@@ -38,11 +38,41 @@ export type StoreSettings = {
|
|||||||
orderProcessSteps: OrderProcessStep[];
|
orderProcessSteps: OrderProcessStep[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Optional CMS modules a business can have. Always-on areas (customers, website, etc.) are not listed. */
|
||||||
|
export const BUSINESS_MODULE_IDS = [
|
||||||
|
'products',
|
||||||
|
'store',
|
||||||
|
'portfolio',
|
||||||
|
'blog',
|
||||||
|
'warehouse',
|
||||||
|
'videos',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type BusinessModuleId = (typeof BUSINESS_MODULE_IDS)[number];
|
||||||
|
|
||||||
|
/** Home dashboard chart types (super-admin selectable). */
|
||||||
|
export const HOME_CHART_IDS = [
|
||||||
|
'none',
|
||||||
|
'orders_30d',
|
||||||
|
'customers_joined_1y',
|
||||||
|
'blog_views_30d',
|
||||||
|
'products_added_1y',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type HomeChartId = (typeof HOME_CHART_IDS)[number];
|
||||||
|
|
||||||
|
export type ModulesSettings = {
|
||||||
|
enabled: BusinessModuleId[];
|
||||||
|
/** Two home-page chart slots (left, right). */
|
||||||
|
charts: [HomeChartId, HomeChartId];
|
||||||
|
};
|
||||||
|
|
||||||
/** Top-level business settings stored in `businesses.settings` JSONB. */
|
/** Top-level business settings stored in `businesses.settings` JSONB. */
|
||||||
export type BusinessSettings = {
|
export type BusinessSettings = {
|
||||||
branding: BrandingSettings;
|
branding: BrandingSettings;
|
||||||
dashboard: DashboardSettings;
|
dashboard: DashboardSettings;
|
||||||
store: StoreSettings;
|
store: StoreSettings;
|
||||||
|
modules: ModulesSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
||||||
@@ -72,6 +102,16 @@ export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Existing tenants without `settings.modules` keep every module enabled. */
|
||||||
|
export const DEFAULT_ENABLED_BUSINESS_MODULES: BusinessModuleId[] = [
|
||||||
|
...BUSINESS_MODULE_IDS,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_HOME_CHARTS: [HomeChartId, HomeChartId] = [
|
||||||
|
'orders_30d',
|
||||||
|
'customers_joined_1y',
|
||||||
|
];
|
||||||
|
|
||||||
export const DEFAULT_BUSINESS_SETTINGS: BusinessSettings = {
|
export const DEFAULT_BUSINESS_SETTINGS: BusinessSettings = {
|
||||||
branding: {
|
branding: {
|
||||||
primaryColor: DEFAULT_BUSINESS_PRIMARY_COLOR_ID,
|
primaryColor: DEFAULT_BUSINESS_PRIMARY_COLOR_ID,
|
||||||
@@ -85,4 +125,8 @@ export const DEFAULT_BUSINESS_SETTINGS: BusinessSettings = {
|
|||||||
onlineSellEnabled: true,
|
onlineSellEnabled: true,
|
||||||
orderProcessSteps: DEFAULT_ORDER_PROCESS_STEPS,
|
orderProcessSteps: DEFAULT_ORDER_PROCESS_STEPS,
|
||||||
},
|
},
|
||||||
|
modules: {
|
||||||
|
enabled: DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||||
|
charts: DEFAULT_HOME_CHARTS,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import {
|
|||||||
normalizeBusinessPrimaryColorId,
|
normalizeBusinessPrimaryColorId,
|
||||||
} from './business-primary-colors';
|
} from './business-primary-colors';
|
||||||
import {
|
import {
|
||||||
|
BUSINESS_MODULE_IDS,
|
||||||
|
BusinessModuleId,
|
||||||
BusinessSettings,
|
BusinessSettings,
|
||||||
DEFAULT_BUSINESS_DASHBOARD_LOCALE,
|
DEFAULT_BUSINESS_DASHBOARD_LOCALE,
|
||||||
DEFAULT_BUSINESS_SETTINGS,
|
DEFAULT_BUSINESS_SETTINGS,
|
||||||
|
DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||||
|
DEFAULT_HOME_CHARTS,
|
||||||
DEFAULT_ORDER_PROCESS_STEPS,
|
DEFAULT_ORDER_PROCESS_STEPS,
|
||||||
|
HOME_CHART_IDS,
|
||||||
type DashboardLocale,
|
type DashboardLocale,
|
||||||
|
type HomeChartId,
|
||||||
OrderProcessStep,
|
OrderProcessStep,
|
||||||
} from './business-settings.types';
|
} from './business-settings.types';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +25,45 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BUSINESS_MODULE_ID_SET = new Set<string>(BUSINESS_MODULE_IDS);
|
||||||
|
const HOME_CHART_ID_SET = new Set<string>(HOME_CHART_IDS);
|
||||||
|
|
||||||
|
export function isBusinessModuleId(value: unknown): value is BusinessModuleId {
|
||||||
|
return typeof value === 'string' && BUSINESS_MODULE_ID_SET.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHomeChartId(value: unknown): value is HomeChartId {
|
||||||
|
return typeof value === 'string' && HOME_CHART_ID_SET.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dedupes and orders enabled modules by canonical BUSINESS_MODULE_IDS order. */
|
||||||
|
export function normalizeEnabledBusinessModules(
|
||||||
|
value: unknown,
|
||||||
|
): BusinessModuleId[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [...DEFAULT_ENABLED_BUSINESS_MODULES];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = new Set<BusinessModuleId>();
|
||||||
|
for (const item of value) {
|
||||||
|
if (isBusinessModuleId(item)) {
|
||||||
|
selected.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BUSINESS_MODULE_IDS.filter((id) => selected.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHomeCharts(value: unknown): [HomeChartId, HomeChartId] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [...DEFAULT_HOME_CHARTS];
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = isHomeChartId(value[0]) ? value[0] : DEFAULT_HOME_CHARTS[0];
|
||||||
|
const second = isHomeChartId(value[1]) ? value[1] : DEFAULT_HOME_CHARTS[1];
|
||||||
|
return [first, second];
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeDashboardLocale(value: unknown): DashboardLocale {
|
export function normalizeDashboardLocale(value: unknown): DashboardLocale {
|
||||||
return value === 'en' || value === 'fa' ? value : DEFAULT_BUSINESS_DASHBOARD_LOCALE;
|
return value === 'en' || value === 'fa' ? value : DEFAULT_BUSINESS_DASHBOARD_LOCALE;
|
||||||
}
|
}
|
||||||
@@ -70,6 +115,8 @@ export function normalizeBusinessSettings(raw: unknown): BusinessSettings {
|
|||||||
? dashboard.expertReviews
|
? dashboard.expertReviews
|
||||||
: {};
|
: {};
|
||||||
const store = isRecord(source.store) ? source.store : {};
|
const store = isRecord(source.store) ? source.store : {};
|
||||||
|
const modules = isRecord(source.modules) ? source.modules : {};
|
||||||
|
const hasModulesKey = Object.prototype.hasOwnProperty.call(source, 'modules');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
branding: {
|
branding: {
|
||||||
@@ -97,6 +144,15 @@ export function normalizeBusinessSettings(raw: unknown): BusinessSettings {
|
|||||||
),
|
),
|
||||||
orderProcessSteps: readOrderProcessSteps(store.orderProcessSteps),
|
orderProcessSteps: readOrderProcessSteps(store.orderProcessSteps),
|
||||||
},
|
},
|
||||||
|
modules: {
|
||||||
|
// Missing `modules` → all enabled (legacy). Explicit `{ enabled: [] }` stays empty.
|
||||||
|
enabled: hasModulesKey
|
||||||
|
? Array.isArray(modules.enabled)
|
||||||
|
? normalizeEnabledBusinessModules(modules.enabled)
|
||||||
|
: []
|
||||||
|
: [...DEFAULT_ENABLED_BUSINESS_MODULES],
|
||||||
|
charts: normalizeHomeCharts(modules.charts),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +185,10 @@ export function mergeBusinessSettings(
|
|||||||
orderProcessSteps:
|
orderProcessSteps:
|
||||||
patch.store?.orderProcessSteps ?? current.store.orderProcessSteps,
|
patch.store?.orderProcessSteps ?? current.store.orderProcessSteps,
|
||||||
},
|
},
|
||||||
|
modules: {
|
||||||
|
enabled: patch.modules?.enabled ?? current.modules.enabled,
|
||||||
|
charts: patch.modules?.charts ?? current.modules.charts,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
|
ArrayMaxSize,
|
||||||
|
ArrayMinSize,
|
||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
IsIn,
|
||||||
@@ -10,6 +12,10 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { ORDER_STEP_COLOR_HEXES } from '../order-step-colors';
|
import { ORDER_STEP_COLOR_HEXES } from '../order-step-colors';
|
||||||
import { BUSINESS_PRIMARY_COLOR_IDS } from '../business-primary-colors';
|
import { BUSINESS_PRIMARY_COLOR_IDS } from '../business-primary-colors';
|
||||||
|
import {
|
||||||
|
BUSINESS_MODULE_IDS,
|
||||||
|
HOME_CHART_IDS,
|
||||||
|
} from '../business-settings.types';
|
||||||
|
|
||||||
class BrandingSettingsDto {
|
class BrandingSettingsDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -78,6 +84,22 @@ class StoreSettingsDto {
|
|||||||
orderProcessSteps?: OrderProcessStepDto[];
|
orderProcessSteps?: OrderProcessStepDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ModulesSettingsDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsIn([...BUSINESS_MODULE_IDS], { each: true })
|
||||||
|
enabled?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(2)
|
||||||
|
@ArrayMaxSize(2)
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsIn([...HOME_CHART_IDS], { each: true })
|
||||||
|
charts?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateBusinessSettingsDto {
|
export class UpdateBusinessSettingsDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@@ -93,4 +115,9 @@ export class UpdateBusinessSettingsDto {
|
|||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => StoreSettingsDto)
|
@Type(() => StoreSettingsDto)
|
||||||
store?: StoreSettingsDto;
|
store?: StoreSettingsDto;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ModulesSettingsDto)
|
||||||
|
modules?: ModulesSettingsDto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ export class DailyActivityQueryDto {
|
|||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@Max(90)
|
@Max(366)
|
||||||
days?: number;
|
days?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export class TenantService {
|
|||||||
domain: normalizedHost,
|
domain: normalizedHost,
|
||||||
primaryColor: settings.branding.primaryColor,
|
primaryColor: settings.branding.primaryColor,
|
||||||
defaultLocale: settings.branding.defaultLocale,
|
defaultLocale: settings.branding.defaultLocale,
|
||||||
|
enabledModules: settings.modules.enabled,
|
||||||
|
homeCharts: settings.modules.charts,
|
||||||
logoUrl: media?.logoMedia?.publicUrl ?? null,
|
logoUrl: media?.logoMedia?.publicUrl ?? null,
|
||||||
faviconUrl:
|
faviconUrl:
|
||||||
media?.faviconMedia?.publicUrl ?? media?.logoMedia?.publicUrl ?? null,
|
media?.faviconMedia?.publicUrl ?? media?.logoMedia?.publicUrl ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user