Files
firefly_reports/create_all_files.py
2026-05-27 13:58:05 +10:00

981 lines
31 KiB
Python

#!/usr/bin/env python3
"""Generate all source code files for Firefly III Analytics"""
import os
from pathlib import Path
def create_dir_and_file(filepath, content):
"""Create directory and file"""
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"{filepath}")
BASE = r"C:\Users\Nabeel\Nextcloud\Projects\firefly_reports"
# Backend files
files = {
# Backend app
f"{BASE}\\backend\\app\\__init__.py": "# Firefly Analytics App\n",
f"{BASE}\\backend\\app\\config.py": """import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
FIREFLY_URL: str = os.getenv("FIREFLY_URL", "https://firefly.scsimedia.duckdns.org")
FIREFLY_API_TOKEN: str = os.getenv("FIREFLY_API_TOKEN", "")
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./data/app.db")
SYNC_INTERVAL_MINUTES: int = int(os.getenv("SYNC_INTERVAL_MINUTES", "30"))
API_TIMEOUT: int = 30
class Config:
env_file = ".env"
settings = Settings()
""",
f"{BASE}\\backend\\app\\models.py": """from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class Transaction(Base):
__tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True)
firefly_id = Column(Integer, unique=True, index=True)
date = Column(DateTime, index=True)
amount = Column(Float)
description = Column(String)
type = Column(String)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
from_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
to_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
category = relationship("Category", foreign_keys=[category_id], back_populates="transactions")
from_account = relationship("Account", foreign_keys=[from_account_id])
to_account = relationship("Account", foreign_keys=[to_account_id])
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
firefly_id = Column(Integer, unique=True, index=True)
name = Column(String, unique=True, index=True)
type = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
transactions = relationship("Transaction", foreign_keys=[Transaction.category_id], back_populates="category")
class Account(Base):
__tablename__ = "accounts"
id = Column(Integer, primary_key=True, index=True)
firefly_id = Column(Integer, unique=True, index=True)
name = Column(String, unique=True, index=True)
type = Column(String)
balance = Column(Float)
currency_code = Column(String)
active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class SyncLog(Base):
__tablename__ = "sync_logs"
id = Column(Integer, primary_key=True, index=True)
entity_type = Column(String)
last_sync = Column(DateTime)
status = Column(String)
message = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
""",
f"{BASE}\\backend\\app\\database.py": """from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.config import settings
from app.models import Base
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db():
Base.metadata.create_all(bind=engine)
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
""",
f"{BASE}\\backend\\app\\main.py": """from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
from app.database import init_db
from app.routers import transactions, categories, accounts, reports, summary
from app.services.sync_service import start_sync_scheduler, stop_sync_scheduler
load_dotenv()
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
start_sync_scheduler()
yield
stop_sync_scheduler()
app = FastAPI(
title="Firefly III Analytics API",
description="Analytics and reporting for Firefly III",
version="0.1.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(transactions.router, prefix="/api/transactions", tags=["transactions"])
app.include_router(categories.router, prefix="/api/categories", tags=["categories"])
app.include_router(accounts.router, prefix="/api/accounts", tags=["accounts"])
app.include_router(reports.router, prefix="/api/reports", tags=["reports"])
app.include_router(summary.router, prefix="/api/summary", tags=["summary"])
@app.get("/")
async def root():
return {"status": "ok", "message": "Firefly III Analytics API"}
@app.get("/health")
async def health():
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
""",
# Routers
f"{BASE}\\backend\\app\\routers\\__init__.py": "# API routers\n",
f"{BASE}\\backend\\app\\routers\\transactions.py": """from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Transaction
from datetime import datetime
router = APIRouter()
@router.get("/")
async def get_transactions(
start_date: str = Query(None),
end_date: str = Query(None),
category_id: int = Query(None),
db: Session = Depends(get_db)
):
query = db.query(Transaction)
if start_date:
query = query.filter(Transaction.date >= datetime.fromisoformat(start_date))
if end_date:
query = query.filter(Transaction.date <= datetime.fromisoformat(end_date))
if category_id:
query = query.filter(Transaction.category_id == category_id)
transactions = query.order_by(Transaction.date.desc()).all()
return {"data": transactions, "count": len(transactions)}
""",
f"{BASE}\\backend\\app\\routers\\categories.py": """from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Category
router = APIRouter()
@router.get("/")
async def get_categories(db: Session = Depends(get_db)):
categories = db.query(Category).all()
return {"data": categories, "count": len(categories)}
""",
f"{BASE}\\backend\\app\\routers\\accounts.py": """from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Account
router = APIRouter()
@router.get("/")
async def get_accounts(db: Session = Depends(get_db)):
accounts = db.query(Account).all()
return {"data": accounts, "count": len(accounts)}
""",
f"{BASE}\\backend\\app\\routers\\reports.py": """from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.database import get_db
from app.models import Transaction, Category
from datetime import datetime
router = APIRouter()
@router.get("/spending-by-category")
async def spending_by_category(
start_date: str = Query(...),
end_date: str = Query(...),
db: Session = Depends(get_db)
):
start = datetime.fromisoformat(start_date)
end = datetime.fromisoformat(end_date)
results = db.query(
Category.name,
func.sum(Transaction.amount).label("total")
).join(
Transaction
).filter(
Transaction.date >= start,
Transaction.date <= end,
Transaction.type == "withdrawal"
).group_by(Category.name).all()
return {
"period": {"start": start.isoformat(), "end": end.isoformat()},
"data": [{"category": r[0], "amount": float(r[1]) if r[1] else 0} for r in results]
}
@router.get("/income-vs-expenses")
async def income_vs_expenses(
start_date: str = Query(...),
end_date: str = Query(...),
db: Session = Depends(get_db)
):
start = datetime.fromisoformat(start_date)
end = datetime.fromisoformat(end_date)
income = db.query(func.sum(Transaction.amount)).filter(
Transaction.date >= start,
Transaction.date <= end,
Transaction.type == "deposit"
).scalar() or 0
expenses = db.query(func.sum(Transaction.amount)).filter(
Transaction.date >= start,
Transaction.date <= end,
Transaction.type == "withdrawal"
).scalar() or 0
return {
"period": {"start": start.isoformat(), "end": end.isoformat()},
"income": float(income),
"expenses": float(expenses),
"net": float(income - expenses)
}
@router.get("/trends")
async def trends(
start_date: str = Query(...),
end_date: str = Query(...),
db: Session = Depends(get_db)
):
start = datetime.fromisoformat(start_date)
end = datetime.fromisoformat(end_date)
results = db.query(
func.date(Transaction.date).label("date"),
func.sum(Transaction.amount).label("amount")
).filter(
Transaction.date >= start,
Transaction.date <= end
).group_by(func.date(Transaction.date)).order_by("date").all()
return {
"period": {"start": start.isoformat(), "end": end.isoformat()},
"data": [{"date": str(r[0]), "amount": float(r[1]) if r[1] else 0} for r in results]
}
""",
f"{BASE}\\backend\\app\\routers\\summary.py": """from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.database import get_db
from app.models import Transaction, Account
from datetime import datetime, timedelta
router = APIRouter()
@router.get("/")
async def get_summary(db: Session = Depends(get_db)):
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
total_assets = db.query(func.sum(Account.balance)).scalar() or 0
recent_transactions = db.query(func.count(Transaction.id)).filter(
Transaction.date >= thirty_days_ago
).scalar() or 0
income = db.query(func.sum(Transaction.amount)).filter(
Transaction.date >= thirty_days_ago,
Transaction.type == "deposit"
).scalar() or 0
expenses = db.query(func.sum(Transaction.amount)).filter(
Transaction.date >= thirty_days_ago,
Transaction.type == "withdrawal"
).scalar() or 0
return {
"total_assets": float(total_assets),
"recent_transactions": recent_transactions,
"last_30_days": {
"income": float(income),
"expenses": float(expenses),
"net": float(income - expenses)
}
}
""",
# Clients
f"{BASE}\\backend\\app\\clients\\__init__.py": "# API clients\n",
f"{BASE}\\backend\\app\\clients\\firefly_client.py": """import httpx
from typing import Optional, List, Dict, Any
from app.config import settings
import logging
logger = logging.getLogger(__name__)
class FireflyClient:
def __init__(self):
self.base_url = settings.FIREFLY_URL.rstrip('/')
self.headers = {
"Authorization": f"Bearer {settings.FIREFLY_API_TOKEN}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/json"
}
async def get_transactions(
self,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: int = 50
) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client:
params = {"limit": limit}
if start_date:
params["start"] = start_date
if end_date:
params["end"] = end_date
try:
response = await client.get(
f"{self.base_url}/api/v1/transactions",
headers=self.headers,
params=params,
timeout=settings.API_TIMEOUT
)
response.raise_for_status()
return response.json().get("data", [])
except Exception as e:
logger.error(f"Error fetching transactions: {e}")
return []
async def get_categories(self) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{self.base_url}/api/v1/categories",
headers=self.headers,
timeout=settings.API_TIMEOUT
)
response.raise_for_status()
return response.json().get("data", [])
except Exception as e:
logger.error(f"Error fetching categories: {e}")
return []
async def get_accounts(self) -> List[Dict[str, Any]]:
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{self.base_url}/api/v1/accounts",
headers=self.headers,
timeout=settings.API_TIMEOUT
)
response.raise_for_status()
return response.json().get("data", [])
except Exception as e:
logger.error(f"Error fetching accounts: {e}")
return []
firefly_client = FireflyClient()
""",
# Services
f"{BASE}\\backend\\app\\services\\__init__.py": "# Services\n",
f"{BASE}\\backend\\app\\services\\sync_service.py": """from apscheduler.schedulers.background import BackgroundScheduler
from app.config import settings
from app.clients.firefly_client import firefly_client
from app.database import SessionLocal
from app.models import Transaction, Category, Account, SyncLog
from datetime import datetime
import logging
import asyncio
logger = logging.getLogger(__name__)
scheduler = BackgroundScheduler()
async def sync_data():
db = SessionLocal()
try:
categories = await firefly_client.get_categories()
for cat in categories:
attrs = cat.get("attributes", {})
existing = db.query(Category).filter_by(firefly_id=cat["id"]).first()
if existing:
existing.name = attrs.get("name")
existing.type = attrs.get("type")
existing.updated_at = datetime.utcnow()
else:
db.add(Category(
firefly_id=cat["id"],
name=attrs.get("name"),
type=attrs.get("type")
))
accounts = await firefly_client.get_accounts()
for acc in accounts:
attrs = acc.get("attributes", {})
existing = db.query(Account).filter_by(firefly_id=acc["id"]).first()
if existing:
existing.name = attrs.get("name")
existing.balance = float(attrs.get("current_balance", 0))
existing.updated_at = datetime.utcnow()
else:
db.add(Account(
firefly_id=acc["id"],
name=attrs.get("name"),
type=attrs.get("type"),
balance=float(attrs.get("current_balance", 0)),
currency_code=attrs.get("currency_code")
))
transactions = await firefly_client.get_transactions()
for txn in transactions:
attrs = txn.get("attributes", {})
existing = db.query(Transaction).filter_by(firefly_id=txn["id"]).first()
if not existing:
db.add(Transaction(
firefly_id=txn["id"],
date=datetime.fromisoformat(attrs.get("date")),
amount=float(attrs.get("amount", 0)),
description=attrs.get("description"),
type=attrs.get("type"),
notes=attrs.get("notes")
))
db.commit()
logger.info("Data sync completed successfully")
except Exception as e:
logger.error(f"Error syncing data: {e}")
db.rollback()
finally:
db.close()
def sync_job():
asyncio.run(sync_data())
def start_sync_scheduler():
if not scheduler.running:
scheduler.add_job(sync_job, 'interval', minutes=settings.SYNC_INTERVAL_MINUTES)
scheduler.start()
logger.info(f"Sync scheduler started (interval: {settings.SYNC_INTERVAL_MINUTES} minutes)")
def stop_sync_scheduler():
if scheduler.running:
scheduler.shutdown()
logger.info("Sync scheduler stopped")
""",
}
# Frontend files
frontend_files = {
f"{BASE}\\frontend\\src\\types.ts": """export interface Transaction {
id: number;
firefly_id: number;
date: string;
amount: number;
description: string;
type: 'withdrawal' | 'deposit' | 'transfer';
category_id?: number;
}
export interface Category {
id: number;
firefly_id: number;
name: string;
type: string;
}
export interface Account {
id: number;
firefly_id: number;
name: string;
type: string;
balance: number;
currency_code: string;
}
export interface ReportData {
period: {
start: string;
end: string;
};
data: any[];
}
export interface SummaryData {
total_assets: number;
recent_transactions: number;
last_30_days: {
income: number;
expenses: number;
net: number;
};
}
""",
f"{BASE}\\frontend\\src\\services\\api.ts": """import axios from 'axios';
import { Transaction, Category, Account, ReportData, SummaryData } from '../types';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const api = axios.create({
baseURL: API_URL,
});
export const transactionService = {
getTransactions: (startDate?: string, endDate?: string) =>
api.get<{ data: Transaction[] }>('/api/transactions', {
params: { start_date: startDate, end_date: endDate },
}),
};
export const categoryService = {
getCategories: () =>
api.get<{ data: Category[] }>('/api/categories'),
};
export const accountService = {
getAccounts: () =>
api.get<{ data: Account[] }>('/api/accounts'),
};
export const reportService = {
getSpendingByCategory: (startDate: string, endDate: string) =>
api.get<ReportData>('/api/reports/spending-by-category', {
params: { start_date: startDate, end_date: endDate },
}),
getIncomeVsExpenses: (startDate: string, endDate: string) =>
api.get<any>('/api/reports/income-vs-expenses', {
params: { start_date: startDate, end_date: endDate },
}),
getTrends: (startDate: string, endDate: string) =>
api.get<ReportData>('/api/reports/trends', {
params: { start_date: startDate, end_date: endDate },
}),
};
export const summaryService = {
getSummary: () =>
api.get<SummaryData>('/api/summary'),
};
""",
f"{BASE}\\frontend\\src\\components\\DateRangePicker.tsx": """import React from 'react';
interface DateRangePickerProps {
startDate: string;
endDate: string;
onStartDateChange: (date: string) => void;
onEndDateChange: (date: string) => void;
onPreset: (days: number) => void;
}
const DateRangePicker: React.FC<DateRangePickerProps> = ({
startDate,
endDate,
onStartDateChange,
onEndDateChange,
onPreset,
}) => {
return (
<div style={{ marginBottom: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<button onClick={() => onPreset(30)}>Last 30 Days</button>
<button onClick={() => onPreset(90)}>Last 90 Days</button>
<button onClick={() => onPreset(365)}>Last Year</button>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="date"
value={startDate}
onChange={(e) => onStartDateChange(e.target.value)}
placeholder="Start Date"
/>
<input
type="date"
value={endDate}
onChange={(e) => onEndDateChange(e.target.value)}
placeholder="End Date"
/>
</div>
</div>
);
};
export default DateRangePicker;
""",
f"{BASE}\\frontend\\src\\components\\SpendingChart.tsx": """import React from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
interface SpendingChartProps {
data: Array<{ category: string; amount: number }>;
}
const COLORS = [
'#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8',
'#82ca9d', '#ffc658', '#ff7c7c', '#8dd1e1', '#d084d0',
];
const SpendingChart: React.FC<SpendingChartProps> = ({ data }) => {
if (!data || data.length === 0) {
return <p>No data available</p>;
}
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
label={({ category, amount }) => `${category}: $${amount.toFixed(2)}`}
outerRadius={120}
fill="#8884d8"
dataKey="amount"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value: number) => `$${value.toFixed(2)}`} />
<Legend />
</PieChart>
</ResponsiveContainer>
);
};
export default SpendingChart;
""",
f"{BASE}\\frontend\\src\\components\\TrendChart.tsx": """import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
interface TrendChartProps {
data: Array<{ date: string; amount: number }>;
}
const TrendChart: React.FC<TrendChartProps> = ({ data }) => {
if (!data || data.length === 0) {
return <p>No data available</p>;
}
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip formatter={(value: number) => `$${value.toFixed(2)}`} />
<Legend />
<Line type="monotone" dataKey="amount" stroke="#8884d8" />
</LineChart>
</ResponsiveContainer>
);
};
export default TrendChart;
""",
f"{BASE}\\frontend\\src\\pages\\Dashboard.tsx": """import React, { useState, useEffect } from 'react';
import { summaryService } from '../services/api';
import { SummaryData } from '../types';
const Dashboard: React.FC = () => {
const [summary, setSummary] = useState<SummaryData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSummary = async () => {
try {
const response = await summaryService.getSummary();
setSummary(response.data);
} catch (error) {
console.error('Error fetching summary:', error);
} finally {
setLoading(false);
}
};
fetchSummary();
}, []);
if (loading) return <p>Loading...</p>;
if (!summary) return <p>Error loading data</p>;
return (
<div style={{ padding: '20px' }}>
<h1>Financial Dashboard</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px', marginTop: '20px' }}>
<div style={{ backgroundColor: '#f5f5f5', padding: '20px', borderRadius: '8px' }}>
<h3>Total Assets</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>${summary.total_assets.toFixed(2)}</p>
</div>
<div style={{ backgroundColor: '#f5f5f5', padding: '20px', borderRadius: '8px' }}>
<h3>Last 30 Days Income</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#00C49F' }}>
${summary.last_30_days.income.toFixed(2)}
</p>
</div>
<div style={{ backgroundColor: '#f5f5f5', padding: '20px', borderRadius: '8px' }}>
<h3>Last 30 Days Expenses</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#FF8042' }}>
${summary.last_30_days.expenses.toFixed(2)}
</p>
</div>
</div>
</div>
);
};
export default Dashboard;
""",
f"{BASE}\\frontend\\src\\pages\\ReportsPage.tsx": """import React, { useState, useEffect } from 'react';
import DateRangePicker from '../components/DateRangePicker';
import SpendingChart from '../components/SpendingChart';
import TrendChart from '../components/TrendChart';
import { reportService } from '../services/api';
import { ReportData } from '../types';
const ReportsPage: React.FC = () => {
const [reportType, setReportType] = useState('spending-by-category');
const [startDate, setStartDate] = useState(
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
);
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
const [reportData, setReportData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const handlePreset = (days: number) => {
const end = new Date();
const start = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
setStartDate(start.toISOString().split('T')[0]);
setEndDate(end.toISOString().split('T')[0]);
};
useEffect(() => {
const fetchReport = async () => {
setLoading(true);
try {
let response;
if (reportType === 'spending-by-category') {
response = await reportService.getSpendingByCategory(startDate, endDate);
} else if (reportType === 'income-vs-expenses') {
response = await reportService.getIncomeVsExpenses(startDate, endDate);
} else if (reportType === 'trends') {
response = await reportService.getTrends(startDate, endDate);
}
setReportData(response?.data);
} catch (error) {
console.error('Error fetching report:', error);
} finally {
setLoading(false);
}
};
fetchReport();
}, [reportType, startDate, endDate]);
return (
<div style={{ padding: '20px' }}>
<h1>Reports</h1>
<DateRangePicker
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPreset={handlePreset}
/>
<select value={reportType} onChange={(e) => setReportType(e.target.value)} style={{ marginBottom: '20px', padding: '10px' }}>
<option value="spending-by-category">Spending by Category</option>
<option value="income-vs-expenses">Income vs Expenses</option>
<option value="trends">Trends</option>
</select>
{loading ? (
<p>Loading report...</p>
) : reportData ? (
<>
{reportType === 'spending-by-category' && <SpendingChart data={reportData.data} />}
{reportType === 'income-vs-expenses' && (
<div>
<p>Income: ${reportData.income.toFixed(2)}</p>
<p>Expenses: ${reportData.expenses.toFixed(2)}</p>
<p>Net: ${reportData.net.toFixed(2)}</p>
</div>
)}
{reportType === 'trends' && <TrendChart data={reportData.data} />}
</>
) : (
<p>No data available</p>
)}
</div>
);
};
export default ReportsPage;
""",
f"{BASE}\\frontend\\src\\App.tsx": """import React, { useState } from 'react';
import Dashboard from './pages/Dashboard';
import ReportsPage from './pages/ReportsPage';
const App: React.FC = () => {
const [currentPage, setCurrentPage] = useState('dashboard');
return (
<div style={{ fontFamily: 'Arial, sans-serif' }}>
<nav style={{ backgroundColor: '#333', color: 'white', padding: '15px', display: 'flex', gap: '20px' }}>
<button
onClick={() => setCurrentPage('dashboard')}
style={{
backgroundColor: currentPage === 'dashboard' ? '#0088FE' : 'transparent',
color: 'white',
border: 'none',
padding: '10px 20px',
cursor: 'pointer',
borderRadius: '4px',
}}
>
Dashboard
</button>
<button
onClick={() => setCurrentPage('reports')}
style={{
backgroundColor: currentPage === 'reports' ? '#0088FE' : 'transparent',
color: 'white',
border: 'none',
padding: '10px 20px',
cursor: 'pointer',
borderRadius: '4px',
}}
>
Reports
</button>
</nav>
<main>
{currentPage === 'dashboard' && <Dashboard />}
{currentPage === 'reports' && <ReportsPage />}
</main>
</div>
);
};
export default App;
""",
f"{BASE}\\frontend\\src\\index.tsx": """import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
""",
f"{BASE}\\frontend\\public\\index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Firefly III Analytics & Reporting Dashboard"
/>
<title>Firefly III Analytics</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
""",
}
# Merge all files
all_files = {**files, **frontend_files}
# Create all files
print("Creating source code files...")
for filepath, content in all_files.items():
create_dir_and_file(filepath, content)
print("\n✓ All source code files created successfully!")
print(f"✓ Created {len(all_files)} files")