Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: idir compliance report dashboard widget #1660

Open
wants to merge 2 commits into
base: release-0.2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/lcfs/db/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,18 @@
"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
return False
else:
return True


async def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

Expand All @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;")
20 changes: 20 additions & 0 deletions backend/lcfs/db/models/compliance/ComplianceReportCountView.py
Original file line number Diff line number Diff line change
@@ -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"
)
19 changes: 19 additions & 0 deletions backend/lcfs/web/api/dashboard/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions backend/lcfs/web/api/dashboard/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from lcfs.web.api.base import BaseSchema
from pydantic import Field


class DirectorReviewCountsSchema(BaseSchema):
Expand All @@ -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)
11 changes: 11 additions & 0 deletions backend/lcfs/web/api/dashboard/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
TransactionCountsSchema,
OrganizarionTransactionCountsSchema,
OrgComplianceReportCountsSchema,
ComplianceReportCountsSchema
)

logger = structlog.get_logger(__name__)
Expand Down Expand Up @@ -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)
)
14 changes: 14 additions & 0 deletions backend/lcfs/web/api/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
TransactionCountsSchema,
OrganizarionTransactionCountsSchema,
OrgComplianceReportCountsSchema,
ComplianceReportCountsSchema
)
from lcfs.db.models.user.Role import RoleEnum

Expand Down Expand Up @@ -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()
8 changes: 7 additions & 1 deletion frontend/src/assets/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<br />improve LCFS.<br />Please send your<br />suggestions and feedback to:<br /><a href=\"mailto:[email protected]\" aria-label=\"Please send your suggestions and feedback to [email protected]\">[email protected]</a>"
Expand Down Expand Up @@ -66,4 +72,4 @@
"awaitingGovReview": "Compliance report(s) awaiting government review",
"noActionRequired": "There are no reports that require any action."
}
}
}
1 change: 1 addition & 0 deletions frontend/src/constants/routes/apiRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/hooks/useDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
}
2 changes: 2 additions & 0 deletions frontend/src/views/Dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -64,6 +65,7 @@ export const Dashboard = () => {
>
<Role roles={[roles.analyst, roles.compliance_manager]}>
<TransactionsCard />
<ComplianceReportCard />
</Role>
<Role roles={[roles.transfers]}>
<OrgTransactionsCard />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => (
<BCTypography
component="span"
variant="h3"
sx={{
color: 'success.main',
marginX: 3
}}
>
{count}
</BCTypography>
)

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 && <CountDisplay count={count} />}
<BCTypography
variant="body2"
color="link"
sx={{
textDecoration: 'underline',
'&:hover': { color: 'info.main' }
}}
onClick={onClick}
>
{text}
</BCTypography>
</>
)
}

return (
<BCWidgetCard
component="div"
disableHover={true}
title={t('dashboard:complianceReports.title')}
sx={{ '& .MuiCardContent-root': { padding: '16px' } }}
content={
isLoading ? (
<Loading message={t('dashboard:complianceReports.loadingMessage')} />
) : (
<Stack spacing={1}>
<BCTypography variant="body2" sx={{ marginBottom: 0 }}>
{t('dashboard:complianceReports.thereAre')}
</BCTypography>
<List
component="div"
sx={{
maxWidth: '100%',
padding: 0,
'& .MuiListItemButton-root': {
padding: '2px 0'
}
}}
>
<ListItemButton component="a" onClick={handleNavigation}>
{renderLinkWithCount(
t('dashboard:complianceReports.crInProgress'),
counts?.pendingReviews || 0,
handleNavigation
)}
</ListItemButton>
</List>
</Stack>
)
}
/>
)
}
Loading