mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
308 lines
7.9 KiB
TypeScript
308 lines
7.9 KiB
TypeScript
import moment from 'moment';
|
|
import 'moment/locale/id';
|
|
import { twMerge } from 'tailwind-merge';
|
|
import clsx, { ClassValue } from 'clsx';
|
|
import { SidebarMenuItem } from '@/components/molecules/SidebarMenu';
|
|
import { OptionType } from '@/components/input/SelectInput';
|
|
import {
|
|
ConstantsApiResponse,
|
|
ProductFlagMapping,
|
|
TransformedConstants,
|
|
} from '@/types/api/constants/constants';
|
|
|
|
// set locale globally
|
|
moment.locale('id');
|
|
|
|
export const sleep = (ms: number = 1000) =>
|
|
new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
export const formatDate = (date: moment.MomentInput, format?: string) => {
|
|
if (!date) return '-';
|
|
|
|
return moment(date).format(format);
|
|
};
|
|
|
|
export const cn = (...inputs: ClassValue[]) => {
|
|
return twMerge(clsx(inputs));
|
|
};
|
|
|
|
export const formatNumber = (
|
|
value: number | bigint | Intl.StringNumericLiteral,
|
|
locale = 'id-ID',
|
|
minimumFractionDigits = 0,
|
|
maximumFractionDigits = 2
|
|
) => {
|
|
return new Intl.NumberFormat(locale, {
|
|
minimumFractionDigits,
|
|
maximumFractionDigits,
|
|
}).format(value);
|
|
};
|
|
|
|
export const safeRound = (num: number, decimals: number) => {
|
|
const factor = 10 ** decimals;
|
|
return Math.round((num + Number.EPSILON) * factor) / factor;
|
|
};
|
|
|
|
export const formatTitleCase = (value: string) => {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/_/g, ' ')
|
|
.split(' ')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
};
|
|
|
|
export function formatVechicleNumber(value: string): string {
|
|
let result = '';
|
|
for (let i = 0; i < (value?.length ?? 0); i++) {
|
|
const curr = value[i];
|
|
const prev = value[i - 1];
|
|
|
|
// Cek apakah terjadi perpindahan dari huruf ke angka atau angka ke huruf
|
|
if (i > 0) {
|
|
const isCurrDigit = /\d/.test(curr);
|
|
const isPrevDigit = /\d/.test(prev);
|
|
|
|
if (isCurrDigit !== isPrevDigit) {
|
|
result += ' ';
|
|
}
|
|
}
|
|
|
|
result += curr;
|
|
}
|
|
|
|
return result.trim().replace(/\s+/g, ' ');
|
|
}
|
|
|
|
export const formatCurrency = (
|
|
value: number | bigint | Intl.StringNumericLiteral,
|
|
currency = 'IDR',
|
|
locale = 'id-ID',
|
|
minimumFractionDigits = 0,
|
|
maximumFractionDigits = 2
|
|
) => {
|
|
return new Intl.NumberFormat(locale, {
|
|
style: 'currency',
|
|
currency,
|
|
minimumFractionDigits,
|
|
maximumFractionDigits,
|
|
}).format(value);
|
|
};
|
|
|
|
/**
|
|
* Retrieves a nested value from an object using a dot-delimited key path.
|
|
* Supports array indexes (e.g., "users.0.name") and returns a default value
|
|
* if the path does not exist.
|
|
*
|
|
* @param obj - The source object to search.
|
|
* @param path - Dot-delimited key string (e.g., "user.address.city").
|
|
* @param defaultValue - Optional value to return if the key path is not found.
|
|
* @returns The value found at the specified path, or the default value.
|
|
*/
|
|
export function getByPath<T, D = undefined>(
|
|
obj: T,
|
|
path: string,
|
|
defaultValue?: D
|
|
): D {
|
|
if (obj == null) return defaultValue as D;
|
|
if (!path) return obj as D;
|
|
|
|
const segments = path.split('.').filter(Boolean);
|
|
let cur: { [key: string]: unknown } = obj;
|
|
|
|
for (const seg of segments) {
|
|
if (cur == null) return defaultValue as D;
|
|
const key: string | number =
|
|
Array.isArray(cur) && /^\d+$/.test(seg) ? Number(seg) : seg;
|
|
if (Object(cur) !== cur || !(key in cur)) {
|
|
return defaultValue as D;
|
|
}
|
|
cur = cur[key] as { [key: string]: unknown };
|
|
}
|
|
|
|
return cur as D;
|
|
}
|
|
|
|
export const convertRowSelectionArrToObj = (
|
|
rowSelectionArr: string[] | number[]
|
|
) => {
|
|
const result: Record<string | number, boolean> = {};
|
|
|
|
rowSelectionArr.forEach((item) => {
|
|
result[item] = true;
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const convertRowSelectionObjToArr = (
|
|
rowSelection: string[] | number[]
|
|
) => {
|
|
const result = Object.keys(rowSelection).map(Number);
|
|
|
|
return result;
|
|
};
|
|
|
|
export const isPathActive = (pathname: string, link?: string) => {
|
|
if (!link) return false;
|
|
|
|
const splittedPathname = pathname.split('/');
|
|
const splittedLink = link.split('/');
|
|
|
|
const isActiveLinkValid = splittedLink.every((linkChunk, idx) => {
|
|
return linkChunk === splittedPathname[idx];
|
|
});
|
|
|
|
return pathname.startsWith(link) && isActiveLinkValid;
|
|
};
|
|
|
|
export function findMenuPath(
|
|
menus: readonly SidebarMenuItem[],
|
|
pathname: string,
|
|
parents: SidebarMenuItem[] = []
|
|
): SidebarMenuItem[] | null {
|
|
for (const menu of menus) {
|
|
const currentPath = [...parents, menu];
|
|
|
|
// Exact match
|
|
if (menu.link === pathname) {
|
|
return currentPath;
|
|
}
|
|
|
|
// Prefix match (useful for pages like /add, /edit, etc.)
|
|
if (pathname.startsWith(menu.link + '/')) {
|
|
if (!menu.submenu) {
|
|
return currentPath;
|
|
}
|
|
}
|
|
|
|
// Search children
|
|
if (menu.submenu) {
|
|
const found = findMenuPath(menu.submenu, pathname, currentPath);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Transform a string value to OptionType with formatted label
|
|
* Example: "AYAM-AFKIR" -> { label: "Ayam Afkir", value: "AYAM-AFKIR" }
|
|
*/
|
|
export function toOption(value: string): OptionType {
|
|
return {
|
|
value,
|
|
label: formatConstantLabel(value),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format constant label by:
|
|
* 1. Replacing underscores/hyphens with spaces
|
|
* 2. Converting to title case
|
|
* 3. Handling special cases
|
|
*/
|
|
export function formatConstantLabel(value: string): string {
|
|
const specialCases: Record<string, string> = {
|
|
'PRE-STARTER': 'Pre Starter',
|
|
BOP: 'BOP',
|
|
SAPRONAK: 'SAPRONAK',
|
|
OVK: 'OVK',
|
|
DOC: 'DOC',
|
|
};
|
|
|
|
if (specialCases[value]) {
|
|
return specialCases[value];
|
|
}
|
|
|
|
const withSpaces = value.replace(/[-_]/g, ' ');
|
|
|
|
return formatTitleCase(withSpaces);
|
|
}
|
|
|
|
/**
|
|
* Transform product_flag_mapping from API format to UI format
|
|
*/
|
|
export function transformProductFlagMapping(
|
|
mapping: ConstantsApiResponse['product_flag_mapping']
|
|
): ProductFlagMapping {
|
|
return {
|
|
flags: mapping.flags.map(toOption),
|
|
options: mapping.options.map((opt) => ({
|
|
flag: toOption(opt.flag),
|
|
sub_flags: opt.sub_flags.map(toOption),
|
|
allow_without_sub_flag: opt.allow_without_sub_flag,
|
|
})),
|
|
sub_flag_to_flag: mapping.sub_flag_to_flag,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform approval workflows from API format to UI format
|
|
*/
|
|
export function transformApprovalWorkflows(
|
|
workflows: ConstantsApiResponse['approval_workflows']
|
|
) {
|
|
return workflows.map((workflow) => ({
|
|
key: workflow.key,
|
|
steps: workflow.steps.map((step) => ({
|
|
value: String(step.step_number),
|
|
label: step.step_name,
|
|
})),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Transform adjustment transaction subtypes from API format to UI format
|
|
*/
|
|
export function transformAdjustmentSubtypes(
|
|
subtypes: ConstantsApiResponse['adjustment']['transaction_subtypes']
|
|
) {
|
|
return {
|
|
RECORDING: subtypes.RECORDING.map(toOption),
|
|
PENJUALAN: subtypes.PENJUALAN.map(toOption),
|
|
PEMBELIAN: subtypes.PEMBELIAN.map(toOption),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform legacy flag aliases from API format to UI format
|
|
*/
|
|
export function transformLegacyFlagAliases(
|
|
aliases: ConstantsApiResponse['legacy_flag_aliases']
|
|
): OptionType[] {
|
|
return Object.entries(aliases).map(([key]) => ({
|
|
value: key,
|
|
label: formatConstantLabel(key),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Transform the entire constants API response to UI format
|
|
*/
|
|
export function transformConstants(
|
|
data: ConstantsApiResponse
|
|
): TransformedConstants {
|
|
return {
|
|
warehouse_types: data.warehouse_types.map(toOption),
|
|
supplier_categories: data.supplier_categories.map(toOption),
|
|
customer_supplier_types: data.customer_supplier_types.map(toOption),
|
|
adjustment: {
|
|
transaction_subtypes: transformAdjustmentSubtypes(
|
|
data.adjustment.transaction_subtypes
|
|
),
|
|
},
|
|
approval_workflows: transformApprovalWorkflows(data.approval_workflows),
|
|
flags: data.flags.map(toOption),
|
|
product_flag_mapping: transformProductFlagMapping(
|
|
data.product_flag_mapping
|
|
),
|
|
legacy_flag_aliases: transformLegacyFlagAliases(data.legacy_flag_aliases),
|
|
stock_log: {
|
|
log_types: data.stock_log.log_types.map(toOption),
|
|
transaction_types: data.stock_log.transaction_types.map(toOption),
|
|
},
|
|
};
|
|
}
|