Files
lti-web-client/src/lib/helper.ts
T

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),
},
};
}