import moment from 'moment' import React from 'react' import { NumericFormat } from 'react-number-format' import Badge from '../Badge/Badge' import { type BadgeProps } from '../Badge/Badge' import Button from '../Button/Button' import Checkbox from '../Form/Checkbox' import { hasValue, largeNumConversion } from '../../services/HelperServiceTyped' import Icon from '../Icons/Icon' import { type IconStringList } from '../Icons/Icon.models' import Mdash from '../Mdash/Mdash' import MdashCheck from '../Mdash/MdashCheck' import PercentageCheck from '../PercentageCheck/PercentageCheck' import Tooltip from '../Tooltip/Tooltip' import { type TooltipProps } from '../Tooltip/Tooltip' import { useMediaQuery } from '../../hooks/responsiveHooks' import { getComparisonDates, isCurrentDateUsedForComparison, } from '../../services/DateHelpers' import styles from './header_metric_group.module.scss' import { type chartColors } from './HeaderMetricGroup' import { type CompareWith, type DateType, determineChangeValue, formatDates, getPartialPeriodDates, getPeriodDisplays, getPreviousPeriodRange, type TimeframeType, } from './HeaderMetricHelpers' import { Skeleton } from '../Skeleton/Skeleton' import { c } from '../../translations/LibraryTranslationService' type HeaderMetricBase = { /** The key override for the metric - if it needs to be different than `title` */ key?: string /** Text to display in metric */ title: string /** Tooltip content to display next to title in metric */ tooltip?: TooltipProps['tooltipContent'] // TODO: We need to deprecate these font size values in favor of tailwind values. /** The font size that will change the main data of the metric. */ fontSize?: 'fs-16' | 'fs-18' | 'fs-22' /** Boolean to determine loading state */ loading: boolean /** Optional className can be added */ className?: string /** Optionally move the change metrics below the main metric. */ showChangeBelowMainMetric?: boolean /** Option to not multiple pctChange value by 100 (default: false) */ noPctConversion?: boolean /** Optional boolean to display beta tag in metric */ beta?: boolean /** Optional props to pass to the Badge component. If provided, this will be used instead of the default beta badge */ badgeProps?: BadgeProps /** Optional prop to add a test id to the HeaderMetric for QA testing */ qaTestId?: string } type HeaderMetricWithLink = HeaderMetricBase & { /** Link to display in metric, internal only */ linkTo?: string /** Router component (e.g. Using Link from react-router-dom) */ routerComponent?: React.ElementType /** Allow any props to be passed in */ routerProp?: string actionButton?: never } type HeaderMetricWithActionButton = HeaderMetricBase & { /** Link to display in metric, internal only */ linkTo?: never /** Action button to display in metric */ actionButton?: React.JSX.Element routerComponent?: never routerProp?: never } type HeaderMetricBaseProps = HeaderMetricWithActionButton | HeaderMetricWithLink type HeaderMetricValueType = HeaderMetricBaseProps & { /** Value to display in metric */ value?: number /** Change value to display in metric */ change?: number /** Percentage change to display in metric */ pctChange?: number /** Currency to display in metric */ currency?: CurrencyProps /** Value format to display in metric */ formatType?: 'number' | 'percentage' // TODO: add currency type and refactor types /** Metric icon to display in metric */ metricIcon?: IconStringList /** Decimal scale applied to value in metric */ decimalScale?: number /** Decimal scale applied to change value in metric */ changeValueDecimalScale?: number /** When we have a percentage value we should use this if we want to round that percentage value */ roundNumber?: boolean /** Reverse the change value display */ reverse?: boolean /** Allow the radio button to be displayed */ showRadio?: boolean /** When showRadio is true, use this prop to handle the function that needs to be called when the radio is clicked */ callout?: ({ ...param }: CalloutProps) => void /** This will set the metric number and checkbox color from list of chartColors */ checkboxColor?: chartColors /** When true, this will set the change value, and change icon to our purple color */ isNeutralColor?: boolean /** Checked state override of the metric radio button by default */ isChecked?: boolean /** Optional className can be added to the metric value */ metricValueClassName?: string /** Display values in a truncated format. i.e. 1.4M instead of 1,400,000 */ truncateValues?: boolean /** Displays the full percentage change value if set to true. Otherwise, it will display `<1%` if set to false */ showLessThanZeroPercentageChange?: boolean /** There are some instances where a custom element needs to replace the percentage values. This is where that should live. */ customSecondaryValue?: React.ReactNode secondaryInfo?: never /** Information needed to display comparison tooltip */ comparisonTooltip?: { /** Optional title to display for the primary metric value on comparison tooltip */ primaryMetricTitle?: string /** The value to display from the comparison period */ comparison: number /** The value to display for the whole comparison period */ wholePreviousPeriodComparison?: number /** The selected date range from the current period */ currentPeriodDates: DateType /** The timeframe object needed for the display text and comparison dates */ timeframe: TimeframeType /** Optional param that will be used for showing compareWith global timeframe filters */ compareWith?: CompareWith /** Optional param to remove current month for comparison if the current month has no data and the timeframe is quarterly/yearly */ hasNoCurrentMonthData?: boolean /** Optional props to show partial period dates */ showPartialPeriodDates?: boolean /** Optional param to show footer content in the tooltip */ footerContent?: React.ReactNode } /** Change value format to display in metric */ changeValueFormatType?: 'number' | 'percentage' } type HeaderMetricTextType = HeaderMetricBaseProps & { /** Any value apart from change metrics. Eg: string, text */ secondaryInfo: string value?: never change?: never pctChange?: never currency?: never formatType?: never metricIcon?: never decimalScale?: never changeValueDecimalScale?: never roundNumber?: never reverse?: never showRadio?: never callout?: never checkboxColor?: never isNeutralColor?: never isChecked?: never metricValueClassName?: never truncateValues?: never showLessThanZeroPercentageChange?: never customSecondaryValue?: never comparisonTooltip?: never changeValueFormatType?: 'number' | 'percentage' } export type HeaderMetricProps = HeaderMetricValueType | HeaderMetricTextType export type CalloutProps = { /** When showRadio is true, use this prop to the function that needs to be called when the radio is clicked */ name: string checked: boolean } export type CurrencyProps = { /** Currency code to display after metric (ex: 100฿) */ currencyCode: string /** Currency symbol to display before metric (ex: $100) */ currencySymbol: string } const HeaderMetric = ({ actionButton, beta, badgeProps, callout, change, checkboxColor = 'purple', className = '', currency, customSecondaryValue, decimalScale = 0, changeValueDecimalScale, fontSize, formatType = 'number', isChecked = false, isNeutralColor = false, linkTo, loading, metricIcon, metricValueClassName = '', noPctConversion = false, pctChange = 0, reverse = false, roundNumber = false, secondaryInfo, showLessThanZeroPercentageChange = true, showRadio = false, title, tooltip, truncateValues, value, showChangeBelowMainMetric = false, comparisonTooltip, routerComponent, routerProp = 'to', changeValueFormatType, qaTestId = 'header-metric', }: HeaderMetricProps): React.JSX.Element => { const screenIsMdMax = useMediaQuery({ type: 'max', breakpoint: 'md' }) const screenIsXlMax = useMediaQuery({ type: 'max', breakpoint: 'xl' }) const changeValueFormatTypeDefault = changeValueFormatType ?? formatType ?? 'number' const RouterComponent = routerComponent const { primaryMetricTitle, comparison, currentPeriodDates, timeframe, compareWith, wholePreviousPeriodComparison, hasNoCurrentMonthData, showPartialPeriodDates, footerContent, } = comparisonTooltip || {} const isLargeNum = (value?: number) => value && value >= 1000 // This condition is to check if we are using compareWith filter and quarter/year is of current period and if so, use current date for endDate if ( isCurrentDateUsedForComparison(timeframe, compareWith) && currentPeriodDates?.endDate && !hasNoCurrentMonthData ) { currentPeriodDates.endDate = moment().format() } const currencySuffix = currency?.currencyCode && currency?.currencyCode !== 'USD' ? ` ${currency?.currencyCode}` : undefined const metricValue = truncateValues ? largeNumConversion(value).val : value const metricChangeValue = truncateValues ? largeNumConversion(change).val : change const valueSuffix = truncateValues ? largeNumConversion(value).suffix : null const changeSuffix = truncateValues ? largeNumConversion(change).suffix : null const metricValueSuffix = `${valueSuffix ? `${valueSuffix} ` : ''}${ currencySuffix ? currencySuffix : '' }` const metricChangeSuffix = `${changeSuffix ? `${changeSuffix} ` : ''}${ currencySuffix ? currencySuffix : '' }` const largeNumDecimalScale = truncateValues && value && value > 1.0e6 ? 2 : decimalScale const largeNumChangeDecimalScale = (truncateValues && change && change > 1.0e6) || changeValueFormatTypeDefault === 'percentage' ? 2 : currency?.currencyCode ? isLargeNum(change) ? 0 : 2 : (changeValueDecimalScale ?? decimalScale) const showChangeValue = (change: number) => { const value = metricChangeValue && changeValueFormatTypeDefault === 'percentage' ? metricChangeValue * 100 : metricChangeValue const { textClassName: textClassNameChange, iconColor: iconColorChange, icon: iconChange, } = determineChangeValue(change, reverse, isNeutralColor) return (
) } const tooltipContent = () => { if (!comparisonTooltip || !change || !currentPeriodDates || !timeframe) return null const comparisonPeriodDates = getComparisonDates({ startDate: currentPeriodDates.startDate, endDate: currentPeriodDates.endDate, timeframe, compareWith: compareWith ?? undefined, hasNoCurrentMonthData: hasNoCurrentMonthData, }) const currentPeriod = getPeriodDisplays({ timeframe: timeframe, column: 1, }) const comparisonPeriod = getPeriodDisplays({ timeframe: timeframe, column: 2, compareWith: compareWith, }) const { textClassName: newChangeTextClassName, iconColor: changeIconColor, icon: newChangeIcon, } = determineChangeValue(change, reverse, isNeutralColor) const { textClassName: newPctChangeTextClassName, iconColor: newPctIconColor, icon: newPctChangeIcon, } = determineChangeValue(pctChange, reverse, isNeutralColor) let currentPeriodDatesText = '' let comparisonPeriodDatesText = '' if ( (timeframe.type === 'historical' || timeframe.type === 'trailing') && timeframe.value === 'custom' && timeframe.compareDisplay ) { // Updates partial period dates to show the custom date range currentPeriodDatesText = timeframe.display comparisonPeriodDatesText = timeframe.compareDisplay } else { // Updates partial period dates to show the current and comparison period dates const partialPeriodDates = getPartialPeriodDates( currentPeriodDates, comparisonPeriodDates, ) currentPeriodDatesText = partialPeriodDates.currentDate comparisonPeriodDatesText = partialPeriodDates.comparisonDate } const dateText = () => { if ( (timeframe.type === 'historical' || timeframe.type === 'trailing') && timeframe.value === 'custom' && timeframe.compareDisplay ) { return c('timeframeVsCompareTimeframe', { timeframeDisplay: timeframe.display, compareTimeframeDisplay: timeframe.compareDisplay, }) } else { return formatDates(currentPeriodDates, comparisonPeriodDates) } } const prevPeriodDisplayText = getPreviousPeriodRange( timeframe, currentPeriodDates.startDate, currentPeriodDates.endDate, compareWith, ) return (
{currentPeriod} vs {comparisonPeriod}
{showPartialPeriodDates ? ( {`* ${c('partialPeriod')}`} ) : ( dateText() )}
{primaryMetricTitle ?? title} {showPartialPeriodDates ? (
{currentPeriodDatesText}
) : null}
{formatType === 'percentage' ? ( ) : ( )}
{c('comparisonPeriod')} {showPartialPeriodDates ? (
{comparisonPeriodDatesText}
) : null}
{formatType === 'percentage' ? ( ) : ( )}
{c('changeValueVsComparison')}
{c('change%VsComparison')}
{newPctChangeIcon !== 'trendEven' && ( )}
{prevPeriodDisplayText && wholePreviousPeriodComparison && showPartialPeriodDates ? (
{comparisonPeriod}
{prevPeriodDisplayText}
{formatType === 'percentage' ? ( ) : ( )}
) : null} {footerContent ? (
{footerContent}
) : null}
) } const metricDisplay = () => { return secondaryInfo ? ( {secondaryInfo} ) : (
{metricIcon && }
{formatType === 'percentage' ? ( ) : ( )}
{!showChangeBelowMainMetric ? : null}
{showChangeBelowMainMetric ? : null}
) } const ChangeDisplay = () => { if (change === undefined) return <> return (
{!loading && (change !== undefined && !!hasValue(pctChange) ? ( customSecondaryValue ? ( customSecondaryValue ) : ( showChangeValue(change) ) ) : (
))}
) } /** Radio button handler */ const inputHandler = () => { callout?.({ name: title, checked: !isChecked }) } const mainAction = () => { return (
) } const routerComponentProps = routerProp && linkTo ? { [routerProp]: linkTo, } : {} return (
{loading ? ( ) : (
{showRadio && (
)} {mainAction()}
{/* actionButton and linkTo are mutually exclusive */} {actionButton && !linkTo && actionButton} {linkTo && !actionButton && routerProp && RouterComponent && ( )}
)}
) } export default HeaderMetric