import { type Dispatch, type SetStateAction } from 'react' import { type SortByProps } from '../components/SortColumn/SortColumn' import styles from '../components/TableComponents/_table.module.scss' import customStyles from '../components/TableComponents/_new-table.module.scss' export const capitalize = (string: string): string => { if (typeof string !== 'string') return '' return string.charAt(0)?.toUpperCase() + string.slice(1) } export const hasValue = (item: unknown): boolean => { return item !== null && item !== undefined } export const notEmpty = (item: unknown): boolean => { return item !== '' && item !== false && item !== null && item !== undefined } export const trimText = (text = '', characterLength: number): string => { // Protecting against passing in 0 or negative numbers. Without this check, we could technically have a result of just the ... if (characterLength < 1) { return text } return text?.length > characterLength ? `${text.substring(0, characterLength).trim()}...` : text } export const debounce = ( callback: (...args: T) => PromiseLike | U, wait: number, ): VoidFunction => { let timer: ReturnType | undefined return (...args: T): Promise => { if (timer) { clearTimeout(timer) } return new Promise((resolve) => { timer = setTimeout(() => resolve(callback(...args)), wait) }) } } //// Active Cell Class //// export type ActiveCellClassProps = { /** activeName - The key that is active for sorting */ activeName: string /** cells - A single key OR an array of keys that contain the keys for a column cell */ cells: string | string[] /** color - If you want a background color other than light-blue. * To use this, you need to create a style in _table.module.scss * and pass that class name in as the string */ color?: string } export const activeCellClass = ({ cells = '', activeName, color, }: ActiveCellClassProps): string => { const isActive = Array.isArray(cells) ? cells.some((key) => key === activeName) : cells === activeName return isActive ? (color ? styles[color] : styles.lightBlueBackground) : '' } //// End Active Cell Class //// //// Has Sticky Column Style //// export type HasStickyColumnStyleProps = { /** The column index position */ colIndex: number /** Total number of columns */ tableHeadersLength: number /** The number of sticky left columns */ stickyLeftColumn?: number /** The number of sticky right columns */ stickyRightColumn?: number /** Boolean value to determine if we want to show sticky borders (For left and right sticky columns) */ isRowScrollable?: boolean } export const hasStickyColumnStyle = ({ colIndex, stickyLeftColumn = 1, stickyRightColumn, tableHeadersLength, }: HasStickyColumnStyleProps): string => { if (stickyLeftColumn && Number(stickyLeftColumn)) { if (Number(stickyLeftColumn) === colIndex) { return `${styles.lastStickyCellLeft} last-sticky-cell-left` } if (Number(stickyLeftColumn + 1) === colIndex) { return styles.extraLeftPadding } } if (Number(stickyRightColumn) === tableHeadersLength - (colIndex - 1)) { return `${styles.firstStickyCellRight} ${styles.extraLeftPadding}` } return '' } // Used for new DesktopTable. Need to get rid of above function and use this one after the complete replacement. export const getStickyColumnBdrStyle = ({ colIndex, stickyLeftColumn = 1, stickyRightColumn, tableHeadersLength, isRowScrollable, }: HasStickyColumnStyleProps): string => { if (stickyLeftColumn && Number(stickyLeftColumn)) { if (Number(stickyLeftColumn) === colIndex) { return isRowScrollable ? customStyles.lastStickyCellLeft : '' } if (Number(stickyLeftColumn + 1) === colIndex) { return isRowScrollable ? customStyles.extraLeftPadding : '' } } if (Number(stickyRightColumn) === tableHeadersLength - (colIndex - 1)) { return isRowScrollable ? `${customStyles.firstStickyCellRight} ${customStyles.extraLeftPadding}` : '' } return '' } /** * List of columns that have string to be sorted, ex: ['seller_name', 'marketplace']; * Pattern has determined that alpha-numeric values be sorted opposite to convention so that `desc` is A-Z, 0-9 and `asc` is 9-0, Z-A **/ type ColumnsWithStringArray = string[] /** string format: `prop:value` or `prop:value:lowercase` */ type ParamString = string export const standardSortParams = ( sortBy: SortByProps, columnsWithStrings?: ColumnsWithStringArray, ): ParamString => { const lowercase = columnsWithStrings?.includes(sortBy.prop) ? `:lowercase` : '' let sortDirection = sortBy.flip ? 'asc' : 'desc' if (columnsWithStrings?.includes(sortBy.prop)) { sortDirection = sortBy.flip ? 'desc' : 'asc' } return `${sortBy.prop}:${sortDirection}${lowercase}` } export const largeNumConversion = ( num?: string | number, ): { val: number; suffix?: string } => { const absVal = Math.abs(Number(num)) const lessThanMillion = absVal < 1.0e6 if (lessThanMillion) { return { val: Number(num) } } const isBillions = absVal >= 1.0e9 const scale = isBillions ? 1.0e9 : 1.0e6 const suffix = isBillions ? 'B' : 'M' const val = Number(num) / scale return { val, suffix } } // Example output: campaign_types_report => Campaign Types Report export const snakeCaseToTitle = (string: string): string => { return string .split('_') .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) .join(' ') } // Standard time in ms for debounce export const DEBOUNCE_STANDARD_TIME = 250 export const replaceDelimiter = ({ text, joinOption, splitSymbol, keepCasing, }: { text: string joinOption?: string | null splitSymbol?: string | null keepCasing?: boolean }): string => { let nameFix = text.split(splitSymbol || '_') if (!keepCasing) { nameFix = nameFix.map((e) => capitalize(e)) } return nameFix.join(joinOption || ' ') } /** * Aggregates values from an object array with optional divisor and formatting */ export const reduceAndOrAverage = ( arr: T[], prop: keyof T, totalToDivideBy?: number | null, extras?: { round?: boolean floor?: boolean prefix?: string suffix?: string percentage?: boolean thousandSeparator?: boolean toFixed?: number }, ): string => { const { round, floor, prefix, suffix, percentage, thousandSeparator, toFixed, } = extras || {} let empty_values = 0 let total = arr?.reduce((sum: number, el: T) => { // Increment empty counter to exclude null/empty values from average calculation if (el[prop] === null || el[prop] === '') { empty_values++ } // Only add non-empty values; apply percentage multiplier if needed return ( sum + (el[prop] !== '' ? Number(el[prop]) : 0) * (percentage ? 100 : 1) ) }, 0) / (totalToDivideBy ? Math.max(1, totalToDivideBy - empty_values) : 1) // ↑ Using Math.max to prevent division by zero if all values were empty if (round) { total = Math.round(total) } if (floor) { total = Math.floor(total) } // Apply precision before localization to ensure decimal places are preserved if (toFixed !== undefined) { total = Number(total.toFixed(toFixed)) } let result: string if (thousandSeparator) { result = total.toLocaleString() } else { result = String(total) } return `${prefix || ''}${result}${suffix || ''}` } type ErrorCallback = | ((value: T) => void) | Dispatch> | unknown type ErrorMessage = 'appdown' | 'timeout' | 'notfound' /** * Checks HTTP error codes and returns appropriate error messages through a callback * @param errorCode - HTTP status code * @param callback - Function to handle the error message * @param value - Optional custom error message */ export const errorCheck = ( errorCode: number | undefined, callback: ErrorCallback, value = '', ): void => { const getMessage = (defaultMsg: ErrorMessage): T => (value || defaultMsg) as T const safeCallback = (msg: T) => { if (typeof callback === 'function') { callback(msg) } } // If errorCode is undefined, treat it as a default error if (errorCode === undefined) { safeCallback(getMessage('appdown')) return } switch (errorCode) { case 503: safeCallback(getMessage('appdown')) break case 500: case 502: case 504: safeCallback(getMessage('timeout')) break case 404: safeCallback(getMessage('notfound')) break default: safeCallback(getMessage('appdown')) break } } export const aggregateDataValues = ( data: T[], propName: keyof T, ): number | null => { function getSum(total: number, num: number): number { return total + num } if (data?.length > 0) { return data ?.map((r) => (propName ? Number(r[propName]) : Number(r))) ?.reduce(getSum, 0) } else { return null } } /** @deprecated - This function is not helpful and is misleading as it does not have any currency logic associated with it. It would be more helpful to have this function along with the ability to format the value with currency code and symbol. */ export const currencyFormat = ( value: number | undefined, toFixed?: number, ): string => { return Number(value) ?.toFixed(toFixed ?? 2) ?.replace(/\d(?=(?:\d{3})+(?!\d))/g, '$&,') } const propertyToSort = ( property: keyof T, isDescending: boolean, ) => { return function (a: T, b: T) { const aValue = a[property] const bValue = b[property] const valueToCompare = aValue === '—' ? null : aValue let value1: string | number let value2: string | number if (isNaN(Number(valueToCompare)) || isNaN(Number(bValue))) { value1 = valueToCompare?.toString()?.toLowerCase() ?? '' value2 = bValue?.toString()?.toLowerCase() ?? '' } else { value1 = Number(valueToCompare) value2 = Number(bValue) } if (valueToCompare === bValue) { return 0 } else if (valueToCompare === null) { return 1 } else if (bValue === null) { return -1 } else if (isDescending) { return value1 < value2 ? 1 : -1 } else { return value1 < value2 ? -1 : 1 } } } export const sortByProperty = ( data: T[], property: keyof T, isDescending?: boolean, ): T[] => { const filteredState = data.sort( propertyToSort(property, isDescending ?? false), ) return filteredState } const SYMBOLOGY_PREFIX_REGEX = /^(\].{2}|~P.{2}|~.{1})(.*)$/ export type ParsedSymbology = { symbology: string value: string } /** * Strips a scanner symbology prefix from a scanned string and returns the prefix * and the remaining value separately. Returns `null` if no prefix is present. * * Supported prefix formats: * - `]` + 2 chars — AIM bracket identifier (e.g. `]C1`) * - `~P` + 2 chars — tilde-P extended prefix (e.g. `~P12`) * - `~` + 1 char — short tilde prefix (e.g. `~A`, `~K`) */ export function parseSymbology(value: string): ParsedSymbology | null { if (value.length < 2) return null if (value[0] !== ']' && value[0] !== '~') return null if (value[0] === ']' && value.length < 3) return null // ~P requires 2 chars after the P; guard here so a partial ~P doesn't fall // through and match the generic ~x pattern prematurely. if (value[0] === '~' && value[1] === 'P' && value.length < 4) return null const match = value.match(SYMBOLOGY_PREFIX_REGEX) if (!match) return null return { symbology: match[1], value: match[2] } }