Fixes for logo and Forecasting
This commit is contained in:
@@ -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 |
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
# If no funding found in forecast year, look for historical funding
|
||||||
logger.debug(f"[{acc.name}] CD account, using $0 starting balance (will be funded in month {funding_month if funding_month else 'unknown'})")
|
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 ---
|
||||||
bal = 0.0
|
if is_cd:
|
||||||
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Forcing balance to $0 (before funding month {funding_month})")
|
# Check if CD has matured
|
||||||
balance = bal
|
if acc.maturity_date:
|
||||||
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
maturity_date = acc.maturity_date
|
||||||
continue
|
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
|
||||||
|
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
|
||||||
|
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
||||||
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user