diff --git a/comps/custom/Popover.tsx b/comps/custom/Popover.tsx index d972526..dc0d586 100755 --- a/comps/custom/Popover.tsx +++ b/comps/custom/Popover.tsx @@ -187,6 +187,8 @@ export function Popover({ content?: React.ReactNode; arrow?: boolean; } & PopoverOptions) { + if (isEditor) return children; + const popover = usePopover({ modal, ...restOptions }); let _content = content; diff --git a/comps/form/field/FieldInput.tsx b/comps/form/field/FieldInput.tsx index 33d41e0..ac98b50 100755 --- a/comps/form/field/FieldInput.tsx +++ b/comps/form/field/FieldInput.tsx @@ -36,6 +36,7 @@ export const FieldInput: FC<{ const errors = fm.error.get(name); let type_field: any = typeof arg.type === "function" ? arg.type() : arg.type; // tipe field + const disabled = typeof field.disabled === "function" ? field.disabled() : field.disabled; let custom = <>; if (field.type === "custom") { let res = arg.custom?.() || <>; @@ -113,7 +114,7 @@ export const FieldInput: FC<{ ? css` border-color: transparent; ` - : field.disabled + : disabled ? "c-border-gray-100" : errors.length > 0 ? field.focused @@ -145,7 +146,7 @@ export const FieldInput: FC<{ "field-inner c-flex-1 c-flex c-items-center", field.type === "link" && "c-justify-end", field.focused && "focused", - field.disabled && "c-pointer-events-none" + disabled && "c-pointer-events-none" )} > {not_ready ? ( diff --git a/comps/form/field/type/TypeDropdown.tsx b/comps/form/field/type/TypeDropdown.tsx index 7b42961..55129fc 100755 --- a/comps/form/field/type/TypeDropdown.tsx +++ b/comps/form/field/type/TypeDropdown.tsx @@ -46,7 +46,6 @@ export const TypeDropdown: FC<{ } if ( field.type === "single-option" && - !value && field.required && local.options.length > 0 ) { @@ -104,6 +103,7 @@ export const TypeDropdown: FC<{ ); } } + const disabled = typeof field.disabled === "function" ? field.disabled() : field.disabled; if (!local.loaded) return ; if (field.type === "single-option") { @@ -129,7 +129,7 @@ export const TypeDropdown: FC<{ return item?.value || search; }} - disabled={field.disabled} + disabled={disabled} allowNew={false} autoPopupWidth={true} focusOpen={true} @@ -165,7 +165,7 @@ export const TypeDropdown: FC<{ autoPopupWidth={true} focusOpen={true} mode={"multi"} - disabled={field.disabled} + disabled={disabled} placeholder={arg.placeholder} options={() => { return local.options; diff --git a/comps/form/field/type/TypeInput.tsx b/comps/form/field/type/TypeInput.tsx index f58315a..cf1f2ce 100755 --- a/comps/form/field/type/TypeInput.tsx +++ b/comps/form/field/type/TypeInput.tsx @@ -96,6 +96,7 @@ export const FieldTypeInput: FC<{ input.change_timeout = setTimeout(fm.render, 300); }; + const disabled = typeof field.disabled === "function" ? field.disabled() : field.disabled; switch (type_field) { case "toggle": return ( @@ -144,7 +145,7 @@ export const FieldTypeInput: FC<{ renderOnChange(); }} value={value || ""} - disabled={field.disabled} + disabled={disabled} className="c-flex-1 c-bg-transparent c-outline-none c-p-2 c-text-sm c-w-full" spellCheck={false} onFocus={() => { @@ -175,7 +176,7 @@ export const FieldTypeInput: FC<{ return ( { diff --git a/comps/form/field/type/TypeMoney.tsx b/comps/form/field/type/TypeMoney.tsx index 78b8e3e..f93b40d 100755 --- a/comps/form/field/type/TypeMoney.tsx +++ b/comps/form/field/type/TypeMoney.tsx @@ -1,5 +1,5 @@ import { useLocal } from "@/utils/use-local"; -import { FC } from "react"; +import { FC, useEffect } from "react"; import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { PropTypeInput } from "./TypeInput"; import { isEmptyString } from "lib/utils/is-empty-string"; @@ -16,11 +16,17 @@ export const FieldMoney: FC<{ display: false as any, ref: null as any, }); + useEffect(() => { + input.value = value; + input.render(); + }, [fm.data[field.name]]); let display: any = null; + const disabled = + typeof field.disabled === "function" ? field.disabled() : field.disabled; const money = formatMoney(Number(value) || 0); return (
-
{isEmptyString(value) ? arg.placeholder : money} -
+
*/} (input.ref = el)} - type={"number"} + type={"text"} onClick={() => {}} onChange={(ev) => { - fm.data[field.name] = Number(ev.currentTarget.value); - fm.render(); - if (field.on_change) { - field.on_change({ - value: Number(fm.data[field.name]), - name: field.name, - fm, - }); + const rawValue = ev.currentTarget.value + .replace(/[^0-9,-]/g, "") + .toString(); + const now = Number(value) || 0; + + if ( + !rawValue.endsWith(",") && + !rawValue.endsWith("-") && + convertionCurrencyNumber(rawValue) !== + convertionCurrencyNumber(input.value) + ) { + fm.data[field.name] = convertionCurrencyNumber( + formatCurrency(rawValue) + ); + fm.render(); + if (field.on_change) { + field.on_change({ + value: convertionCurrencyNumber( + formatCurrency(fm.data[field.name]) + ), + name: field.name, + fm, + }); + } + input.value = formatCurrency(fm.data[field.name]); + input.render(); + } else { + input.value = rawValue; + input.render(); } }} - value={value} - disabled={field.disabled} + value={formatCurrency(input.value)} + disabled={disabled} className={cx( - !input.display ? "c-hidden" : "", + // !input.display ? "c-hidden" : "", "c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full" )} spellCheck={false} @@ -62,8 +89,8 @@ export const FieldMoney: FC<{ field.focused = true; field.render(); }} + placeholder={arg.placeholder || ""} onBlur={() => { - console.log("blur"); field.focused = false; input.display = !input.display; input.render(); @@ -73,9 +100,91 @@ export const FieldMoney: FC<{ ); }; +const convertionCurrencyNumber = (value: string) => { + if (!value) return null; + let numberString = value.toString().replace(/[^0-9,-]/g, ""); + if (numberString.endsWith(",")) { + return Number(numberString.replace(",", "")) || 0; + } + if (numberString.endsWith("-")) { + return Number(numberString.replace("-", "")) || 0; + } + const rawValue = numberString.replace(/[^0-9,-]/g, "").replace(",", "."); + return parseFloat(rawValue) || 0; + return Number(numberString) || 0; +}; +const formatCurrency = (value: any) => { + // Menghapus semua karakter kecuali angka, koma, dan tanda minusif (value === null || value === undefined) return ''; + if (!value) return ""; + let numberString = ""; + if (typeof value === "number") { + numberString = formatMoney(value); + } else { + numberString = value.toString().replace(/[^0-9,-]/g, ""); + } + if (numberString.endsWith("-") && numberString.startsWith("-")) { + return "-"; + } else if (numberString.endsWith(",")) { + const isNegative = numberString.startsWith("-"); + numberString = numberString.replace("-", ""); + const split = numberString.split(","); + if (isNumberOrCurrency(split[0]) === "Number") { + split[0] = formatMoney(Number(split[0])); + } + let rupiah = split[0]; + rupiah = split[1] !== undefined ? rupiah + "," + split[1] : rupiah; + return (isNegative ? "-" : "") + rupiah; + } else { + const isNegative = numberString.startsWith("-"); + numberString = numberString.replace("-", ""); + const split = numberString.split(","); + if (isNumberOrCurrency(split[0]) === "Number") { + split[0] = formatMoney(Number(split[0])); + } + let rupiah = split[0]; + rupiah = split[1] !== undefined ? rupiah + "," + split[1] : rupiah; + return (isNegative ? "-" : "") + rupiah; + } +}; export const formatMoney = (res: number) => { const formattedAmount = new Intl.NumberFormat("id-ID", { minimumFractionDigits: 0, }).format(res); return formattedAmount; }; +const isNumberOrCurrency = (input: any) => { + // Pengecekan apakah input adalah angka biasa + + if (typeof input === "string") { + let rs = input; + if (input.startsWith("-")) { + rs = rs.replace("-", ""); + } + const dots = rs.match(/\./g); + if (dots && dots.length > 1) { + return "Currency"; + } else if (dots && dots.length === 1) { + if (!hasNonZeroDigitAfterDecimal(rs)) { + return "Currency"; + } else { + return "Number"; + } + } + } + if (!isNaN(input)) { + return "Number"; + } + // Pengecekan apakah input adalah format mata uang dengan pemisah ribuan + const currencyRegex = /^-?Rp?\s?\d{1,3}(\.\d{3})*$/; + if (currencyRegex.test(input)) { + return "Currency"; + } + + // Jika tidak terdeteksi sebagai angka atau format mata uang, kembalikan null atau sesuai kebutuhan + return null; +}; +const hasNonZeroDigitAfterDecimal = (input: string) => { + // Ekspresi reguler untuk mencocokkan angka 1-9 setelah koma atau titik + const regex = /[.,]\d*[1-9]\d*/; + return regex.test(input); +}; diff --git a/comps/form/field/type/TypeUpload.tsx b/comps/form/field/type/TypeUpload.tsx index cfecfd7..4c74e98 100755 --- a/comps/form/field/type/TypeUpload.tsx +++ b/comps/form/field/type/TypeUpload.tsx @@ -21,6 +21,7 @@ export const FieldUpload: FC<{ drop: false as boolean, }); let display: any = null; + const disabled = typeof field.disabled === "function" ? field.disabled() : field.disabled; return (
{ + return ""; + }, + where: () => { + return {} as ${ + opt.parent_table + ? `Prisma.${opt.parent_table}WhereInput` + : `Record` + }; + }, + breadcrumbs: (existing: any[]) => { + return [...existing]; + }, +})`, + ], + }, + }, + }); + } + const load = on_load_rel({ pk: res.pk, table: arg.relation.to.table, diff --git a/comps/form/gen/gen-form.ts b/comps/form/gen/gen-form.ts index 0e5400e..78518b6 100755 --- a/comps/form/gen/gen-form.ts +++ b/comps/form/gen/gen-form.ts @@ -37,9 +37,11 @@ export const generateForm = async ( return; } if (pk) { - const is_md = + let is_md: boolean | string = item.edit?.parent?.item?.component?.id === "cb52075a-14ab-455a-9847-6f1d929a2a73"; + if (!is_md) is_md = ""; + if (data["on_load"]) { result.on_load = { mode: "raw", @@ -56,8 +58,9 @@ export const generateForm = async ( md.render(); } `, + is_md: true, } - : {}, + : { is_md }, }), }; } @@ -67,17 +70,14 @@ export const generateForm = async ( value: `\ async ({ form, error, fm }: IForm) => { let result = false; - try { - -${ - is_md && - `\ + try {${ + is_md && + `\ if (typeof md !== "undefined") { fm.status = "saving"; md.render(); }` -} - + } const data = { ...form }; const record = {} as Record; diff --git a/comps/form/gen/on_load.ts b/comps/form/gen/on_load.ts index 5cd3287..449b845 100755 --- a/comps/form/gen/on_load.ts +++ b/comps/form/gen/on_load.ts @@ -14,6 +14,7 @@ export const on_load = ({ opt?: { before_load?: string; after_load?: string; + is_md?: boolean | string; }; }) => { const sample: any = {}; @@ -29,18 +30,26 @@ export const on_load = ({ } } + let is_md: string | boolean = + typeof opt?.is_md === "undefined" ? true : !!opt?.is_md; + if (!is_md) is_md = ""; + return `\ async (opt) => { - if (isEditor) return ${JSON.stringify(sample)}; + if (isEditor) return ${JSON.stringify(sample, null, 2)}; let raw_id = params.id; +${ + is_md && + `\ if (typeof md === 'object' && md.selected && md.pk) { const pk = md.pk?.name; if (md.selected[pk]) { raw_id = md.selected[pk]; } } - +` +} ${opt?.before_load ? opt.before_load : `let id = raw_id`} let item = {}; if (id){ @@ -63,9 +72,7 @@ async (opt) => { where, select: gen.select, }); - ${opt?.after_load ? opt?.after_load : ""} - return item; } else { ${opt?.after_load ? opt?.after_load : ""} diff --git a/comps/form/gen/on_load_rel.ts b/comps/form/gen/on_load_rel.ts index 24f3e90..ecd8311 100755 --- a/comps/form/gen/on_load_rel.ts +++ b/comps/form/gen/on_load_rel.ts @@ -29,7 +29,6 @@ export const on_load_rel = ({ !isEmptyString(type) && ["checkbox", "typeahead", "button"].includes(type as any); - console.log(skip_select, type); return `\ async (arg: { reload: () => Promise; diff --git a/comps/form/typings.ts b/comps/form/typings.ts index d6b5799..3284a37 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -52,7 +52,7 @@ export type FieldProp = { required_msg: (name: string) => string | ReactElement; on_change: (arg: { value: any }) => void | Promise; PassProp: any; - disabled: "y" | "n"; + disabled: ("y" | "n") | (() => true | false); child: any; selection: "single" | "multi"; prefix: any; @@ -152,7 +152,7 @@ export type FieldInternal = { width: FieldProp["width"]; required: boolean; focused: boolean; - disabled: boolean; + disabled: boolean | (() => boolean); required_msg: FieldProp["required_msg"]; col?: GFCol; ref?: any; diff --git a/comps/form/utils/use-field.tsx b/comps/form/utils/use-field.tsx index 2733244..df84154 100755 --- a/comps/form/utils/use-field.tsx +++ b/comps/form/utils/use-field.tsx @@ -23,7 +23,6 @@ export const useField = ( const label = typeof arg.label === "string" ? arg.label : arg.label(); const required = typeof arg.required === "string" ? arg.required : arg.required(); - const update_field = { name: name.replace(/\s*/gi, ""), label: label, @@ -35,7 +34,7 @@ export const useField = ( custom: arg.custom, required: required === "y", required_msg: arg.required_msg, - disabled: arg.disabled === "y", + disabled: typeof arg.disabled === "function" ? arg.disabled : arg.disabled === "y", on_change: arg.on_change, }; diff --git a/comps/list/TableList.tsx b/comps/list/TableList.tsx index b434adf..10c0d33 100755 --- a/comps/list/TableList.tsx +++ b/comps/list/TableList.tsx @@ -399,7 +399,7 @@ export const TableList: FC = ({ renderHeaderCell(props) { return (
- + {/* */}
); }, diff --git a/comps/menu/Menu.tsx b/comps/menu/Menu.tsx index 23ba5c8..0773315 100755 --- a/comps/menu/Menu.tsx +++ b/comps/menu/Menu.tsx @@ -1,14 +1,13 @@ import { getPathname } from "lib/utils/pathname"; import { useLocal } from "lib/utils/use-local"; import get from "lodash.get"; -import { FC } from "react"; +import { FC, useRef } from "react"; import { IMenu, MenuProp } from "../../preset/menu/utils/type-menu"; export const Menu: FC = (props) => { const imenu = props.menu[0]; let role = props.role; role = props.on_init() as string; - const PassProp = props.PassProp; let menu = imenu[role] || []; const pathname = getPathname(); @@ -26,8 +25,10 @@ export const Menu: FC = (props) => { local.render(); } } + const ref = useRef(null); return (
= ({ on_close, open, child }) => { createPortal(
(local.ref = e)} - className="c-w-screen c-h-screen c-bg-transparent c-flex c-flex-row c-items-center c-justify-center" + className="c-w-screen c-h-screen relative c-bg-transparent c-flex c-flex-row c-items-center c-justify-center" onClick={(e) => { if (local.ref) { if (e.target === local.ref) { diff --git a/exports.tsx b/exports.tsx index d4824bd..052d8c4 100755 --- a/exports.tsx +++ b/exports.tsx @@ -1,5 +1,6 @@ export { FieldLoading } from "@/comps/ui/field-loading"; import { lazify, lazifyMany } from "@/utils/lazify"; +export { Popover } from "./comps/custom/Popover"; /** Master - Detail - List - Form */ export const MasterDetail = lazify( @@ -80,7 +81,7 @@ export { FormatValue } from "@/utils/format-value"; export { GetValue } from "@/utils/get-value"; export { password } from "@/utils/password"; export { prasi_events, call_prasi_events } from "lib/utils/prasi-events"; - +export { getFilter } from "@/comps/filter/utils/get-filter"; /** Session */ export { Login } from "@/preset/login/Login"; export { generateLogin } from "@/preset/login/utils/generate"; diff --git a/preset/menu/Menu.tsx b/preset/menu/Menu.tsx index bb6f8df..49ab522 100755 --- a/preset/menu/Menu.tsx +++ b/preset/menu/Menu.tsx @@ -1,7 +1,7 @@ import { getPathname } from "lib/utils/pathname"; import { useLocal } from "lib/utils/use-local"; import get from "lodash.get"; -import { FC, useEffect } from "react"; +import { FC, useEffect, useRef } from "react"; import { IMenu, MenuProp } from "./utils/type-menu"; // import { icon } from "../../.."; @@ -22,7 +22,7 @@ export const Menu: FC = (props) => { let role = props.role; role = props.on_init(); let menu = get(imenu, role) || []; - + const ref = useRef(null); const local = useLocal({ ...local_default }); if (local.pathname !== getPathname()) { @@ -39,7 +39,10 @@ export const Menu: FC = (props) => { }, [props.mode]); return ( -
+
= (prop) => { const { value, gen_fields, name, tree_depth, mode } = prop; if (gen_fields) { @@ -54,7 +54,14 @@ export const FormatValue: FC<{ } catch (ex: any) { return "-"; } - } else if (mode === "timeago") { + } else if (mode === "date") { + if (!value || isEmptyString(value)) return "-"; + try { + return formatDate(dayjs(value), "DD MMMM YYYY"); + } catch (ex: any) { + return "-"; + } + }else if (mode === "timeago") { if (!value || isEmptyString(value)) return "-"; try { return timeAgo(dayjs(value));