Skip to content

Commit 1ca1d79

Browse files
committed
refactor: fe and be improvements and added jsdocs to new components
1 parent c79f6d0 commit 1ca1d79

10 files changed

Lines changed: 138 additions & 32 deletions

File tree

backend/ops_api/ops/resources/agreements.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,12 @@ def get(self) -> Response:
161161
request_schema = AgreementRequestSchema()
162162
data = request_schema.load(request.args.to_dict(flat=False))
163163

164+
include_procurement_list = data.pop("include_procurement", [False])
165+
include_procurement = include_procurement_list[0] if include_procurement_list else False
166+
164167
logger.debug("Beginning agreement queries")
165168
service = AgreementsService(current_app.db_session)
166-
agreements, metadata = service.get_list(agreement_classes, data)
169+
agreements, metadata = service.get_list(agreement_classes, data, include_procurement)
167170
logger.debug("Agreement queries complete")
168171

169172
logger.debug("Serializing results")

backend/ops_api/ops/schemas/agreements.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class Meta:
153153
only_my = fields.List(fields.Boolean(), required=False)
154154
award_type = fields.List(fields.Enum(AgreementClassification), required=False)
155155
exact_match = fields.List(fields.Boolean(), required=False, load_default=[True])
156+
include_procurement = fields.List(fields.Boolean(), required=False, load_default=[False])
156157

157158

158159
class AgreementFiltersQueryParametersSchema(Schema):

backend/ops_api/ops/services/agreements.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -415,14 +415,18 @@ def get(self, id: int) -> Agreement:
415415
return agreement
416416

