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:
2026-02-18 14:28:46 -05:00
parent e0272f9d8a
commit 01502e07bc
29 changed files with 1792 additions and 142 deletions

View File

@@ -42,6 +42,7 @@ CREATE TABLE shared.users (
oauth_provider VARCHAR(50),
oauth_provider_id VARCHAR(255),
last_login_at TIMESTAMPTZ,
is_superadmin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
@@ -51,7 +52,7 @@ CREATE TABLE shared.user_organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES shared.organizations(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL CHECK (role IN ('president', 'treasurer', 'secretary', 'member_at_large', 'manager', 'homeowner')),
role VARCHAR(50) NOT NULL CHECK (role IN ('president', 'treasurer', 'secretary', 'member_at_large', 'manager', 'homeowner', 'admin', 'viewer')),
is_active BOOLEAN DEFAULT TRUE,
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, organization_id)

84
db/seed/reseed.sh Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# ============================================================
# HOA LedgerIQ - Repeatable Seed Data Script
# ============================================================
# Usage: ./db/seed/reseed.sh
#
# This script will:
# 1. Drop the tenant schema (if it exists)
# 2. Remove the test users and organization
# 3. Re-run the full seed SQL to recreate everything fresh
#
# Prerequisites:
# - PostgreSQL container must be running (docker compose up -d postgres)
# - DATABASE_URL or PGHOST/PGUSER etc must be configured
# ============================================================
set -e
# Configuration
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-hoa_platform}"
DB_USER="${DB_USER:-hoa_admin}"
DB_PASS="${DB_PASS:-hoa_secure_pass}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SEED_FILE="$SCRIPT_DIR/seed.sql"
echo "============================================"
echo " HOA LedgerIQ - Reseed Database"
echo "============================================"
echo ""
echo "Host: $DB_HOST:$DB_PORT"
echo "Database: $DB_NAME"
echo ""
# Check if seed file exists
if [ ! -f "$SEED_FILE" ]; then
echo "ERROR: Seed file not found at $SEED_FILE"
exit 1
fi
CLEANUP_SQL=$(cat <<'EOSQL'
DROP SCHEMA IF EXISTS tenant_sunrise_valley CASCADE;
DELETE FROM shared.user_organizations WHERE user_id IN (
SELECT id FROM shared.users WHERE email IN ('admin@sunrisevalley.org', 'viewer@sunrisevalley.org')
);
DELETE FROM shared.users WHERE email IN ('admin@sunrisevalley.org', 'viewer@sunrisevalley.org');
DELETE FROM shared.organizations WHERE schema_name = 'tenant_sunrise_valley';
EOSQL
)
# Check if we can connect directly
export PGPASSWORD="$DB_PASS"
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1" > /dev/null 2>&1; then
echo "Step 1: Cleaning existing tenant data..."
echo "$CLEANUP_SQL" | psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME"
echo "Step 2: Running seed SQL..."
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$SEED_FILE"
else
# Try via docker
echo "Direct connection failed. Trying via Docker..."
DOCKER_CMD="docker compose exec -T postgres psql -U $DB_USER -d $DB_NAME"
echo "Step 1: Cleaning existing tenant data..."
echo "$CLEANUP_SQL" | $DOCKER_CMD
echo "Step 2: Running seed SQL..."
$DOCKER_CMD < "$SEED_FILE"
fi
echo ""
echo "============================================"
echo " Reseed complete!"
echo "============================================"
echo ""
echo " Test Accounts:"
echo " Admin: admin@sunrisevalley.org / password123"
echo " (SuperAdmin + President role)"
echo ""
echo " Viewer: viewer@sunrisevalley.org / password123"
echo " (Homeowner role)"
echo "============================================"

View File

