Skip to content

Commit

Permalink
feat: add risk cards to project simulation
Browse files Browse the repository at this point in the history
  • Loading branch information
lhguerra committed Dec 17, 2024
1 parent 67408ce commit af2b015
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 101 deletions.
3 changes: 3 additions & 0 deletions app/graphql/types/project_simulation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

module Types
class ProjectSimulationType < Types::BaseObject
field :operational_risk, Float, null: true
field :team_operational_risk, Float, null: true

field :current_monte_carlo_weeks_max, Float, null: true
field :current_monte_carlo_weeks_min, Float, null: true
field :current_monte_carlo_weeks_std_dev, Float, null: true
Expand Down
20 changes: 17 additions & 3 deletions app/graphql/types/project_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ProjectType < Types::BaseObject
field :project_consolidations_weekly, [Types::ProjectConsolidationType], null: true
field :project_members, [Types::ProjectMemberType], null: true
field :project_simulation, Types::ProjectSimulationType, null: true do
argument :end_date, GraphQL::Types::ISO8601Date, required: true
argument :remaining_work, Int, required: true
argument :throughputs, [Int], required: true
end
Expand Down Expand Up @@ -110,11 +111,16 @@ def unscored_demands
object.demands.kept.unscored_demands
end

def project_simulation(remaining_work:, throughputs:)
def project_simulation(remaining_work:, throughputs:, end_date:)
project_based_montecarlo_durations = Stats::StatisticsService.instance.run_montecarlo(remaining_work, throughputs, 500)
team_based_montecarlo_durations = compute_team_monte_carlo_weeks(remaining_work, throughputs)

operational_risk, team_operational_risk = operational_risk_info(end_date, project_based_montecarlo_durations, team_based_montecarlo_durations)

