Skip to content

Commit

Permalink
Staleness in issues tab - 30-60-90 (#2259)
Browse files Browse the repository at this point in the history
* Staleness in issues tab - 30-60-90

* Staleness in issues tab - 30-60-90

* staleness in 30-60-90 updates

* staleness in 30-60-90 updates

* lintfixes
  • Loading branch information
dinesh-aot authored May 22, 2024
1 parent 1346840 commit 7c3a9f0
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 31 deletions.
18 changes: 18 additions & 0 deletions epictrack-api/src/api/models/queries/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""This exports all of query modules"""


from .work_issue_queries import WorkIssueQuery
38 changes: 38 additions & 0 deletions epictrack-api/src/api/models/queries/work_issue_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Model to handle all complex operations related to Work Issues."""
from typing import List
from sqlalchemy import and_
from api.models import WorkIssues, db


# pylint: disable=too-few-public-methods
class WorkIssueQuery:
"""Query module for complex work issue queries"""

@classmethod
def find_work_issues_by_work_ids(cls, work_ids: List[int]) -> List[WorkIssues]:
"""Find work issues by work ids"""
results = (
db.session.query(WorkIssues)
.filter(
and_(
WorkIssues.work_id.in_(work_ids),
WorkIssues.is_active.is_(True),
WorkIssues.is_deleted.is_(False),
)
)
.all()
)
return results
31 changes: 31 additions & 0 deletions epictrack-api/src/api/reports/thirty_sixty_ninety_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from io import BytesIO
from typing import Dict, List

from operator import attrgetter
from pytz import utc
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
Expand All @@ -21,7 +22,10 @@
from api.models.event_type import EventTypeEnum
from api.models.special_field import EntityEnum
from api.models.work import WorkStateEnum
from api.models.work_issues import WorkIssues
from api.services.special_field import SpecialFieldService
from api.services.work_issues import WorkIssuesService
from api.schemas import response as res

from .report_factory import ReportFactory

Expand Down Expand Up @@ -157,6 +161,7 @@ def _fetch_data(self, report_date):

def _format_data(self, data):
data = super()._format_data(data)
data = self._update_work_issues(data)
response = {
"30": [],
"60": [],
Expand Down Expand Up @@ -207,6 +212,32 @@ def _format_data(self, data):
response["90"].append(work)
return response

def _update_work_issues(self, data) -> list[WorkIssues]:
"""Combine the result with work issues"""
work_ids = set((work["work_id"] for work in data))
work_issues = WorkIssuesService.find_work_issues_by_work_ids(work_ids)
for result_item in data:
issue_per_work = [
issue
for issue in work_issues
if issue.work_id == result_item["work_id"]
and issue.is_high_priority is True
]
for issue in issue_per_work:
latest_update = max(
(
issue_update
for issue_update in issue.updates
if issue_update.is_approved
),
key=attrgetter("posted_date"),
)
setattr(issue, "latest_update", latest_update)
result_item["work_issues"] = res.WorkIssuesLatestUpdateResponseSchema(many=True).dump(
issue_per_work
)
return data

def generate_report(
self, report_date: datetime, return_type
): # pylint: disable=too-many-locals
Expand Down
35 changes: 27 additions & 8 deletions epictrack-api/src/api/schemas/response/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
from .act_section_response import ActSectionResponseSchema
from .action_template_response import ActionTemplateResponseSchema
from .event_configuration_response import EventConfigurationResponseSchema
from .event_response import EventDateChangePosibilityCheckResponseSchema, EventResponseSchema
from .event_response import (
EventDateChangePosibilityCheckResponseSchema,
EventResponseSchema,
)
from .event_template_response import EventTemplateResponseSchema
from .indigenous_nation_response import IndigenousResponseNationSchema, WorkIndigenousNationResponseSchema, \
IndigenousNationConsultationResponseSchema
from .indigenous_nation_response import (
IndigenousResponseNationSchema,
WorkIndigenousNationResponseSchema,
IndigenousNationConsultationResponseSchema,
)
from .list_type_response import ListTypeResponseSchema
from .outcome_configuration_response import OutcomeConfigurationResponseSchema
from .outcome_template_response import OutcomeTemplateResponseSchema
Expand All @@ -30,12 +36,25 @@
from .special_field_response import SpecialFieldResponseSchema
from .staff_response import StaffResponseSchema
from .staff_work_role_response import StaffWorkRoleResponseSchema
from .task_response import TaskEventResponseSchema, TaskResponseSchema, TaskTemplateResponseSchema, \
TaskEventByStaffResponseSchema
from .task_response import (
TaskEventResponseSchema,
TaskResponseSchema,
TaskTemplateResponseSchema,
TaskEventByStaffResponseSchema,
)
from .types_response import SubTypeResponseSchema, TypeResponseSchema
from .user_group_response import UserGroupResponseSchema
from .user_response import UserResponseSchema
from .work_response import (
WorkIssuesResponseSchema, WorkIssueUpdatesResponseSchema, WorkPhaseAdditionalInfoResponseSchema,
WorkPhaseResponseSchema, WorkPhaseTemplateAvailableResponse, WorkResourceResponseSchema, WorkResponseSchema,
WorkStaffRoleReponseSchema, WorkStatusResponseSchema, WorkPhaseByIdResponseSchema)
WorkIssuesResponseSchema,
WorkIssueUpdatesResponseSchema,
WorkPhaseAdditionalInfoResponseSchema,
WorkPhaseResponseSchema,
WorkPhaseTemplateAvailableResponse,
WorkResourceResponseSchema,
WorkResponseSchema,
WorkStaffRoleReponseSchema,
WorkStatusResponseSchema,
WorkPhaseByIdResponseSchema,
WorkIssuesLatestUpdateResponseSchema,
)
15 changes: 15 additions & 0 deletions epictrack-api/src/api/schemas/response/work_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,18 @@ class Meta(AutoSchemaBase.Meta):
unknown = EXCLUDE

updates = fields.Nested(WorkIssueUpdatesResponseSchema(many=True), dump_default=[])


class WorkIssuesLatestUpdateResponseSchema(
AutoSchemaBase
): # pylint: disable=too-many-ancestors,too-few-public-methods
"""Work Issues model schema class"""

class Meta(AutoSchemaBase.Meta):
"""Meta information"""

model = WorkIssues
include_fk = True
unknown = EXCLUDE

latest_update = fields.Nested(WorkIssueUpdatesResponseSchema(), dump_default=[])
7 changes: 7 additions & 0 deletions epictrack-api/src/api/services/work_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from api.utils import TokenInfo
from api.utils.roles import Role as KeycloakRole, Membership
from api.services import authorisation
from api.models.queries import WorkIssueQuery


class WorkIssuesService: # pylint: disable=too-many-public-methods
Expand All @@ -39,6 +40,12 @@ def find_work_issue_by_id(cls, work_id, issue_id):
results = WorkIssuesModel.find_by_params({"work_id": work_id, "id": issue_id})
return results[0] if results else None

@classmethod
def find_work_issues_by_work_ids(cls, work_ids):
"""Find all work issues by work ids"""
results = WorkIssueQuery.find_work_issues_by_work_ids(work_ids)
return results

@classmethod
def create_work_issue_and_updates(cls, work_id, issue_data: Dict):
"""Create a new work issue and its updates."""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from "react";
import React, { useCallback, useMemo } from "react";
import { Container } from "@mui/system";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Alert,
Box,
Chip,
Divider,
Grid,
Skeleton,
Tab,
Expand All @@ -22,19 +24,24 @@ import {
RESULT_STATUS,
REPORT_TYPE,
DISPLAY_DATE_FORMAT,
StalenessEnum,
REPORT_STALENESS_THRESHOLD,
} from "../../../constants/application-constant";
import { dateUtils } from "../../../utils";
import Icons from "../../icons";
import { IconProps } from "../../icons/type";
import ReportHeader from "../shared/report-header/ReportHeader";
import { ETPageContainer } from "../../shared";
import { staleLevel } from "utils/uiUtils";

const IndicatorIcon: React.FC<IconProps> = Icons["IndicatorIcon"];
export default function ThirtySixtyNinety() {
const [reports, setReports] = React.useState({});
const [showReportDateBanner, setShowReportDateBanner] =
React.useState<boolean>(false);
const [selectedTab, setSelectedTab] = React.useState(0);
const [reportDate, setReportDate] = React.useState<string>();
const [resultStatus, setResultStatus] = React.useState<string>();

const FILENAME_PREFIX = "30_60_90_Report";
React.useEffect(() => {
const diff = dateUtils.diff(
Expand All @@ -55,16 +62,48 @@ export default function ThirtySixtyNinety() {
);
setResultStatus(RESULT_STATUS.LOADED);
if (reportData.status === 200) {
setReports((reportData.data as never)["data"]);
const result = (reportData.data as never)["data"];
Object.keys(result).forEach((key) => {
(result[key] as []).forEach((resultItem: any) => {
(resultItem.work_issues as []).forEach((workIssue: any) => {
if (stalenessLevel(workIssue) === StalenessEnum.CRITICAL)
workIssue["staleness"] = StalenessEnum.CRITICAL;
else if (stalenessLevel(workIssue) === StalenessEnum.WARN)
workIssue["staleness"] = StalenessEnum.WARN;
else workIssue["staleness"] = StalenessEnum.GOOD;
});
});
});
setReports(result);
}

if (reportData.status === 204) {
setResultStatus(RESULT_STATUS.NO_RECORD);
}
} catch (error) {
setResultStatus(RESULT_STATUS.ERROR);
}
}, [reportDate]);

const isStaleIndicatorRequired = (reportItem: any) => {
return (reportItem["work_issues"] as []).some(
(workIssue) => stalenessLevel(workIssue) === StalenessEnum.CRITICAL
);
};
const stalenessLevel = (workIssue: any) => {
const stalenessThreshold =
REPORT_STALENESS_THRESHOLD[REPORT_TYPE.REPORT_30_60_90];
const diffDays = dateUtils.diff(
reportDate || "",
workIssue["latest_update"]["posted_date"],
"days"
);
if (diffDays > stalenessThreshold[StalenessEnum.CRITICAL])
return StalenessEnum.CRITICAL;
else if (diffDays > stalenessThreshold[StalenessEnum.WARN])
return StalenessEnum.WARN;
else return StalenessEnum.GOOD;
};

const downloadPDFReport = React.useCallback(async () => {
try {
fetchReportData();
Expand Down Expand Up @@ -171,6 +210,21 @@ export default function ThirtySixtyNinety() {
<Tab label="Basic" />
<Tab label="Work Short Description" />
<Tab label="Status" />
<Tab
sx={{
display: "flex",
flexDirection: "row",
gap: "0.5rem",
}}
label={
<>
Issues
{isStaleIndicatorRequired(item) && (
<IndicatorIcon />
)}
</>
}
/>
<Tab label="Decision Information" />
</Tabs>
<TabPanel value={selectedTab} index={0}>
Expand Down Expand Up @@ -211,6 +265,54 @@ export default function ThirtySixtyNinety() {
{item["work_status_text"]}
</TabPanel>
<TabPanel value={selectedTab} index={3}>
<Grid
container
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
gap: "1rem",
}}
>
{(item["work_issues"] as []).map((issue) => (
<Box>
<Grid container>
<Grid item xs={2}>
<Chip
style={{
marginRight: "0.5rem",
borderRadius: "4px",
fontSize: "12px",
width: "100px",
...staleLevel(issue["staleness"]),
}}
label={
<>
<b>
{dateUtils.formatDate(
issue["latest_update"][
"posted_date"
],
DISPLAY_DATE_FORMAT
)}
</b>
</>
}
/>
</Grid>
<Grid item xs={10}>
{issue["title"]}
</Grid>
<Grid item xs={12}>
{issue["latest_update"]["description"]}
</Grid>
</Grid>
<Divider flexItem />
</Box>
))}
</Grid>
</TabPanel>
<TabPanel value={selectedTab} index={4}>
{item["decision_information"]}
</TabPanel>
</AccordionDetails>
Expand Down
Loading

0 comments on commit 7c3a9f0

Please sign in to comment.