Fixes for logo and Forecasting

This commit is contained in:
2025-07-31 15:13:10 -04:00
parent b2e963a6c6
commit fef7ae9aad
4 changed files with 127 additions and 24 deletions

View File

@@ -1 +1,16 @@
<svg width="200" height="50" viewBox="0 0 200 50" xmlns="http://www.w3.org/2000/svg">
<!-- Light blue circle background -->
<circle cx="25" cy="25" r="20" fill="#87CEEB"/>
<!-- White upward arrow -->
<path d="M15 30 L25 15 L35 30 L30 30 L30 35 L20 35 L20 30 Z" fill="white"/>
<!-- White bar chart -->
<rect x="32" y="25" width="3" height="8" fill="white"/>
<rect x="37" y="20" width="3" height="13" fill="white"/>
<!-- HOApro text -->
<text x="55" y="32" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#1E3A8A">
HOApro
</text>
</svg>

Before

Width:  |  Height:  |  Size: 1 B

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -320,8 +320,8 @@ const Forecasting: React.FC = () => {
// Create data with different itemStyle for actual vs projected // Create data with different itemStyle for actual vs projected
const styledData = chartData.map((row, dataIdx) => { const styledData = chartData.map((row, dataIdx) => {
const rowDate = new Date(row.rawDate); const rowDate = new Date(row.rawDate);
const isProjected = rowDate.getFullYear() > 2025 || const isProjected = rowDate.getFullYear() > currentYear ||
(rowDate.getFullYear() === 2025 && rowDate.getMonth() >= 7); // July and later (rowDate.getFullYear() === currentYear && rowDate.getMonth() > currentMonth);
return { return {
value: row[key], value: row[key],
@@ -353,8 +353,8 @@ const Forecasting: React.FC = () => {
// Add total as a line with actual vs projected styling // Add total as a line with actual vs projected styling
const totalStyledData = chartData.map((row, dataIdx) => { const totalStyledData = chartData.map((row, dataIdx) => {
const rowDate = new Date(row.rawDate); const rowDate = new Date(row.rawDate);
const isProjected = rowDate.getFullYear() > 2025 || const isProjected = rowDate.getFullYear() > currentYear ||
(rowDate.getFullYear() === 2025 && rowDate.getMonth() >= 7); // July and later (rowDate.getFullYear() === currentYear && rowDate.getMonth() > currentMonth);
return { return {
value: row.total, value: row.total,
@@ -378,12 +378,9 @@ const Forecasting: React.FC = () => {
smooth: true, smooth: true,
}; };
// Add a markLine to show the boundary between actual and projected data // Add a markLine to show the boundary between actual and projected data
// Position the line between July and August (month 6 and 7, 0-based) // Only add the markLine if we're viewing the current year and have data beyond the current month
const julyIndex = 6; // July is month 7, but 0-based index is 6 console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && chartData.length > currentMonth + 1 });
const augustIndex = 7; // August is month 8, but 0-based index is 7 const markLine = (year === currentYear && chartData.length > currentMonth + 1) ? {
// Only add the markLine if we're viewing 2025 and have data for August
const markLine = (year === 2025 && chartData.length > augustIndex) ? {
symbol: 'none', symbol: 'none',
lineStyle: { lineStyle: {
color: '#666', color: '#666',
@@ -398,7 +395,7 @@ const Forecasting: React.FC = () => {
color: '#666' color: '#666'
}, },
data: [ data: [
{ xAxis: julyIndex + 0.5 } // Position between July and August { xAxis: currentMonth + 0.5 } // Position between current month and next month
] ]
} : null; } : null;

View File

@@ -660,6 +660,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
primary_account = acc primary_account = acc
break break
alert_threshold = 1000.0 alert_threshold = 1000.0
# 2. Get historical balances for each account # 2. Get historical balances for each account
history_by_account = defaultdict(list) history_by_account = defaultdict(list)
histories = db.query(models.AccountBalanceHistory).filter(models.AccountBalanceHistory.account_id.in_(account_ids)).order_by(models.AccountBalanceHistory.date.asc()).all() histories = db.query(models.AccountBalanceHistory).filter(models.AccountBalanceHistory.account_id.in_(account_ids)).order_by(models.AccountBalanceHistory.date.asc()).all()
@@ -707,13 +708,16 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
earliest_actual_month = None earliest_actual_month = None
if monthly_actuals: if monthly_actuals:
earliest_actual_month = min([m for (y, m) in monthly_actuals.keys() if y == year], default=None) earliest_actual_month = min([m for (y, m) in monthly_actuals.keys() if y == year], default=None)
# --- CD-specific logic (must happen before balance determination) --- # --- CD-specific logic (must happen before balance determination) ---
is_cd = hasattr(acc, 'account_type') and acc.account_type and acc.account_type.name.lower() == 'cd' is_cd = hasattr(acc, 'account_type') and acc.account_type and acc.account_type.name.lower() == 'cd'
funding_month = None funding_month = None
funding_amount = 0.0 funding_amount = 0.0
if is_cd: if is_cd:
# Find first reconciled deposit/transfer into this CD in the forecast year # Find first reconciled deposit/transfer into this CD in ANY year (not just forecast year)
cd_funding_tx = None cd_funding_tx = None
# Look for funding transactions in the forecast year first
for m in range(1, months + 1): for m in range(1, months + 1):
month_txs = txs_by_year_month.get((acc.id, year, m), []) month_txs = txs_by_year_month.get((acc.id, year, m), [])
for tx in month_txs: for tx in month_txs:
@@ -725,10 +729,52 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
funding_amount = cd_funding_tx.amount funding_amount = cd_funding_tx.amount
logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}") logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}")
break break
# For CDs, always start with $0 balance - they don't exist before funding
balance = 0.0
logger.debug(f"[{acc.name}] CD account, using $0 starting balance (will be funded in month {funding_month if funding_month else 'unknown'})")
# If no funding found in forecast year, look for historical funding
if not cd_funding_tx:
# Get all historical transactions for this CD
all_cd_txs = db.query(models.Transaction).filter(
models.Transaction.account_id == acc.id,
models.Transaction.type.in_(('deposit', 'transfer')),
models.Transaction.reconciled == True
).order_by(models.Transaction.date.asc()).all()
if all_cd_txs:
cd_funding_tx = all_cd_txs[0] # First funding transaction
funding_month = cd_funding_tx.date.month
funding_amount = cd_funding_tx.amount
logger.debug(f"CD funding for {acc.name} (id={acc.id}) found in historical data: month {funding_month}, amount={funding_amount}")
# For CDs, determine starting balance based on whether they should exist
if cd_funding_tx:
# Calculate if CD should exist at the start of the forecast year
cd_start_age_months = (year - cd_funding_tx.date.year) * 12 + (1 - cd_funding_tx.date.month)
if cd_start_age_months >= 0:
# CD should exist, use previous year's ending balance or validated balance
if prev_dec_balance is not None:
balance = prev_dec_balance
logger.debug(f"[{acc.name}] CD account, using Dec {year-1} ending balance: {balance}")
elif validated_balances[acc.id] > 0:
balance = validated_balances[acc.id]
logger.debug(f"[{acc.name}] CD account, using validated balance: {balance}")
else:
# Calculate balance from funding amount and interest
balance = funding_amount
# Apply interest for months since funding
if acc.interest_rate and acc.interest_rate > 0:
for month_offset in range(cd_start_age_months):
interest = balance * (acc.interest_rate / 100.0) / 12.0
balance += interest
logger.debug(f"[{acc.name}] CD account, calculated balance from funding: {balance}")
else:
# CD hasn't been funded yet
balance = 0.0
logger.debug(f"[{acc.name}] CD account, not funded yet, using $0 starting balance")
else:
# No funding transaction found, start with $0
balance = 0.0
logger.debug(f"[{acc.name}] CD account, no funding transaction found, using $0 starting balance")
# --- Find the latest balance from any previous year as starting point --- # --- Find the latest balance from any previous year as starting point ---
latest_prev_balance = None latest_prev_balance = None
latest_prev_date = None latest_prev_date = None
@@ -767,6 +813,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
forecast_month = ((m - 1) % 12) + 1 forecast_month = ((m - 1) % 12) + 1
month_date = datetime(forecast_year, forecast_month, 1) month_date = datetime(forecast_year, forecast_month, 1)
key = (forecast_year, forecast_month) key = (forecast_year, forecast_month)
# --- Universal rule: For months before the first validated balance, use $0 --- # --- Universal rule: For months before the first validated balance, use $0 ---
if earliest_actual_month is not None and forecast_year == year and forecast_month < earliest_actual_month: if earliest_actual_month is not None and forecast_year == year and forecast_month < earliest_actual_month:
bal = 0.0 bal = 0.0
@@ -774,13 +821,57 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
balance = bal balance = bal
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal}) forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
continue continue
# --- CD logic: $0 until funded, even if historical ---
if is_cd and (funding_month is None or forecast_month < funding_month): # --- CD logic: Check if CD should exist at this point ---
if is_cd:
# Check if CD has matured
if acc.maturity_date:
maturity_date = acc.maturity_date
if isinstance(maturity_date, str):
maturity_date = datetime.fromisoformat(maturity_date.replace('Z', '+00:00'))
# If CD has matured before this month, it should be $0
if maturity_date and maturity_date < month_date:
bal = 0.0 bal = 0.0
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Forcing balance to $0 (before funding month {funding_month})") logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Matured on {maturity_date}, balance = $0")
balance = bal
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
# Add matured CD funds to primary account if this is the maturity month
if (maturity_date.year == forecast_year and
maturity_date.month == forecast_month and
primary_account_id is not None):
# Find the CD balance just before maturity
prev_balance = balance
if forecast_month > 1:
prev_month_date = datetime(forecast_year, forecast_month - 1, 1)
prev_key = (forecast_year, forecast_month - 1)
if prev_key in monthly_actuals:
prev_balance = monthly_actuals[prev_key]
else:
# Calculate previous month's balance
prev_balance = balance # This will be calculated in the loop
# Add matured amount to primary account's cash flow
if prev_balance > 0:
cashflow_by_year_month[(forecast_year, forecast_month)] += prev_balance
logger.debug(f"CD {acc.name} matured: adding ${prev_balance} to primary account cash flow")
continue
# Check if CD should exist based on funding date
if funding_month is not None:
# Calculate the CD's age in months from funding
cd_age_months = (forecast_year - cd_funding_tx.date.year) * 12 + (forecast_month - cd_funding_tx.date.month)
# If CD hasn't been funded yet, it should be $0
if cd_age_months < 0:
bal = 0.0
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Not funded yet (age: {cd_age_months} months), balance = $0")
balance = bal balance = bal
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal}) forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
continue continue
# Use actual if available (for non-CDs, or for CDs after funding) # Use actual if available (for non-CDs, or for CDs after funding)
if key in monthly_actuals: if key in monthly_actuals:
bal = monthly_actuals[key] bal = monthly_actuals[key]