622 lines
22 KiB
TypeScript
622 lines
22 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { db } from "../../db/index";
|
|
import { eq, desc, and, or, isNull, inArray, sql } from "drizzle-orm";
|
|
import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema } from "@shared/schema";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const router = Router();
|
|
|
|
// Helper: Retorna IDs de tenants que o usuário pode acessar
|
|
async function getAllowedTenantIds(user: any): Promise<number[] | null> {
|
|
// null = acesso total (master)
|
|
if (!user.tenantId || user.tenantType === "master") return null;
|
|
|
|
if (user.tenantType === "partner") {
|
|
// Partner vê seu próprio tenant + clientes vinculados
|
|
const clientRelations = await db.select({ clientId: partnerClients.clientId })
|
|
.from(partnerClients)
|
|
.where(eq(partnerClients.partnerId, user.tenantId));
|
|
return [user.tenantId, ...clientRelations.map(r => r.clientId)];
|
|
}
|
|
|
|
// Client vê apenas seu próprio tenant
|
|
return [user.tenantId];
|
|
}
|
|
|
|
// Helper: Verifica se usuário pode acessar um tenant específico
|
|
async function canAccessTenant(user: any, tenantId: number): Promise<boolean> {
|
|
const allowed = await getAllowedTenantIds(user);
|
|
if (allowed === null) return true; // Master pode tudo
|
|
return allowed.includes(tenantId);
|
|
}
|
|
|
|
router.use((req: Request, res: Response, next) => {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Não autorizado" });
|
|
}
|
|
const user = req.user as any;
|
|
if (user.role !== "admin") {
|
|
return res.status(403).json({ error: "Acesso negado. Apenas administradores." });
|
|
}
|
|
next();
|
|
});
|
|
|
|
router.get("/profiles", async (req: Request, res: Response) => {
|
|
try {
|
|
const allProfiles = await db.select().from(profiles).orderBy(profiles.name);
|
|
res.json(allProfiles);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.post("/profiles", async (req: Request, res: Response) => {
|
|
try {
|
|
const { name, description, type, allowedModules, status } = req.body;
|
|
const [profile] = await db.insert(profiles).values({
|
|
name,
|
|
description,
|
|
type: type || "custom",
|
|
allowedModules: allowedModules || [],
|
|
status: status || "active",
|
|
}).returning();
|
|
res.status(201).json(profile);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.patch("/profiles/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { name, description, allowedModules, status } = req.body;
|
|
const [profile] = await db.update(profiles)
|
|
.set({ name, description, allowedModules, status, updatedAt: new Date() })
|
|
.where(eq(profiles.id, id))
|
|
.returning();
|
|
if (!profile) return res.status(404).json({ error: "Perfil não encontrado" });
|
|
res.json(profile);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.delete("/profiles/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const [profile] = await db.select().from(profiles).where(eq(profiles.id, id));
|
|
if (!profile) return res.status(404).json({ error: "Perfil não encontrado" });
|
|
if (profile.isSystem === 1) {
|
|
return res.status(400).json({ error: "Não é possível excluir perfis do sistema" });
|
|
}
|
|
await db.delete(profiles).where(eq(profiles.id, id));
|
|
res.status(204).send();
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get("/users", async (req: Request, res: Response) => {
|
|
try {
|
|
const allUsers = await db.select({
|
|
id: users.id,
|
|
username: users.username,
|
|
name: users.name,
|
|
email: users.email,
|
|
role: users.role,
|
|
profileId: users.profileId,
|
|
partnerId: users.partnerId,
|
|
status: users.status,
|
|
lastLoginAt: users.lastLoginAt,
|
|
createdAt: users.createdAt,
|
|
}).from(users).orderBy(users.name);
|
|
|
|
const usersWithProfile = await Promise.all(allUsers.map(async (user) => {
|
|
let profile = null;
|
|
let partner = null;
|
|
if (user.profileId) {
|
|
const [p] = await db.select().from(profiles).where(eq(profiles.id, user.profileId));
|
|
profile = p;
|
|
}
|
|
if (user.partnerId) {
|
|
const [pt] = await db.select().from(crmPartners).where(eq(crmPartners.id, user.partnerId));
|
|
partner = pt;
|
|
}
|
|
return { ...user, profile, partner };
|
|
}));
|
|
|
|
res.json(usersWithProfile);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.patch("/users/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const { name, email, role, profileId, partnerId, status } = req.body;
|
|
const [user] = await db.update(users)
|
|
.set({ name, email, role, profileId, partnerId, status })
|
|
.where(eq(users.id, id))
|
|
.returning();
|
|
if (!user) return res.status(404).json({ error: "Usuário não encontrado" });
|
|
res.json(user);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.patch("/users/:id/status", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const { status } = req.body;
|
|
const [user] = await db.update(users)
|
|
.set({ status })
|
|
.where(eq(users.id, id))
|
|
.returning();
|
|
if (!user) return res.status(404).json({ error: "Usuário não encontrado" });
|
|
res.json(user);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.patch("/partners/:id/status", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { status } = req.body;
|
|
const [partner] = await db.update(crmPartners)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(eq(crmPartners.id, id))
|
|
.returning();
|
|
if (!partner) return res.status(404).json({ error: "Parceiro não encontrado" });
|
|
res.json(partner);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.patch("/clients/:id/status", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { status } = req.body;
|
|
const [client] = await db.update(crmClients)
|
|
.set({ status, updatedAt: new Date() })
|
|
.where(eq(crmClients.id, id))
|
|
.returning();
|
|
if (!client) return res.status(404).json({ error: "Cliente não encontrado" });
|
|
res.json(client);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get("/stats", async (req: Request, res: Response) => {
|
|
try {
|
|
const [allUsers] = await db.select({ count: db.$count(users) }).from(users);
|
|
const [activeUsers] = await db.select({ count: db.$count(users) }).from(users).where(eq(users.status, "active"));
|
|
const [allProfiles] = await db.select({ count: db.$count(profiles) }).from(profiles);
|
|
const [allPartners] = await db.select({ count: db.$count(crmPartners) }).from(crmPartners);
|
|
const [activePartners] = await db.select({ count: db.$count(crmPartners) }).from(crmPartners).where(eq(crmPartners.status, "active"));
|
|
const [allClients] = await db.select({ count: db.$count(crmClients) }).from(crmClients);
|
|
const [activeClients] = await db.select({ count: db.$count(crmClients) }).from(crmClients).where(eq(crmClients.status, "active"));
|
|
|
|
res.json({
|
|
users: { total: allUsers.count, active: activeUsers.count },
|
|
profiles: { total: allProfiles.count },
|
|
partners: { total: allPartners.count, active: activePartners.count },
|
|
clients: { total: allClients.count, active: activeClients.count },
|
|
});
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get("/libraries", async (req: Request, res: Response) => {
|
|
try {
|
|
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
|
|
const dependencies = Object.entries(packageJson.dependencies || {}).map(([name, version]) => ({
|
|
name,
|
|
version: String(version).replace(/^\^|~/, ""),
|
|
type: "production",
|
|
category: categorizePackage(name),
|
|
}));
|
|
|
|
const devDependencies = Object.entries(packageJson.devDependencies || {}).map(([name, version]) => ({
|
|
name,
|
|
version: String(version).replace(/^\^|~/, ""),
|
|
type: "development",
|
|
category: categorizePackage(name),
|
|
}));
|
|
|
|
res.json({
|
|
nodejs: {
|
|
dependencies,
|
|
devDependencies,
|
|
total: dependencies.length + devDependencies.length,
|
|
},
|
|
python: {
|
|
dependencies: [],
|
|
total: 0,
|
|
note: "Python microservices planejados para próximas versões",
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
function categorizePackage(name: string): string {
|
|
if (name.includes("react") || name.includes("radix") || name.includes("tailwind")) return "UI/Frontend";
|
|
if (name.includes("express") || name.includes("passport") || name.includes("session")) return "Backend/Auth";
|
|
if (name.includes("drizzle") || name.includes("pg") || name.includes("connect-pg")) return "Database";
|
|
if (name.includes("socket") || name.includes("ws") || name.includes("whatsapp")) return "Real-time/Comunicação";
|
|
if (name.includes("openai") || name.includes("ai")) return "AI/ML";
|
|
if (name.includes("zod") || name.includes("hook-form")) return "Validação/Forms";
|
|
if (name.includes("vite") || name.includes("typescript") || name.includes("tsx") || name.includes("esbuild")) return "Build/Dev Tools";
|
|
if (name.includes("lucide") || name.includes("framer")) return "Icons/Animações";
|
|
if (name.includes("pdf") || name.includes("docx") || name.includes("csv") || name.includes("zip")) return "Documentos/Arquivos";
|
|
if (name.includes("recharts") || name.includes("chart")) return "Visualização";
|
|
if (name.includes("date") || name.includes("day-picker")) return "Data/Tempo";
|
|
return "Utilitários";
|
|
}
|
|
|
|
// ==========================================
|
|
// MULTI-TENANT MANAGEMENT ROUTES
|
|
// ==========================================
|
|
|
|
// GET /api/admin/tenants - List all tenants with hierarchy
|
|
router.get("/tenants", async (req: Request, res: Response) => {
|
|
try {
|
|
const { type } = req.query;
|
|
const user = req.user as any;
|
|
|
|
const allTenants = await db.select().from(tenants).orderBy(desc(tenants.createdAt));
|
|
|
|
// Filtrar por tenant do usuário
|
|
let filteredByAccess = allTenants;
|
|
if (user.tenantType === "partner") {
|
|
// Partner vê apenas seu próprio tenant e seus clientes
|
|
const clientRelations = await db.select().from(partnerClients)
|
|
.where(eq(partnerClients.partnerId, user.tenantId));
|
|
const clientIds = clientRelations.map(r => r.clientId);
|
|
filteredByAccess = allTenants.filter(t =>
|
|
t.id === user.tenantId || clientIds.includes(t.id)
|
|
);
|
|
} else if (user.tenantType === "client") {
|
|
// Client vê apenas seu próprio tenant
|
|
filteredByAccess = allTenants.filter(t => t.id === user.tenantId);
|
|
}
|
|
// Master (tenantType === "master") vê todos
|
|
|
|
// Filter by type if specified
|
|
const filteredTenants = type
|
|
? filteredByAccess.filter(t => t.tenantType === type)
|
|
: filteredByAccess;
|
|
|
|
// Add parent tenant info
|
|
const tenantsWithParent = await Promise.all(filteredTenants.map(async (tenant) => {
|
|
let parentTenant = null;
|
|
if (tenant.parentTenantId) {
|
|
const [parent] = await db.select().from(tenants).where(eq(tenants.id, tenant.parentTenantId));
|
|
parentTenant = parent ? { id: parent.id, name: parent.name, tenantType: parent.tenantType } : null;
|
|
}
|
|
|
|
// Count child tenants
|
|
const childTenants = allTenants.filter(t => t.parentTenantId === tenant.id);
|
|
|
|
return { ...tenant, parentTenant, childCount: childTenants.length };
|
|
}));
|
|
|
|
res.json(tenantsWithParent);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/admin/tenants - Create new tenant
|
|
router.post("/tenants", async (req: Request, res: Response) => {
|
|
try {
|
|
const validated = insertTenantSchema.parse(req.body);
|
|
const [tenant] = await db.insert(tenants).values(validated).returning();
|
|
res.status(201).json(tenant);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// PATCH /api/admin/tenants/:id - Update tenant
|
|
router.patch("/tenants/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const user = req.user as any;
|
|
|
|
// Verificar permissão de acesso
|
|
if (!await canAccessTenant(user, id)) {
|
|
return res.status(403).json({ error: "Sem permissão para modificar este tenant" });
|
|
}
|
|
|
|
const { name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt } = req.body;
|
|
|
|
const [tenant] = await db.update(tenants)
|
|
.set({
|
|
name, email, phone, plan, status, tenantType, parentTenantId, partnerCode,
|
|
commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt,
|
|
updatedAt: new Date()
|
|
})
|
|
.where(eq(tenants.id, id))
|
|
.returning();
|
|
|
|
if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" });
|
|
res.json(tenant);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/admin/tenants/:id - Delete tenant
|
|
router.delete("/tenants/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const user = req.user as any;
|
|
|
|
// Apenas master pode excluir tenants
|
|
if (user.tenantType !== "master") {
|
|
return res.status(403).json({ error: "Apenas master pode excluir tenants" });
|
|
}
|
|
|
|
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, id));
|
|
if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" });
|
|
if (tenant.tenantType === "master") {
|
|
return res.status(400).json({ error: "Não é possível excluir o tenant master" });
|
|
}
|
|
await db.delete(tenants).where(eq(tenants.id, id));
|
|
res.status(204).send();
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// GET /api/admin/tenants/hierarchy - Get hierarchy tree
|
|
router.get("/tenants/hierarchy", async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as any;
|
|
const allowedIds = await getAllowedTenantIds(user);
|
|
|
|
let allTenants = await db.select().from(tenants).orderBy(tenants.name);
|
|
|
|
// Filtrar por acesso do usuário
|
|
if (allowedIds !== null) {
|
|
allTenants = allTenants.filter(t => allowedIds.includes(t.id));
|
|
}
|
|
|
|
// Build hierarchy tree
|
|
const buildTree = (parentId: number | null): any[] => {
|
|
return allTenants
|
|
.filter(t => t.parentTenantId === parentId)
|
|
.map(t => ({
|
|
...t,
|
|
children: buildTree(t.id)
|
|
}));
|
|
};
|
|
|
|
// Start from master tenants (no parent) or user's tenant
|
|
const hierarchy = allowedIds === null
|
|
? buildTree(null)
|
|
: allTenants.map(t => ({ ...t, children: [] }));
|
|
res.json(hierarchy);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// TENANT PLANS ROUTES
|
|
// ==========================================
|
|
|
|
// GET /api/admin/plans - List all plans
|
|
router.get("/plans", async (req: Request, res: Response) => {
|
|
try {
|
|
const allPlans = await db.select().from(tenantPlans).orderBy(tenantPlans.sortOrder);
|
|
res.json(allPlans);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/admin/plans - Create new plan
|
|
router.post("/plans", async (req: Request, res: Response) => {
|
|
try {
|
|
const validated = insertTenantPlanSchema.parse(req.body);
|
|
const [plan] = await db.insert(tenantPlans).values(validated).returning();
|
|
res.status(201).json(plan);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// PATCH /api/admin/plans/:id - Update plan
|
|
router.patch("/plans/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { name, description, maxUsers, maxStorageMb, features, monthlyPrice, yearlyPrice, trialDays, isActive, sortOrder } = req.body;
|
|
|
|
const [plan] = await db.update(tenantPlans)
|
|
.set({ name, description, maxUsers, maxStorageMb, features, monthlyPrice, yearlyPrice, trialDays, isActive, sortOrder })
|
|
.where(eq(tenantPlans.id, id))
|
|
.returning();
|
|
|
|
if (!plan) return res.status(404).json({ error: "Plano não encontrado" });
|
|
res.json(plan);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/admin/plans/:id - Delete plan
|
|
router.delete("/plans/:id", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
await db.delete(tenantPlans).where(eq(tenantPlans.id, id));
|
|
res.status(204).send();
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// PARTNER-CLIENT RELATIONSHIPS
|
|
// ==========================================
|
|
|
|
// GET /api/admin/partner-clients - List partner-client relationships
|
|
router.get("/partner-clients", async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as any;
|
|
const allowedIds = await getAllowedTenantIds(user);
|
|
|
|
let relationships = await db.select().from(partnerClients).orderBy(desc(partnerClients.startedAt));
|
|
|
|
// Filtrar por acesso do usuário
|
|
if (allowedIds !== null) {
|
|
relationships = relationships.filter(r =>
|
|
allowedIds.includes(r.partnerId) || allowedIds.includes(r.clientId)
|
|
);
|
|
}
|
|
|
|
// Add partner and client info
|
|
const withDetails = await Promise.all(relationships.map(async (rel) => {
|
|
const [partner] = await db.select().from(tenants).where(eq(tenants.id, rel.partnerId));
|
|
const [client] = await db.select().from(tenants).where(eq(tenants.id, rel.clientId));
|
|
return {
|
|
...rel,
|
|
partner: partner ? { id: partner.id, name: partner.name } : null,
|
|
client: client ? { id: client.id, name: client.name } : null,
|
|
};
|
|
}));
|
|
|
|
res.json(withDetails);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/admin/partner-clients - Create partner-client relationship
|
|
router.post("/partner-clients", async (req: Request, res: Response) => {
|
|
try {
|
|
const validated = insertPartnerClientSchema.parse(req.body);
|
|
const [relationship] = await db.insert(partnerClients).values(validated).returning();
|
|
res.status(201).json(relationship);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// PARTNER COMMISSIONS
|
|
// ==========================================
|
|
|
|
// GET /api/admin/commissions - List all commissions
|
|
router.get("/commissions", async (req: Request, res: Response) => {
|
|
try {
|
|
const user = req.user as any;
|
|
const allowedIds = await getAllowedTenantIds(user);
|
|
const { partnerId, status } = req.query;
|
|
|
|
let allCommissions = await db.select().from(partnerCommissions).orderBy(desc(partnerCommissions.createdAt));
|
|
|
|
// Filtrar por acesso do usuário
|
|
if (allowedIds !== null) {
|
|
allCommissions = allCommissions.filter(c =>
|
|
allowedIds.includes(c.partnerId) || allowedIds.includes(c.clientId)
|
|
);
|
|
}
|
|
|
|
if (partnerId) {
|
|
allCommissions = allCommissions.filter(c => c.partnerId === parseInt(partnerId as string));
|
|
}
|
|
if (status) {
|
|
allCommissions = allCommissions.filter(c => c.status === status);
|
|
}
|
|
|
|
// Add partner and client info
|
|
const withDetails = await Promise.all(allCommissions.map(async (comm) => {
|
|
const [partner] = await db.select().from(tenants).where(eq(tenants.id, comm.partnerId));
|
|
const [client] = await db.select().from(tenants).where(eq(tenants.id, comm.clientId));
|
|
return {
|
|
...comm,
|
|
partner: partner ? { id: partner.id, name: partner.name } : null,
|
|
client: client ? { id: client.id, name: client.name } : null,
|
|
};
|
|
}));
|
|
|
|
res.json(withDetails);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// PATCH /api/admin/commissions/:id/approve - Approve commission
|
|
router.patch("/commissions/:id/approve", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const [commission] = await db.update(partnerCommissions)
|
|
.set({ status: "approved", approvedAt: new Date() })
|
|
.where(eq(partnerCommissions.id, id))
|
|
.returning();
|
|
|
|
if (!commission) return res.status(404).json({ error: "Comissão não encontrada" });
|
|
res.json(commission);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// PATCH /api/admin/commissions/:id/pay - Mark commission as paid
|
|
router.patch("/commissions/:id/pay", async (req: Request, res: Response) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const { paymentReference } = req.body;
|
|
|
|
const [commission] = await db.update(partnerCommissions)
|
|
.set({ status: "paid", paidAt: new Date(), paymentReference })
|
|
.where(eq(partnerCommissions.id, id))
|
|
.returning();
|
|
|
|
if (!commission) return res.status(404).json({ error: "Comissão não encontrada" });
|
|
res.json(commission);
|
|
} catch (error: any) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// GET /api/admin/tenants/stats - Tenant statistics
|
|
router.get("/tenants/stats", async (req: Request, res: Response) => {
|
|
try {
|
|
const allTenants = await db.select().from(tenants);
|
|
|
|
const stats = {
|
|
total: allTenants.length,
|
|
byType: {
|
|
master: allTenants.filter(t => t.tenantType === "master").length,
|
|
partner: allTenants.filter(t => t.tenantType === "partner").length,
|
|
client: allTenants.filter(t => t.tenantType === "client").length,
|
|
},
|
|
byStatus: {
|
|
active: allTenants.filter(t => t.status === "active").length,
|
|
trial: allTenants.filter(t => t.status === "trial").length,
|
|
suspended: allTenants.filter(t => t.status === "suspended").length,
|
|
cancelled: allTenants.filter(t => t.status === "cancelled").length,
|
|
}
|
|
};
|
|
|
|
res.json(stats);
|
|
} catch (error: any) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|