Implement Phase 2 features: roles, assessment groups, budget import, Kanban
- Add hierarchical roles: SuperUser Admin (is_superadmin flag), Tenant Admin, Tenant User with separate /admin route and admin panel - Add Assessment Groups module for property type-based assessment rates (SFHs, Condos, Estate Lots with different regular/special rates) - Enhance Chart of Accounts: initial balance on create (with journal entry), archive/restore accounts, edit all fields including account number & fund type - Add Budget CSV import with downloadable template and account mapping - Add Capital Projects Kanban board with drag-and-drop between year columns, table/kanban view toggle, and PDF export via browser print - Update seed data with assessment groups, second test user, superadmin flag - Create repeatable reseed.sh script for clean database population - Fix AgingReportPage Mantine v7 Table prop compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,8 +16,8 @@ export class AccountsController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List all accounts' })
|
||||
findAll(@Query('fundType') fundType?: string) {
|
||||
return this.accountsService.findAll(fundType);
|
||||
findAll(@Query('fundType') fundType?: string, @Query('includeArchived') includeArchived?: string) {
|
||||
return this.accountsService.findAll(fundType, includeArchived === 'true');
|
||||
}
|
||||
|
||||
@Get('trial-balance')
|
||||
|
||||
@@ -7,12 +7,21 @@ import { UpdateAccountDto } from './dto/update-account.dto';
|
||||
export class AccountsService {
|
||||
constructor(private tenant: TenantService) {}
|
||||
|
||||
async findAll(fundType?: string) {
|
||||
let sql = 'SELECT * FROM accounts WHERE is_active = true';
|
||||
async findAll(fundType?: string, includeArchived?: boolean) {
|
||||
let sql = 'SELECT * FROM accounts';
|
||||
const params: any[] = [];
|
||||
const conditions: string[] = [];
|
||||
|
||||
if (!includeArchived) {
|
||||
conditions.push('is_active = true');
|
||||
}
|
||||
if (fundType) {
|
||||
sql += ' AND fund_type = $1';
|
||||
params.push(fundType);
|
||||
conditions.push(`fund_type = $${params.length}`);
|
||||
}
|
||||
|
||||
if (conditions.length) {
|
||||
sql += ' WHERE ' + conditions.join(' AND ');
|
||||
}
|
||||
sql += ' ORDER BY account_number';
|
||||
return this.tenant.query(sql, params);
|
||||
@@ -47,7 +56,51 @@ export class AccountsService {
|
||||
dto.is1099Reportable || false,
|
||||
],
|
||||
);
|
||||
return rows[0];
|
||||
const account = rows[0];
|
||||
|
||||
// Create opening balance journal entry if initialBalance is provided and non-zero
|
||||
if (dto.initialBalance && dto.initialBalance !== 0) {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
|
||||
// Find or use the current fiscal period
|
||||
const periods = await this.tenant.query(
|
||||
'SELECT id FROM fiscal_periods WHERE year = $1 AND month = $2',
|
||||
[year, month],
|
||||
);
|
||||
if (periods.length) {
|
||||
const fiscalPeriodId = periods[0].id;
|
||||
const absAmount = Math.abs(dto.initialBalance);
|
||||
|
||||
// Determine debit/credit based on account type
|
||||
const isDebitNormal = ['asset', 'expense'].includes(dto.accountType);
|
||||
const debit = isDebitNormal ? absAmount : 0;
|
||||
const credit = isDebitNormal ? 0 : absAmount;
|
||||
|
||||
// Create the journal entry
|
||||
const jeRows = await this.tenant.query(
|
||||
`INSERT INTO journal_entries (entry_date, description, entry_type, fiscal_period_id, is_posted, posted_at, created_by)
|
||||
VALUES (CURRENT_DATE, $1, 'opening_balance', $2, true, NOW(), $3)
|
||||
RETURNING id`,
|
||||
[
|
||||
`Opening balance for ${dto.name}`,
|
||||
fiscalPeriodId,
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
],
|
||||
);
|
||||
|
||||
if (jeRows.length) {
|
||||
await this.tenant.query(
|
||||
`INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit, memo)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[jeRows[0].id, account.id, debit, credit, 'Opening balance'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateAccountDto) {
|
||||
@@ -62,6 +115,9 @@ export class AccountsService {
|
||||
|
||||
if (dto.name !== undefined) { sets.push(`name = $${idx++}`); params.push(dto.name); }
|
||||
if (dto.description !== undefined) { sets.push(`description = $${idx++}`); params.push(dto.description); }
|
||||
if (dto.accountNumber !== undefined) { sets.push(`account_number = $${idx++}`); params.push(dto.accountNumber); }
|
||||
if (dto.accountType !== undefined) { sets.push(`account_type = $${idx++}`); params.push(dto.accountType); }
|
||||
if (dto.fundType !== undefined) { sets.push(`fund_type = $${idx++}`); params.push(dto.fundType); }
|
||||
if (dto.is1099Reportable !== undefined) { sets.push(`is_1099_reportable = $${idx++}`); params.push(dto.is1099Reportable); }
|
||||
if (dto.isActive !== undefined) { sets.push(`is_active = $${idx++}`); params.push(dto.isActive); }
|
||||
|
||||
|
||||
@@ -32,4 +32,8 @@ export class CreateAccountDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
is1099Reportable?: boolean;
|
||||
|
||||
@ApiProperty({ required: false, default: 0 })
|
||||
@IsOptional()
|
||||
initialBalance?: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsIn } from 'class-validator';
|
||||
import { IsString, IsOptional, IsBoolean, IsIn, IsInt } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@@ -26,4 +26,14 @@ export class UpdateAccountDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
accountNumber?: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsIn(['operating', 'reserve'])
|
||||
@IsOptional()
|
||||
fundType?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user