417417
def get_list(
418-
self, agreement_classes: list[Type[Agreement]], data: dict[str, Any]
418+
self,
419+
agreement_classes: list[Type[Agreement]],
420+
data: dict[str, Any],
421+
include_procurement: bool = False,
419422
) -> tuple[list[Agreement], dict[str, Any]]:
420423
"""
421424
Get list of agreements with optional filtering and pagination.
422425
423426
Args:
424427
agreement_classes: List of Agreement subclasses to query (e.g., ContractAgreement, GrantAgreement)
425428
data: Dictionary containing filter parameters including limit and offset
429+
include_procurement: When True, eager-load BLIs/trackers and compute procurement metrics
426430
427431
Returns:
428432
Tuple of (paginated agreements list, metadata dict with count/limit/offset)
@@ -433,7 +437,7 @@ def get_list(
433437
# Collect all agreements across types using existing resource helpers
434438
all_results = []
435439
for agreement_cls in agreement_classes:
436-
agreements = _get_agreements(self.db_session, agreement_cls, data)
440+
agreements = _get_agreements(self.db_session, agreement_cls, data, include_procurement)
437441
all_results.extend(agreements)
438442

439443
# Filter by award_type (computed property, must be done post-query)
@@ -454,10 +458,15 @@ def get_list(
454458
# Calculate aggregate totals before pagination (for summary cards)
455459
totals = _compute_agreement_totals(all_results)
456460

457-
# Calculate procurement overview and step summary before pagination
458-
overview_fiscal_year = filters.fiscal_year[0] if filters.fiscal_year and len(filters.fiscal_year) == 1 else None
459-
procurement_overview = _compute_procurement_overview(all_results, overview_fiscal_year)
460-
procurement_step_summary = _compute_procurement_step_summary(all_results, overview_fiscal_year)
461+
# Calculate procurement overview and step summary before pagination (only when requested)
462+
procurement_overview = None
463+
procurement_step_summary = None
464+
if include_procurement:
465+
overview_fiscal_year = (
466+
filters.fiscal_year[0] if filters.fiscal_year and len(filters.fiscal_year) == 1 else None
467+
)
468+
procurement_overview = _compute_procurement_overview(all_results, overview_fiscal_year)
469+
procurement_step_summary = _compute_procurement_step_summary(all_results, overview_fiscal_year)
461470

462471
# Apply pagination slicing
463472
if filters.limit is not None and filters.offset is not None:
@@ -836,6 +845,13 @@ def _validate_special_topics(db_session: Session, special_topics: List[SpecialTo
836845
raise ValidationError({"special_topics": [f"Special Topic IDs do not exist: {invalid_special_topic_ids}"]})
837846

838847

848+
def _percent(value, total):
849+
"""Return ``value / total`` as a rounded whole-number percentage, or 0.0 when *total* is zero."""
850+
if total == 0:
851+
return 0.0
852+
return float(round((float(value) / float(total)) * 100))
853+
854+
839855
def _compute_agreement_totals(all_results: list[Agreement]) -> dict[str, Any]:
840856
"""Compute aggregate totals across all filtered agreements for summary cards."""
841857
totals = {
@@ -917,11 +933,6 @@ def _compute_procurement_overview(all_results: list[Agreement], fiscal_year: int
917933
# This means per-status percentages may not sum to 100%.
918934
total_agreements = len(all_results)
919935

920-
def _percent(value, total):
921-
if total == 0:
922-
return 0.0
923-
return float(round((float(value) / float(total)) * 100))
924-
925936
status_data = []
926937
for status in tracked_statuses:
927938
amount = amount_by_status[status]
@@ -988,11 +999,6 @@ def _compute_procurement_step_summary(all_results: list[Agreement], fiscal_year:
988999

9891000
total_agreement_count = sum(agreements_by_step.values())
9901001

991-
def _percent(value, total):
992-
if total == 0:
993-
return 0.0
994-
return float(round((float(value) / float(total)) * 100))
995-
9961002
step_data = []
9971003
for step in range(1, 7):
9981004
step_data.append(
@@ -1010,8 +1016,13 @@ def _percent(value, total):
10101016
}
10111017

10121018

1013-
def _get_agreements(session: Session, agreement_cls: Type[Agreement], data: dict[str, Any]) -> Sequence[Agreement]:
1014-
query = _build_base_query(agreement_cls)
1019+
def _get_agreements(
1020+
session: Session,
1021+
agreement_cls: Type[Agreement],
1022+
data: dict[str, Any],
1023+
include_procurement: bool = False,
1024+
) -> Sequence[Agreement]:
1025+
query = _build_base_query(agreement_cls, include_procurement)
10151026
query = _apply_filters(query, agreement_cls, data)
10161027

10171028
logger.debug(f"query: {query}")
@@ -1020,18 +1031,16 @@ def _get_agreements(session: Session, agreement_cls: Type[Agreement], data: dict
10201031
return _filter_by_ownership(all_results, data.get("only_my", []))
10211032

10221033

1023-
def _build_base_query(agreement_cls: Type[Agreement]) -> Select[tuple[Agreement]]:
1024-
return (
1025-
select(agreement_cls)
1026-
.distinct()
1027-
.join(BudgetLineItem, isouter=True)
1028-
.join(CAN, isouter=True)
1029-
.options(
1034+
def _build_base_query(agreement_cls: Type[Agreement], include_procurement: bool = False) -> Select[tuple[Agreement]]:
1035+
query = select(agreement_cls).distinct().join(BudgetLineItem, isouter=True).join(CAN, isouter=True)
1036+
1037+
if include_procurement:
1038+
query = query.options(
10301039
selectinload(agreement_cls.budget_line_items),
10311040
selectinload(agreement_cls.procurement_trackers),
10321041
)
1033-
.order_by(agreement_cls.id)
1034-
)
1042+
1043+
return query.order_by(agreement_cls.id)
10351044

10361045

10371046
def _apply_filters(query: Select[Agreement], agreement_cls: Type[Agreement], data: dict[str, Any]) -> Select[Agreement]:

frontend/src/api/opsAPI.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ export const opsApi = createApi({
9797
projectTitle,
9898
contractNumber,
9999
awardType,
100-
awardingEntityId
100+
awardingEntityId,
101+
includeProcurement
101102
},
102103
onlyMy,
103104
sortConditions,
@@ -142,6 +143,9 @@ export const opsApi = createApi({
142143
if (awardingEntityId) {
143144
awardingEntityId.forEach((id) => queryParams.push(`awarding_entity_id=${id}`));
144145
}
146+
if (includeProcurement) {
147+
queryParams.push("include_procurement=true");
148+
}
145149
if (onlyMy) {
146150
queryParams.push("only_my=true");
147151
}
@@ -1059,9 +1063,6 @@ export const opsApi = createApi({
10591063
}),
10601064
getProcurementTrackersByAgreementIds: builder.query({
10611065
query: (agreementIds) => {
1062-
if (!agreementIds || agreementIds.length === 0) {
1063-
return `/procurement-trackers/?agreement_id=-1&limit=0`;
1064-
}
10651066
const queryParams = agreementIds.map((id) => `agreement_id=${id}`);
10661067
queryParams.push(`limit=${agreementIds.length}`);
10671068
return `/procurement-trackers/?${queryParams.join("&")}`;

frontend/src/pages/procurementDashboard/ProcShopFilter.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
/**
2+
* @typedef {Object} ProcShopFilterProps
3+
* @property {string} value - The currently selected procurement shop abbreviation or "all".
4+
* @property {(value: string) => void} onChange - Callback when the selection changes.
5+
* @property {string[]} [options] - List of procurement shop abbreviations.
6+
*/
7+
8+
/**
9+
* @component ProcShopFilter
10+
* @param {ProcShopFilterProps} props
11+
* @returns {JSX.Element}
12+
*/
113
const ProcShopFilter = ({ value, onChange, options = [] }) => {
214
return (
315
<div

frontend/src/pages/procurementDashboard/ProcurementDashboardPage.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const ProcurementDashboard = () => {
4545
const { agreements, metadata, isLoading, error } = useGetAllAgreements({
4646
filters: {
4747
fiscalYear: [CURRENT_FISCAL_YEAR],
48+
includeProcurement: true,
4849
...(awardTypeFilter ? { awardType: [{ awardType: awardTypeFilter }] } : {}),
4950
...(selectedProcShopId ? { awardingEntityId: [selectedProcShopId] } : {})
5051
}

frontend/src/pages/procurementDashboard/ProcurementOverviewCard.jsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ const buildStatusData = (procurementOverview) => {
3434
return { statusData, totalAmount: total_amount, totalAgreements: total_agreements };
3535
};
3636

37+
/**
38+
* @typedef {Object} StatusDataItem
39+
* @property {string} status - Status key (e.g. "PLANNED", "IN_EXECUTION", "OBLIGATED").
40+
* @property {string} label - Human-readable status label.
41+
* @property {number} amount - Dollar amount for this status.
42+
* @property {number} amount_percent - Percentage of total amount.
43+
* @property {number} agreements - Number of agreements with this status.
44+
* @property {number} agreements_percent - Percentage of total agreements.
45+
*/
46+
47+
/**
48+
* @typedef {Object} ProcurementOverview
49+
* @property {StatusDataItem[]} status_data - Breakdown by BLI status.
50+
* @property {number} total_amount - Total dollar amount across all tracked statuses.
51+
* @property {number} total_agreements - Total number of agreements in the result set.
52+
*/
53+
54+
/**
55+
* @typedef {Object} ProcurementOverviewCardProps
56+
* @property {ProcurementOverview | null} procurementOverview - Overview data from the API.
57+
* @property {number} fiscalYear - The fiscal year being displayed.
58+
* @property {boolean} isLoading - Whether data is still loading.
59+
* @property {*} error - Error object, if any.
60+
*/
61+
62+
/**
63+
* @component ProcurementOverviewCard
64+
* @param {ProcurementOverviewCardProps} props
65+
* @returns {JSX.Element}
66+
*/
3767
const ProcurementOverviewCard = ({ procurementOverview, fiscalYear, isLoading, error }) => {
3868
const { statusData, totalAmount, totalAgreements } = useMemo(
3969
() => buildStatusData(procurementOverview),

frontend/src/pages/procurementDashboard/ProcurementStepSummaryCard.jsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import CustomLayerComponent from "../../components/UI/DataViz/ResponsiveDonutWit
44
import RoundedBox from "../../components/UI/RoundedBox";
55
import StepLegendItem from "./StepLegendItem";
66

7+
/**
8+
* @typedef {Object} StepDataItem
9+
* @property {number} id - The step number (used as chart segment id).
10+
* @property {string} label - Display label (e.g. "Step 1").
11+
* @property {string} color - CSS color for the chart segment and legend.
12+
* @property {number} value - Number of agreements in this step.
13+
* @property {number} percent - Percentage of total agreements.
14+
*/
15+
16+
/**
17+
* @typedef {Object} ProcurementStepSummaryCardProps
18+
* @property {StepDataItem[]} [stepData] - Per-step agreement counts for the donut chart.
19+
* @property {number} fiscalYear - The fiscal year being displayed.
20+
*/
21+
22+
/**
23+
* @component ProcurementStepSummaryCard
24+
* @param {ProcurementStepSummaryCardProps} props
25+
* @returns {JSX.Element}
26+
*/
727
const ProcurementStepSummaryCard = ({ stepData = [], fiscalYear }) => {
828
const [percent, setPercent] = React.useState("");
929
const [hoverId, setHoverId] = React.useState(-1);

frontend/src/pages/procurementDashboard/ProcurementSummaryCards.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ const buildStepData = (procurementStepSummary) => {
5656
return { stepData, budgetByStep };
5757
};
5858

59+
/**
60+
* @typedef {Object} ProcurementSummaryCardsProps
61+
* @property {import("./ProcurementOverviewCard").ProcurementOverview | null} procurementOverview - Overview data from the API.
62+
* @property {Object | null} procurementStepSummary - Step summary data from the API.
63+
* @property {number} fiscalYear - The fiscal year being displayed.
64+
* @property {boolean} isLoading - Whether data is still loading.
65+
* @property {*} error - Error object, if any.
66+
*/
67+
68+
/**
69+
* @component ProcurementSummaryCards
70+
* @param {ProcurementSummaryCardsProps} props
71+
* @returns {JSX.Element}
72+
*/
5973
const ProcurementSummaryCards = ({ procurementOverview, procurementStepSummary, fiscalYear, isLoading, error }) => {
6074
const { stepData, budgetByStep } = useMemo(() => buildStepData(procurementStepSummary), [procurementStepSummary]);
6175

frontend/src/pages/procurementDashboard/StepLegendItem.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import { faCircle } from "@fortawesome/free-solid-svg-icons";
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
33
import Tag from "../../components/UI/Tag/Tag";
44

5+
/**
6+
* @typedef {Object} StepLegendItemProps
7+
* @property {number} id - The step id.
8+
* @property {number} activeId - The currently hovered/active step id.
9+
* @property {string} label - The step label (e.g. "Step 1").
10+
* @property {number} value - The number of agreements in this step.
11+
* @property {string} color - CSS color for the legend dot.
12+
* @property {number} percent - The percentage of total agreements.
13+
*/
14+
15+
/**
16+
* @component StepLegendItem
17+
* @param {StepLegendItemProps} props
18+
* @returns {JSX.Element}
19+
*/
520
const StepLegendItem = ({ id, activeId, label, value, color, percent }) => {
621
const isActive = activeId === id;
722
return (

0 commit comments

Comments
 (0)