708 lines
22 KiB
Python
708 lines
22 KiB
Python
"""Vela Platform — FastAPI application with API endpoints and Jinja2 frontend."""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from fastapi import Depends, FastAPI, Form, HTTPException, Request, status
|
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from auth import (
|
|
create_access_token,
|
|
get_current_user,
|
|
get_page_user,
|
|
hash_password,
|
|
require_admin,
|
|
verify_password,
|
|
)
|
|
from database import Base, SessionLocal, engine, get_db
|
|
from models import Service, ServiceComponent, Transaction, User
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# App setup
|
|
# ---------------------------------------------------------------------------
|
|
app = FastAPI(title="Vela Platform", version="1.0.0")
|
|
|
|
# Mount templates via Jinja2
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
HARDCODED_OPEX = 800.0 # MAD/month
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: serialize a service with its components
|
|
# ---------------------------------------------------------------------------
|
|
def service_to_dict(svc: Service) -> dict:
|
|
return {
|
|
"id": svc.id,
|
|
"name": svc.name,
|
|
"description": svc.description,
|
|
"sell_price": svc.sell_price,
|
|
"cost_price": svc.cost_price,
|
|
"active": svc.active,
|
|
"components": [
|
|
{
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"unit_cost": c.unit_cost,
|
|
"unit_sell": c.unit_sell,
|
|
"quantity": c.quantity,
|
|
"notes": c.notes,
|
|
}
|
|
for c in svc.components
|
|
],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Startup: create tables + seed data + create component tables
|
|
# ---------------------------------------------------------------------------
|
|
COMPONENT_TEMPLATES = {
|
|
"Atlas": [
|
|
("HDD (1TB)", 600.0, 1000.0, 1, "Enterprise HDD"),
|
|
("RAM (16GB)", 300.0, 500.0, 1, "ECC RAM"),
|
|
("CPU alloc", 200.0, 400.0, 1, "CPU core allocation"),
|
|
("Casing & PSU", 200.0, 300.0, 1, "Chassis + power"),
|
|
("Bandwidth 1Gbps", 200.0, 500.0, 1, "Network port"),
|
|
("Setup & config", 300.0, 600.0, 1, "One-time setup amortised"),
|
|
("Support", 200.0, 700.0, 1, "Monthly support"),
|
|
],
|
|
"Atlas+": [
|
|
("HDD (1TB)", 600.0, 1000.0, 1, "Enterprise HDD"),
|
|
("SSD (256GB)", 400.0, 700.0, 1, "Cache SSD"),
|
|
("RAM (32GB)", 600.0, 1000.0, 1, "ECC RAM"),
|
|
("CPU alloc", 300.0, 600.0, 1, "CPU core allocation"),
|
|
("Casing & PSU", 200.0, 300.0, 1, "Chassis + power"),
|
|
("Bandwidth 1Gbps", 200.0, 500.0, 1, "Network port"),
|
|
("Sauvegarde", 300.0, 500.0, 1, "Backup service"),
|
|
("Setup & config", 200.0, 400.0, 1, "One-time setup amortised"),
|
|
("Support", 200.0, 700.0, 1, "Monthly support"),
|
|
],
|
|
"Rif": [
|
|
("SSD (500GB)", 300.0, 600.0, 1, "NVMe SSD"),
|
|
("RAM (16GB)", 300.0, 500.0, 1, "ECC RAM"),
|
|
("CPU alloc", 200.0, 400.0, 1, "CPU core allocation"),
|
|
("Casing & PSU", 200.0, 300.0, 1, "Chassis + power"),
|
|
("Bandwidth 1Gbps", 200.0, 500.0, 1, "Network port"),
|
|
("Setup & config", 300.0, 600.0, 1, "One-time setup amortised"),
|
|
("Support", 200.0, 700.0, 1, "Monthly support"),
|
|
],
|
|
"Rif+": [
|
|
("SSD (500GB)", 300.0, 600.0, 1, "NVMe SSD"),
|
|
("HDD (1TB)", 600.0, 1000.0, 1, "Enterprise HDD"),
|
|
("RAM (32GB)", 600.0, 1000.0, 1, "ECC RAM"),
|
|
("CPU alloc", 300.0, 600.0, 1, "CPU core allocation"),
|
|
("Casing & PSU", 200.0, 300.0, 1, "Chassis + power"),
|
|
("Bandwidth 1Gbps", 200.0, 500.0, 1, "Network port"),
|
|
("Sauvegarde", 300.0, 500.0, 1, "Backup service"),
|
|
("Setup & config", 200.0, 400.0, 1, "One-time setup amortised"),
|
|
("Support", 200.0, 700.0, 1, "Monthly support"),
|
|
],
|
|
}
|
|
|
|
|
|
def seed_database():
|
|
"""Create tables and seed services (no default users — first-run setup creates them)."""
|
|
Base.metadata.create_all(bind=engine)
|
|
db = SessionLocal()
|
|
try:
|
|
# Seed services + components (only if no services exist yet)
|
|
if not db.query(Service).first():
|
|
for name, desc, sell, cost in [
|
|
("Atlas", "Core storage service", 4000.0, 2000.0),
|
|
("Atlas+", "Premium storage + backup service", 5000.0, 2800.0),
|
|
("Rif", "Core server service", 3500.0, 1700.0),
|
|
("Rif+", "Premium server + backup service", 4500.0, 2200.0),
|
|
]:
|
|
svc = Service(
|
|
name=name,
|
|
description=desc,
|
|
sell_price=sell,
|
|
cost_price=cost,
|
|
active=True,
|
|
)
|
|
db.add(svc)
|
|
db.flush()
|
|
|
|
for cname, ucost, usell, qty, notes in COMPONENT_TEMPLATES.get(name, []):
|
|
db.add(
|
|
ServiceComponent(
|
|
service_id=svc.id,
|
|
name=cname,
|
|
unit_cost=ucost,
|
|
unit_sell=usell,
|
|
quantity=qty,
|
|
notes=notes,
|
|
)
|
|
)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
seed_database()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: check if setup is complete
|
|
# ---------------------------------------------------------------------------
|
|
def is_setup_complete(db: Session) -> bool:
|
|
"""Returns True if at least one admin user exists."""
|
|
return db.query(User).filter(User.role == "admin").first() is not None
|
|
|
|
|
|
def require_setup(db: Session = Depends(get_db)):
|
|
"""Dependency that raises 404 if setup is not complete (caller handles redirect)."""
|
|
return is_setup_complete(db)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: compute monthly P&L
|
|
# ---------------------------------------------------------------------------
|
|
def compute_pnl(month: str, db: Session) -> dict:
|
|
"""Compute P&L for a given YYYY-MM month."""
|
|
transactions = db.query(Transaction).filter(Transaction.month == month).all()
|
|
|
|
total_revenue = 0.0
|
|
total_cost = 0.0
|
|
for txn in transactions:
|
|
total_revenue += txn.revenue
|
|
total_cost += txn.cost
|
|
|
|
gross_profit = total_revenue - total_cost
|
|
gross_margin = (gross_profit / total_revenue * 100) if total_revenue > 0 else 0.0
|
|
opex = HARDCODED_OPEX
|
|
net_profit = gross_profit - opex
|
|
net_margin = (net_profit / total_revenue * 100) if total_revenue > 0 else 0.0
|
|
|
|
return {
|
|
"month": month,
|
|
"total_revenue": round(total_revenue, 2),
|
|
"total_cost": round(total_cost, 2),
|
|
"gross_profit": round(gross_profit, 2),
|
|
"gross_margin_pct": round(gross_margin, 2),
|
|
"opex": opex,
|
|
"net_profit": round(net_profit, 2),
|
|
"net_margin_pct": round(net_margin, 2),
|
|
}
|
|
|
|
|
|
def get_trend(db: Session, months: int = 12) -> list:
|
|
"""Return P&L for the last N months (those with transactions)."""
|
|
month_rows = (
|
|
db.query(Transaction.month)
|
|
.distinct()
|
|
.order_by(Transaction.month.desc())
|
|
.limit(months)
|
|
.all()
|
|
)
|
|
month_list = [row[0] for row in reversed(month_rows)]
|
|
return [compute_pnl(m, db) for m in month_list]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: Auth
|
|
# ---------------------------------------------------------------------------
|
|
@app.post("/api/auth/login")
|
|
def api_login(
|
|
username: str = Form(...),
|
|
password: str = Form(...),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
user = db.query(User).filter(User.username == username).first()
|
|
if not user or not verify_password(password, user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid username or password",
|
|
)
|
|
token = create_access_token(user.id, user.role)
|
|
response = JSONResponse({
|
|
"token": token,
|
|
"user": {
|
|
"id": user.id,
|
|
"username": user.username,
|
|
"display_name": user.display_name,
|
|
"role": user.role,
|
|
},
|
|
})
|
|
response.set_cookie(
|
|
key="token",
|
|
value=token,
|
|
httponly=True,
|
|
max_age=86400,
|
|
samesite="lax",
|
|
secure=False,
|
|
path="/",
|
|
)
|
|
return response
|
|
|
|
|
|
@app.get("/api/auth/me")
|
|
def api_me(current_user: User = Depends(get_current_user)):
|
|
return {
|
|
"id": current_user.id,
|
|
"username": current_user.username,
|
|
"display_name": current_user.display_name,
|
|
"role": current_user.role,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: Services
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/api/services")
|
|
def api_list_services(db: Session = Depends(get_db)):
|
|
return [service_to_dict(s) for s in db.query(Service).order_by(Service.name).all()]
|
|
|
|
|
|
@app.get("/api/services/{service_id}")
|
|
def api_get_service(service_id: int, db: Session = Depends(get_db)):
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
return service_to_dict(svc)
|
|
|
|
|
|
@app.post("/api/services")
|
|
def api_create_service(
|
|
name: str = Form(...),
|
|
description: str = Form(""),
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
svc = Service(
|
|
name=name,
|
|
description=description,
|
|
sell_price=0.0,
|
|
cost_price=0.0,
|
|
active=True,
|
|
)
|
|
db.add(svc)
|
|
db.commit()
|
|
db.refresh(svc)
|
|
return service_to_dict(svc)
|
|
|
|
|
|
@app.put("/api/services/{service_id}")
|
|
def api_update_service(
|
|
service_id: int,
|
|
name: str = Form(...),
|
|
description: str = Form(""),
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
svc.name = name
|
|
svc.description = description
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
return service_to_dict(svc)
|
|
|
|
|
|
@app.delete("/api/services/{service_id}")
|
|
def api_deactivate_service(
|
|
service_id: int,
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
svc.active = False
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: Service Components
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/api/services/{service_id}/components")
|
|
def api_list_components(service_id: int, db: Session = Depends(get_db)):
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
return [
|
|
{
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"unit_cost": c.unit_cost,
|
|
"unit_sell": c.unit_sell,
|
|
"quantity": c.quantity,
|
|
"notes": c.notes,
|
|
}
|
|
for c in svc.components
|
|
]
|
|
|
|
|
|
class ComponentInput(BaseModel):
|
|
name: str
|
|
unit_cost: float = 0.0
|
|
unit_sell: float = 0.0
|
|
quantity: int = 1
|
|
notes: str = ""
|
|
|
|
|
|
@app.post("/api/services/{service_id}/components")
|
|
def api_create_component(
|
|
service_id: int,
|
|
input: ComponentInput,
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
|
|
comp = ServiceComponent(
|
|
service_id=service_id,
|
|
name=input.name,
|
|
unit_cost=input.unit_cost,
|
|
unit_sell=input.unit_sell,
|
|
quantity=input.quantity,
|
|
notes=input.notes,
|
|
)
|
|
db.add(comp)
|
|
db.flush()
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
db.refresh(comp)
|
|
return {
|
|
"id": comp.id,
|
|
"name": comp.name,
|
|
"unit_cost": comp.unit_cost,
|
|
"unit_sell": comp.unit_sell,
|
|
"quantity": comp.quantity,
|
|
"notes": comp.notes,
|
|
}
|
|
|
|
|
|
@app.put("/api/components/{component_id}")
|
|
def api_update_component(
|
|
component_id: int,
|
|
input: ComponentInput,
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
comp = db.query(ServiceComponent).filter(ServiceComponent.id == component_id).first()
|
|
if not comp:
|
|
raise HTTPException(status_code=404, detail="Component not found")
|
|
|
|
comp.name = input.name
|
|
comp.unit_cost = input.unit_cost
|
|
comp.unit_sell = input.unit_sell
|
|
comp.quantity = input.quantity
|
|
comp.notes = input.notes
|
|
db.flush()
|
|
|
|
svc = db.query(Service).filter(Service.id == comp.service_id).first()
|
|
if svc:
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
|
|
return {
|
|
"id": comp.id,
|
|
"name": comp.name,
|
|
"unit_cost": comp.unit_cost,
|
|
"unit_sell": comp.unit_sell,
|
|
"quantity": comp.quantity,
|
|
"notes": comp.notes,
|
|
}
|
|
|
|
|
|
@app.delete("/api/components/{component_id}")
|
|
def api_delete_component(
|
|
component_id: int,
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
comp = db.query(ServiceComponent).filter(ServiceComponent.id == component_id).first()
|
|
if not comp:
|
|
raise HTTPException(status_code=404, detail="Component not found")
|
|
|
|
svc = db.query(Service).filter(Service.id == comp.service_id).first()
|
|
db.delete(comp)
|
|
db.flush() # <-- ensures the deleted component is removed before recompute
|
|
if svc:
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: Transactions
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/api/transactions")
|
|
def api_list_transactions(
|
|
month: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
query = db.query(Transaction)
|
|
if month:
|
|
query = query.filter(Transaction.month == month)
|
|
transactions = query.order_by(Transaction.created_at.desc()).all()
|
|
return [
|
|
{
|
|
"id": t.id,
|
|
"service_id": t.service_id,
|
|
"service_name": t.service.name,
|
|
"quantity": t.quantity,
|
|
"revenue": t.revenue,
|
|
"cost": t.cost,
|
|
"month": t.month,
|
|
"notes": t.notes,
|
|
"created_by": t.created_by,
|
|
"creator_name": t.creator.display_name,
|
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
|
}
|
|
for t in transactions
|
|
]
|
|
|
|
|
|
class CreateTransactionInput(BaseModel):
|
|
service_id: int
|
|
quantity: int
|
|
month: str
|
|
notes: str = ""
|
|
|
|
|
|
@app.post("/api/transactions")
|
|
def api_create_transaction(
|
|
input: CreateTransactionInput,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
service = db.query(Service).filter(Service.id == input.service_id).first()
|
|
if not service or not service.active:
|
|
raise HTTPException(status_code=400, detail="Invalid or inactive service")
|
|
|
|
txn = Transaction(
|
|
service_id=input.service_id,
|
|
quantity=input.quantity,
|
|
month=input.month,
|
|
notes=input.notes,
|
|
created_by=current_user.id,
|
|
)
|
|
db.add(txn)
|
|
db.commit()
|
|
db.refresh(txn)
|
|
return {
|
|
"id": txn.id,
|
|
"service_id": txn.service_id,
|
|
"service_name": txn.service.name,
|
|
"quantity": txn.quantity,
|
|
"revenue": txn.revenue,
|
|
"cost": txn.cost,
|
|
"month": txn.month,
|
|
"notes": txn.notes,
|
|
"created_by": txn.created_by,
|
|
"created_at": txn.created_at.isoformat() if txn.created_at else None,
|
|
}
|
|
|
|
|
|
@app.delete("/api/transactions/{transaction_id}")
|
|
def api_delete_transaction(
|
|
transaction_id: int,
|
|
db: Session = Depends(get_db),
|
|
_admin: User = Depends(require_admin),
|
|
):
|
|
txn = db.query(Transaction).filter(Transaction.id == transaction_id).first()
|
|
if not txn:
|
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
|
db.delete(txn)
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: P&L
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/api/pnl")
|
|
def api_pnl(month: str, db: Session = Depends(get_db)):
|
|
return compute_pnl(month, db)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API: Dashboard
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/api/dashboard")
|
|
def api_dashboard(db: Session = Depends(get_db)):
|
|
current_month = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
pnl = compute_pnl(current_month, db)
|
|
trend = get_trend(db, months=12)
|
|
return {
|
|
"current_month": pnl,
|
|
"trend": trend,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Frontend Routes
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/setup", response_class=HTMLResponse)
|
|
def setup_page(request: Request, db: Session = Depends(get_db)):
|
|
"""Serve the first-time setup page."""
|
|
if is_setup_complete(db):
|
|
return RedirectResponse(url="/login")
|
|
return templates.TemplateResponse(request, "setup.html")
|
|
|
|
|
|
class SetupInput(BaseModel):
|
|
admin_username: str
|
|
admin_password: str
|
|
admin_display: str = "Administrator"
|
|
|
|
|
|
@app.post("/api/setup")
|
|
def api_setup(
|
|
input: SetupInput,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create the first admin user (only works if no admin exists yet)."""
|
|
if is_setup_complete(db):
|
|
raise HTTPException(status_code=400, detail="Setup already completed")
|
|
|
|
if not input.admin_username.strip() or len(input.admin_password) < 4:
|
|
raise HTTPException(status_code=400, detail="Username required, password min 4 chars")
|
|
|
|
# Create admin
|
|
admin = User(
|
|
username=input.admin_username.strip(),
|
|
password_hash=hash_password(input.admin_password),
|
|
display_name=input.admin_display.strip() or input.admin_username.strip(),
|
|
role="admin",
|
|
)
|
|
db.add(admin)
|
|
|
|
# Create guest viewer
|
|
import secrets
|
|
guest_pass = secrets.token_hex(6)
|
|
viewer = User(
|
|
username="viewer",
|
|
password_hash=hash_password(guest_pass),
|
|
display_name="Viewer",
|
|
role="viewer",
|
|
)
|
|
db.add(viewer)
|
|
db.commit()
|
|
db.refresh(admin)
|
|
db.refresh(viewer)
|
|
|
|
# Auto-login as admin
|
|
token = create_access_token(admin.id, admin.role)
|
|
response = JSONResponse({
|
|
"token": token,
|
|
"guest_password": guest_pass,
|
|
"user": {
|
|
"id": admin.id,
|
|
"username": admin.username,
|
|
"display_name": admin.display_name,
|
|
"role": admin.role,
|
|
},
|
|
})
|
|
response.set_cookie(
|
|
key="token", value=token,
|
|
httponly=True, max_age=86400, samesite="lax",
|
|
secure=False, path="/",
|
|
)
|
|
return response
|
|
|
|
|
|
@app.get("/login", response_class=HTMLResponse)
|
|
def login_page(request: Request, db: Session = Depends(get_db)):
|
|
"""Serve the login page. Redirect to /setup if no admin exists."""
|
|
if not is_setup_complete(db):
|
|
return RedirectResponse(url="/setup")
|
|
return templates.TemplateResponse(request, "login.html")
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def dashboard_page(
|
|
request: Request,
|
|
current_user: Optional[User] = Depends(get_page_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Serve the dashboard page."""
|
|
if not is_setup_complete(db):
|
|
return RedirectResponse(url="/setup")
|
|
if not current_user:
|
|
return RedirectResponse(url="/login")
|
|
current_month = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
pnl = compute_pnl(current_month, db)
|
|
trend = get_trend(db, months=12)
|
|
services = db.query(Service).filter(Service.active == True).all()
|
|
return templates.TemplateResponse(
|
|
request, "dashboard.html",
|
|
{
|
|
"current_user": current_user,
|
|
"pnl": pnl,
|
|
"trend": trend,
|
|
"services": services,
|
|
},
|
|
)
|
|
|
|
|
|
@app.get("/transactions", response_class=HTMLResponse)
|
|
def transactions_page(
|
|
request: Request,
|
|
current_user: Optional[User] = Depends(get_page_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Serve the transactions page."""
|
|
if not is_setup_complete(db):
|
|
return RedirectResponse(url="/setup")
|
|
if not current_user:
|
|
return RedirectResponse(url="/login")
|
|
current_month = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
services = db.query(Service).filter(Service.active == True).all()
|
|
return templates.TemplateResponse(
|
|
request, "transactions.html",
|
|
{
|
|
"current_user": current_user,
|
|
"services": services,
|
|
"current_month": current_month,
|
|
},
|
|
)
|
|
|
|
|
|
@app.get("/services", response_class=HTMLResponse)
|
|
def services_page(
|
|
request: Request,
|
|
current_user: Optional[User] = Depends(get_page_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Serve the services management page."""
|
|
if not is_setup_complete(db):
|
|
return RedirectResponse(url="/setup")
|
|
if not current_user:
|
|
return RedirectResponse(url="/login")
|
|
services = db.query(Service).order_by(Service.name).all()
|
|
services_json = [service_to_dict(s) for s in services]
|
|
return templates.TemplateResponse(
|
|
request, "services.html",
|
|
{
|
|
"services": services,
|
|
"services_json": services_json,
|
|
"current_user": current_user,
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logout
|
|
# ---------------------------------------------------------------------------
|
|
@app.get("/logout")
|
|
def logout():
|
|
return RedirectResponse(url="/login")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entrypoint
|
|
# ---------------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run("main:app", host="0.0.0.0", port=8788, reload=True)
|