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( 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 = {}; 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 = { '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), }, }; }