Catalyst / admin/Apex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Apex

public

Bid Management and Orchestration Tool with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Apex / Synapse-Apexv2 / backend / app / routers / resources.py 5075 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import csv
import io
from decimal import Decimal

from fastapi import APIRouter, Depends, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.deps import audit, require_roles
from app.models import ROLE_ADMIN, ROLE_FINANCE, LicenceCost, OtherCost, ResourceType, User
from app.schemas import (
    LicenceIn,
    LicenceOut,
    ResourceTypeIn,
    ResourceTypeOut,
)

router = APIRouter(prefix="/api/resources", tags=["resources"])
finance = require_roles(ROLE_ADMIN, ROLE_FINANCE)


# --- resource types ---
@router.get("/types", response_model=list[ResourceTypeOut])
def list_types(db: Session = Depends(get_db), _: User = Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE, "bid_manager"))):
    return db.execute(
        select(ResourceType).where(ResourceType.active == True).order_by(ResourceType.name)  # noqa: E712
    ).scalars().all()


@router.post("/types", response_model=ResourceTypeOut, status_code=201)
def create_type(body: ResourceTypeIn, db: Session = Depends(get_db), actor: User = Depends(finance)):
    rt = ResourceType(**body.model_dump())
    db.add(rt)
    db.commit()
    db.refresh(rt)
    audit(db, actor, "resource_type_created", "resource_type", rt.id)
    return rt


@router.patch("/types/{rt_id}", response_model=ResourceTypeOut)
def update_type(rt_id: int, body: ResourceTypeIn, db: Session = Depends(get_db), actor: User = Depends(finance)):
    rt = db.get(ResourceType, rt_id)
    if not rt:
        raise HTTPException(404, "Not found")
    for k, v in body.model_dump().items():
        setattr(rt, k, v)
    db.commit()
    db.refresh(rt)
    audit(db, actor, "resource_type_updated", "resource_type", rt.id)
    return rt


@router.delete("/types/{rt_id}")
def delete_type(rt_id: int, db: Session = Depends(get_db), _: User = Depends(finance)):
    rt = db.get(ResourceType, rt_id)
    if rt:
        rt.active = False
        db.commit()
    return {"ok": True}


@router.get("/types/export.csv")
def export_types(db: Session = Depends(get_db), _: User = Depends(finance)):
    rows = db.execute(select(ResourceType)).scalars().all()
    buf = io.StringIO()
    w = csv.writer(buf)
    w.writerow(["name", "role_family", "skills", "hourly_cost_rate", "hourly_sell_rate", "currency", "active"])
    for r in rows:
        w.writerow([r.name, r.role_family, "|".join(r.skills), r.hourly_cost_rate, r.hourly_sell_rate, r.currency, r.active])
    buf.seek(0)
    return StreamingResponse(
        iter([buf.getvalue()]),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=resource_types.csv"},
    )


@router.post("/types/import.csv")
async def import_types(file: UploadFile, db: Session = Depends(get_db), actor: User = Depends(finance)):
    content = (await file.read()).decode("utf-8")
    reader = csv.DictReader(io.StringIO(content))
    count = 0
    for row in reader:
        db.add(ResourceType(
            name=row.get("name", "").strip(),
            role_family=row.get("role_family", "").strip(),
            skills=[s for s in row.get("skills", "").split("|") if s],
            hourly_cost_rate=Decimal(row.get("hourly_cost_rate") or "0"),
            hourly_sell_rate=Decimal(row.get("hourly_sell_rate") or "0"),
            daily_cost_rate=Decimal(row.get("hourly_cost_rate") or "0") * 8,
            daily_sell_rate=Decimal(row.get("hourly_sell_rate") or "0") * 8,
            currency=row.get("currency", "GBP").strip() or "GBP",
        ))
        count += 1
    db.commit()
    audit(db, actor, "resource_types_imported", "resource_type", None, {"count": count})
    return {"imported": count}


# --- licences ---
@router.get("/licences", response_model=list[LicenceOut])
def list_licences(db: Session = Depends(get_db), _: User = Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE, "bid_manager"))):
    return db.execute(
        select(LicenceCost).where(LicenceCost.active == True).order_by(LicenceCost.name)  # noqa: E712
    ).scalars().all()


@router.post("/licences", response_model=LicenceOut, status_code=201)
def create_licence(body: LicenceIn, db: Session = Depends(get_db), actor: User = Depends(finance)):
    lic = LicenceCost(**body.model_dump())
    db.add(lic)
    db.commit()
    db.refresh(lic)
    audit(db, actor, "licence_created", "licence", lic.id)
    return lic


@router.patch("/licences/{lic_id}", response_model=LicenceOut)
def update_licence(lic_id: int, body: LicenceIn, db: Session = Depends(get_db), _: User = Depends(finance)):
    lic = db.get(LicenceCost, lic_id)
    if not lic:
        raise HTTPException(404, "Not found")
    for k, v in body.model_dump().items():
        setattr(lic, k, v)
    db.commit()
    db.refresh(lic)
    return lic


@router.delete("/licences/{lic_id}")
def delete_licence(lic_id: int, db: Session = Depends(get_db), _: User = Depends(finance)):
    lic = db.get(LicenceCost, lic_id)
    if lic:
        lic.active = False
        db.commit()
    return {"ok": True}