mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-08 05:19:48 -05:00
755faa22c3
Implement comprehensive budget monitoring and forecasting feature with: Database & Models: - Add BudgetAlert model for tracking project budget alerts - Create migration 039_add_budget_alerts_table with proper indexes - Support alert types: 80_percent, 100_percent, over_budget - Add acknowledgment tracking with user and timestamp Budget Forecasting Utilities: - Implement burn rate calculation (daily/weekly/monthly) - Add completion date estimation based on burn rate - Create resource allocation analysis per team member - Build cost trend analysis with configurable granularity - Add automatic budget alert detection with deduplication Routes & API: - Create budget_alerts blueprint with dashboard and detail views - Add API endpoints for burn rate, completion estimates, and trends - Implement resource allocation and cost trend API endpoints - Add alert acknowledgment and manual budget check endpoints - Fix log_event() calls to use keyword arguments UI Templates: - Design modern budget dashboard with Tailwind CSS - Create detailed project budget analysis page with charts - Add gradient stat cards with color-coded status indicators - Implement responsive layouts with full dark mode support - Add smooth animations and toast notifications - Integrate Chart.js for cost trend visualization Project Integration: - Add Budget Alerts link to Finance navigation menu - Enhance project view page with budget overview card - Show budget progress bars with status indicators - Add Budget Analysis button to project header and dashboard - Display real-time budget status with color-coded badges Visual Enhancements: - Use gradient backgrounds for stat cards (blue/green/yellow/red) - Add status badges with icons (healthy/warning/critical/over) - Implement smooth progress bars with embedded percentages - Support responsive grid layouts for all screen sizes - Ensure proper type conversion (Decimal to float) in templates Scheduled Tasks: - Register budget alert checking job (runs every 6 hours) - Integrate with existing APScheduler tasks - Add logging for alert creation and monitoring This feature provides project managers with real-time budget insights, predictive analytics, and proactive alerts to prevent budget overruns.
151 lines
6.1 KiB
Python
151 lines
6.1 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
|
|
class BudgetAlert(db.Model):
|
|
"""Budget alert model for tracking project budget warnings and notifications"""
|
|
|
|
__tablename__ = 'budget_alerts'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False, index=True)
|
|
|
|
# Alert details
|
|
alert_type = db.Column(db.String(20), nullable=False) # 'warning_80', 'warning_100', 'over_budget'
|
|
alert_level = db.Column(db.String(20), nullable=False) # 'info', 'warning', 'critical'
|
|
budget_consumed_percent = db.Column(db.Numeric(5, 2), nullable=False) # Percentage of budget consumed
|
|
budget_amount = db.Column(db.Numeric(10, 2), nullable=False) # Budget at time of alert
|
|
consumed_amount = db.Column(db.Numeric(10, 2), nullable=False) # Amount consumed at time of alert
|
|
|
|
# Alert message and status
|
|
message = db.Column(db.Text, nullable=False)
|
|
is_acknowledged = db.Column(db.Boolean, default=False, nullable=False)
|
|
acknowledged_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True, index=True)
|
|
acknowledged_at = db.Column(db.DateTime, nullable=True)
|
|
|
|
# Metadata
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
|
|
|
|
# Relationships
|
|
# project relationship defined via backref
|
|
|
|
def __init__(self, project_id, alert_type, alert_level, budget_consumed_percent,
|
|
budget_amount, consumed_amount, message):
|
|
self.project_id = project_id
|
|
self.alert_type = alert_type
|
|
self.alert_level = alert_level
|
|
self.budget_consumed_percent = budget_consumed_percent
|
|
self.budget_amount = budget_amount
|
|
self.consumed_amount = consumed_amount
|
|
self.message = message
|
|
|
|
def __repr__(self):
|
|
return f'<BudgetAlert {self.alert_type} for Project {self.project_id}>'
|
|
|
|
def acknowledge(self, user_id):
|
|
"""Mark this alert as acknowledged by a user"""
|
|
self.is_acknowledged = True
|
|
self.acknowledged_by = user_id
|
|
self.acknowledged_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
def to_dict(self):
|
|
"""Convert budget alert to dictionary for API responses"""
|
|
return {
|
|
'id': self.id,
|
|
'project_id': self.project_id,
|
|
'project_name': self.project.name if self.project else None,
|
|
'alert_type': self.alert_type,
|
|
'alert_level': self.alert_level,
|
|
'budget_consumed_percent': float(self.budget_consumed_percent),
|
|
'budget_amount': float(self.budget_amount),
|
|
'consumed_amount': float(self.consumed_amount),
|
|
'message': self.message,
|
|
'is_acknowledged': self.is_acknowledged,
|
|
'acknowledged_by': self.acknowledged_by,
|
|
'acknowledged_at': self.acknowledged_at.isoformat() if self.acknowledged_at else None,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
@classmethod
|
|
def get_active_alerts(cls, project_id=None, acknowledged=False):
|
|
"""Get active alerts, optionally filtered by project"""
|
|
query = cls.query.filter_by(is_acknowledged=acknowledged)
|
|
|
|
if project_id:
|
|
query = query.filter_by(project_id=project_id)
|
|
|
|
return query.order_by(cls.created_at.desc()).all()
|
|
|
|
@classmethod
|
|
def create_alert(cls, project_id, alert_type, budget_consumed_percent,
|
|
budget_amount, consumed_amount):
|
|
"""Create a new budget alert"""
|
|
# Determine alert level based on type
|
|
alert_levels = {
|
|
'warning_80': 'warning',
|
|
'warning_100': 'critical',
|
|
'over_budget': 'critical'
|
|
}
|
|
alert_level = alert_levels.get(alert_type, 'info')
|
|
|
|
# Generate alert message
|
|
message = cls._generate_message(alert_type, budget_consumed_percent,
|
|
budget_amount, consumed_amount)
|
|
|
|
# Check if similar alert already exists (avoid duplicates)
|
|
recent_alert = cls.query.filter_by(
|
|
project_id=project_id,
|
|
alert_type=alert_type,
|
|
is_acknowledged=False
|
|
).filter(
|
|
cls.created_at >= datetime.utcnow() - datetime.timedelta(hours=24)
|
|
).first()
|
|
|
|
if recent_alert:
|
|
return recent_alert
|
|
|
|
# Create new alert
|
|
alert = cls(
|
|
project_id=project_id,
|
|
alert_type=alert_type,
|
|
alert_level=alert_level,
|
|
budget_consumed_percent=budget_consumed_percent,
|
|
budget_amount=budget_amount,
|
|
consumed_amount=consumed_amount,
|
|
message=message
|
|
)
|
|
|
|
db.session.add(alert)
|
|
db.session.commit()
|
|
|
|
return alert
|
|
|
|
@staticmethod
|
|
def _generate_message(alert_type, budget_consumed_percent, budget_amount, consumed_amount):
|
|
"""Generate alert message based on alert type"""
|
|
messages = {
|
|
'warning_80': f'Warning: Project has consumed {budget_consumed_percent:.1f}% of budget (${consumed_amount:.2f} of ${budget_amount:.2f})',
|
|
'warning_100': f'Alert: Project has reached 100% of budget (${consumed_amount:.2f} of ${budget_amount:.2f})',
|
|
'over_budget': f'Critical: Project is over budget by ${consumed_amount - budget_amount:.2f} ({budget_consumed_percent:.1f}% consumed)'
|
|
}
|
|
return messages.get(alert_type, 'Budget alert')
|
|
|
|
@classmethod
|
|
def get_alert_summary(cls, project_id=None):
|
|
"""Get summary statistics for budget alerts"""
|
|
query = cls.query
|
|
|
|
if project_id:
|
|
query = query.filter_by(project_id=project_id)
|
|
|
|
total_alerts = query.count()
|
|
unacknowledged_alerts = query.filter_by(is_acknowledged=False).count()
|
|
critical_alerts = query.filter_by(alert_level='critical', is_acknowledged=False).count()
|
|
|
|
return {
|
|
'total_alerts': total_alerts,
|
|
'unacknowledged_alerts': unacknowledged_alerts,
|
|
'critical_alerts': critical_alerts
|
|
}
|
|
|