feat: Excel export of all projects for full-access admins
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Generated
+2049
-6
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -53,12 +53,13 @@
|
|||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/client": "6.16.0",
|
"@prisma/client": "6.16.0",
|
||||||
|
"@types/jalaali-js": "^1.2.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
"jalaali-js": "^2.0.1",
|
"jalaali-js": "^2.0.1",
|
||||||
"@types/jalaali-js": "^1.2.0",
|
|
||||||
"mysql2": "^3.23.1",
|
"mysql2": "^3.23.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { PluginsModule } from './plugins/plugins.module';
|
|||||||
import { SupportModule } from './support/support.module';
|
import { SupportModule } from './support/support.module';
|
||||||
import { NotificationsModule } from './notifications/notifications.module';
|
import { NotificationsModule } from './notifications/notifications.module';
|
||||||
import { AdminsModule } from './admins/admins.module';
|
import { AdminsModule } from './admins/admins.module';
|
||||||
|
import { ExportModule } from './export/export.module';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { ReplicaWatchModule } from './replica-watch/replica-watch.module';
|
import { ReplicaWatchModule } from './replica-watch/replica-watch.module';
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ import { ReplicaWatchModule } from './replica-watch/replica-watch.module';
|
|||||||
PluginsModule,
|
PluginsModule,
|
||||||
SupportModule,
|
SupportModule,
|
||||||
AdminsModule,
|
AdminsModule,
|
||||||
|
ExportModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isFullAccessRole } from './permissions';
|
||||||
|
|
||||||
|
/** Only role=super_admin (full access). Limited admins are blocked. */
|
||||||
|
@Injectable()
|
||||||
|
export class FullAccessGuard implements CanActivate {
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const user = context.switchToHttp().getRequest()?.user as
|
||||||
|
| { role?: string | null }
|
||||||
|
| undefined;
|
||||||
|
if (!isFullAccessRole(user?.role)) {
|
||||||
|
throw new ForbiddenException('فقط مدیران با دسترسی کامل میتوانند این کار را انجام دهند');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Controller, Get, Res, UseGuards } from '@nestjs/common';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { FullAccessGuard } from '../auth/full-access.guard';
|
||||||
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
|
@Controller('export')
|
||||||
|
@UseGuards(JwtAuthGuard, FullAccessGuard)
|
||||||
|
export class ExportController {
|
||||||
|
constructor(private readonly exportService: ExportService) {}
|
||||||
|
|
||||||
|
@Get('projects.xlsx')
|
||||||
|
async projectsXlsx(@Res() res: Response) {
|
||||||
|
const { buffer, filename } = await this.exportService.buildWorkbook();
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
||||||
|
);
|
||||||
|
res.send(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
import { ExportController } from './export.controller';
|
||||||
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [ExportController],
|
||||||
|
providers: [ExportService],
|
||||||
|
})
|
||||||
|
export class ExportModule {}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as ExcelJS from 'exceljs';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import {
|
||||||
|
formatIranDate,
|
||||||
|
formatIranDateTime,
|
||||||
|
iranDaysUntil,
|
||||||
|
} from '../common/iran-time.util';
|
||||||
|
|
||||||
|
const PRODUCT_LABEL: Record<string, string> = {
|
||||||
|
hamkar: 'همکار',
|
||||||
|
khanegi: 'خانگی',
|
||||||
|
other: 'سایتهای دیگر',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExportService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async buildWorkbook(): Promise<{ buffer: Buffer; filename: string }> {
|
||||||
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
|
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||||
|
include: {
|
||||||
|
smsAccount: true,
|
||||||
|
serverAccounts: { orderBy: { createdAt: 'asc' } },
|
||||||
|
pluginSubscriptions: { orderBy: { createdAt: 'asc' } },
|
||||||
|
supportSubscriptions: { orderBy: { createdAt: 'asc' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
workbook.creator = 'SuperAdmin Abshode';
|
||||||
|
workbook.created = new Date();
|
||||||
|
|
||||||
|
this.addProjectsSheet(workbook, tenants);
|
||||||
|
this.addServersSheet(workbook, tenants);
|
||||||
|
this.addPluginsSheet(workbook, tenants);
|
||||||
|
this.addSupportsSheet(workbook, tenants);
|
||||||
|
this.addSmsSheet(workbook, tenants);
|
||||||
|
|
||||||
|
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
const stamp = formatIranDate(new Date()).replace(/\//g, '-');
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
filename: `superadmin-export-${stamp}.xlsx`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private addProjectsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||||
|
const sheet = workbook.addWorksheet('پروژهها', {
|
||||||
|
views: [{ rightToLeft: true }],
|
||||||
|
});
|
||||||
|
sheet.columns = [
|
||||||
|
{ header: 'محصول', key: 'product', width: 14 },
|
||||||
|
{ header: 'نام فارسی', key: 'nameFa', width: 24 },
|
||||||
|
{ header: 'نام لاتین', key: 'nameEn', width: 18 },
|
||||||
|
{ header: 'slug', key: 'slug', width: 18 },
|
||||||
|
{ header: 'دامنه اصلی', key: 'domain', width: 22 },
|
||||||
|
{ header: 'دامنههای اضافی', key: 'domains', width: 28 },
|
||||||
|
{ header: 'فعال', key: 'isActive', width: 8 },
|
||||||
|
{ header: 'مسدود دسترسی سرور', key: 'serverAccessBlocked', width: 16 },
|
||||||
|
{ header: 'ورژن اپ', key: 'appVersion', width: 10 },
|
||||||
|
{ header: 'ظاهر خانگی', key: 'uiVariant', width: 12 },
|
||||||
|
{ header: 'node_server', key: 'nodeServer', width: 28 },
|
||||||
|
{ header: 'node_ws', key: 'nodeWs', width: 24 },
|
||||||
|
{ header: 'app_url', key: 'appUrl', width: 24 },
|
||||||
|
{ header: 'admin_url', key: 'adminUrl', width: 24 },
|
||||||
|
{ header: 'رنگ اصلی', key: 'primaryColor', width: 12 },
|
||||||
|
{ header: 'رنگ ثانویه', key: 'secondaryColor', width: 12 },
|
||||||
|
{ header: 'رنگ پسزمینه', key: 'backgroundColor', width: 12 },
|
||||||
|
{ header: 'لوگو', key: 'avatarUrl', width: 28 },
|
||||||
|
{ header: 'تعداد سرور', key: 'serversCount', width: 10 },
|
||||||
|
{ header: 'تعداد افزونه', key: 'pluginsCount', width: 10 },
|
||||||
|
{ header: 'تعداد پشتیبانی', key: 'supportsCount', width: 12 },
|
||||||
|
{ header: 'موجودی پیامک (تومان)', key: 'smsBalanceToman', width: 16 },
|
||||||
|
{ header: 'پیامک مسدود', key: 'smsBlocked', width: 12 },
|
||||||
|
{ header: 'یادداشت', key: 'notes', width: 30 },
|
||||||
|
{ header: 'تاریخ ایجاد', key: 'createdAt', width: 18 },
|
||||||
|
{ header: 'آخرین بهروزرسانی', key: 'updatedAt', width: 18 },
|
||||||
|
];
|
||||||
|
this.styleHeader(sheet);
|
||||||
|
|
||||||
|
for (const t of tenants) {
|
||||||
|
const balanceRials = t.smsAccount?.balanceRials;
|
||||||
|
sheet.addRow({
|
||||||
|
product: PRODUCT_LABEL[t.product] || t.product,
|
||||||
|
nameFa: t.nameFa,
|
||||||
|
nameEn: t.nameEn || '',
|
||||||
|
slug: t.slug,
|
||||||
|
domain: t.domain,
|
||||||
|
domains: this.formatDomains(t.domains),
|
||||||
|
isActive: t.isActive ? 'بله' : 'خیر',
|
||||||
|
serverAccessBlocked: t.serverAccessBlocked ? 'بله' : 'خیر',
|
||||||
|
appVersion: t.appVersion || '',
|
||||||
|
uiVariant: t.product === 'khanegi' ? t.uiVariant : '',
|
||||||
|
nodeServer: t.nodeServer || '',
|
||||||
|
nodeWs: t.nodeWs || '',
|
||||||
|
appUrl: t.appUrl || '',
|
||||||
|
adminUrl: t.adminUrl || '',
|
||||||
|
primaryColor: t.primaryColor || '',
|
||||||
|
secondaryColor: t.secondaryColor || '',
|
||||||
|
backgroundColor: t.backgroundColor || '',
|
||||||
|
avatarUrl: t.avatarUrl || '',
|
||||||
|
serversCount: t.serverAccounts.length,
|
||||||
|
pluginsCount: t.pluginSubscriptions.length,
|
||||||
|
supportsCount: t.supportSubscriptions.length,
|
||||||
|
smsBalanceToman:
|
||||||
|
balanceRials == null ? '' : Math.round(balanceRials / 10),
|
||||||
|
smsBlocked: t.smsAccount?.isBlocked ? 'بله' : t.smsAccount ? 'خیر' : '',
|
||||||
|
notes: t.notes || '',
|
||||||
|
createdAt: formatIranDateTime(t.createdAt),
|
||||||
|
updatedAt: formatIranDateTime(t.updatedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addServersSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||||
|
const sheet = workbook.addWorksheet('سرورها', {
|
||||||
|
views: [{ rightToLeft: true }],
|
||||||
|
});
|
||||||
|
sheet.columns = [
|
||||||
|
{ header: 'محصول', key: 'product', width: 14 },
|
||||||
|
{ header: 'نام پروژه', key: 'tenantName', width: 24 },
|
||||||
|
{ header: 'دامنه', key: 'domain', width: 22 },
|
||||||
|
{ header: 'نام سرور', key: 'serverName', width: 20 },
|
||||||
|
{ header: 'اختصاصی', key: 'isDedicated', width: 10 },
|
||||||
|
{ header: 'مشخصات', key: 'specs', width: 28 },
|
||||||
|
{ header: 'قیمت (تومان)', key: 'priceToman', width: 14 },
|
||||||
|
{ header: 'مدت (ماه)', key: 'planMonths', width: 10 },
|
||||||
|
{ header: 'تاریخ شارژ', key: 'chargedAt', width: 14 },
|
||||||
|
{ header: 'تاریخ انقضا', key: 'expiresAt', width: 14 },
|
||||||
|
{ header: 'روز تا انقضا', key: 'daysLeft', width: 12 },
|
||||||
|
{ header: 'هشدار (روز قبل)', key: 'alertDaysBefore', width: 14 },
|
||||||
|
{ header: 'توضیحات', key: 'description', width: 28 },
|
||||||
|
];
|
||||||
|
this.styleHeader(sheet);
|
||||||
|
|
||||||
|
for (const t of tenants) {
|
||||||
|
for (const s of t.serverAccounts) {
|
||||||
|
sheet.addRow({
|
||||||
|
product: PRODUCT_LABEL[t.product] || t.product,
|
||||||
|
tenantName: t.nameFa,
|
||||||
|
domain: t.domain,
|
||||||
|
serverName: s.serverName || '',
|
||||||
|
isDedicated: s.isDedicated ? 'بله' : 'خیر',
|
||||||
|
specs: s.specs || '',
|
||||||
|
priceToman: s.priceRials == null ? '' : Math.round(s.priceRials / 10),
|
||||||
|
planMonths: s.planMonths ?? '',
|
||||||
|
chargedAt: s.chargedAt ? formatIranDate(s.chargedAt) : '',
|
||||||
|
expiresAt: s.expiresAt ? formatIranDate(s.expiresAt) : '',
|
||||||
|
daysLeft: s.expiresAt ? this.daysLeftLabel(s.expiresAt) : '',
|
||||||
|
alertDaysBefore: s.alertDaysBefore,
|
||||||
|
description: s.description || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addPluginsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||||
|
const sheet = workbook.addWorksheet('افزونهها', {
|
||||||
|
views: [{ rightToLeft: true }],
|
||||||
|
});
|
||||||
|
sheet.columns = [
|
||||||
|
{ header: 'محصول', key: 'product', width: 14 },
|
||||||
|
{ header: 'نام پروژه', key: 'tenantName', width: 24 },
|
||||||
|
{ header: 'دامنه', key: 'domain', width: 22 },
|
||||||
|
{ header: 'نام افزونه', key: 'name', width: 22 },
|
||||||
|
{ header: 'کلید تنظیمات', key: 'settingKey', width: 18 },
|
||||||
|
{ header: 'فعال', key: 'isActive', width: 8 },
|
||||||
|
{ header: 'مشخصات', key: 'specs', width: 24 },
|
||||||
|
{ header: 'قیمت (تومان)', key: 'priceToman', width: 14 },
|
||||||
|
{ header: 'مدت (ماه)', key: 'planMonths', width: 10 },
|
||||||
|
{ header: 'تاریخ شارژ', key: 'chargedAt', width: 14 },
|
||||||
|
{ header: 'تاریخ انقضا', key: 'expiresAt', width: 14 },
|
||||||
|
{ header: 'روز تا انقضا', key: 'daysLeft', width: 12 },
|
||||||
|
{ header: 'قطع خودکار', key: 'autoCutOnExpiry', width: 12 },
|
||||||
|
{ header: 'هشدار (روز قبل)', key: 'alertDaysBefore', width: 14 },
|
||||||
|
{ header: 'توضیحات', key: 'description', width: 28 },
|
||||||
|
];
|
||||||
|
this.styleHeader(sheet);
|
||||||
|
|
||||||
|
for (const t of tenants) {
|
||||||
|
for (const p of t.pluginSubscriptions) {
|
||||||
|
sheet.addRow({
|
||||||
|
product: PRODUCT_LABEL[t.product] || t.product,
|
||||||
|
tenantName: t.nameFa,
|
||||||
|
domain: t.domain,
|
||||||
|
name: p.name,
|
||||||
|
settingKey: p.settingKey || '',
|
||||||
|
isActive: p.isActive ? 'بله' : 'خیر',
|
||||||
|
specs: p.specs || '',
|
||||||
|
priceToman: p.priceRials == null ? '' : Math.round(p.priceRials / 10),
|
||||||
|
planMonths: p.planMonths ?? '',
|
||||||
|
chargedAt: p.chargedAt ? formatIranDate(p.chargedAt) : '',
|
||||||
|
expiresAt: p.expiresAt ? formatIranDate(p.expiresAt) : '',
|
||||||
|
daysLeft: p.expiresAt ? this.daysLeftLabel(p.expiresAt) : '',
|
||||||
|
autoCutOnExpiry: p.autoCutOnExpiry ? 'بله' : 'خیر',
|
||||||
|
alertDaysBefore: p.alertDaysBefore,
|
||||||
|
description: p.description || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSupportsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||||
|
const sheet = workbook.addWorksheet('پشتیبانی', {
|
||||||
|
views: [{ rightToLeft: true }],
|
||||||
|
});
|
||||||
|
sheet.columns = [
|
||||||
|
{ header: 'محصول', key: 'product', width: 14 },
|
||||||
|
{ header: 'نام پروژه', key: 'tenantName', width: 24 },
|
||||||
|
{ header: 'دامنه', key: 'domain', width: 22 },
|
||||||
|
{ header: 'نام پشتیبانی', key: 'name', width: 22 },
|
||||||
|
{ header: 'فعال', key: 'isActive', width: 8 },
|
||||||
|
{ header: 'رایگان', key: 'isFree', width: 8 },
|
||||||
|
{ header: 'مشخصات', key: 'specs', width: 24 },
|
||||||
|
{ header: 'قیمت (تومان)', key: 'priceToman', width: 14 },
|
||||||
|
{ header: 'مدت (ماه)', key: 'planMonths', width: 10 },
|
||||||
|
{ header: 'تاریخ شارژ', key: 'chargedAt', width: 14 },
|
||||||
|
{ header: 'تاریخ انقضا', key: 'expiresAt', width: 14 },
|
||||||
|
{ header: 'روز تا انقضا', key: 'daysLeft', width: 12 },
|
||||||
|
{ header: 'هشدار (روز قبل)', key: 'alertDaysBefore', width: 14 },
|
||||||
|
{ header: 'توضیحات', key: 'description', width: 28 },
|
||||||
|
];
|
||||||
|
this.styleHeader(sheet);
|
||||||
|
|
||||||
|
for (const t of tenants) {
|
||||||
|
for (const s of t.supportSubscriptions) {
|
||||||
|
sheet.addRow({
|
||||||
|
product: PRODUCT_LABEL[t.product] || t.product,
|
||||||
|
tenantName: t.nameFa,
|
||||||
|
domain: t.domain,
|
||||||
|
name: s.name,
|
||||||
|
isActive: s.isActive ? 'بله' : 'خیر',
|
||||||
|
isFree: s.isFree ? 'بله' : 'خیر',
|
||||||
|
specs: s.specs || '',
|
||||||
|
priceToman: s.priceRials == null ? '' : Math.round(s.priceRials / 10),
|
||||||
|
planMonths: s.planMonths ?? '',
|
||||||
|
chargedAt: s.chargedAt ? formatIranDate(s.chargedAt) : '',
|
||||||
|
expiresAt: s.expiresAt ? formatIranDate(s.expiresAt) : '',
|
||||||
|
daysLeft: s.expiresAt ? this.daysLeftLabel(s.expiresAt) : '',
|
||||||
|
alertDaysBefore: s.alertDaysBefore,
|
||||||
|
description: s.description || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSmsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||||
|
const sheet = workbook.addWorksheet('پیامک', {
|
||||||
|
views: [{ rightToLeft: true }],
|
||||||
|
});
|
||||||
|
sheet.columns = [
|
||||||
|
{ header: 'محصول', key: 'product', width: 14 },
|
||||||
|
{ header: 'نام پروژه', key: 'tenantName', width: 24 },
|
||||||
|
{ header: 'دامنه', key: 'domain', width: 22 },
|
||||||
|
{ header: 'موجودی (تومان)', key: 'balanceToman', width: 14 },
|
||||||
|
{ header: 'تعرفه هر بخش (تومان)', key: 'pricePerPartToman', width: 16 },
|
||||||
|
{ header: 'مسدود', key: 'isBlocked', width: 10 },
|
||||||
|
{ header: 'آستانه مسدود خودکار (تومان)', key: 'autoBlockBelowToman', width: 18 },
|
||||||
|
{ header: 'آستانه قرمز (تومان)', key: 'alertCriticalToman', width: 16 },
|
||||||
|
{ header: 'آستانه زرد (تومان)', key: 'alertWarningToman', width: 16 },
|
||||||
|
{ header: 'درگاه', key: 'gatewayProvider', width: 14 },
|
||||||
|
{ header: 'ارسال امروز (پیام)', key: 'todayMessages', width: 14 },
|
||||||
|
{ header: 'ارسال امروز (بخش)', key: 'todayParts', width: 14 },
|
||||||
|
{ header: 'هزینه امروز (تومان)', key: 'todayCostToman', width: 14 },
|
||||||
|
{ header: 'ارسال ماه (پیام)', key: 'monthMessages', width: 14 },
|
||||||
|
{ header: 'ارسال ماه (بخش)', key: 'monthParts', width: 14 },
|
||||||
|
{ header: 'هزینه ماه (تومان)', key: 'monthCostToman', width: 14 },
|
||||||
|
];
|
||||||
|
this.styleHeader(sheet);
|
||||||
|
|
||||||
|
for (const t of tenants) {
|
||||||
|
const a = t.smsAccount;
|
||||||
|
if (!a) continue;
|
||||||
|
sheet.addRow({
|
||||||
|
product: PRODUCT_LABEL[t.product] || t.product,
|
||||||
|
tenantName: t.nameFa,
|
||||||
|
domain: t.domain,
|
||||||
|
balanceToman: Math.round(a.balanceRials / 10),
|
||||||
|
pricePerPartToman: Math.round(a.pricePerPartRials / 10),
|
||||||
|
isBlocked: a.isBlocked ? 'بله' : 'خیر',
|
||||||
|
autoBlockBelowToman:
|
||||||
|
a.autoBlockBelowRials == null
|
||||||
|
? ''
|
||||||
|
: Math.round(a.autoBlockBelowRials / 10),
|
||||||
|
alertCriticalToman: Math.round(a.alertCriticalRials / 10),
|
||||||
|
alertWarningToman: Math.round(a.alertWarningRials / 10),
|
||||||
|
gatewayProvider: a.gatewayProvider || '',
|
||||||
|
todayMessages: a.todayMessages,
|
||||||
|
todayParts: a.todayParts,
|
||||||
|
todayCostToman: Math.round(a.todayCostRials / 10),
|
||||||
|
monthMessages: a.monthMessages,
|
||||||
|
monthParts: a.monthParts,
|
||||||
|
monthCostToman: Math.round(a.monthCostRials / 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used only for typing the Prisma include result. */
|
||||||
|
private load() {
|
||||||
|
return this.prisma.tenant.findMany({
|
||||||
|
include: {
|
||||||
|
smsAccount: true,
|
||||||
|
serverAccounts: true,
|
||||||
|
pluginSubscriptions: true,
|
||||||
|
supportSubscriptions: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private styleHeader(sheet: ExcelJS.Worksheet) {
|
||||||
|
const row = sheet.getRow(1);
|
||||||
|
row.font = { bold: true };
|
||||||
|
row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||||
|
row.height = 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDomains(raw: unknown): string {
|
||||||
|
if (!raw) return '';
|
||||||
|
if (Array.isArray(raw)) return raw.map(String).filter(Boolean).join('، ');
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed)
|
||||||
|
? parsed.map(String).filter(Boolean).join('، ')
|
||||||
|
: raw;
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private daysLeftLabel(expiresAt: Date): string {
|
||||||
|
return String(iranDaysUntil(expiresAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user