Skip to content

Commit e3ae3ca

Browse files
authored
Merge pull request #244 from tuttle-dev/forecast
feat: Add dashboard view for freelance business insights
2 parents 29472a2 + e0058d3 commit e3ae3ca

12 files changed

Lines changed: 1594 additions & 37 deletions

File tree

tuttle/app/dashboard/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .intent import DashboardIntent
2+
from .view import DashboardView

tuttle/app/dashboard/intent.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Business logic for the dashboard view."""
2+
3+
4+
from ..core.abstractions import SQLModelDataSourceMixin, Intent
5+
from ..core.intent_result import IntentResult
6+
7+
from ...model import Contract, Invoice, Project, FinancialGoal
8+
from ...kpi import compute_kpis, monthly_revenue_breakdown, project_budget_status
9+
from ...forecasting import revenue_curve
10+
11+
12+
class DashboardIntent(SQLModelDataSourceMixin, Intent):
13+
"""Gathers data for the freelance business dashboard."""
14+
15+
def __init__(self):
16+
SQLModelDataSourceMixin.__init__(self)
17+
18+
def get_kpis(self) -> IntentResult:
19+
"""Compute KPI summary from all invoices, contracts, projects."""
20+
try:
21+
invoices = self.query(Invoice)
22+
contracts = self.query(Contract)
23+
projects = self.query(Project)
24+
kpis = compute_kpis(invoices, contracts, projects)
25+
return IntentResult(was_intent_successful=True, data=kpis)
26+
except Exception as e:
27+
return IntentResult(
28+
was_intent_successful=False,
29+
error_msg="Failed to compute KPIs.",
30+
log_message=f"DashboardIntent.get_kpis: {e}",
31+
exception=e,
32+
)
33+
34+
def get_monthly_revenue(self, n_months: int = 12) -> IntentResult:
35+
"""Get monthly revenue breakdown for the last n months."""
36+
try:
37+
invoices = self.query(Invoice)
38+
data = monthly_revenue_breakdown(invoices, n_months=n_months)
39+
return IntentResult(was_intent_successful=True, data=data)
40+
except Exception as e:
41+
return IntentResult(
42+
was_intent_successful=False,
43+
error_msg="Failed to load monthly revenue.",
44+
log_message=f"DashboardIntent.get_monthly_revenue: {e}",
45+
exception=e,
46+
)
47+
48+
def get_revenue_curve(self, forecast_months: int = 6) -> IntentResult:
49+
"""Get combined historical + forecast revenue curve."""
50+
try:
51+
invoices = self.query(Invoice)
52+
contracts = self.query(Contract)
53+
data = revenue_curve(invoices, contracts, forecast_months=forecast_months)
54+
return IntentResult(was_intent_successful=True, data=data)
55+
except Exception as e:
56+
return IntentResult(
57+
was_intent_successful=False,
58+
error_msg="Failed to generate revenue forecast.",
59+
log_message=f"DashboardIntent.get_revenue_curve: {e}",
60+
exception=e,
61+
)
62+
63+
def get_project_budgets(self) -> IntentResult:
64+
"""Get budget utilization for all projects."""
65+
try:
66+
projects = self.query(Project)
67+
data = project_budget_status(projects)
68+
return IntentResult(was_intent_successful=True, data=data)
69+
except Exception as e:
70+
return IntentResult(
71+
was_intent_successful=False,
72+
error_msg="Failed to load project budgets.",
73+
log_message=f"DashboardIntent.get_project_budgets: {e}",
74+
exception=e,
75+
)
76+
77+
def get_financial_goals(self) -> IntentResult:
78+
"""Load all financial goals."""
79+
try:
80+
goals = self.query(FinancialGoal)
81+
return IntentResult(was_intent_successful=True, data=goals)
82+
except Exception as e:
83+
return IntentResult(
84+
was_intent_successful=False,
85+
error_msg="Failed to load financial goals.",
86+
log_message=f"DashboardIntent.get_financial_goals: {e}",
87+
exception=e,
88+
)
89+
90+
def save_financial_goal(self, goal: FinancialGoal) -> IntentResult:
91+
"""Save a financial goal."""
92+
try:
93+
self.store(goal)
94+
return IntentResult(was_intent_successful=True, data=goal)
95+
except Exception as e:
96+
return IntentResult(
97+
was_intent_successful=False,
98+
error_msg="Failed to save financial goal.",
99+
log_message=f"DashboardIntent.save_financial_goal: {e}",
100+
exception=e,
101+
)
102+
103+
def delete_financial_goal(self, goal_id: int) -> IntentResult:
104+
"""Delete a financial goal by ID."""
105+
try:
106+
self.delete_by_id(FinancialGoal, goal_id)
107+
return IntentResult(was_intent_successful=True, data=None)
108+
except Exception as e:
109+
return IntentResult(
110+
was_intent_successful=False,
111+
error_msg="Failed to delete financial goal.",
112+
log_message=f"DashboardIntent.delete_financial_goal: {e}",
113+
exception=e,
114+
)

0 commit comments

Comments
 (0)