mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 21:16:46 +00:00
[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
This commit is contained in:
parent
ea249c1dc5
commit
2d6a8a4bcc
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { formatCurrency, renderDate } from '../../defaults/formatters';
|
||||||
import { ProgressBar } from '../items/ProgressBar';
|
import { ProgressBar } from '../items/ProgressBar';
|
||||||
import { ModelType } from '../render/ModelType';
|
import { ModelType } from '../render/ModelType';
|
||||||
import { RenderOwner } from '../render/User';
|
import { RenderOwner } from '../render/User';
|
||||||
@ -77,8 +78,9 @@ export function TargetDateColumn(): TableColumn {
|
|||||||
return {
|
return {
|
||||||
accessor: 'target_date',
|
accessor: 'target_date',
|
||||||
title: t`Target Date`,
|
title: t`Target Date`,
|
||||||
sortable: true
|
sortable: true,
|
||||||
// TODO: custom renderer which alerts user if target date is overdue
|
// 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 {
|
return {
|
||||||
accessor: 'creation_date',
|
accessor: 'creation_date',
|
||||||
title: t`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 {
|
return {
|
||||||
accessor: 'shipment_date',
|
accessor: 'shipment_date',
|
||||||
title: t`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`,
|
title: title ?? t`Currency`,
|
||||||
sortable: sortable ?? true,
|
sortable: sortable ?? true,
|
||||||
render: (record: any) => {
|
render: (record: any) => {
|
||||||
let value = record[accessor];
|
|
||||||
let currency_key = currency_accessor ?? `${accessor}_currency`;
|
let currency_key = currency_accessor ?? `${accessor}_currency`;
|
||||||
currency = currency ?? record[currency_key];
|
return formatCurrency(record[accessor], {
|
||||||
|
currency: currency ?? record[currency_key]
|
||||||
// TODO: A better render which correctly formats money values
|
});
|
||||||
return `${value} ${currency}`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { renderDate } from '../../../defaults/formatters';
|
||||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
import { ThumbnailHoverCard } from '../../images/Thumbnail';
|
import { ThumbnailHoverCard } from '../../images/Thumbnail';
|
||||||
@ -78,7 +79,8 @@ function buildOrderTableColumns(): TableColumn[] {
|
|||||||
{
|
{
|
||||||
accessor: 'completion_date',
|
accessor: 'completion_date',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Completed`
|
title: t`Completed`,
|
||||||
|
render: (record: any) => renderDate(record.completion_date)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'issued_by',
|
accessor: 'issued_by',
|
||||||
|
@ -3,6 +3,7 @@ import { Group, Text } from '@mantine/core';
|
|||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { formatCurrency, renderDate } from '../../../defaults/formatters';
|
||||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||||
import { Thumbnail } from '../../images/Thumbnail';
|
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
|
// TODO: Note, if not "In stock" we don't want to display the actual location here
|
||||||
return record?.location_detail?.pathstring ?? record.location ?? '-';
|
return record?.location_detail?.pathstring ?? record.location ?? '-';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
// TODO: stocktake column
|
// 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: purchase order
|
||||||
// TODO: Supplier part
|
// 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: stock value
|
||||||
// TODO: packaging
|
// TODO: packaging
|
||||||
// TODO: notes
|
// TODO: notes
|
||||||
|
86
src/frontend/src/defaults/formatters.tsx
Normal file
86
src/frontend/src/defaults/formatters.tsx
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,11 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { ApiPaths, apiUrl } from './ApiState';
|
import { ApiPaths, apiUrl } from './ApiState';
|
||||||
import { Setting } from './states';
|
import { Setting, SettingsLookup } from './states';
|
||||||
|
|
||||||
export interface SettingsStateProps {
|
export interface SettingsStateProps {
|
||||||
settings: Setting[];
|
settings: Setting[];
|
||||||
|
lookup: SettingsLookup;
|
||||||
fetchSettings: () => void;
|
fetchSettings: () => void;
|
||||||
endpoint: ApiPaths;
|
endpoint: ApiPaths;
|
||||||
}
|
}
|
||||||
@ -19,12 +20,16 @@ export interface SettingsStateProps {
|
|||||||
export const useGlobalSettingsState = create<SettingsStateProps>(
|
export const useGlobalSettingsState = create<SettingsStateProps>(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
settings: [],
|
settings: [],
|
||||||
|
lookup: {},
|
||||||
endpoint: ApiPaths.settings_global_list,
|
endpoint: ApiPaths.settings_global_list,
|
||||||
fetchSettings: async () => {
|
fetchSettings: async () => {
|
||||||
await api
|
await api
|
||||||
.get(apiUrl(ApiPaths.settings_global_list))
|
.get(apiUrl(ApiPaths.settings_global_list))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
set({ settings: response.data });
|
set({
|
||||||
|
settings: response.data,
|
||||||
|
lookup: generate_lookup(response.data)
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error fetching global settings:', error);
|
console.error('Error fetching global settings:', error);
|
||||||
@ -38,15 +43,30 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
|
|||||||
*/
|
*/
|
||||||
export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
|
export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
|
||||||
settings: [],
|
settings: [],
|
||||||
|
lookup: {},
|
||||||
endpoint: ApiPaths.settings_user_list,
|
endpoint: ApiPaths.settings_user_list,
|
||||||
fetchSettings: async () => {
|
fetchSettings: async () => {
|
||||||
await api
|
await api
|
||||||
.get(apiUrl(ApiPaths.settings_user_list))
|
.get(apiUrl(ApiPaths.settings_user_list))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
set({ settings: response.data });
|
set({
|
||||||
|
settings: response.data,
|
||||||
|
lookup: generate_lookup(response.data)
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error fetching user settings:', 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;
|
||||||
|
}
|
||||||
|
@ -87,3 +87,6 @@ export type ErrorResponse = {
|
|||||||
statusText: string;
|
statusText: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
export type SettingsLookup = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user