feat: bulk apply Niazi modular brand colors to Khanegi tenants
Add prisma:bulk-khanegi-modular-brand (skip Aygin/newTheme). Palette: header #0a1b37, accent #e0be7f, bg #ffffff; bumps appVersion. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"prisma:import-sms-batch": "ts-node --transpile-only prisma/import-sms-batch.ts",
|
"prisma:import-sms-batch": "ts-node --transpile-only prisma/import-sms-batch.ts",
|
||||||
"prisma:inventory-khanegi-sms": "ts-node --transpile-only prisma/inventory-khanegi-sms.ts",
|
"prisma:inventory-khanegi-sms": "ts-node --transpile-only prisma/inventory-khanegi-sms.ts",
|
||||||
"prisma:bulk-khanegi-sms-defaults": "ts-node --transpile-only prisma/bulk-khanegi-sms-defaults.ts",
|
"prisma:bulk-khanegi-sms-defaults": "ts-node --transpile-only prisma/bulk-khanegi-sms-defaults.ts",
|
||||||
|
"prisma:bulk-khanegi-modular-brand": "ts-node --transpile-only prisma/bulk-khanegi-modular-brand.ts",
|
||||||
"prisma:copy-sms-between-tenants": "ts-node --transpile-only prisma/copy-sms-between-tenants.ts",
|
"prisma:copy-sms-between-tenants": "ts-node --transpile-only prisma/copy-sms-between-tenants.ts",
|
||||||
"prisma:list-tenants": "ts-node --transpile-only prisma/list-tenants.ts"
|
"prisma:list-tenants": "ts-node --transpile-only prisma/list-tenants.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* Apply Niazi-style modular brand colors to all Khanegi tenants
|
||||||
|
* that are NOT newTheme (Aygin stays newTheme).
|
||||||
|
*
|
||||||
|
* Colors (from app.niazitala.ir):
|
||||||
|
* primary #0a1b37 (هدر)
|
||||||
|
* secondary #e0be7f (اکسنت)
|
||||||
|
* background #ffffff (پسزمینه)
|
||||||
|
*
|
||||||
|
* Also forces uiVariant=modular for non-Aygin Khanegi rows.
|
||||||
|
*
|
||||||
|
* Dry-run:
|
||||||
|
* KHANEGI_BRAND_DRY_RUN=1 npm run prisma:bulk-khanegi-modular-brand
|
||||||
|
*
|
||||||
|
* Apply:
|
||||||
|
* npm run prisma:bulk-khanegi-modular-brand
|
||||||
|
*
|
||||||
|
* Optional source override:
|
||||||
|
* KHANEGI_BRAND_SOURCE_DOMAIN=app.niazitala.ir npm run prisma:bulk-khanegi-modular-brand
|
||||||
|
*/
|
||||||
|
import { KhanegiUiVariant, PrismaClient, ProductType } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const FALLBACK_BRAND = {
|
||||||
|
primaryColor: '#0a1b37',
|
||||||
|
secondaryColor: '#e0be7f',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
};
|
||||||
|
|
||||||
|
function env(name: string, fallback = '') {
|
||||||
|
return String(process.env[name] ?? fallback).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHost(value?: string | null) {
|
||||||
|
if (!value) return '';
|
||||||
|
return String(value)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^https?:\/\//, '')
|
||||||
|
.split('/')[0]
|
||||||
|
.split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAyginTenant(domain: string, domains: unknown, slug: string) {
|
||||||
|
const hosts = new Set<string>();
|
||||||
|
const primary = normalizeHost(domain);
|
||||||
|
if (primary) hosts.add(primary);
|
||||||
|
const extra = Array.isArray(domains) ? domains : [];
|
||||||
|
for (const d of extra) {
|
||||||
|
const n = normalizeHost(String(d));
|
||||||
|
if (n) hosts.add(n);
|
||||||
|
}
|
||||||
|
if (String(slug || '').toLowerCase().includes('aygin')) return true;
|
||||||
|
for (const h of hosts) {
|
||||||
|
if (h.includes('aygingold') || h === 'app.aygingold.ir') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpAppVersion(current?: string | null): string {
|
||||||
|
const v = String(current || '1').trim() || '1';
|
||||||
|
if (/^\d+$/.test(v)) return String(Number(v) + 1);
|
||||||
|
const semver = v.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`;
|
||||||
|
return `${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveBrand() {
|
||||||
|
const sourceDomain =
|
||||||
|
env('KHANEGI_BRAND_SOURCE_DOMAIN', 'app.niazitala.ir') || 'app.niazitala.ir';
|
||||||
|
const host = normalizeHost(sourceDomain);
|
||||||
|
|
||||||
|
const tenants = await prisma.tenant.findMany({
|
||||||
|
where: { product: ProductType.khanegi, isActive: true },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
domain: true,
|
||||||
|
domains: true,
|
||||||
|
primaryColor: true,
|
||||||
|
secondaryColor: true,
|
||||||
|
backgroundColor: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = tenants.find((t) => {
|
||||||
|
const candidates = new Set<string>();
|
||||||
|
const primary = normalizeHost(t.domain);
|
||||||
|
if (primary) candidates.add(primary);
|
||||||
|
const extra = Array.isArray(t.domains) ? t.domains : [];
|
||||||
|
for (const d of extra) {
|
||||||
|
const n = normalizeHost(String(d));
|
||||||
|
if (n) candidates.add(n);
|
||||||
|
}
|
||||||
|
return candidates.has(host);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
console.warn(
|
||||||
|
`Source tenant not found for ${host}; using hardcoded Niazi palette.`,
|
||||||
|
);
|
||||||
|
return { ...FALLBACK_BRAND, sourceDomain: host, sourceFound: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryColor: source.primaryColor || FALLBACK_BRAND.primaryColor,
|
||||||
|
secondaryColor: source.secondaryColor || FALLBACK_BRAND.secondaryColor,
|
||||||
|
backgroundColor: source.backgroundColor || FALLBACK_BRAND.backgroundColor,
|
||||||
|
sourceDomain: host,
|
||||||
|
sourceFound: true,
|
||||||
|
sourceId: source.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const dryRun = env('KHANEGI_BRAND_DRY_RUN') === '1';
|
||||||
|
const brand = await resolveBrand();
|
||||||
|
|
||||||
|
const tenants = await prisma.tenant.findMany({
|
||||||
|
where: { product: ProductType.khanegi, isActive: true },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
domain: true,
|
||||||
|
domains: true,
|
||||||
|
slug: true,
|
||||||
|
nameFa: true,
|
||||||
|
uiVariant: true,
|
||||||
|
primaryColor: true,
|
||||||
|
secondaryColor: true,
|
||||||
|
backgroundColor: true,
|
||||||
|
appVersion: true,
|
||||||
|
},
|
||||||
|
orderBy: { domain: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const targets = tenants.filter(
|
||||||
|
(t) => !isAyginTenant(t.domain, t.domains, t.slug),
|
||||||
|
);
|
||||||
|
const skippedAygin = tenants.filter((t) =>
|
||||||
|
isAyginTenant(t.domain, t.domains, t.slug),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
dryRun,
|
||||||
|
brand: {
|
||||||
|
primaryColor: brand.primaryColor,
|
||||||
|
secondaryColor: brand.secondaryColor,
|
||||||
|
backgroundColor: brand.backgroundColor,
|
||||||
|
sourceDomain: brand.sourceDomain,
|
||||||
|
sourceFound: brand.sourceFound,
|
||||||
|
},
|
||||||
|
totalKhanegi: tenants.length,
|
||||||
|
targets: targets.length,
|
||||||
|
skippedAygin: skippedAygin.map((t) => ({
|
||||||
|
domain: t.domain,
|
||||||
|
uiVariant: t.uiVariant,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
for (const t of targets) {
|
||||||
|
const next = {
|
||||||
|
uiVariant: KhanegiUiVariant.modular,
|
||||||
|
primaryColor: brand.primaryColor,
|
||||||
|
secondaryColor: brand.secondaryColor,
|
||||||
|
backgroundColor: brand.backgroundColor,
|
||||||
|
appVersion: bumpAppVersion(t.appVersion),
|
||||||
|
};
|
||||||
|
|
||||||
|
const changed =
|
||||||
|
t.uiVariant !== next.uiVariant ||
|
||||||
|
t.primaryColor !== next.primaryColor ||
|
||||||
|
t.secondaryColor !== next.secondaryColor ||
|
||||||
|
t.backgroundColor !== next.backgroundColor;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
domain: t.domain,
|
||||||
|
nameFa: t.nameFa,
|
||||||
|
changed,
|
||||||
|
before: {
|
||||||
|
uiVariant: t.uiVariant,
|
||||||
|
primaryColor: t.primaryColor,
|
||||||
|
secondaryColor: t.secondaryColor,
|
||||||
|
backgroundColor: t.backgroundColor,
|
||||||
|
appVersion: t.appVersion,
|
||||||
|
},
|
||||||
|
after: next,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!changed || dryRun) continue;
|
||||||
|
|
||||||
|
await prisma.tenant.update({
|
||||||
|
where: { id: t.id },
|
||||||
|
data: next,
|
||||||
|
});
|
||||||
|
updated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Aygin stays newTheme (do not touch colors unless variant wrong)
|
||||||
|
for (const t of skippedAygin) {
|
||||||
|
if (t.uiVariant === KhanegiUiVariant.newTheme) continue;
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
domain: t.domain,
|
||||||
|
action: dryRun ? 'would_force_newTheme' : 'force_newTheme',
|
||||||
|
before: t.uiVariant,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (dryRun) continue;
|
||||||
|
await prisma.tenant.update({
|
||||||
|
where: { id: t.id },
|
||||||
|
data: {
|
||||||
|
uiVariant: KhanegiUiVariant.newTheme,
|
||||||
|
appVersion: bumpAppVersion(t.appVersion),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ dryRun, updated, done: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user