@@ -48,14 +48,15 @@ BEGIN
-- Check if user exists
SELECT id INTO v_user_id FROM shared.users WHERE email = 'admin@sunrisevalley.org';
IF v_user_id IS NULL THEN
INSERT INTO shared.users (id, email, password_hash, first_name, last_name)
INSERT INTO shared.users (id, email, password_hash, first_name, last_name, is_superadmin)
VALUES (
uuid_generate_v4(),
'admin@sunrisevalley.org',
-- bcrypt hash of 'password123'
'$2b$10$1mtM00QBNQpAsyopajk3BeFY5DdxksvRYuM1E8qB.ePjCIYkfHMHO',
'Sarah',
'Johnson'
'Johnson',
true
) RETURNING id INTO v_user_id;
END IF;
@@ -78,6 +79,26 @@ IF v_org_id IS NULL THEN
VALUES (v_user_id, v_org_id, 'president');
END IF;
-- Create a second test user (viewer/homeowner)
DECLARE v_viewer_id UUID;
BEGIN
SELECT id INTO v_viewer_id FROM shared.users WHERE email = 'viewer@sunrisevalley.org';
IF v_viewer_id IS NULL THEN
INSERT INTO shared.users (id, email, password_hash, first_name, last_name, is_superadmin)
VALUES (
uuid_generate_v4(),
'viewer@sunrisevalley.org',
'$2b$10$1mtM00QBNQpAsyopajk3BeFY5DdxksvRYuM1E8qB.ePjCIYkfHMHO',
'Mike',
'Resident',
false
) RETURNING id INTO v_viewer_id;
INSERT INTO shared.user_organizations (user_id, organization_id, role)
VALUES (v_viewer_id, v_org_id, 'homeowner');
END IF;
END;
-- ============================================================
-- 2. Create tenant schema (if not exists)
-- ============================================================
@@ -147,6 +168,19 @@ CREATE TABLE IF NOT EXISTS %I.journal_entry_lines (
memo TEXT
)', v_schema);
EXECUTE format('
CREATE TABLE IF NOT EXISTS %I.assessment_groups (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
description TEXT,
regular_assessment DECIMAL(10,2) NOT NULL DEFAULT 0.00,
special_assessment DECIMAL(10,2) DEFAULT 0.00,
unit_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)', v_schema);
EXECUTE format('
CREATE TABLE IF NOT EXISTS %I.units (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
@@ -164,6 +198,7 @@ CREATE TABLE IF NOT EXISTS %I.units (
owner_phone VARCHAR(20),
is_rented BOOLEAN DEFAULT FALSE,
monthly_assessment DECIMAL(10,2),
assessment_group_id UUID,
status VARCHAR(20) DEFAULT ''active'',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
@@ -314,6 +349,7 @@ EXECUTE format('DELETE FROM %I.investment_accounts', v_schema);
EXECUTE format('DELETE FROM %I.reserve_components', v_schema);
EXECUTE format('DELETE FROM %I.vendors', v_schema);
EXECUTE format('DELETE FROM %I.units', v_schema);
EXECUTE format('DELETE FROM %I.assessment_groups', v_schema);
EXECUTE format('DELETE FROM %I.fiscal_periods', v_schema);
EXECUTE format('DELETE FROM %I.accounts', v_schema);
@@ -376,6 +412,15 @@ FOR v_month IN 1..12 LOOP
USING v_year - 1, v_month, 'closed';
END LOOP;
-- ============================================================
-- 4b. Seed Assessment Groups
-- ============================================================
EXECUTE format('INSERT INTO %I.assessment_groups (name, description, regular_assessment, special_assessment, unit_count) VALUES
(''Single Family Homes'', ''Standard single family detached homes (Units 1-20)'', 350.00, 0.00, 20),
(''Patio Homes'', ''Medium-sized patio homes (Units 21-35)'', 425.00, 0.00, 15),
(''Estate Lots'', ''Large estate lots (Units 36-50)'', 500.00, 75.00, 15)
', v_schema);
-- ============================================================
-- 5. Seed 50 units
-- ============================================================
@@ -394,17 +439,26 @@ DECLARE
'Mitchell','Carter','Roberts'];
v_unit_num INT;
v_assess NUMERIC;
v_ag_id UUID;
v_ag_name TEXT;
BEGIN
FOR v_unit_num IN 1..50 LOOP
-- Vary assessment based on unit size
v_assess := CASE
WHEN v_unit_num <= 20 THEN 350.00 -- standard
WHEN v_unit_num <= 35 THEN 425.00 -- medium
ELSE 500.00 -- large
END;
-- Vary assessment based on unit size and assign assessment group
IF v_unit_num <= 20 THEN
v_assess := 350.00;
v_ag_name := 'Single Family Homes';
ELSIF v_unit_num <= 35 THEN
v_assess := 425.00;
v_ag_name := 'Patio Homes';
ELSE
v_assess := 500.00;
v_ag_name := 'Estate Lots';
END IF;
EXECUTE format('INSERT INTO %I.units (unit_number, address_line1, city, state, zip_code, owner_name, owner_email, owner_phone, monthly_assessment, square_footage)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)', v_schema)
EXECUTE format('SELECT id FROM %I.assessment_groups WHERE name = $1', v_schema) INTO v_ag_id USING v_ag_name;
EXECUTE format('INSERT INTO %I.units (unit_number, address_line1, city, state, zip_code, owner_name, owner_email, owner_phone, monthly_assessment, square_footage, assessment_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)', v_schema)
USING
LPAD(v_unit_num::TEXT, 3, '0'),
(100 + v_unit_num * 2)::TEXT || ' Sunrise Valley Drive',
@@ -413,7 +467,8 @@ BEGIN
LOWER(v_first_names[v_unit_num]) || '.' || LOWER(v_last_names[v_unit_num]) || '@email.com',
'(480) 555-' || LPAD((1000 + v_unit_num)::TEXT, 4, '0'),
v_assess,
CASE WHEN v_unit_num <= 20 THEN 1200 WHEN v_unit_num <= 35 THEN 1600 ELSE 2000 END;
CASE WHEN v_unit_num <= 20 THEN 1200 WHEN v_unit_num <= 35 THEN 1600 ELSE 2000 END,
v_ag_id;
END LOOP;
END;
@@ -779,6 +834,7 @@ EXECUTE format('INSERT INTO %I.capital_projects (name, description, estimated_co
', v_schema) USING v_year;
RAISE NOTICE 'Seed data created successfully for Sunrise Valley HOA!';
RAISE NOTICE 'Login: admin@sunrisevalley.org / password123';
RAISE NOTICE 'Admin Login: admin@sunrisevalley.org / password123 (SuperAdmin + President)';
RAISE NOTICE 'Viewer Login: viewer@sunrisevalley.org / password123 (Homeowner)';
END $$;