From 2d6a8a4bcca442c1d68fedceb51351382febb174 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sun, 5 Nov 2023 23:25:13 +0100 Subject: [PATCH] [PUI] Add more formatters (#5771) * Added general data formatters * Added usage of new date formatter * Added usage of new date formatter * Added usage of currency formatter * style cleanup * Moved to use real user and server settings * fixed type * Added in formatters again * cleaned up unsued imports --- .../src/components/tables/ColumnRenderers.tsx | 18 ++-- .../tables/build/BuildOrderTable.tsx | 4 +- .../tables/stock/StockItemTable.tsx | 30 ++++++- src/frontend/src/defaults/formatters.tsx | 86 +++++++++++++++++++ src/frontend/src/states/SettingsState.tsx | 26 +++++- src/frontend/src/states/states.tsx | 3 + 6 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 src/frontend/src/defaults/formatters.tsx diff --git a/src/frontend/src/components/tables/ColumnRenderers.tsx b/src/frontend/src/components/tables/ColumnRenderers.tsx index bad1d3621b..c8246434fa 100644 --- a/src/frontend/src/components/tables/ColumnRenderers.tsx +++ b/src/frontend/src/components/tables/ColumnRenderers.tsx @@ -3,6 +3,7 @@ */ import { t } from '@lingui/macro'; +import { formatCurrency, renderDate } from '../../defaults/formatters'; import { ProgressBar } from '../items/ProgressBar'; import { ModelType } from '../render/ModelType'; import { RenderOwner } from '../render/User'; @@ -77,8 +78,9 @@ export function TargetDateColumn(): TableColumn { return { accessor: 'target_date', title: t`Target Date`, - sortable: true + sortable: true, // TODO: custom renderer which alerts user if target date is overdue + render: (record: any) => renderDate(record.target_date) }; } @@ -86,7 +88,8 @@ export function CreationDateColumn(): TableColumn { return { accessor: 'creation_date', title: t`Creation Date`, - sortable: true + sortable: true, + render: (record: any) => renderDate(record.creation_date) }; } @@ -94,7 +97,8 @@ export function ShipmentDateColumn(): TableColumn { return { accessor: 'shipment_date', title: t`Shipment Date`, - sortable: true + sortable: true, + render: (record: any) => renderDate(record.shipment_date) }; } @@ -116,12 +120,10 @@ export function CurrencyColumn({ title: title ?? t`Currency`, sortable: sortable ?? true, render: (record: any) => { - let value = record[accessor]; let currency_key = currency_accessor ?? `${accessor}_currency`; - currency = currency ?? record[currency_key]; - - // TODO: A better render which correctly formats money values - return `${value} ${currency}`; + return formatCurrency(record[accessor], { + currency: currency ?? record[currency_key] + }); } }; } diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index 950961e81d..a1ff687cc7 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -2,6 +2,7 @@ import { t } from '@lingui/macro'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { renderDate } from '../../../defaults/formatters'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ThumbnailHoverCard } from '../../images/Thumbnail'; @@ -78,7 +79,8 @@ function buildOrderTableColumns(): TableColumn[] { { accessor: 'completion_date', sortable: true, - title: t`Completed` + title: t`Completed`, + render: (record: any) => renderDate(record.completion_date) }, { accessor: 'issued_by', diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index 1e15bf3b10..f2dbd5d0d8 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -3,6 +3,7 @@ import { Group, Text } from '@mantine/core'; import { ReactNode, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { formatCurrency, renderDate } from '../../../defaults/formatters'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; @@ -167,13 +168,34 @@ function stockItemTableColumns(): TableColumn[] { // TODO: Note, if not "In stock" we don't want to display the actual location here return record?.location_detail?.pathstring ?? record.location ?? '-'; } - } + }, // TODO: stocktake column - // TODO: expiry date - // TODO: last updated + { + accessor: 'expiry_date', + sortable: true, + title: t`Expiry Date`, + switchable: true, + render: (record: any) => renderDate(record.expiry_date) + }, + { + accessor: 'updated', + sortable: true, + title: t`Last Updated`, + switchable: true, + render: (record: any) => renderDate(record.updated) + }, // TODO: purchase order // TODO: Supplier part - // TODO: purchase price + { + accessor: 'purchase_price', + sortable: true, + title: t`Purchase Price`, + switchable: true, + render: (record: any) => + formatCurrency(record.purchase_price, { + currency: record.purchase_price_currency + }) + } // TODO: stock value // TODO: packaging // TODO: notes diff --git a/src/frontend/src/defaults/formatters.tsx b/src/frontend/src/defaults/formatters.tsx new file mode 100644 index 0000000000..f5992b793a --- /dev/null +++ b/src/frontend/src/defaults/formatters.tsx @@ -0,0 +1,86 @@ +import dayjs from 'dayjs'; + +import { + useGlobalSettingsState, + useUserSettingsState +} from '../states/SettingsState'; + +interface formatCurrencyOptionsType { + digits?: number; + minDigits?: number; + currency?: string; + locale?: string; +} + +/* + * format currency (money) value based on current settings + * + * Options: + * - currency: Currency code (uses default value if none provided) + * - locale: Locale specified (uses default value if none provided) + * - digits: Maximum number of significant digits (default = 10) + */ +export function formatCurrency( + value: number, + options: formatCurrencyOptionsType = {} +) { + if (value == null) { + return null; + } + + const global_settings = useGlobalSettingsState.getState().lookup; + + let maxDigits = options.digits || global_settings.PRICING_DECIMAL_PLACES || 6; + maxDigits = Number(maxDigits); + let minDigits = + options.minDigits || global_settings.PRICING_DECIMAL_PLACES_MIN || 0; + minDigits = Number(minDigits); + + // Extract default currency information + let currency = + options.currency || global_settings.INVENTREE_DEFAULT_CURRENCY || 'USD'; + + // Extract locale information + let locale = options.locale || navigator.language || 'en-US'; + + let formatter = new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency, + maximumFractionDigits: maxDigits, + minimumFractionDigits: minDigits + }); + + return formatter.format(value); +} + +interface renderDateOptionsType { + showTime?: boolean; +} + +/* + * Render the provided date in the user-specified format. + * + * The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22 + * The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed. + */ +export function renderDate(date: string, options: renderDateOptionsType = {}) { + if (!date) { + return '-'; + } + + const user_settings = useUserSettingsState.getState().lookup; + let fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD'; + + if (options.showTime) { + fmt += ' HH:mm'; + } + + const m = dayjs(date); + + if (m.isValid()) { + return m.format(fmt); + } else { + // Invalid input string, simply return provided value + return date; + } +} diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx index f7bdb60218..622644ff29 100644 --- a/src/frontend/src/states/SettingsState.tsx +++ b/src/frontend/src/states/SettingsState.tsx @@ -5,10 +5,11 @@ import { create } from 'zustand'; import { api } from '../App'; import { ApiPaths, apiUrl } from './ApiState'; -import { Setting } from './states'; +import { Setting, SettingsLookup } from './states'; export interface SettingsStateProps { settings: Setting[]; + lookup: SettingsLookup; fetchSettings: () => void; endpoint: ApiPaths; } @@ -19,12 +20,16 @@ export interface SettingsStateProps { export const useGlobalSettingsState = create( (set, get) => ({ settings: [], + lookup: {}, endpoint: ApiPaths.settings_global_list, fetchSettings: async () => { await api .get(apiUrl(ApiPaths.settings_global_list)) .then((response) => { - set({ settings: response.data }); + set({ + settings: response.data, + lookup: generate_lookup(response.data) + }); }) .catch((error) => { console.error('Error fetching global settings:', error); @@ -38,15 +43,30 @@ export const useGlobalSettingsState = create( */ export const useUserSettingsState = create((set, get) => ({ settings: [], + lookup: {}, endpoint: ApiPaths.settings_user_list, fetchSettings: async () => { await api .get(apiUrl(ApiPaths.settings_user_list)) .then((response) => { - set({ settings: response.data }); + set({ + settings: response.data, + lookup: generate_lookup(response.data) + }); }) .catch((error) => { console.error('Error fetching user settings:', error); }); } })); + +/* + return a lookup dictionary for the value of the provided Setting list +*/ +function generate_lookup(data: Setting[]) { + let lookup_dir: SettingsLookup = {}; + for (let setting of data) { + lookup_dir[setting.key] = setting.value; + } + return lookup_dir; +} diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index 27c21038d0..51cb84c920 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -87,3 +87,6 @@ export type ErrorResponse = { statusText: string; message?: string; }; +export type SettingsLookup = { + [key: string]: string; +};