Phase 2 tweaks: admin tenant creation, unit delete, frequency, UI overhaul
- Admin panel: create tenants with org + first user, manage org status (active/suspended/archived), contract number and plan level fields - Units: delete with invoice check, assessment group dropdown binding - Assessment groups: frequency field (monthly/quarterly/annual) with income calculations normalized to monthly equivalents - Sidebar: grouped nav sections (Financials, Assessments, Transactions, Planning, Reports, Admin), renamed Chart of Accounts to Accounts - Header: replaced text with SVG logo - Capital projects: Kanban as default view, table-only PDF export, Future category (beyond 5-year plan) - Auth: block login for suspended/archived organizations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
import { Controller, Get, Post, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Body, Param, UseGuards, Req, ForbiddenException, BadRequestException } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { OrganizationsService } from '../organizations/organizations.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@ApiTags('admin')
|
||||
@Controller('admin')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AdminController {
|
||||
constructor(private usersService: UsersService) {}
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private orgService: OrganizationsService,
|
||||
) {}
|
||||
|
||||
private async requireSuperadmin(req: any) {
|
||||
const user = await this.usersService.findById(req.user.userId || req.user.sub);
|
||||
@@ -42,4 +47,77 @@ export class AdminController {
|
||||
await this.usersService.setSuperadmin(id, body.isSuperadmin);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post('tenants')
|
||||
async createTenant(@Req() req: any, @Body() body: {
|
||||
orgName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
addressLine1?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zipCode?: string;
|
||||
contractNumber?: string;
|
||||
planLevel?: string;
|
||||
fiscalYearStartMonth?: number;
|
||||
adminEmail: string;
|
||||
adminPassword: string;
|
||||
adminFirstName: string;
|
||||
adminLastName: string;
|
||||
}) {
|
||||
await this.requireSuperadmin(req);
|
||||
|
||||
if (!body.orgName || !body.adminEmail || !body.adminPassword) {
|
||||
throw new BadRequestException('Organization name, admin email and password are required');
|
||||
}
|
||||
|
||||
// Check if admin email already exists
|
||||
const existingUser = await this.usersService.findByEmail(body.adminEmail);
|
||||
let userId: string;
|
||||
|
||||
if (existingUser) {
|
||||
userId = existingUser.id;
|
||||
} else {
|
||||
// Create the first user for this tenant
|
||||
const passwordHash = await bcrypt.hash(body.adminPassword, 12);
|
||||
const newUser = await this.usersService.create({
|
||||
email: body.adminEmail,
|
||||
passwordHash,
|
||||
firstName: body.adminFirstName,
|
||||
lastName: body.adminLastName,
|
||||
});
|
||||
userId = newUser.id;
|
||||
}
|
||||
|
||||
// Create the organization + tenant schema + membership
|
||||
const org = await this.orgService.create({
|
||||
name: body.orgName,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
addressLine1: body.addressLine1,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
zipCode: body.zipCode,
|
||||
contractNumber: body.contractNumber,
|
||||
planLevel: body.planLevel || 'standard',
|
||||
fiscalYearStartMonth: body.fiscalYearStartMonth || 1,
|
||||
}, userId);
|
||||
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
|
||||
@Put('organizations/:id/status')
|
||||
async updateOrgStatus(
|
||||
@Req() req: any,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { status: string },
|
||||
) {
|
||||
await this.requireSuperadmin(req);
|
||||
const validStatuses = ['active', 'suspended', 'trial', 'archived'];
|
||||
if (!validStatuses.includes(body.status)) {
|
||||
throw new BadRequestException(`Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
||||
}
|
||||
const org = await this.orgService.updateStatus(id, body.status);
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user