- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
321 lines
12 KiB
JavaScript
321 lines
12 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.SuperadminService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const tenant_entity_1 = require("../tenants/entities/tenant.entity");
|
|
const user_entity_1 = require("../auth/entities/user.entity");
|
|
const subscription_entity_1 = require("../billing/entities/subscription.entity");
|
|
let SuperadminService = class SuperadminService {
|
|
constructor(tenantRepository, userRepository, subscriptionRepository) {
|
|
this.tenantRepository = tenantRepository;
|
|
this.userRepository = userRepository;
|
|
this.subscriptionRepository = subscriptionRepository;
|
|
}
|
|
async listTenants(query) {
|
|
const { page = 1, limit = 10, search, status, sortBy = 'created_at', sortOrder = 'DESC' } = query;
|
|
const where = {};
|
|
if (search) {
|
|
where.name = (0, typeorm_2.ILike)(`%${search}%`);
|
|
}
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
const [tenants, total] = await this.tenantRepository.findAndCount({
|
|
where,
|
|
order: { [sortBy]: sortOrder },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
});
|
|
const tenantsWithStats = await Promise.all(tenants.map(async (tenant) => {
|
|
const userCount = await this.userRepository.count({
|
|
where: { tenant_id: tenant.id },
|
|
});
|
|
const subscription = await this.subscriptionRepository.findOne({
|
|
where: { tenant_id: tenant.id },
|
|
relations: ['plan'],
|
|
});
|
|
return {
|
|
...tenant,
|
|
userCount,
|
|
subscription,
|
|
};
|
|
}));
|
|
return {
|
|
data: tenantsWithStats,
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
async getTenant(id) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.NotFoundException('Tenant not found');
|
|
}
|
|
const userCount = await this.userRepository.count({
|
|
where: { tenant_id: id },
|
|
});
|
|
const subscription = await this.subscriptionRepository.findOne({
|
|
where: { tenant_id: id },
|
|
relations: ['plan'],
|
|
});
|
|
return {
|
|
...tenant,
|
|
userCount,
|
|
subscription,
|
|
};
|
|
}
|
|
async createTenant(dto) {
|
|
const existingTenant = await this.tenantRepository.findOne({
|
|
where: { slug: dto.slug },
|
|
});
|
|
if (existingTenant) {
|
|
throw new common_1.ConflictException('A tenant with this slug already exists');
|
|
}
|
|
const tenant = this.tenantRepository.create({
|
|
name: dto.name,
|
|
slug: dto.slug,
|
|
domain: dto.domain,
|
|
logo_url: dto.logo_url,
|
|
plan_id: dto.plan_id,
|
|
status: dto.status || 'trial',
|
|
trial_ends_at: dto.status === 'trial' ? new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) : null,
|
|
});
|
|
return this.tenantRepository.save(tenant);
|
|
}
|
|
async updateTenant(id, dto) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.NotFoundException('Tenant not found');
|
|
}
|
|
Object.assign(tenant, dto);
|
|
return this.tenantRepository.save(tenant);
|
|
}
|
|
async updateTenantStatus(id, dto) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.NotFoundException('Tenant not found');
|
|
}
|
|
tenant.status = dto.status;
|
|
if (dto.reason) {
|
|
tenant.metadata = {
|
|
...tenant.metadata,
|
|
statusChangeReason: dto.reason,
|
|
statusChangedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
return this.tenantRepository.save(tenant);
|
|
}
|
|
async deleteTenant(id) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.NotFoundException('Tenant not found');
|
|
}
|
|
const userCount = await this.userRepository.count({
|
|
where: { tenant_id: id },
|
|
});
|
|
if (userCount > 0) {
|
|
throw new common_1.BadRequestException('Cannot delete tenant with active users. Please remove all users first or suspend the tenant.');
|
|
}
|
|
await this.tenantRepository.remove(tenant);
|
|
}
|
|
async getTenantUsers(tenantId, page = 1, limit = 10) {
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: tenantId },
|
|
});
|
|
if (!tenant) {
|
|
throw new common_1.NotFoundException('Tenant not found');
|
|
}
|
|
const [users, total] = await this.userRepository.findAndCount({
|
|
where: { tenant_id: tenantId },
|
|
order: { created_at: 'DESC' },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
});
|
|
return {
|
|
data: users,
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
async getDashboardStats() {
|
|
const [totalTenants, activeTenants, trialTenants, suspendedTenants, totalUsers, newTenantsThisMonth,] = await Promise.all([
|
|
this.tenantRepository.count(),
|
|
this.tenantRepository.count({ where: { status: 'active' } }),
|
|
this.tenantRepository.count({ where: { status: 'trial' } }),
|
|
this.tenantRepository.count({ where: { status: 'suspended' } }),
|
|
this.userRepository.count(),
|
|
this.tenantRepository
|
|
.createQueryBuilder('tenant')
|
|
.where('tenant.created_at >= :startOfMonth', {
|
|
startOfMonth: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
|
})
|
|
.getCount(),
|
|
]);
|
|
return {
|
|
totalTenants,
|
|
activeTenants,
|
|
trialTenants,
|
|
suspendedTenants,
|
|
totalUsers,
|
|
newTenantsThisMonth,
|
|
};
|
|
}
|
|
async getTenantGrowth(months = 12) {
|
|
const result = [];
|
|
const now = new Date();
|
|
for (let i = months - 1; i >= 0; i--) {
|
|
const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59);
|
|
const count = await this.tenantRepository
|
|
.createQueryBuilder('tenant')
|
|
.where('tenant.created_at >= :startDate', { startDate })
|
|
.andWhere('tenant.created_at <= :endDate', { endDate })
|
|
.getCount();
|
|
result.push({
|
|
month: startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
|
|
count,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
async getUserGrowth(months = 12) {
|
|
const result = [];
|
|
const now = new Date();
|
|
for (let i = months - 1; i >= 0; i--) {
|
|
const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59);
|
|
const count = await this.userRepository
|
|
.createQueryBuilder('user')
|
|
.where('user.created_at >= :startDate', { startDate })
|
|
.andWhere('user.created_at <= :endDate', { endDate })
|
|
.getCount();
|
|
result.push({
|
|
month: startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
|
|
count,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
async getPlanDistribution() {
|
|
const subscriptions = await this.subscriptionRepository
|
|
.createQueryBuilder('sub')
|
|
.leftJoinAndSelect('sub.plan', 'plan')
|
|
.where('sub.status = :status', { status: 'active' })
|
|
.getMany();
|
|
const planCounts = {};
|
|
let total = 0;
|
|
for (const sub of subscriptions) {
|
|
const planName = sub.plan?.display_name || sub.plan?.name || 'Unknown';
|
|
planCounts[planName] = (planCounts[planName] || 0) + 1;
|
|
total++;
|
|
}
|
|
const tenantsWithSubscription = subscriptions.map(s => s.tenant_id);
|
|
const freeCount = await this.tenantRepository
|
|
.createQueryBuilder('tenant')
|
|
.where('tenant.id NOT IN (:...ids)', {
|
|
ids: tenantsWithSubscription.length > 0 ? tenantsWithSubscription : ['00000000-0000-0000-0000-000000000000']
|
|
})
|
|
.getCount();
|
|
if (freeCount > 0) {
|
|
planCounts['Free'] = freeCount;
|
|
total += freeCount;
|
|
}
|
|
return Object.entries(planCounts).map(([plan, count]) => ({
|
|
plan,
|
|
count,
|
|
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
|
|
}));
|
|
}
|
|
async getStatusDistribution() {
|
|
const statuses = ['active', 'trial', 'suspended', 'canceled'];
|
|
const total = await this.tenantRepository.count();
|
|
const result = await Promise.all(statuses.map(async (status) => {
|
|
const count = await this.tenantRepository.count({ where: { status } });
|
|
return {
|
|
status: status.charAt(0).toUpperCase() + status.slice(1),
|
|
count,
|
|
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
|
|
};
|
|
}));
|
|
return result;
|
|
}
|
|
async getTopTenants(limit = 10) {
|
|
const tenants = await this.tenantRepository.find({
|
|
order: { created_at: 'ASC' },
|
|
take: 100,
|
|
});
|
|
const tenantsWithCounts = await Promise.all(tenants.map(async (tenant) => {
|
|
const userCount = await this.userRepository.count({
|
|
where: { tenant_id: tenant.id },
|
|
});
|
|
const subscription = await this.subscriptionRepository.findOne({
|
|
where: { tenant_id: tenant.id },
|
|
relations: ['plan'],
|
|
});
|
|
return {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
slug: tenant.slug,
|
|
userCount,
|
|
status: tenant.status,
|
|
planName: subscription?.plan?.display_name || 'Free',
|
|
};
|
|
}));
|
|
return tenantsWithCounts
|
|
.sort((a, b) => b.userCount - a.userCount)
|
|
.slice(0, limit);
|
|
}
|
|
async getMetricsSummary() {
|
|
const [tenantGrowth, userGrowth, planDistribution, statusDistribution, topTenants] = await Promise.all([
|
|
this.getTenantGrowth(12),
|
|
this.getUserGrowth(12),
|
|
this.getPlanDistribution(),
|
|
this.getStatusDistribution(),
|
|
this.getTopTenants(10),
|
|
]);
|
|
return {
|
|
tenantGrowth,
|
|
userGrowth,
|
|
planDistribution,
|
|
statusDistribution,
|
|
topTenants,
|
|
};
|
|
}
|
|
};
|
|
exports.SuperadminService = SuperadminService;
|
|
exports.SuperadminService = SuperadminService = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, typeorm_1.InjectRepository)(tenant_entity_1.Tenant)),
|
|
__param(1, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
|
|
__param(2, (0, typeorm_1.InjectRepository)(subscription_entity_1.Subscription)),
|
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository])
|
|
], SuperadminService);
|
|
//# sourceMappingURL=superadmin.service.js.map
|