From 6e3a47d0ec662c28f4095b8f97c7654c3b2cfc08 Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Thu, 9 Jan 2025 18:14:07 -0800 Subject: [PATCH 1/2] feat: idir compliance report dashboard widget --- backend/lcfs/db/migrations/env.py | 5 + .../versions/2025-01-10-00-35_10863452ccd2.py | 68 ++++++++++++ .../compliance/ComplianceReportCountView.py | 20 ++++ backend/lcfs/web/api/dashboard/repo.py | 19 ++++ backend/lcfs/web/api/dashboard/schema.py | 5 + backend/lcfs/web/api/dashboard/services.py | 11 ++ backend/lcfs/web/api/dashboard/views.py | 14 +++ frontend/src/assets/locales/en/dashboard.json | 8 +- frontend/src/constants/routes/apiRoutes.js | 1 + frontend/src/hooks/useDashboard.js | 13 +++ frontend/src/views/Dashboard/Dashboard.jsx | 2 + .../cards/idir/ComplianceReportCard.jsx | 104 ++++++++++++++++++ 12 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py create mode 100644 backend/lcfs/db/models/compliance/ComplianceReportCountView.py create mode 100644 frontend/src/views/Dashboard/components/cards/idir/ComplianceReportCard.jsx diff --git a/backend/lcfs/db/migrations/env.py b/backend/lcfs/db/migrations/env.py index dc0e8daa3..e28e674e0 100644 --- a/backend/lcfs/db/migrations/env.py +++ b/backend/lcfs/db/migrations/env.py @@ -33,8 +33,10 @@ "mv_director_review_transaction_count", "mv_org_compliance_report_count", "transaction_status_view", + "mv_compliance_report_count", ] + def include_object(object, name, type_, reflected, compare_to): if type_ == "table" and name in exclude_tables: # Exclude these tables from autogenerate @@ -42,6 +44,7 @@ def include_object(object, name, type_, reflected, compare_to): else: return True + async def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -65,6 +68,7 @@ async def run_migrations_offline() -> None: with context.begin_transaction(): context.run_migrations() + def do_run_migrations(connection: Connection) -> None: """ Run actual sync migrations. @@ -80,6 +84,7 @@ def do_run_migrations(connection: Connection) -> None: with context.begin_transaction(): context.run_migrations() + async def run_migrations_online() -> None: """ Run migrations in 'online' mode. diff --git a/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py b/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py new file mode 100644 index 000000000..edff2e050 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py @@ -0,0 +1,68 @@ +"""mv for compliance report count for the dashboard + +Revision ID: 10863452ccd2 +Revises: 94306eca5261 +Create Date: 2025-01-10 00:35:24.596718 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "10863452ccd2" +down_revision = "94306eca5261" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE MATERIALIZED VIEW mv_compliance_report_count AS + SELECT + CASE current_status_id + WHEN 2 THEN 'Submitted' + WHEN 3 THEN 'Recommended by Analysts' + WHEN 4 THEN 'Recommended by Manager' + END as status, + COUNT(*) as count + FROM compliance_report + WHERE current_status_id IN (2,3,4) + GROUP BY current_status_id; + """ + ) + + op.execute( + """ + CREATE UNIQUE INDEX mv_compliance_report_count_idx + ON mv_compliance_report_count (status); + """ + ) + + op.execute( + """ + CREATE OR REPLACE FUNCTION refresh_mv_compliance_report_count() + RETURNS TRIGGER AS $$ + BEGIN + REFRESH MATERIALIZED VIEW CONCURRENTLY mv_compliance_report_count; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + """ + ) + + op.execute( + """ + CREATE TRIGGER refresh_mv_compliance_report_count_after_change + AFTER INSERT OR UPDATE OR DELETE ON compliance_report + FOR EACH STATEMENT EXECUTE FUNCTION refresh_mv_compliance_report_count(); + """ + ) + + +def downgrade() -> None: + op.execute( + "DROP TRIGGER IF EXISTS refresh_mv_compliance_report_count_after_change ON compliance_report;") + op.execute("DROP FUNCTION IF EXISTS refresh_mv_compliance_report_count();") + op.execute("DROP MATERIALIZED VIEW IF EXISTS mv_compliance_report_count;") diff --git a/backend/lcfs/db/models/compliance/ComplianceReportCountView.py b/backend/lcfs/db/models/compliance/ComplianceReportCountView.py new file mode 100644 index 000000000..b164e7387 --- /dev/null +++ b/backend/lcfs/db/models/compliance/ComplianceReportCountView.py @@ -0,0 +1,20 @@ +from sqlalchemy import Column, Integer, String +from lcfs.db.base import BaseModel + + +class ComplianceReportCountView(BaseModel): + __tablename__ = "mv_compliance_report_count" + __table_args__ = { + "extend_existing": True, + "comment": "Materialized view for counting compliance reports by review status", + } + + status = Column( + String, + primary_key=True, + comment="Status name (Submitted, Recommended by Analysts, Recommended by Manager)" + ) + count = Column( + Integer, + comment="Count of compliance reports for this status" + ) diff --git a/backend/lcfs/web/api/dashboard/repo.py b/backend/lcfs/web/api/dashboard/repo.py index 3d7ab51c0..aff0f6634 100644 --- a/backend/lcfs/web/api/dashboard/repo.py +++ b/backend/lcfs/web/api/dashboard/repo.py @@ -12,6 +12,9 @@ from lcfs.db.models.compliance.OrgComplianceReportCountView import ( OrgComplianceReportCountView, ) +from lcfs.db.models.compliance.ComplianceReportCountView import ( + ComplianceReportCountView, +) logger = structlog.get_logger(__name__) @@ -70,3 +73,19 @@ async def get_org_compliance_report_counts(self, organization_id: int): "in_progress": getattr(counts, "count_in_progress", 0), "awaiting_gov_review": getattr(counts, "count_awaiting_gov_review", 0), } + + @repo_handler + async def get_compliance_report_counts(self): + query = select( + func.coalesce( + func.sum(ComplianceReportCountView.count), + 0 + ).label('pending_reviews') + ) + + result = await self.db.execute(query) + row = result.fetchone() + + return { + "pending_reviews": row.pending_reviews + } diff --git a/backend/lcfs/web/api/dashboard/schema.py b/backend/lcfs/web/api/dashboard/schema.py index f57217603..bb0d803d2 100644 --- a/backend/lcfs/web/api/dashboard/schema.py +++ b/backend/lcfs/web/api/dashboard/schema.py @@ -1,4 +1,5 @@ from lcfs.web.api.base import BaseSchema +from pydantic import Field class DirectorReviewCountsSchema(BaseSchema): @@ -21,3 +22,7 @@ class OrganizarionTransactionCountsSchema(BaseSchema): class OrgComplianceReportCountsSchema(BaseSchema): in_progress: int awaiting_gov_review: int + + +class ComplianceReportCountsSchema(BaseSchema): + pending_reviews: int = Field(default=0) diff --git a/backend/lcfs/web/api/dashboard/services.py b/backend/lcfs/web/api/dashboard/services.py index 112b709a1..6e433f9b4 100644 --- a/backend/lcfs/web/api/dashboard/services.py +++ b/backend/lcfs/web/api/dashboard/services.py @@ -7,6 +7,7 @@ TransactionCountsSchema, OrganizarionTransactionCountsSchema, OrgComplianceReportCountsSchema, + ComplianceReportCountsSchema ) logger = structlog.get_logger(__name__) @@ -55,3 +56,13 @@ async def get_org_compliance_report_counts( in_progress=counts.get("in_progress", 0), awaiting_gov_review=counts.get("awaiting_gov_review", 0), ) + + @service_handler + async def get_compliance_report_counts( + self + ) -> ComplianceReportCountsSchema: + counts = await self.repo.get_compliance_report_counts() + + return ComplianceReportCountsSchema( + pending_reviews=counts.get("pending_reviews", 0) + ) diff --git a/backend/lcfs/web/api/dashboard/views.py b/backend/lcfs/web/api/dashboard/views.py index df8f12c01..cabde7d16 100644 --- a/backend/lcfs/web/api/dashboard/views.py +++ b/backend/lcfs/web/api/dashboard/views.py @@ -8,6 +8,7 @@ TransactionCountsSchema, OrganizarionTransactionCountsSchema, OrgComplianceReportCountsSchema, + ComplianceReportCountsSchema ) from lcfs.db.models.user.Role import RoleEnum @@ -59,3 +60,16 @@ async def get_org_compliance_report_counts( """Endpoint to retrieve counts for organization compliance report items""" organization_id = request.user.organization.organization_id return await service.get_org_compliance_report_counts(organization_id) + + +@router.get( + "/compliance-report-counts", + response_model=ComplianceReportCountsSchema +) +@view_handler([RoleEnum.ANALYST, RoleEnum.COMPLIANCE_MANAGER]) +async def get_compliance_report_counts( + request: Request, + service: DashboardServices = Depends(), +): + """Endpoint to retrieve count of compliance reports pending review""" + return await service.get_compliance_report_counts() diff --git a/frontend/src/assets/locales/en/dashboard.json b/frontend/src/assets/locales/en/dashboard.json index c88fa945b..e3af42ce5 100644 --- a/frontend/src/assets/locales/en/dashboard.json +++ b/frontend/src/assets/locales/en/dashboard.json @@ -26,6 +26,12 @@ "viewAllTransactions": "View all transactions", "loadingMessage": "Loading transactions card..." }, + "complianceReports": { + "title": "Compliance reports", + "thereAre": "There are:", + "crInProgress": "Compliance Report(s) in progress", + "loadingMessage": "Loading Compliance Report card..." + }, "feedback": { "title": "We want to hear from you", "email": "We are always striving to
improve LCFS.
Please send your
suggestions and feedback to:
lcfs@gov.bc.ca" @@ -66,4 +72,4 @@ "awaitingGovReview": "Compliance report(s) awaiting government review", "noActionRequired": "There are no reports that require any action." } -} +} \ No newline at end of file diff --git a/frontend/src/constants/routes/apiRoutes.js b/frontend/src/constants/routes/apiRoutes.js index b8f7a1af5..f2cd2a5b4 100644 --- a/frontend/src/constants/routes/apiRoutes.js +++ b/frontend/src/constants/routes/apiRoutes.js @@ -65,6 +65,7 @@ export const apiRoutes = { saveAllocationAgreements: '/allocation-agreement/save', allocationAgreementSearch: '/allocation-agreement/search?', OrgComplianceReportCounts: '/dashboard/org-compliance-report-counts', + complianceReportCounts: '/dashboard/compliance-report-counts', organizationSearch: '/organizations/search?', getUserActivities: '/users/:userID/activity', getAllUserActivities: '/users/activities/all', diff --git a/frontend/src/hooks/useDashboard.js b/frontend/src/hooks/useDashboard.js index 96b4cb83c..6a60a94eb 100644 --- a/frontend/src/hooks/useDashboard.js +++ b/frontend/src/hooks/useDashboard.js @@ -57,3 +57,16 @@ export const useOrgComplianceReportCounts = (options = {}) => { ...options }) } + +export const useComplianceReportCounts = () => { + const client = useApiService() + const path = apiRoutes.complianceReportCounts + + return useQuery({ + queryKey: ['compliance-report-counts'], + queryFn: async () => { + const response = await client.get(path) + return response.data + } + }) +} diff --git a/frontend/src/views/Dashboard/Dashboard.jsx b/frontend/src/views/Dashboard/Dashboard.jsx index 7d6a66052..015feb9b2 100644 --- a/frontend/src/views/Dashboard/Dashboard.jsx +++ b/frontend/src/views/Dashboard/Dashboard.jsx @@ -14,6 +14,7 @@ import { OrgComplianceReportsCard } from './components/cards' import OrganizationsSummaryCard from './components/cards/idir/OrganizationsSummaryCard' +import { ComplianceReportCard } from './components/cards/idir/ComplianceReportCard' export const Dashboard = () => { return ( @@ -64,6 +65,7 @@ export const Dashboard = () => { > + diff --git a/frontend/src/views/Dashboard/components/cards/idir/ComplianceReportCard.jsx b/frontend/src/views/Dashboard/components/cards/idir/ComplianceReportCard.jsx new file mode 100644 index 000000000..2f1b9b610 --- /dev/null +++ b/frontend/src/views/Dashboard/components/cards/idir/ComplianceReportCard.jsx @@ -0,0 +1,104 @@ +import BCTypography from '@/components/BCTypography' +import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard' +import Loading from '@/components/Loading' +import { ROUTES } from '@/constants/routes' +import { useComplianceReportCounts } from '@/hooks/useDashboard' +import { List, ListItemButton, Stack } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' + +const CountDisplay = ({ count }) => ( + + {count} + +) + +export const ComplianceReportCard = () => { + const { t } = useTranslation(['dashboard']) + const navigate = useNavigate() + const { data: counts, isLoading } = useComplianceReportCounts() + console.log('ComplianceReportCard counts:', counts) + + const handleNavigation = () => { + navigate(ROUTES.REPORTS, { + state: { + filters: [ + { + field: 'status', + filter: [ + 'Submitted', + 'Recommended by analyst', + 'Recommended by manager' + ], + filterType: 'text', + type: 'set' + } + ] + } + }) + } + + const renderLinkWithCount = (text, count, onClick) => { + return ( + <> + {count != null && } + + {text} + + + ) + } + + return ( + + ) : ( + + + {t('dashboard:complianceReports.thereAre')} + + + + {renderLinkWithCount( + t('dashboard:complianceReports.crInProgress'), + counts?.pendingReviews || 0, + handleNavigation + )} + + + + ) + } + /> + ) +} From 631b64f5056fc0620d261fbbb79241d8b450137d Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Mon, 13 Jan 2025 13:29:33 -0800 Subject: [PATCH 2/2] fix: migration revision fix --- .../db/migrations/versions/2025-01-10-00-35_10863452ccd2.py | 2 +- .../db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py b/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py index edff2e050..b1cdca64a 100644 --- a/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py +++ b/backend/lcfs/db/migrations/versions/2025-01-10-00-35_10863452ccd2.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "10863452ccd2" -down_revision = "94306eca5261" +down_revision = "fa98709e7952" branch_labels = None depends_on = None diff --git a/backend/lcfs/db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py b/backend/lcfs/db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py index 54a2bef13..839943fc0 100644 --- a/backend/lcfs/db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py +++ b/backend/lcfs/db/migrations/versions/2025-01-10-13-39_d25e7c47659e.py @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "d25e7c47659e" -down_revision = "fa98709e7952" +down_revision = "10863452ccd2" branch_labels = None depends_on = None