ICTU/quality-time

View on GitHub
components/frontend/src/subject/Subject.js

Summary

Maintainability
D
1 day
Test Coverage
import "./Subject.css"

import { bool, func, string } from "prop-types"
import { useContext } from "react"

import { DataModel } from "../context/DataModel"
import {
    datesPropType,
    measurementsPropType,
    metricsPropType,
    optionalDatePropType,
    reportPropType,
    reportsPropType,
    settingsPropType,
    stringsPropType,
} from "../sharedPropTypes"
import {
    getMetricComment,
    getMetricIssueIds,
    getMetricName,
    getMetricResponseOverrun,
    getMetricResponseTimeLeft,
    getMetricStatus,
    getMetricTags,
    getMetricTarget,
    getMetricUnit,
    getMetricValue,
    getSourceName,
    sortWithLocaleCompare,
    visibleMetrics,
} from "../utils"
import { CommentSegment } from "../widgets/CommentSegment"
import { SubjectTable } from "./SubjectTable"
import { SubjectTitle } from "./SubjectTitle"

function sortMetrics(dataModel, metrics, sortDirection, sortColumn, report, measurements) {
    const status_order = {
        unknown: "0",
        target_not_met: "1",
        near_target_met: "2",
        debt_target_met: "3",
        target_met: "4",
        informative: "5",
    }
    const sorters = {
        name: (m1, m2) => {
            const m1_name = getMetricName(m1[1], dataModel)
            const m2_name = getMetricName(m2[1], dataModel)
            return m1_name.localeCompare(m2_name)
        },
        measurement: (m1, m2) => {
            const m1_measurement = getMetricValue(m1[1], dataModel)
            const m2_measurement = getMetricValue(m2[1], dataModel)
            return m1_measurement.localeCompare(m2_measurement, undefined, { numeric: true })
        },
        target: (m1, m2) => {
            const m1_target = getMetricTarget(m1[1])
            const m2_target = getMetricTarget(m2[1])
            return m1_target.localeCompare(m2_target, undefined, { numeric: true })
        },
        comment: (m1, m2) => {
            const m1_comment = getMetricComment(m1[1])
            const m2_comment = getMetricComment(m2[1])
            return m1_comment.localeCompare(m2_comment)
        },
        status: (m1, m2) => {
            const m1_status = status_order[getMetricStatus(m1[1])]
            const m2_status = status_order[getMetricStatus(m2[1])]
            return m1_status.localeCompare(m2_status)
        },
        source: (m1, m2) => {
            let m1SourceNames = Object.values(m1[1].sources).map((source) => getSourceName(source, dataModel))
            sortWithLocaleCompare(m1SourceNames)
            let m2SourceNames = Object.values(m2[1].sources).map((source) => getSourceName(source, dataModel))
            sortWithLocaleCompare(m2SourceNames)
            return m1SourceNames.join().localeCompare(m2SourceNames.join())
        },
        issues: (m1, m2) => {
            const m1_issues = getMetricIssueIds(m1[1]).join()
            const m2_issues = getMetricIssueIds(m2[1]).join()
            return m1_issues.localeCompare(m2_issues)
        },
        tags: (m1, m2) => {
            const m1_tags = getMetricTags(m1[1]).join()
            const m2_tags = getMetricTags(m2[1]).join()
            return m1_tags.localeCompare(m2_tags)
        },
        unit: (m1, m2) => {
            const m1_unit = getMetricUnit(m1[1], dataModel)
            const m2_unit = getMetricUnit(m2[1], dataModel)
            return m1_unit.localeCompare(m2_unit)
        },
        time_left: (m1, m2) => {
            const m1_time_left = getMetricResponseTimeLeft(m1[1], report) ?? 0
            const m2_time_left = getMetricResponseTimeLeft(m2[1], report) ?? 0
            return m1_time_left - m2_time_left
        },
        overrun: (m1, m2) => {
            const m1_overrun = getMetricResponseOverrun(m1[0], m1[1], report, measurements, dataModel)
            const m2_overrun = getMetricResponseOverrun(m2[0], m2[1], report, measurements, dataModel)
            return m1_overrun.totalOverrun - m2_overrun.totalOverrun
        },
    }
    metrics.sort(sorters[sortColumn])
    if (sortDirection === "descending") {
        metrics.reverse()
    }
}

function subjectIsEmptyDueToFilters(atReportsOverview, filteredMetrics, metrics, settings) {
    // Hide the subject if it has no metrics after filtering and at least one of the following conditions holds:
    // - the user is at the reports overview page
    // - the user is hiding all metrics or metrics that don't require action
    // - the user is hiding metrics by tag
    // - the subject has metrics before filtering
    // This ensures (in combination with resetting the metricsToHide and hiddenTags settings after adding a subject)
    // that newly added subjects are not immediately hidden
    return (
        Object.keys(filteredMetrics).length === 0 &&
        (atReportsOverview ||
            !settings.metricsToHide.isDefault() ||
            !settings.hiddenTags.isDefault() ||
            Object.keys(metrics).length !== 0)
    )
}
subjectIsEmptyDueToFilters.propTypes = {
    atReportsOverview: bool,
    filteredMetrics: metricsPropType,
    metrics: metricsPropType,
    settings: settingsPropType,
}

export function Subject({
    atReportsOverview,
    changed_fields,
    dates,
    firstSubject,
    handleSort,
    lastSubject,
    measurements,
    report,
    report_date,
    reports,
    settings,
    subject_uuid,
    reload,
}) {
    const subject = report.subjects[subject_uuid]
    const metrics = visibleMetrics(subject.metrics, settings.metricsToHide.value, settings.hiddenTags.value)
    const dataModel = useContext(DataModel)
    if (subjectIsEmptyDueToFilters(atReportsOverview, metrics, subject.metrics, settings)) {
        return null
    }
    let metricEntries = Object.entries(metrics)
    if (settings.sortColumn.value !== "") {
        sortMetrics(
            dataModel,
            metricEntries,
            settings.sortDirection.value,
            settings.sortColumn.value,
            report,
            measurements,
        )
    }

    return (
        <div id={subject_uuid}>
            <div className="sticky">
                <SubjectTitle
                    atReportsOverview={atReportsOverview}
                    firstSubject={firstSubject}
                    lastSubject={lastSubject}
                    reload={reload}
                    report={report}
                    settings={settings}
                    subject={subject}
                    subject_uuid={subject_uuid}
                />
            </div>
            <CommentSegment comment={subject.comment} />
            <SubjectTable
                changed_fields={changed_fields}
                dates={dates}
                handleSort={handleSort}
                measurements={measurements}
                metricEntries={metricEntries}
                reload={reload}
                report={report}
                reportDate={report_date}
                reports={reports}
                settings={settings}
                subject={subject}
                subject_uuid={subject_uuid}
            />
        </div>
    )
}
Subject.propTypes = {
    atReportsOverview: bool,
    changed_fields: stringsPropType,
    dates: datesPropType,
    firstSubject: bool,
    handleSort: func,
    lastSubject: bool,
    measurements: measurementsPropType,
    report: reportPropType,
    report_date: optionalDatePropType,
    reports: reportsPropType,
    settings: settingsPropType,
    subject_uuid: string,
    reload: func,
}