Phase 3: Optimize & clean up — unified projects, account enhancements, new tenant fix

- Unify reserve_components + capital_projects into single projects model with
  full CRUD backend and new Projects page frontend
- Rewrite Capital Planning to read from unified projects/planning endpoint;
  add empty state directing users to Projects page when no planning items exist
- Add default designation to assessment groups with auto-set on first creation;
  units now require an assessment group (pre-populated with default)
- Add primary account designation (one per fund type) and balance adjustment
  via journal entries against equity offset accounts (3000/3100)
- Add computed investment fields (interest earned, maturity value, days remaining)
  with PostgreSQL date arithmetic fix for DATE - DATE integer result
- Restructure sidebar: investments in Accounts tab, Year-End under Reports,
  Planning section with Projects and Capital Planning
- Fix new tenant creation seeding unwanted default chart of accounts —
  new tenants now start with a blank slate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:32:35 -05:00
parent 17fdacc0f2
commit 301f8a7bde
20 changed files with 1760 additions and 145 deletions

View File

@@ -6,10 +6,6 @@ export class AssessmentGroupsService {
constructor(private tenant: TenantService) {}
async findAll() {
// Normalize all income calculations to monthly equivalent
// monthly: amount * units (already monthly)
// quarterly: amount/3 * units (convert to monthly)
// annual: amount/12 * units (convert to monthly)
return this.tenant.query(`
SELECT ag.*,
(SELECT COUNT(*) FROM units u WHERE u.assessment_group_id = ag.id) as actual_unit_count,
@@ -39,17 +35,38 @@ export class AssessmentGroupsService {
return rows[0];
}
async create(dto: any) {
async getDefault() {
const rows = await this.tenant.query(
`INSERT INTO assessment_groups (name, description, regular_assessment, special_assessment, unit_count, frequency)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
[dto.name, dto.description || null, dto.regularAssessment || 0, dto.specialAssessment || 0, dto.unitCount || 0, dto.frequency || 'monthly'],
'SELECT * FROM assessment_groups WHERE is_default = true AND is_active = true LIMIT 1',
);
return rows.length ? rows[0] : null;
}
async create(dto: any) {
const existingGroups = await this.tenant.query('SELECT COUNT(*) as cnt FROM assessment_groups');
const isFirstGroup = parseInt(existingGroups[0].cnt) === 0;
const shouldBeDefault = dto.isDefault || isFirstGroup;
if (shouldBeDefault) {
await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true');
}
const rows = await this.tenant.query(
`INSERT INTO assessment_groups (name, description, regular_assessment, special_assessment, unit_count, frequency, is_default)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
[dto.name, dto.description || null, dto.regularAssessment || 0, dto.specialAssessment || 0,
dto.unitCount || 0, dto.frequency || 'monthly', shouldBeDefault],
);
return rows[0];
}
async update(id: string, dto: any) {
await this.findOne(id);
if (dto.isDefault === true) {
await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true');
}
const sets: string[] = [];
const params: any[] = [];
let idx = 1;
@@ -61,6 +78,7 @@ export class AssessmentGroupsService {
if (dto.unitCount !== undefined) { sets.push(`unit_count = $${idx++}`); params.push(dto.unitCount); }
if (dto.isActive !== undefined) { sets.push(`is_active = $${idx++}`); params.push(dto.isActive); }
if (dto.frequency !== undefined) { sets.push(`frequency = $${idx++}`); params.push(dto.frequency); }
if (dto.isDefault !== undefined) { sets.push(`is_default = $${idx++}`); params.push(dto.isDefault); }
if (!sets.length) return this.findOne(id);
@@ -74,6 +92,16 @@ export class AssessmentGroupsService {
return rows[0];
}
async setDefault(id: string) {
await this.findOne(id);
await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true');
await this.tenant.query(
'UPDATE assessment_groups SET is_default = true, updated_at = NOW() WHERE id = $1',
[id],
);
return this.findOne(id);
}
async getSummary() {
const rows = await this.tenant.query(`
SELECT