julong-lib/components/form/Form.tsx

401 lines
12 KiB
TypeScript

"use client";
import { useLocal } from "@/lib/utils/use-local";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { useEffect } from "react";
import { toast } from "sonner";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "../ui/resize";
import get from "lodash.get";
import { Skeleton } from "../ui/Skeleton";
import { normalDate } from "@/lib/utils/date";
type Local<T> = {
data: T | null;
submit: () => Promise<void>;
render: () => void;
};
export const Form: React.FC<any> = ({
children,
header,
onLoad,
onSubmit,
onFooter,
showResize,
mode,
className,
onInit,
afterLoad,
toastMessage,
}) => {
const local = useLocal({
ready: false,
data: null as any | null,
btn_ready: true,
submit: async () => {
toast.info(
<>
<Loader2
className={cx(
"h-4 w-4 animate-spin-important",
css`
animation: spin 1s linear infinite !important;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
)}
/>
{toastMessage ? `${toastMessage}...` : "Saving..."}
</>
);
local.btn_ready = false;
local.render();
try {
const fieldDate: any = local?.fields;
let isError = false;
let error: Record<string, string> = {};
try {
const dateFields = Object.values(fieldDate).filter(
(field: any) => get(field, "type") === "date"
);
if (dateFields.length) {
dateFields.forEach((e: any) => {
if (e?.name) {
local.data[e.name] = normalDate(local.data[e.name]);
}
});
local.render();
}
} catch (ex) {
console.error("Error processing date fields:", ex);
}
if (mode !== "view") {
const fieldRequired = Object.values(fieldDate).filter(
(field: any) => field?.required || field?.type === "table"
);
if (fieldRequired.length) {
fieldRequired.forEach((e: any) => {
let keys = e?.name;
const type = e?.type;
if (type === "table" && e?.fields?.length) {
e.fields.forEach((item: any, index: number) => {
let errorChilds: Record<string, string> = {};
const fieldRequired = Object.values(item?.fields).filter(
(field: any) => field?.required
);
fieldRequired.forEach((subField: any) => {
let keySub = subField?.name;
const typeSub = subField?.type;
const val = get(local.data, `${keys}[${index}].${keySub}`);
if (["dropdown-async", "multi-async"].includes(typeSub)) {
keySub = subField?.target || subField?.name;
}
if (
[
"multi-dropdown",
"checkbox",
"multi-upload",
"multi-async",
].includes(typeSub)
) {
if (
!Array.isArray(get(local.data, keys)) ||
!val?.length
) {
errorChilds[subField.name] =
"This field requires at least one item.";
isError = true;
}
} else if (!val) {
errorChilds[subField.name] =
"Please fill out this field.";
isError = true;
}
console.log({
keySub,
data: get(local.data, `${keys}[${index}]`),
val,
});
});
item.error = errorChilds;
});
} else {
if (["dropdown-async", "multi-async"].includes(type)) {
keys = e?.target || e?.name;
}
const val = get(local.data, keys);
if (
[
"multi-dropdown",
"checkbox",
"multi-upload",
"multi-async",
].includes(type)
) {
if (!Array.isArray(val) || !val?.length) {
error[e.name] = "This field requires at least one item.";
isError = true;
}
} else if (!val) {
error[e.name] = "Please fill out this field.";
isError = true;
}
}
});
}
}
local.error = error;
local.render();
if (isError) {
throw new Error("please check your input field.");
} else {
await onSubmit(local);
}
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
{toastMessage ? `${toastMessage} success` : "Record Saved"}
</div>
</div>
);
}, 100);
} catch (ex: any) {
const msg = get(ex, "response.data.meta.message") || ex.message;
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
{toastMessage
? `${toastMessage} failed ${msg}.`
: `Submit Failed ${msg}.`}
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
local.btn_ready = true;
local.render();
},
reload: async () => {
local.ready = false;
local.render();
toast.info(
<>
<Loader2
className={cx(
"h-4 w-4 animate-spin-important",
css`
animation: spin 1s linear infinite !important;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
)}
/>
{"Loading..."}
</>
);
local.data = null;
local.render();
const res = await onLoad();
local.ready = true;
local.data = res;
local.render();
if (typeof afterLoad === "function") {
afterLoad(local);
}
setTimeout(() => {
toast.dismiss();
}, 100);
// if (res instanceof Promise) {
// res.then((data) => {
// local.ready = true;
// local.data = data;
// local.render(); // Panggil render setelah data diperbarui
// // toast.dismiss();
// // toast.success("Data Loaded Successfully!");
// });
// } else {
// local.ready = true;
// local.data = res;
// local.render(); // Panggil render untuk memicu re-render
// toast.dismiss();
// toast.success("Data Loaded Successfully!");
// }
},
fields: {} as any,
render: () => {},
error: {} as any,
onChange: () => {},
mode,
});
useEffect(() => {
local.onChange();
}, [local.data]);
useEffect(() => {
const run = async () => {
if (typeof onInit === "function") {
onInit(local);
}
local.ready = false;
local.render();
toast.info(
<>
<Loader2
className={cx(
"h-4 w-4 animate-spin-important",
css`
animation: spin 1s linear infinite !important;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
)}
/>
{"Loading..."}
</>
);
const res = await onLoad();
local.ready = true;
local.data = res;
local.render();
if (typeof afterLoad === "function") {
await afterLoad(local);
}
setTimeout(() => {
toast.dismiss();
}, 100);
};
run();
}, []);
// Tambahkan dependency ke header agar reaktif
const HeaderComponent = typeof header === "function" ? header(local) : <></>;
if (!local.ready)
return (
<div className="flex flex-grow flex-row items-center justify-center">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-16 w-[250px]" />
</div>
</div>
);
return (
<div className={`flex-grow flex-col flex h-full ${className}`}>
<div className="flex flex-row">{HeaderComponent}</div>
{showResize ? (
// Resize panels...
<ResizablePanelGroup direction="vertical" className="rounded-lg border">
<ResizablePanel className="border-none flex flex-col">
<form
className="flex flex-grow flex-col"
onSubmit={(e) => {
e.preventDefault();
local.submit();
}}
>
{local.ready ? (
children(local)
) : (
<div>
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)}
</form>
</ResizablePanel>
<ResizableHandle className="border-none" />
<ResizablePanel className="border-t-2 flex flex-row flex-grow">
{typeof onFooter === "function" ? onFooter(local) : null}
</ResizablePanel>
</ResizablePanelGroup>
) : (
<>
<form
className={cx(
"flex flex-col ",
typeof onFooter === "function" ? "" : "flex-grow"
)}
onSubmit={(e) => {
e.preventDefault();
local.submit();
}}
>
{local.ready ? (
children(local)
) : (
<div className="flex flex-grow flex-row items-center justify-center">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-16 w-[200px]" />
</div>
</div>
)}
</form>
{typeof onFooter === "function" ? (
<div
className={cx(
"flex flex-grow flex-col",
css`
.tbl {
position: relative;
}
`
)}
>
{onFooter(local)}
</div>
) : (
<></>
)}
</>
)}
</div>
);
};