{
operational_risk: operational_risk,
team_operational_risk: team_operational_risk,

monte_carlo_p80: Stats::StatisticsService.instance.percentile(80, project_based_montecarlo_durations),
current_monte_carlo_weeks_max: project_based_montecarlo_durations.max,
current_monte_carlo_weeks_min: project_based_montecarlo_durations.min,
Expand Down Expand Up @@ -304,11 +310,19 @@ def lead_time_breakdown

private

def operational_risk_info(end_date, project_based_montecarlo_durations, team_based_montecarlo_durations)
remaining_weeks = ((end_date - Time.zone.today) / 7).ceil
operational_risk = 1 - Stats::StatisticsService.instance.compute_odds_to_deadline(remaining_weeks, project_based_montecarlo_durations)
team_operational_risk = 1 - Stats::StatisticsService.instance.compute_odds_to_deadline(remaining_weeks, team_based_montecarlo_durations)

[operational_risk, team_operational_risk]
end

def compute_team_monte_carlo_weeks(remaining_work, throughputs)
team = object.team

project_wip = object.max_work_in_progress || 1
team_wip = team.max_work_in_progress || 1
project_wip = [object.max_work_in_progress, 1].max
team_wip = [team.max_work_in_progress, 1].max || 1
project_share_in_team_flow = project_wip.to_f / team_wip

project_share_team_throughput_data = throughputs.map { |throughput| throughput * project_share_in_team_flow }
Expand Down
114 changes: 40 additions & 74 deletions app/spa/src/components/Projects/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,12 @@ import { Box } from "@mui/material"
import { ReactNode, useContext } from "react"
import { useTranslation } from "react-i18next"
import { useLocation, useParams } from "react-router-dom"
import Card, { CardType } from "../Card"
import { MessagesContext } from "../../contexts/MessageContext"
import useProjectInfo from "../../hooks/useProjectInfo"
import ActionMenu from "../menu/ActionMenu"
import BasicPage from "../BasicPage"
import { Tabs } from "../Tabs"

export const PROJECT_STANDARD_FRAGMENT = gql`
fragment ProjectStandardFragment on Project {
id
name
company {
id
name
slug
}
}
`

type ProjectPageProps = {
pageName: string
children: ReactNode
loading?: boolean
}

const cardTypeByRisk = (risk: number) => {
if (risk > 0.5 && risk <= 0.7) {
return CardType.WARNING
} else if (risk > 0.7) {
return CardType.ERROR
}

return CardType.SUCCESS
}

const GENERATE_PROJECT_CACHE_MUTATION = gql`
mutation GenerateProjectCache($projectId: ID!) {
generateProjectCache(projectId: $projectId) {
statusMessage
}
}
`

type ProjectCacheDTO = {
generateProjectCache?: {
statusMessage: string
}
}
import ProjectRiskCards from "../../modules/project/components/ProjectRiskCards"

export const ProjectPage = ({
pageName,
Expand Down Expand Up @@ -123,16 +81,6 @@ export const ProjectPage = ({
},
]

const currentOperationalRisk = projectInfo?.currentRiskToDeadline || 0
const currentRiskToDeadlinePercentage = (
currentOperationalRisk * 100
).toFixed(2)
const remainingDays = projectInfo?.remainingDays
const currentTeamRisk = projectInfo?.currentTeamBasedRisk || 0
const currentTeamRiskPercentage = (currentTeamRisk * 100).toFixed(2)
const cardTypeTeamRisk = cardTypeByRisk(currentTeamRisk)
const cardTypeOperationalRisk = cardTypeByRisk(currentOperationalRisk)

const actions = [
{
name: t("settings_actions.update_cache"),
Expand Down Expand Up @@ -163,27 +111,12 @@ export const ProjectPage = ({
loading={loading || queryLoading}
>
<>
{projectIsRunning && (
<Box sx={{ display: "flex", my: 2 }}>
<Card
style={{ width: "350px", marginRight: "20px" }}
title={t("cards.operational_risk")}
subtitle={t("cards.operational_risk_message", {
days: remainingDays,
percentage: currentRiskToDeadlinePercentage,
})}
type={cardTypeOperationalRisk}
/>

<Card
style={{ width: "350px" }}
title={t("cards.operational_risk_team_data")}
subtitle={t("cards.operational_risk_team_data_message", {
risk: currentTeamRiskPercentage,
})}
type={cardTypeTeamRisk}
/>
</Box>
{projectIsRunning && projectInfo && (
<ProjectRiskCards
remainingDays={projectInfo.remainingDays || 0}
currentOperationalRisk={projectInfo.currentRiskToDeadline || 0}
currentTeamRisk={projectInfo.currentTeamBasedRisk || 0}
/>
)}
<Box
sx={{
Expand All @@ -199,3 +132,36 @@ export const ProjectPage = ({
</BasicPage>
)
}

export const PROJECT_STANDARD_FRAGMENT = gql`
fragment ProjectStandardFragment on Project {
id
name
endDate
company {
id
name
slug
}
}
`

type ProjectPageProps = {
pageName: string
children: ReactNode
loading?: boolean
}

const GENERATE_PROJECT_CACHE_MUTATION = gql`
mutation GenerateProjectCache($projectId: ID!) {
generateProjectCache(projectId: $projectId) {
statusMessage
}
}
`

type ProjectCacheDTO = {
generateProjectCache?: {
statusMessage: string
}
}
20 changes: 4 additions & 16 deletions app/spa/src/hooks/useProjectInfo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { gql, useQuery } from "@apollo/client"
import { Project } from "../modules/project/project.types"

export const PROJECT_QUERY = gql`
query ProjectInfo($projectId: ID!) {
Expand All @@ -9,6 +10,7 @@ export const PROJECT_QUERY = gql`
remainingDays
currentTeamBasedRisk
running
endDate
company {
id
name
Expand All @@ -18,24 +20,10 @@ export const PROJECT_QUERY = gql`
}
`

type ProjectInfo = {
project: {
id: number
name: string
currentRiskToDeadline: number
remainingDays: number
currentTeamBasedRisk: number
running: boolean
company: {
id: string
name: string
slug: string
}
}
type ProjectInfoDTO = {
project?: Project
}

type ProjectInfoDTO = ProjectInfo | undefined

const useProjectInfo = (projectId: string) => {
const { data, loading, error } = useQuery<ProjectInfoDTO>(PROJECT_QUERY, {
variables: { projectId },
Expand Down
59 changes: 59 additions & 0 deletions app/spa/src/modules/project/components/ProjectRiskCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Card, { CardType } from "../../../components/Card"
import { Box } from "@mui/material"
import { useTranslation } from "react-i18next"

const ProjectRiskCards = ({
remainingDays,
currentOperationalRisk,
currentTeamRisk,
}: ProjectRiskCardsProps) => {
const { t } = useTranslation(["generalProjectPage"])

const currentRiskToDeadlinePercentage = (
currentOperationalRisk * 100
).toFixed(2)
const currentTeamRiskPercentage = (currentTeamRisk * 100).toFixed(2)
const cardTypeTeamRisk = cardTypeByRisk(currentTeamRisk)
const cardTypeOperationalRisk = cardTypeByRisk(currentOperationalRisk)

return (
<Box sx={{ display: "flex", my: 2 }}>
<Card
style={{ width: "350px", marginRight: "20px" }}
title={t("cards.operational_risk")}
subtitle={t("cards.operational_risk_message", {
days: remainingDays,
percentage: currentRiskToDeadlinePercentage,
})}
type={cardTypeOperationalRisk}
/>

<Card
style={{ width: "350px" }}
title={t("cards.operational_risk_team_data")}
subtitle={t("cards.operational_risk_team_data_message", {
risk: currentTeamRiskPercentage,
})}
type={cardTypeTeamRisk}
/>
</Box>
)
}

type ProjectRiskCardsProps = {
remainingDays: number
currentOperationalRisk: number
currentTeamRisk: number
}

const cardTypeByRisk = (risk: number) => {
if (risk > 0.5 && risk <= 0.7) {
return CardType.WARNING
} else if (risk > 0.7) {
return CardType.ERROR
}

return CardType.SUCCESS
}

export default ProjectRiskCards
35 changes: 28 additions & 7 deletions app/spa/src/modules/project/components/ProjectRiskSimulation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,31 @@ import { FieldValues, useForm } from "react-hook-form"
import { gql, useLazyQuery } from "@apollo/client"
import ProjectMonteCarloData from "./ProjectMonteCarloData"
import ProjectMonteCarloTeamData from "./ProjectMonteCarloTeamData"
import ProjectRiskCards from "./ProjectRiskCards"
import { differenceInDays, parseISO } from "date-fns"

const ProjectRiskSimulation = ({ project }: ProjectRiskSimulationProps) => {
const { register, handleSubmit } = useForm()

const [simulateProjectRisk, { data }] = useLazyQuery<ProjectSimulationDTO>(
PROJECT_SIMULATION_QUERY
)
const [simulateProjectRisk, { data, variables }] =
useLazyQuery<ProjectSimulationDTO>(PROJECT_SIMULATION_QUERY)

const onSubmit = (values: FieldValues) => {
simulateProjectRisk({
variables: {
projectId: project.id,
remainingWork: Number(values.remainingWork || 0),
throughputs: values.throughputs.split(",").map(Number),
endDate: values.endDate,
},
})
}

const projectSimulation = data?.project?.projectSimulation
const remainingDays = differenceInDays(
parseISO(variables?.endDate),
new Date()
)

return (
<Box marginY={4}>
Expand Down Expand Up @@ -77,12 +83,22 @@ const ProjectRiskSimulation = ({ project }: ProjectRiskSimulationProps) => {

{projectSimulation && project && (
<>
<ProjectRiskCards
remainingDays={remainingDays}
currentOperationalRisk={projectSimulation.operationalRisk || 0}
currentTeamRisk={projectSimulation.teamOperationalRisk || 0}
/>

<ProjectMonteCarloData
currentMonteCarloWeeksMin={project.currentMonteCarloWeeksMin || 0}
currentMonteCarloWeeksMax={project.currentMonteCarloWeeksMax || 0}
monteCarloP80={project.monteCarloP80 || 0}
currentMonteCarloWeeksMin={
projectSimulation.currentMonteCarloWeeksMin || 0
}
currentMonteCarloWeeksMax={
projectSimulation.currentMonteCarloWeeksMax || 0
}
monteCarloP80={projectSimulation.monteCarloP80 || 0}
currentMonteCarloWeeksStdDev={
project.currentMonteCarloWeeksStdDev || 0
projectSimulation.currentMonteCarloWeeksStdDev || 0
}
/>

Expand Down Expand Up @@ -113,13 +129,18 @@ const PROJECT_SIMULATION_QUERY = gql`
$projectId: ID!
$remainingWork: Int!
$throughputs: [Int!]!
$endDate: ISO8601Date!
) {
project(id: $projectId) {
id
projectSimulation(
remainingWork: $remainingWork
throughputs: $throughputs
endDate: $endDate
) {
operationalRisk
teamOperationalRisk
currentMonteCarloWeeksMin
currentMonteCarloWeeksMax
monteCarloP80
Expand Down
7 changes: 6 additions & 1 deletion app/spa/src/modules/project/project.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ export type Project = {
}

type ProjectSimulation = {
id: string
operationalRisk?: number
teamOperationalRisk?: number
currentMonteCarloWeeksMin?: number
currentMonteCarloWeeksMax?: number
monteCarloP80?: number
currentMonteCarloWeeksStdDev?: number
teamMonteCarloWeeksMin?: number
teamMonteCarloWeeksMax?: number
teamMonteCarloP80?: number
Expand Down
Loading

0 comments on commit af2b015

Please sign in to comment.