From 31680f29c7c3e1940830b90ff2dfb14a64cacc5c Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Fri, 17 Jan 2025 14:06:58 +0700 Subject: [PATCH] banyak bos --- components/Popover/Popover.tsx | 25 +- components/form/Field.tsx | 86 +- components/form/FormBetter.tsx | 10 +- components/form/field/TypeColor.tsx | 119 +++ components/form/field/TypeInput.tsx | 69 +- components/form/field/TypeRichText.tsx | 821 ++++++++++++++++++ components/form/field/TypeTag.tsx | 137 +++ components/form/field/Typeahead.tsx | 4 + components/partials/NavbarFlow.tsx | 64 +- components/partials/Sidebar.tsx | 214 +++-- components/tablelist/TableList.tsx | 506 ++++++----- components/ui/CalenderFull.tsx | 349 ++++++++ .../Datepicker/components/Calendar/Days.tsx | 278 ++++-- .../Datepicker/components/Calendar/Months.tsx | 19 +- .../Datepicker/components/Calendar/Week.tsx | 24 +- .../Datepicker/components/Calendar/Years.tsx | 2 + .../Datepicker/components/Calendar/index.tsx | 271 ++++-- components/ui/Datepicker/components/utils.tsx | 21 +- components/ui/Datepicker/helpers/index.ts | 19 +- components/ui/Datepicker/types/index.ts | 123 +-- components/ui/FieldColorPopover.tsx | 68 ++ components/ui/PinterestLayout.tsx | 4 - components/ui/accordion.tsx | 81 ++ components/ui/alert.tsx | 5 +- components/ui/button-rich-text.tsx | 21 + components/ui/button.tsx | 19 +- helpers/user.ts | 31 + package.json | 16 + utils/apix.ts | 85 ++ utils/cloneFm.ts | 12 +- utils/document_type.ts | 42 + utils/event.ts | 49 +- 32 files changed, 3008 insertions(+), 586 deletions(-) create mode 100644 components/form/field/TypeColor.tsx create mode 100644 components/form/field/TypeRichText.tsx create mode 100644 components/form/field/TypeTag.tsx create mode 100644 components/ui/CalenderFull.tsx create mode 100644 components/ui/FieldColorPopover.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/button-rich-text.tsx create mode 100644 helpers/user.ts create mode 100644 utils/apix.ts create mode 100644 utils/document_type.ts diff --git a/components/Popover/Popover.tsx b/components/Popover/Popover.tsx index f90a991..a7049aa 100644 --- a/components/Popover/Popover.tsx +++ b/components/Popover/Popover.tsx @@ -46,7 +46,9 @@ export function usePopover({ const arrowRef = React.useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); const [labelId, setLabelId] = React.useState(); - const [descriptionId, setDescriptionId] = React.useState(); + const [descriptionId, setDescriptionId] = React.useState< + string | undefined + >(); // Determine whether the popover is open const open = controlledOpen ?? uncontrolledOpen; @@ -113,7 +115,6 @@ export function usePopover({ ); } - function mapPlacementSideToCSSProperty(placement: Placement) { const staticPosition = placement.split("-")[0]; @@ -186,6 +187,7 @@ export function Popover({ className, classNameTrigger, arrow, + popoverClassName, ...restOptions }: { root?: HTMLElement; @@ -193,6 +195,7 @@ export function Popover({ classNameTrigger?: string; children: React.ReactNode; content?: React.ReactNode; + popoverClassName?: string; arrow?: boolean; } & PopoverOptions) { const popover = usePopover({ modal, ...restOptions }); @@ -216,14 +219,17 @@ export function Popover({ {_content} {(typeof arrow === "undefined" || arrow) && } @@ -269,7 +275,6 @@ export const PopoverTrigger = React.forwardRef< ); }); - export const PopoverContent = React.forwardRef< HTMLDivElement, React.HTMLProps diff --git a/components/form/Field.tsx b/components/form/Field.tsx index e4418dc..9036312 100644 --- a/components/form/Field.tsx +++ b/components/form/Field.tsx @@ -1,9 +1,13 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { FieldCheckbox } from "./field/TypeCheckbox"; import { TypeDropdown } from "./field/TypeDropdown"; import { TypeInput } from "./field/TypeInput"; import { TypeUpload } from "./field/TypeUpload"; import { FieldUploadMulti } from "./field/TypeUploadMulti"; +import { TypeRichText } from "./field/TypeRichText"; +import { TypeTag } from "./field/TypeTag"; +import get from "lodash.get"; +import { getNumber } from "@/lib/utils/getNumber"; export const Field: React.FC = ({ fm, @@ -18,8 +22,13 @@ export const Field: React.FC = ({ onChange, className, style, + prefix, + suffix, }) => { let result = null; + + const suffixRef = useRef(null); + const prefixRef = useRef(null); const is_disable = fm.mode === "view" ? true : disabled; const error = fm.error?.[name]; useEffect(() => { @@ -42,6 +51,8 @@ export const Field: React.FC = ({ fm.render(); } }, []); + const before = typeof prefix === "function" ? prefix() : prefix; + const after = typeof suffix === "function" ? suffix() : suffix; return ( <>
= ({
+ {before && ( +
+ {before} +
+ )} {["upload"].includes(type) ? ( <> = ({ mode="single" /> + ) : ["richtext"].includes(type) ? ( + <> + + + ) : ["tag"].includes(type) ? ( + <> + + ) : ( <> = ({ type={type} disabled={is_disable} onChange={onChange} + className={cx( + before && + css` + padding-left: ${getNumber( + get(prefixRef, "current.clientWidth") + ) + 10}px; + `, + after && + css` + padding-right: ${getNumber( + get(suffixRef, "current.clientWidth") + ) + 10}px; + ` + )} /> )} + {after && ( +
+ {after} +
+ )}
{error ? (
{error}
diff --git a/components/form/FormBetter.tsx b/components/form/FormBetter.tsx index 0e0dd08..af04dc3 100644 --- a/components/form/FormBetter.tsx +++ b/components/form/FormBetter.tsx @@ -19,14 +19,16 @@ export const FormBetter: React.FC = ({ }); useEffect(() => {}, [fm.data]); return ( -
+
{typeof fm === "object" && typeof onTitle === "function" ? ( -
{onTitle(fm)}
+
+ {onTitle(fm)} +
) : ( <> )} -
-
+
+
= ({ + value, + onChangePicker, + onClose, +}) => { + const meta = useLocal({ + originalValue: "", + inputValue: value, + rgbValue: "", + selectedEd: "" as string, + }); + useEffect(() => { + meta.inputValue = value || ""; + const convertColor = tinycolor(meta.inputValue); + meta.rgbValue = convertColor.toRgbString(); + meta.render(); + }, [value]); + const tin = tinycolor(meta.inputValue); + return ( +
+
{ + e.stopPropagation(); + e.preventDefault(); + }} + > + + { + if (color) { + meta.inputValue = color; + onChangePicker(color); + const convertColor = tinycolor(meta.inputValue); + meta.rgbValue = convertColor.toRgbString(); + } + }} + /> + +
+
+
+ { + const color = e.currentTarget.value; + meta.inputValue = color; + onChangePicker(color); + }} + /> +
+ +
+ {meta.inputValue !== "" && ( + <> +
{ + meta.inputValue = ""; + onChangePicker(""); + }} + > + Reset +
+ + )} + + {onClose && ( +
+ Close +
+ )} +
+
+
+ ); +}; diff --git a/components/form/field/TypeInput.tsx b/components/form/field/TypeInput.tsx index 6c354f8..9898521 100644 --- a/components/form/field/TypeInput.tsx +++ b/components/form/field/TypeInput.tsx @@ -3,6 +3,8 @@ import Datepicker from "../../ui/Datepicker"; import { Input } from "../../ui/input"; import { Textarea } from "../../ui/text-area"; import { useEffect } from "react"; +import tinycolor from "tinycolor2"; +import { FieldColorPicker } from "../../ui/FieldColorPopover"; export const TypeInput: React.FC = ({ name, @@ -13,12 +15,28 @@ export const TypeInput: React.FC = ({ type, field, onChange, + className, }) => { let value: any = fm.data?.[name] || ""; const input = useLocal({ value: 0 as any, ref: null as any, + open: false, }); + const meta = useLocal({ + originalValue: "", + inputValue: value, + rgbValue: "", + selectedEd: "" as string, + }); + useEffect(() => { + if (type === "color") { + meta.inputValue = value || ""; + const convertColor = tinycolor(meta.inputValue); + meta.rgbValue = convertColor.toRgbString(); + meta.render(); + } + }, [value]); useEffect(() => { if (type === "money") { input.value = @@ -67,6 +85,51 @@ export const TypeInput: React.FC = ({ ); break; + case "color": + return ( +
+
+ { + fm.data[name] = val; + fm.render(); + }} + onOpen={() => { + input.open = true; + input.render(); + }} + onClose={() => { + input.open = false; + input.render(); + }} + open={input.open} + showHistory={false} + > +
'); + `, + "cursor-pointer rounded-md" + )} + > +
+
+
+
+
+ ); + break; + case "date": return ( <> @@ -111,7 +174,8 @@ export const TypeInput: React.FC = ({ ? "rgb(243 244 246)" : "transparant"} ? ""; - ` + `, + className )} required={required} placeholder={placeholder || ""} @@ -165,7 +229,8 @@ export const TypeInput: React.FC = ({ css` background-color: ${disabled ? "rgb(243 244 246)" : "transparant"} ? ""; - ` + `, + className )} disabled={disabled} required={required} diff --git a/components/form/field/TypeRichText.tsx b/components/form/field/TypeRichText.tsx new file mode 100644 index 0000000..2f9478f --- /dev/null +++ b/components/form/field/TypeRichText.tsx @@ -0,0 +1,821 @@ +import { useLocal } from "@/lib/utils/use-local"; +import { Input } from "../../ui/input"; +import { useEffect, useState } from "react"; +import { + useEditor, + EditorContent, + useCurrentEditor, + EditorProvider, +} from "@tiptap/react"; +import Underline from "@tiptap/extension-underline"; +import Link from "@tiptap/extension-link"; +import StarterKit from "@tiptap/starter-kit"; +import { Color } from "@tiptap/extension-color"; +import ListItem from "@tiptap/extension-list-item"; +import TextAlign from "@tiptap/extension-text-align"; +import TextStyle from "@tiptap/extension-text-style"; +import { Popover } from "../../Popover/Popover"; +import { ButtonBetter } from "../../ui/button"; +import get from "lodash.get"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs"; +import Table from "@tiptap/extension-table"; +import TableCell from "@tiptap/extension-table-cell"; +import TableHeader from "@tiptap/extension-table-header"; +import TableRow from "@tiptap/extension-table-row"; +import { ButtonRichText } from "../../ui/button-rich-text"; + +export const TypeRichText: React.FC = ({ + name, + fm, + placeholder, + disabled = false, + required, + type, + field, + onChange, +}) => { + let value: any = fm.data?.[name] || ""; + const input = useLocal({ + value: 0 as any, + ref: null as any, + open: false, + }); + const [url, setUrl] = useState(null as any); + const local = useLocal({ + open: false, + data: ["General", "Table"], + tab: 0, + active: "General", + }); + useEffect(() => {}, [fm.data?.[name]]); + + useEffect(() => {}, []); + const MenuBar = () => { + const { editor } = useCurrentEditor(); + if (disabled) return <>; + if (!editor) { + return null; + } + + return ( +
+ + + {local.data.map((e, idx) => { + return ( +
+ { + local.tab = idx; + local.active = e; + local.render(); + }} + className={cx( + "p-1.5 px-4 border text-sm font-normal data-[state=active]:font-bold data-[state=active]:bg-white border-none border-r bg-transparent data-[state=active]:text-primary data-[state=active]:shadow-none data-[state=active]:border-none", + !idx + ? "ml-1.5" + : idx++ === local.data.length + ? "mr-2" + : "" + )} + key={e} + > + {e} + +
+
+ ); + })} +
+ +
+ + + + + + + + + + + + + + + + + { + if (!editor.isActive("link")) { + local.open = open; + local.render(); + } + }} + open={local.open} + content={ +
+ { + setUrl(get(e, "currentTarget.value")); + }} + /> +
+ { + if (url) { + try { + editor + .chain() + .focus() + .extendMarkRange("link") + .setLink({ href: url }) + .run(); + local.open = false; + local.render(); + setUrl(""); + } catch (e: any) { + alert(e.message); + } + } + }} + > + Add + +
+
+ } + > + +
+
+
+ + +
+ { + e.preventDefault(); + e.stopPropagation(); + editor + .chain() + .focus() + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); + }} + disabled={false} + active={false} + > + + + + + { + e.preventDefault(); + e.stopPropagation(); + editor.commands.setCellAttribute( + "className", + "tiptap-border-none" + ); + }} + disabled={false} + active={false} + > + + + + + + { + e.preventDefault(); + e.stopPropagation(); + editor.commands.setCellAttribute("className", ""); + }} + disabled={false} + active={false} + > + + + + +
+
+
+
+ ); + }; + const CustomTable = Table.extend({ + resizable: true, + addAttributes() { + return { + class: { + default: null, + }, + }; + }, + }); + + const extensions = [ + Color.configure({ types: [TextStyle.name, ListItem.name] }), + Table.configure({ + resizable: true, + HTMLAttributes: { + class: "my-custom-class", // Tambahkan kelas default + }, + }), + TableRow, + TableHeader.extend({ + addAttributes() { + return { + ...this.parent?.(), + className: { + default: null, // Nilai default tidak ada + parseHTML: (element) => element.getAttribute("class") || null, // Ambil class dari elemen + renderHTML: (attributes) => { + if (!attributes.className) { + return {}; + } + return { + class: attributes.className, // Tambahkan class ke HTML output + }; + }, + }, + backgroundColor: { + default: null, // Nilai default + parseHTML: (element) => element.style.backgroundColor || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {}; + } + return { + style: `background-color: ${attributes.backgroundColor};`, + }; + }, + }, + }; + }, + }), + TableCell.extend({ + addAttributes() { + return { + ...this.parent?.(), + className: { + default: null, // Nilai default tidak ada + parseHTML: (element) => element.getAttribute("class") || null, // Ambil class dari elemen + renderHTML: (attributes) => { + if (!attributes.className) { + return {}; + } + return { + class: attributes.className, // Tambahkan class ke HTML output + }; + }, + }, + backgroundColor: { + default: null, // Nilai default + parseHTML: (element) => element.style.backgroundColor || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {}; + } + return { + style: `background-color: ${attributes.backgroundColor};`, + }; + }, + }, + }; + }, + }), + TextAlign.configure({ + types: ["heading", "paragraph"], + }), + Underline, + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: "https", + protocols: ["http", "https"], + isAllowedUri: (url: any, ctx: any) => { + try { + // construct URL + const parsedUrl = url.includes(":") + ? new URL(url) + : new URL(`${ctx.defaultProtocol}://${url}`); + + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false; + } + + // disallowed protocols + const disallowedProtocols = ["ftp", "file", "mailto"]; + const protocol = parsedUrl.protocol.replace(":", ""); + + if (disallowedProtocols.includes(protocol)) { + return false; + } + + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map((p: any) => + typeof p === "string" ? p : p.scheme + ); + + if (!allowedProtocols.includes(protocol)) { + return false; + } + + // disallowed domains + const disallowedDomains = [ + "example-phishing.com", + "malicious-site.net", + ]; + const domain = parsedUrl.hostname; + + if (disallowedDomains.includes(domain)) { + return false; + } + + // all checks have passed + return true; + } catch { + return false; + } + }, + shouldAutoLink: (url: any) => { + try { + // construct URL + const parsedUrl = url.includes(":") + ? new URL(url) + : new URL(`https://${url}`); + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = [ + "example-no-autolink.com", + "another-no-autolink.com", + ]; + const domain = parsedUrl.hostname; + + return !disallowedDomains.includes(domain); + } catch { + return false; + } + }, + }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + orderedList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + }), + ]; + + return ( +
+ } + extensions={extensions} + onUpdate={({ editor }) => { + fm.data[name] = editor.getHTML(); + fm.render(); + }} + content={fm.data[name]} + editable={!disabled} + > +
+ ); +}; diff --git a/components/form/field/TypeTag.tsx b/components/form/field/TypeTag.tsx new file mode 100644 index 0000000..077652c --- /dev/null +++ b/components/form/field/TypeTag.tsx @@ -0,0 +1,137 @@ +import { useLocal } from "@/lib/utils/use-local"; +import { Input } from "../../ui/input"; +import { useEffect, useRef, useState } from "react"; +import { + useEditor, + EditorContent, + useCurrentEditor, + EditorProvider, +} from "@tiptap/react"; +import Link from "@tiptap/extension-link"; +import StarterKit from "@tiptap/starter-kit"; +import { Color } from "@tiptap/extension-color"; +import ListItem from "@tiptap/extension-list-item"; +import TextStyle from "@tiptap/extension-text-style"; +import { Popover } from "../../Popover/Popover"; +import { ButtonBetter } from "../../ui/button"; +import get from "lodash.get"; + +export const TypeTag: React.FC = ({ + name, + fm, + placeholder, + disabled = false, + required, + type, + field, + onChange, +}) => { + const [tags, setTags] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [editingIndex, setEditingIndex] = useState(null); // Index tag yang sedang diedit + const [tempValue, setTempValue] = useState(""); // Nilai sementara untuk pengeditan + const tagRefs = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + if (get(fm, `data.[${name}].length`)) { + setTags(fm.data?.[name]); + } + }, []); + useEffect(() => { + fm.data[name] = tags; + fm.render(); + }, [inputValue]); + const handleSaveEdit = (index: number) => { + if (!disabled) return; + const updatedTags = [...tags]; + updatedTags[index] = tempValue.trim(); // Update nilai tag + setTags(updatedTags); + setEditingIndex(null); // Keluar dari mode edit + setTempValue(""); // Reset nilai sementara + }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!disabled) return; + if (e.key === "Enter" && inputValue) { + e.preventDefault(); + setTags([...tags, inputValue]); + setInputValue(""); + } else if (e.key === "Backspace" && !inputValue && tags.length > 0) { + setTags(tags.slice(0, -1)); + } + }; + const handleFocusTag = (index: number) => { + if (!disabled) return; + setEditingIndex(index); // Masuk ke mode edit + setTempValue(tags[index]); // Isi nilai sementara dengan nilai tag + setTimeout(() => { + tagRefs.current[index]?.focus(); // Fokus pada elemen yang diedit + }, 0); + }; + const removeTag = (index: number) => { + if (!disabled) return; + setTags(tags.filter((_, i) => i !== index)); + }; + + return ( +
+ {tags.map((tag, index) => ( +
+ {disabled ? ( +
{tag}
+ ) : ( +
handleSaveEdit(index)} + onKeyDown={(e) => { + if (!disabled) return; + if (e.key === "Enter") { + e.preventDefault(); + handleSaveEdit(index); + } + if (e.key === "Escape") { + setEditingIndex(null); + } + }} + onClick={() => { + handleFocusTag(index); + }} + onInput={(e) => + setTempValue((e.target as HTMLDivElement).innerText) + } + > + {tag} +
+ )} + + {!disabled && ( + + )} +
+ ))} + {!disabled && ( + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + className="rounded-md flex-grow border-none outline-none text-sm focus:shadow-none focus:ring-0 focus:border-none focus:outline-none" + placeholder="Add a option..." + /> + )} +
+ ); +}; diff --git a/components/form/field/Typeahead.tsx b/components/form/field/Typeahead.tsx index 48d5627..c7049ac 100644 --- a/components/form/field/Typeahead.tsx +++ b/components/form/field/Typeahead.tsx @@ -446,6 +446,10 @@ export const Typeahead: FC<{ } local.open = open; local.render(); + + if (!open) { + resetSearch(); + } }} showEmpty={!allow_new} className={popupClassName} diff --git a/components/partials/NavbarFlow.tsx b/components/partials/NavbarFlow.tsx index 0c70161..aad349e 100644 --- a/components/partials/NavbarFlow.tsx +++ b/components/partials/NavbarFlow.tsx @@ -1,13 +1,6 @@ "use client"; import React, { FC } from "react"; -import { - Avatar, - DarkThemeToggle, - Dropdown, - Label, - Navbar, - TextInput, -} from "flowbite-react"; +import { Avatar, Dropdown, Navbar } from "flowbite-react"; import { HiArchive, HiBell, @@ -16,50 +9,27 @@ import { HiEye, HiInbox, HiLogout, - HiMenuAlt1, HiOutlineTicket, - HiSearch, HiShoppingBag, HiUserCircle, HiUsers, HiViewGrid, - HiX, } from "react-icons/hi"; import { siteurl } from "@/lib/utils/siteurl"; import { get_user } from "@/lib/utils/get_user"; import api from "@/lib/utils/axios"; const NavFlow: React.FC = ({ minimaze }) => { return ( - -
+ +
-
- {true && ( - - )} - - - - Man Power Management - - -
- -
-
+
+
+
-
+ +
@@ -403,10 +373,16 @@ const UserDropdown: FC = function () { arrowIcon={false} inline label={ - - User menu +
+
+
+
+ {get_user("employee.name") ? get_user("employee.name") : "-"} +
+
+
- +
} > @@ -430,8 +406,10 @@ const UserDropdown: FC = function () { { - await api.delete(process.env.NEXT_PUBLIC_BASE_URL + "/api/destroy-cookies"); - localStorage.removeItem('user'); + await api.delete( + process.env.NEXT_PUBLIC_BASE_URL + "/api/destroy-cookies" + ); + localStorage.removeItem("user"); if (typeof window === "object") navigate(`${process.env.NEXT_PUBLIC_API_PORTAL}/logout`); }} diff --git a/components/partials/Sidebar.tsx b/components/partials/Sidebar.tsx index 78389ea..625dfb6 100644 --- a/components/partials/Sidebar.tsx +++ b/components/partials/Sidebar.tsx @@ -57,8 +57,8 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { const renderTree = (items: TreeMenuItem[], depth: number = 0) => { return items.map((item, index) => { const hasChildren = item.children && item.children.length > 0; - const isActive = item.href && detectCase(currentPage, item.href); - const isParentActive = hasChildren && isChildActive(item.children!); + let isActive = item.href && detectCase(currentPage, item.href); + let isParentActive = hasChildren && isChildActive(item.children!); const [isOpen, setIsOpen] = useState(isParentActive); useEffect(() => { if (isParentActive) { @@ -71,14 +71,41 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { return ( {hasChildren ? ( -
  • +
  • + {mini && isParentActive && ( +
    + + + +
    + )} +
    = ({ data, minimaze, mini }) => {
    {!depth ? (
    = ({ data, minimaze, mini }) => { {!mini ? ( <> -
    +
    {item.title}
    -
    +
    {isOpen ? : }
    @@ -140,8 +173,27 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { <> )}
    -
    + {mini && isParentActive && ( +
    + + + +
    + )} +
    = ({ data, minimaze, mini }) => {
  • ) : ( -
  • +
  • + {isActive && ( +
    + + + +
    + )} { if (item?.href) setCurrentPage(item.href); }} className={classNames( - " flex-row flex items-center cursor-pointer items-center w-full rounded-lg text-base font-normal text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-700 flex flex-row py-2.5 px-4", + "relative flex-row flex items-center cursor-pointer items-center w-full rounded-full rounded-r-none text-base text-gray-900 flex flex-row py-2.5 px-4", isActive - ? " py-2.5 px-4 text-base font-normal text-dark-500 rounded-lg group shadow-[#31367875] transition-all duration-200 dark:bg-gray-700" - : "", + ? " py-2.5 px-4 text-base rounded-full rounded-r-none group " + : " font-normal", + mini ? "transition-all duration-200" : "", isActive ? !depth - ? " bg-white shadow-md hover:bg-gray-200 hover:!bg-white " - : "bg-gray-100" - : "", + ? " bg-layer font-normal" + : " bg-layer text-primary font-bold" + : "text-white", css` & > span { white-space: wrap !important; @@ -183,10 +259,8 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { {!depth ? (
    @@ -197,14 +271,35 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { )} {!mini ? ( <> -
    - {item.title} -
    +
    {item.title}
    ) : ( <> )}
    + {isActive && ( +
    + + + +
    + )}
  • )} @@ -214,46 +309,27 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { }; return ( -
    +
    div { + background: transparent; + padding-top: 0; + padding-right: 0; + } + ` + )} > - {/* {!local.ready ? ( -
    -
    -
    -
    - - -
    - -
    - - -
    - -
    -
    -
    - ) : ( - )} */} - -
    +
    diff --git a/components/tablelist/TableList.tsx b/components/tablelist/TableList.tsx index 8b802ff..4045055 100644 --- a/components/tablelist/TableList.tsx +++ b/components/tablelist/TableList.tsx @@ -16,7 +16,6 @@ import React, { FC, useCallback, useEffect, useState } from "react"; import { Breadcrumb, Button, - Checkbox, Label, Modal, Table, @@ -44,12 +43,15 @@ import { InputSearch } from "../ui/input-search"; import { Input } from "../ui/input"; import { FaChevronDown } from "react-icons/fa"; import get from "lodash.get"; +import { Checkbox } from "../ui/checkbox"; +import { getNumber } from "@/lib/utils/getNumber"; +import { formatMoney } from "../form/field/TypeInput"; export const TableList: React.FC = ({ name, column, onLoad, - take = 50, + take = 20, header, disabledPagination, disabledHeader, @@ -57,6 +59,8 @@ export const TableList: React.FC = ({ hiddenNoRow, disabledHoverRow, onInit, + onCount, + feature, }) => { const [data, setData] = useState([]); const sideLeft = @@ -71,11 +75,16 @@ export const TableList: React.FC = ({ status: string; progress: number; }; + const checkbox = + Array.isArray(feature) && feature?.length + ? feature.includes("checkbox") + : false; const local = useLocal({ table: null as any, data: [] as any[], sort: {} as any, search: null as any, + count: 0 as any, addRow: (row: any) => { setData((prev) => [...prev, row]); local.data.push(row); @@ -145,51 +154,45 @@ export const TableList: React.FC = ({ }, }); useEffect(() => { - if (typeof onInit === "function") { - onInit(local); - } - toast.info( - <> - { + toast.info( + <> + - {"Loading..."} - - ); - if (Array.isArray(onLoad)) { - local.data = onLoad; - local.render(); - setData(onLoad); - } else { - const res: any = onLoad({ - search: local.search, - sort: local.sort, - take, - paging: 1, - }); - if (res instanceof Promise) { - res.then((e) => { - local.data = e; - local.render(); - setData(e); - setTimeout(() => { - toast.dismiss(); - }, 2000); - }); + ` + )} + /> + {"Loading..."} + + ); + if (typeof onCount === "function") { + const res = await onCount(); + local.count = res; + local.render(); + } + + if (Array.isArray(onLoad)) { + local.data = onLoad; + local.render(); + setData(onLoad); } else { + const res: any = await onLoad({ + search: local.search, + sort: local.sort, + take, + paging: 1, + }); local.data = res; local.render(); setData(res); @@ -197,13 +200,53 @@ export const TableList: React.FC = ({ toast.dismiss(); }, 2000); } + }; + if (typeof onInit === "function") { + onInit(local); } + run(); }, []); + useEffect(() => { + // console.log("PERUBAHAN"); + }, [data]); + const objectNull = {}; const defaultColumns: ColumnDef[] = init_column(column); const [sorting, setSorting] = React.useState([]); - const [columns] = React.useState(() => [ - ...defaultColumns, - ]); + const [columns] = React.useState(() => + checkbox + ? [ + { + id: "select", + width: 10, + header: ({ table }) => ( + { + table.getToggleAllRowsSelectedHandler(); + const handler = table.getToggleAllRowsSelectedHandler(); + handler(e); // Pastikan ini memanggil fungsi handler yang benar + }} + /> + ), + cell: ({ row }) => ( +
    + { + const handler = row.getToggleSelectedHandler(); + handler(e); // Pastikan ini memanggil fungsi handler yang benar + }} + /> +
    + ), + sortable: false, + }, + ...defaultColumns, + ] + : [...defaultColumns] + ); const [columnResizeMode, setColumnResizeMode] = React.useState("onChange"); @@ -218,25 +261,29 @@ export const TableList: React.FC = ({ : { getPaginationRowModel: getPaginationRowModel(), }; + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 20, + }); const table = useReactTable({ data: data, columnResizeMode, + pageCount: Math.ceil(local.count / 20), + manualPagination: true, columnResizeDirection, columns, + enableRowSelection: true, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, initialState: { pagination: { - pageIndex: 0, //custom initial page index - pageSize: 25, //custom default page size + pageIndex: 0, + pageSize: 20, //custom default page size }, }, state: { - pagination: { - pageIndex: 0, - pageSize: 50, - }, + pagination, sorting, }, ...paginationConfig, @@ -263,19 +310,9 @@ export const TableList: React.FC = ({ <>
    {!disabledHeader ? ( -
    +
    - {false ? ( -
    -

    - All {name ? `${name}s` : ``} -

    -
    - ) : ( - <> - )} -
    {sideLeft ? ( sideLeft(local) @@ -333,9 +370,35 @@ export const TableList: React.FC = ({
    - +
    {!disabledHeadTable ? ( - + {table.getHeaderGroups().map((headerGroup) => ( = ({ (e: any) => e?.name === name ); const isSort = - typeof col?.sortable === "boolean" + name === "select" + ? false + : typeof col?.sortable === "boolean" ? col.sortable : true; + const resize = + name === "select" + ? false + : typeof col?.resize === "boolean" + ? col.resize + : true; return (
    = ({ }} key={header.id} colSpan={header.colSpan} - className="relative px-2 py-2 text-sm py-1 " + className={cx( + "relative px-2 py-2 text-sm py-1 uppercase", + name === "select" && + css` + max-width: 5px; + ` + )} >
    = ({ isSort ? " cursor-pointer" : "" )} > -
    +
    {header.isPlaceholder ? null : flexRender( @@ -438,13 +524,19 @@ export const TableList: React.FC = ({ header.column.resetSize(), onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), - className: `resizer w-0.5 bg-gray-300 ${ - table.options.columnResizeDirection - } ${ - header.column.getIsResizing() - ? "isResizing" - : "" - }`, + className: cx( + `resizer bg-[#b3c9fe] cursor-e-resize ${ + table.options.columnResizeDirection + } ${ + header.column.getIsResizing() + ? "isResizing" + : "" + }`, + css` + width: 1px; + cursor: e-resize !important; + ` + ), style: { transform: columnResizeMode === "onEnd" && @@ -474,15 +566,16 @@ export const TableList: React.FC = ({ <> )} - + {table.getRowModel().rows.map((row, idx) => ( {row.getVisibleCells().map((cell) => { @@ -542,11 +635,19 @@ export const TableList: React.FC = ({
    table.nextPage()} onPrevPage={() => table.previousPage()} disabledNextPage={!table.getCanNextPage()} disabledPrevPage={!table.getCanPreviousPage()} page={table.getState().pagination.pageIndex + 1} + setPage={(page: any) => { + setPagination({ + pageIndex: page, + pageSize: 20, + }); + }} countPage={table.getPageCount()} countData={local.data.length} take={take} @@ -565,138 +666,139 @@ export const Pagination: React.FC = ({ disabledNextPage, disabledPrevPage, page, - countPage, - countData, - take, + count, + list, + setPage, onChangePage, }) => { const local = useLocal({ page: 1 as any, + pagination: [] as any, }); useEffect(() => { local.page = page; + local.pagination = getPagination(page, Math.ceil(count / 20)); local.render(); - }, [page]); + }, [page, count]); return ( -
    -
    -
    { - if (!disabledPrevPage) { - onPrevPage(); - } - }} - className={classNames( - "inline-flex justify-center rounded p-1 ", - disabledPrevPage - ? "text-gray-200" - : "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900" - )} - > - Previous page - -
    -
    { - if (!disabledNextPage) { - onNextPage(); - } - }} - className={classNames( - "inline-flex justify-center rounded p-1 ", - disabledNextPage - ? "text-gray-200" - : "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900" - )} - > - Next page - -
    - - Page  - {page} -  of  - {countPage} - - - - | Go to page: - { - e.preventDefault(); - const page = Number(local.page); - if (!page) { - local.page = 0; - } else if (page > countPage) { - local.page = countPage; - } - local.render(); - onChangePage(local.page - 1); - }} - > - { - local.page = e.target.value; - local.render(); - debouncedHandler(() => { - const page = Number(local.page); - if (!page) { - local.page = 0; - } else if (page > countPage) { - local.page = countPage; - } - local.render(); - onChangePage(local.page - 1); - }, 1500); - }} - /> - - +
    +
    + Showing {local.page * 20 - 19} to{" "} + {list.data?.length >= 20 + ? local.page * 20 + : local.page === 1 && Math.ceil(count / 20) === 1 + ? list.data?.length + : local.page * 20 - 19 + list.data?.length}{" "} + of {formatMoney(getNumber(count))} results
    -
    - {!disabledPrevPage ? ( - <> -
    { - if (!disabledPrevPage) { - onPrevPage(); - } - }} - className={classNames( - "cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300" - )} - > - - Previous -
    - - ) : ( - <> - )} - {!disabledNextPage ? ( - <> -
    { - if (!disabledNextPage) { - onNextPage(); - } - }} - className={classNames( - "cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300" - )} - > - Next - -
    - - ) : ( - <> - )} +
    +
    + +
    +
    +
    +
    +
    { + if (!disabledPrevPage) { + onPrevPage(); + } + }} + className={cx( + "flex flex-row items-center gap-x-2 justify-center rounded p-1 ", + disabledPrevPage + ? "text-gray-200 border-gray-200 border px-2" + : "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2" + )} + > + + Previous +
    +
    { + if (!disabledNextPage) { + onNextPage(); + } + }} + className={cx( + "flex flex-row items-center gap-x-2 justify-center rounded p-1 ", + disabledNextPage + ? "text-gray-200 border-gray-200 border px-2" + : "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2" + )} + > + Next + +
    +
    ); }; + +const getPagination = (currentPage: number, totalPages: number) => { + const pagination: { label: string; active: boolean }[] = []; + const maxVisible = 5; // Jumlah maksimal elemen yang ditampilkan + const halfRange = Math.floor((maxVisible - 3) / 2); + + if (totalPages <= maxVisible) { + // Jika total halaman lebih kecil dari batas, tampilkan semua halaman + for (let i = 1; i <= totalPages; i++) { + pagination.push({ label: i.toString(), active: i === currentPage }); + } + } else { + pagination.push({ label: "1", active: currentPage === 1 }); // Halaman pertama selalu ada + + if (currentPage > halfRange + 2) { + pagination.push({ label: "...", active: false }); // Awal titik-titik + } + + const startPage = Math.max(2, currentPage - halfRange); + const endPage = Math.min(totalPages - 1, currentPage + halfRange); + + for (let i = startPage; i <= endPage; i++) { + pagination.push({ label: i.toString(), active: i === currentPage }); + } + + if (currentPage < totalPages - halfRange - 1) { + pagination.push({ label: "...", active: false }); // Akhir titik-titik + } + + pagination.push({ + label: totalPages.toString(), + active: currentPage === totalPages, + }); // Halaman terakhir selalu ada + } + + return pagination; +}; diff --git a/components/ui/CalenderFull.tsx b/components/ui/CalenderFull.tsx new file mode 100644 index 0000000..6d5578b --- /dev/null +++ b/components/ui/CalenderFull.tsx @@ -0,0 +1,349 @@ +import dayjs from "dayjs"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + COLORS, + DATE_FORMAT, + DEFAULT_COLOR, + LANGUAGE, +} from "./Datepicker/constants"; +import { ColorKeys, DatepickerType, Period } from "./Datepicker/types"; +import { useLocal } from "@/lib/utils/use-local"; +import { formatDate, nextMonth, previousMonth } from "./Datepicker/helpers"; +import useOnClickOutside from "./Datepicker/hooks"; +import DatepickerContext from "./Datepicker/contexts/DatepickerContext"; +import Calendar from "./Datepicker/components/Calendar"; +const CalenderFull: React.FC = ({ + primaryColor = "blue", + value = null, + onChange, + useRange = true, + showFooter = false, + showShortcuts = false, + configs = undefined, + asSingle = false, + placeholder = null, + separator = "~", + startFrom = null, + i18n = LANGUAGE, + disabled = false, + inputClassName = null, + containerClassName = null, + toggleClassName = null, + toggleIcon = undefined, + displayFormat = DATE_FORMAT, + readOnly = false, + minDate = null, + maxDate = null, + dateLooking = "forward", + disabledDates = null, + inputId, + inputName, + startWeekOn = "sun", + classNames = undefined, + popoverDirection = undefined, + mode = "daily", + onMark, + onLoad, + style, +}) => { + const local = useLocal({ open: false }); + // Ref + const containerRef = useRef(null); + const calendarContainerRef = useRef(null); + const arrowRef = useRef(null); + + // State + const [firstDate, setFirstDate] = useState( + startFrom && dayjs(startFrom).isValid() ? dayjs(startFrom) : dayjs() + ); + const [secondDate, setSecondDate] = useState( + nextMonth(firstDate) + ); + const [period, setPeriod] = useState({ + start: null, + end: null, + }); + const [dayHover, setDayHover] = useState(null); + const [inputText, setInputText] = useState(""); + const [inputRef, setInputRef] = useState(React.createRef()); + + // Custom Hooks use + useOnClickOutside(calendarContainerRef, () => { + const container = calendarContainerRef.current; + if (container) { + hideDatepicker(); + } + }); + useEffect(() => {}, []); + // Functions + const hideDatepicker = useCallback(() => { + local.open = false; + local.render(); + }, []); + + /* Start First */ + const firstGotoDate = useCallback( + (date: dayjs.Dayjs) => { + const newDate = dayjs(formatDate(date)); + const reformatDate = dayjs(formatDate(secondDate)); + if (newDate.isSame(reformatDate) || newDate.isAfter(reformatDate)) { + setSecondDate(nextMonth(date)); + } + setFirstDate(date); + }, + [secondDate] + ); + + const previousMonthFirst = useCallback(() => { + firstGotoDate(previousMonth(firstDate)); + }, [firstDate]); + + const nextMonthFirst = useCallback(() => { + firstGotoDate(nextMonth(firstDate)); + }, [firstDate, firstGotoDate]); + + const changeFirstMonth = useCallback( + (month: number) => { + firstGotoDate( + dayjs(`${firstDate.year()}-${month < 10 ? "0" : ""}${month}-01`) + ); + }, + [firstDate, firstGotoDate] + ); + + const changeFirstYear = useCallback( + (year: number) => { + firstGotoDate(dayjs(`${year}-${firstDate.month() + 1}-01`)); + }, + [firstDate, firstGotoDate] + ); + /* End First */ + + /* Start Second */ + const secondGotoDate = useCallback( + (date: dayjs.Dayjs) => { + const newDate = dayjs(formatDate(date, displayFormat)); + const reformatDate = dayjs(formatDate(firstDate, displayFormat)); + if (newDate.isSame(reformatDate) || newDate.isBefore(reformatDate)) { + setFirstDate(previousMonth(date)); + } + setSecondDate(date); + }, + [firstDate, displayFormat] + ); + + const previousMonthSecond = useCallback(() => { + secondGotoDate(previousMonth(secondDate)); + }, [secondDate, secondGotoDate]); + + const nextMonthSecond = useCallback(() => { + setSecondDate(nextMonth(secondDate)); + }, [secondDate]); + + const changeSecondMonth = useCallback( + (month: number) => { + secondGotoDate( + dayjs(`${secondDate.year()}-${month < 10 ? "0" : ""}${month}-01`) + ); + }, + [secondDate, secondGotoDate] + ); + + const changeSecondYear = useCallback( + (year: number) => { + secondGotoDate(dayjs(`${year}-${secondDate.month() + 1}-01`)); + }, + [secondDate, secondGotoDate] + ); + /* End Second */ + + // UseEffects & UseLayoutEffect + useEffect(() => { + const container = containerRef.current; + const calendarContainer = calendarContainerRef.current; + const arrow = arrowRef.current; + + if (container && calendarContainer && arrow) { + const detail = container.getBoundingClientRect(); + const screenCenter = window.innerWidth / 2; + const containerCenter = (detail.right - detail.x) / 2 + detail.x; + + if (containerCenter > screenCenter) { + arrow.classList.add("right-0"); + arrow.classList.add("mr-3.5"); + calendarContainer.classList.add("right-0"); + } + } + }, []); + + useEffect(() => { + if (value && value.startDate && value.endDate) { + const startDate = dayjs(value.startDate); + const endDate = dayjs(value.endDate); + const validDate = startDate.isValid() && endDate.isValid(); + const condition = + validDate && (startDate.isSame(endDate) || startDate.isBefore(endDate)); + if (condition) { + setPeriod({ + start: formatDate(startDate), + end: formatDate(endDate), + }); + setInputText( + `${formatDate(startDate, displayFormat)}${ + asSingle + ? "" + : ` ${separator} ${formatDate(endDate, displayFormat)}` + }` + ); + } + } + + if (value && value.startDate === null && value.endDate === null) { + setPeriod({ + start: null, + end: null, + }); + setInputText(""); + } + }, [asSingle, value, displayFormat, separator]); + + useEffect(() => { + if (startFrom && dayjs(startFrom).isValid()) { + const startDate = value?.startDate; + const endDate = value?.endDate; + if (startDate && dayjs(startDate).isValid()) { + setFirstDate(dayjs(startDate)); + if (!asSingle) { + if ( + endDate && + dayjs(endDate).isValid() && + dayjs(endDate).startOf("month").isAfter(dayjs(startDate)) + ) { + setSecondDate(dayjs(endDate)); + } else { + setSecondDate(nextMonth(dayjs(startDate))); + } + } + } else { + setFirstDate(dayjs(startFrom)); + setSecondDate(nextMonth(dayjs(startFrom))); + } + } + }, [asSingle, startFrom, value]); + + // Variables + const safePrimaryColor = useMemo(() => { + if (COLORS.includes(primaryColor)) { + return primaryColor as ColorKeys; + } + return DEFAULT_COLOR; + }, [primaryColor]); + const contextValues = useMemo(() => { + return { + asSingle, + primaryColor: safePrimaryColor, + configs, + calendarContainer: calendarContainerRef, + arrowContainer: arrowRef, + hideDatepicker, + period, + changePeriod: (newPeriod: Period) => setPeriod(newPeriod), + dayHover, + changeDayHover: (newDay: string | null) => setDayHover(newDay), + inputText, + changeInputText: (newText: string) => setInputText(newText), + updateFirstDate: (newDate: dayjs.Dayjs) => firstGotoDate(newDate), + changeDatepickerValue: onChange, + showFooter, + placeholder, + separator, + i18n, + value, + disabled, + inputClassName, + containerClassName, + toggleClassName, + toggleIcon, + readOnly, + displayFormat, + minDate, + maxDate, + dateLooking, + disabledDates, + inputId, + inputName, + startWeekOn, + classNames, + onChange, + input: inputRef, + popoverDirection, + }; + }, [ + asSingle, + safePrimaryColor, + configs, + hideDatepicker, + period, + dayHover, + inputText, + onChange, + showFooter, + placeholder, + separator, + i18n, + value, + disabled, + inputClassName, + containerClassName, + toggleClassName, + toggleIcon, + readOnly, + displayFormat, + minDate, + maxDate, + dateLooking, + disabledDates, + inputId, + inputName, + startWeekOn, + classNames, + inputRef, + popoverDirection, + firstGotoDate, + ]); + + const containerClassNameOverload = useMemo(() => { + const defaultContainerClassName = "relative w-full text-gray-700"; + return typeof containerClassName === "function" + ? containerClassName(defaultContainerClassName) + : typeof containerClassName === "string" && containerClassName !== "" + ? containerClassName + : defaultContainerClassName; + }, [containerClassName]); + + return ( + + + + ); +}; + +export default CalenderFull; diff --git a/components/ui/Datepicker/components/Calendar/Days.tsx b/components/ui/Datepicker/components/Calendar/Days.tsx index 65ee7a4..23a24ca 100644 --- a/components/ui/Datepicker/components/Calendar/Days.tsx +++ b/components/ui/Datepicker/components/Calendar/Days.tsx @@ -1,6 +1,12 @@ import dayjs from "dayjs"; import isBetween from "dayjs/plugin/isBetween"; -import React, { useCallback, useContext } from "react"; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { BG_COLOR, TEXT_COLOR } from "../../constants"; import DatepickerContext from "../../contexts/DatepickerContext"; @@ -11,6 +17,8 @@ import { classNames as cn, } from "../../helpers"; import { Period } from "../../types"; +import get from "lodash.get"; +import { getNumber } from "@/lib/utils/getNumber"; dayjs.extend(isBetween); @@ -26,16 +34,24 @@ interface Props { onClickPreviousDays: (day: number) => void; onClickDay: (day: number) => void; onClickNextDays: (day: number) => void; - onIcon?: (day: number, date: Date) => any; + onIcon?: (day: number, date: Date, data?: any) => any; + style?: string; } - const Days: React.FC = ({ calendarData, onClickPreviousDays, onClickDay, onClickNextDays, onIcon, + style, }) => { + // Ref + const calendarRef = useRef(null); + const markRef = useRef(null); + const [height, setHeight] = useState(0); + const [heightItem, setHeightItem] = useState(0); + const [maxItem, setMaxItem] = useState(0); + const [width, setWidth] = useState(0); // Contexts const { primaryColor, @@ -76,17 +92,13 @@ const Days: React.FC = ({ ) { className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium rounded-full`; } else if (dayjs(fullDay).isSame(period.start)) { - className = ` ${ - BG_COLOR["500"][primaryColor] - } text-white font-medium ${ + className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium ${ dayjs(fullDay).isSame(dayHover) && !period.end ? "rounded-full" : "rounded-l-full" }`; } else if (dayjs(fullDay).isSame(period.end)) { - className = ` ${ - BG_COLOR["500"][primaryColor] - } text-white font-medium ${ + className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium ${ dayjs(fullDay).isSame(dayHover) && !period.start ? "rounded-full" : "rounded-r-full" @@ -241,13 +253,16 @@ const Days: React.FC = ({ const buttonClass = useCallback( (day: number, type: "current" | "next" | "previous") => { - const baseClass = - "flex items-center justify-center w-10 h-10 relative"; + let baseClass = `calender-day flex items-center justify-center ${ + style === "custom" ? " w-6 h-6 m-1" : "w-12 h-12 lg:w-10 lg:h-10" + } relative`; if (type === "current") { return cn( baseClass, !activeDateData(day).active ? hoverClassByDay(day) + : style === "custom" + ? "" : activeDateData(day).className, isDateDisabled(day, type) && "text-gray-400 cursor-not-allowed" ); @@ -400,65 +415,222 @@ const Days: React.FC = ({ }`; } const res = new Date(fullDay); - return typeof onIcon === "function" ? onIcon(day, res) : null; + return typeof onIcon === "function" + ? onIcon(day, res, { + ref: calendarRef, + height, + maxItem, + width, + heightItem, + }) + : null; }; + useEffect(() => { + if (calendarRef?.current && markRef?.current) { + const card = getNumber(get(calendarRef, "current.clientWidth")); + const cardHeight = getNumber(get(markRef, "current.clientHeight")); + const heightItem = 20; // perkiraan + setWidth(card - 2); + setHeight(cardHeight); + setMaxItem(Math.floor(cardHeight / heightItem)); + setHeightItem(20); + // setMaxItem + const day = 3; + const fullwidth = card * 7; + const percent = (7 / 3) * 100; + } + }, [calendarRef.current, markRef.current]); return ( -
    +
    {calendarData.days.previous.map((item, index) => ( - +
    + {style === "custom" ? ( + <> + +
    + {load_marker(item, "previous")} + {/* + {index === 1 && ( +
    + 1 more +
    + )} */} +
    + + ) : ( + <> + + + )} +
    +
    ))} {calendarData.days.current.map((item, index) => ( - +
    + {style === "custom" ? ( + <> + +
    + {load_marker(item, "current")} +
    + + ) : ( + <> + + + )} +
    +
    ))} {calendarData.days.next.map((item, index) => ( - +
    + {style === "custom" ? ( + <> + +
    {load_marker(item, "next")}
    + + ) : ( + <> + + + )} +
    +
    ))}
    ); diff --git a/components/ui/Datepicker/components/Calendar/Months.tsx b/components/ui/Datepicker/components/Calendar/Months.tsx index 9319b6d..6aa5a55 100644 --- a/components/ui/Datepicker/components/Calendar/Months.tsx +++ b/components/ui/Datepicker/components/Calendar/Months.tsx @@ -9,13 +9,19 @@ import { RoundedButton } from "../utils"; interface Props { currentMonth: number; clickMonth: (month: number) => void; + style?: string; } -const Months: React.FC = ({ currentMonth, clickMonth }) => { +const Months: React.FC = ({ currentMonth, clickMonth, style }) => { const { i18n } = useContext(DatepickerContext); loadLanguageModule(i18n); return ( -
    +
    {MONTHS.map((item) => ( = ({ currentMonth, clickMonth }) => { clickMonth(item); }} active={currentMonth === item} + style={style} > - <>{dayjs(`2022-${item}-01`).locale(i18n).format("MMM")} + {style === "custom" ? ( +
    + {dayjs(`2022-${item}-01`).locale(i18n).format("MMMM")} +
    + ) : ( + <>{dayjs(`2022-${item}-01`).locale(i18n).format("MMM")} + )}
    ))}
    diff --git a/components/ui/Datepicker/components/Calendar/Week.tsx b/components/ui/Datepicker/components/Calendar/Week.tsx index 9c1bfea..98e2003 100644 --- a/components/ui/Datepicker/components/Calendar/Week.tsx +++ b/components/ui/Datepicker/components/Calendar/Week.tsx @@ -4,8 +4,10 @@ import React, { useContext, useMemo } from "react"; import { DAYS } from "../../constants"; import DatepickerContext from "../../contexts/DatepickerContext"; import { loadLanguageModule, shortString, ucFirst } from "../../helpers"; - -const Week: React.FC = () => { +interface Props { + style?: string; +} +const Week: React.FC = ({ style }) => { const { i18n, startWeekOn } = useContext(DatepickerContext); loadLanguageModule(i18n); const startDateModifier = useMemo(() => { @@ -33,11 +35,25 @@ const Week: React.FC = () => { }, [startWeekOn]); return ( -
    +
    {DAYS.map((item) => (
    {ucFirst( shortString( diff --git a/components/ui/Datepicker/components/Calendar/Years.tsx b/components/ui/Datepicker/components/Calendar/Years.tsx index e60a4a3..3256d2b 100644 --- a/components/ui/Datepicker/components/Calendar/Years.tsx +++ b/components/ui/Datepicker/components/Calendar/Years.tsx @@ -11,6 +11,7 @@ interface Props { minYear: number | null; maxYear: number | null; clickYear: (data: number) => void; + style?: string; } const Years: React.FC = ({ @@ -19,6 +20,7 @@ const Years: React.FC = ({ minYear, maxYear, clickYear, + style, }) => { const { dateLooking } = useContext(DatepickerContext); diff --git a/components/ui/Datepicker/components/Calendar/index.tsx b/components/ui/Datepicker/components/Calendar/index.tsx index e3a0338..6ead770 100644 --- a/components/ui/Datepicker/components/Calendar/index.tsx +++ b/components/ui/Datepicker/components/Calendar/index.tsx @@ -34,6 +34,7 @@ import Week from "./Week"; import Years from "./Years"; import { DateType } from "../../types"; +import { Popover } from "@/lib/components/Popover/Popover"; interface Props { date: dayjs.Dayjs; @@ -44,7 +45,9 @@ interface Props { changeMonth: (month: number) => void; changeYear: (year: number) => void; mode?: "monthly" | "daily"; - onMark?: (day: number, date: Date) => any; + onMark?: (day: number, date: Date, data?: any) => any; + style?: "custom" | "prasi"; + onLoad?: (e?: any) => void | Promise; } const Calendar: React.FC = ({ @@ -57,6 +60,8 @@ const Calendar: React.FC = ({ changeYear, onMark, mode = "daily", + style = "prasi", + onLoad, }) => { // Contexts const { @@ -77,6 +82,8 @@ const Calendar: React.FC = ({ const [showMonths, setShowMonths] = useState(false); const [showYears, setShowYears] = useState(false); const [year, setYear] = useState(date.year()); + const [openPopover, setOpenPopover] = useState(false); + const [openPopoverYear, setOpenPopoverYear] = useState(false); useEffect(() => { if (mode === "monthly") { setShowMonths(true); @@ -90,7 +97,12 @@ const Calendar: React.FC = ({ getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn) ); }, [date, startWeekOn]); - + const previousDate = useCallback(() => { + const day = getLastDaysInMonth( + previousMonth(date), + getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn) + ); + }, [date, startWeekOn]); const current = useCallback(() => { return getDaysInMonth(formatDate(date)); }, [date]); @@ -245,17 +257,79 @@ const Calendar: React.FC = ({ setYear(date.year()); }, [date]); + const getMonth = (month?: string) => { + const value: any = date; + const currentDate: any = new Date(value); + const previousMonthDate = new Date(currentDate); + previousMonthDate.setDate(1); + switch (month) { + case "before": + previousMonthDate.setMonth(currentDate.getMonth() - 1); + break; + case "after": + previousMonthDate.setMonth(currentDate.getMonth() + 1); + break; + default: + break; + } + return previousMonthDate; + }; // Variables const calendarData = useMemo(() => { - return { + const data = { + previous: previous(), + current: current(), + next: next(), + }; + const result = { date: date, - days: { - previous: previous(), - current: current(), - next: next(), + days: data, + time: { + previous: data?.previous?.length + ? data.previous.map((e) => { + return new Date( + getMonth("before").getFullYear(), + getMonth("before").getMonth(), + e + ); + }) + : [], + current: data?.current?.length + ? data.current.map((e) => { + return new Date( + getMonth().getFullYear(), + getMonth().getMonth(), + e + ); + }) + : [], + next: data?.next?.length + ? data.next.map((e) => { + return new Date( + getMonth("after").getFullYear(), + getMonth("after").getMonth(), + e + ); + }) + : [], }, }; + return result; }, [current, date, next, previous]); + useEffect(() => { + if (typeof onLoad === "function") { + const run = async () => { + if (typeof onLoad === "function") { + const param = dayjs(formatDate(date)).toDate(); + await onLoad({ + date: param, + calender: calendarData, + }); + } + }; + run(); + } + }, [calendarData, date]); const minYear = React.useMemo( () => (minDate && dayjs(minDate).isValid() ? dayjs(minDate).year() : null), [minDate] @@ -264,88 +338,131 @@ const Calendar: React.FC = ({ () => (maxDate && dayjs(maxDate).isValid() ? dayjs(maxDate).year() : null), [maxDate] ); + const isCustom = style === "custom"; return ( -
    +
    - {!showMonths && !showYears && ( -
    - - - + {style === "custom" ? ( +
    +
    +
    + + +
    +
    + {calendarData.date.locale(i18n).format("MMMM")}{" "} + {calendarData.date.year()} +
    +
    +
    - )} + ) : ( +
    + {!showMonths && !showYears && ( +
    + + + +
    + )} - {showYears && ( -
    - { - setYear(year - 12); - }} - > - - -
    - )} + {showYears && ( +
    + { + setYear(year - 12); + }} + > + + +
    + )} -
    -
    - { - setShowMonths(!showMonths); - hideYears(); - }} - > - <>{calendarData.date.locale(i18n).format("MMM")} - -
    +
    +
    + { + setShowMonths(!showMonths); + hideYears(); + }} + > + <>{calendarData.date.locale(i18n).format("MMM")} + +
    -
    - { - setShowYears(!showYears); - hideMonths(); - }} - > -
    {calendarData.date.year()}
    -
    -
    -
    +
    + { + setShowYears(!showYears); + hideMonths(); + }} + > +
    {calendarData.date.year()}
    +
    +
    +
    - {showYears && ( -
    - { - setYear(year + 12); - }} - > - - -
    - )} + {showYears && ( +
    + { + setYear(year + 12); + }} + > + + +
    + )} - {!showMonths && !showYears && ( -
    - - - + {!showMonths && !showYears && ( +
    + + + +
    + )}
    )}
    -
    +
    {showMonths && ( )} @@ -361,27 +478,19 @@ const Calendar: React.FC = ({ {!showMonths && !showYears && ( <> - + { - if(typeof onMark === "function"){ - return onMark(day, date) + style={style} + onIcon={(day, date, data) => { + if (typeof onMark === "function") { + return onMark(day, date, data); } - return <> - if (new Date().getDate() === day) - return ( -
    -
    - ! -
    -
    - ); - return <> + return <>; }} /> diff --git a/components/ui/Datepicker/components/utils.tsx b/components/ui/Datepicker/components/utils.tsx index 129a1c6..c7fac0a 100644 --- a/components/ui/Datepicker/components/utils.tsx +++ b/components/ui/Datepicker/components/utils.tsx @@ -14,6 +14,8 @@ interface Button { roundedFull?: boolean; padding?: string; active?: boolean; + style?: string; + className?: string; } export const DateIcon: React.FC = ({ className = "w-6 h-6" }) => { @@ -205,27 +207,28 @@ export const RoundedButton: React.FC + ); +}; diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 8bcf2d5..7447bee 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -9,16 +9,15 @@ const btn = cva( " text-white px-4 py-1.5 group active-menu-icon relative flex items-stretch justify-center p-0.5 text-center border border-transparent text-white enabled:hover:bg-cyan-800 rounded-md" ); const buttonVariants = cva( - "cursor-pointer px-4 py-1.5 group relative flex items-stretch justify-center p-0.5 text-center border inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "cursor-pointer px-4 py-1.5 group relative flex items-stretch justify-center p-0.5 text-center inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: - "bg-primary text-white shadow hover:bg-primary/90 active-menu-icon", - reject: - "bg-red-500 text-white shadow hover:bg-red-500 active-menu-icon", - destructive: - "bg-red-500 text-white shadow-sm hover:bg-destructive/90", + "border bg-primary text-white shadow hover:bg-primary/90 active-menu-icon", + reject: + "bg-red-500 text-white shadow hover:bg-red-500 active-menu-icon", + destructive: "bg-red-500 text-white shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: @@ -58,8 +57,12 @@ const ButtonBetter = React.forwardRef( ); } ); -const ButtonContainer: FC = ({ children, className, variant = "default" }) => { - const vr = variant ? variant: "default" +const ButtonContainer: FC = ({ + children, + className, + variant = "default", +}) => { + const vr = variant ? variant : "default"; return (
    {children}
    diff --git a/helpers/user.ts b/helpers/user.ts new file mode 100644 index 0000000..190fede --- /dev/null +++ b/helpers/user.ts @@ -0,0 +1,31 @@ +import api from "../utils/axios"; +import { userRoleMe } from "../utils/getAccess"; + +export const userToken = async () => { + const user = localStorage.getItem("user"); + if (user) { + const w = window as any; + w.user = JSON.parse(user); + }else{ + const res = await api.get(`${process.env.NEXT_PUBLIC_API_PORTAL}/api/check-jwt-token`); + const jwt = res.data.data; + console.log({jwt}) + if (!jwt) return ; + try { + await api.post(process.env.NEXT_PUBLIC_BASE_URL + "/api/cookies", { + token: jwt, + }); + const user = await api.get( + `${process.env.NEXT_PUBLIC_API_PORTAL}/api/users/me` + ); + const us = user.data.data; + console.log({us}) + if (us) { + localStorage.setItem("user", JSON.stringify(user.data.data)); + const roles = await userRoleMe(); + } + } catch (e) { + } + console.log({res}) + } +}; diff --git a/package.json b/package.json index c9f7e47..f13d63e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@emotion/css": "^11.13.5", "@faker-js/faker": "^9.2.0", "@floating-ui/react": "^0.26.28", + "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.4", @@ -13,6 +14,18 @@ "@radix-ui/react-tabs": "^1.1.1", "@react-pdf/renderer": "^4.1.5", "@tanstack/react-table": "^8.20.5", + "@tiptap/extension-color": "^2.11.2", + "@tiptap/extension-link": "^2.11.2", + "@tiptap/extension-list-item": "^2.11.2", + "@tiptap/extension-table": "^2.11.2", + "@tiptap/extension-table-cell": "^2.11.2", + "@tiptap/extension-table-header": "^2.11.2", + "@tiptap/extension-table-row": "^2.11.2", + "@tiptap/extension-text-align": "^2.11.2", + "@tiptap/extension-underline": "^2.11.2", + "@tiptap/pm": "^2.11.2", + "@tiptap/react": "^2.11.2", + "@tiptap/starter-kit": "^2.11.2", "@types/js-cookie": "^3.0.6", "@types/lodash.get": "^4.4.9", "@types/lodash.uniqby": "^4.7.9", @@ -35,10 +48,12 @@ "lodash.uniqby": "^4.7.0", "lucide-react": "^0.462.0", "next": "15.0.3", + "react-colorful": "^5.6.1", "react-icons": "^5.3.0", "react-resizable-panels": "^2.1.7", "react-slick": "^0.30.2", "sonner": "^1.7.0", + "tinycolor2": "^1.6.0", "uuid": "^11.0.3", "xlsx": "^0.18.5" }, @@ -47,6 +62,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "@types/react-slick": "^0.23.13", + "@types/tinycolor2": "^1.4.6", "eslint": "^8", "eslint-config-next": "15.0.3", "postcss": "^8", diff --git a/utils/apix.ts b/utils/apix.ts new file mode 100644 index 0000000..2a27631 --- /dev/null +++ b/utils/apix.ts @@ -0,0 +1,85 @@ +import get from "lodash.get"; +import api from "./axios"; + +type apixType = { + port: "portal" | "recruitment" | "mpp"; + path: string; + method?: "get" | "delete" | "post" | "put"; + data?: any; + value?: any; + validate?: "object" | "array" | "dropdown"; + keys?: { + value?: string; + label: string | ((item: any) => string); + }; +}; +export const apix = async ({ + port = "portal", + method = "get", + data, + value, + path, + validate = "object", + keys, +}: apixType) => { + const root_url = `${ + port === "portal" + ? process.env.NEXT_PUBLIC_API_PORTAL + : port === "recruitment" + ? process.env.NEXT_PUBLIC_API_RECRUITMENT + : port === "mpp" + ? process.env.NEXT_PUBLIC_API_MPP + : "" + }${path}`; + let result = null as any; + try { + try { + switch (method) { + case "get": + result = await api.get(root_url); + break; + + case "post": + result = await api.post(root_url, data); + break; + + case "put": + result = await api.put(root_url, data); + break; + + case "delete": + result = await api.delete(root_url, data); + break; + + default: + throw new Error(`Unsupported HTTP method: ${method}`); + } + } catch (ex: any) { + console.error( + "API Error:", + get(ex, "response.data.meta.message") || ex.message + ); + } + const val = get(result, value); + return validate === "object" + ? get(result, value) + : validate === "dropdown" && Array.isArray(get(result, value)) + ? val.map((e: any) => { + return { + value: keys?.value ? get(e, keys?.value) : get(e, "id"), + label: + typeof keys?.label === "function" + ? keys.label(e) + : keys?.label + ? get(e, keys?.label) + : null, + }; + }) + : Array.isArray(get(result, value)) + ? get(result, value) + : []; + } catch (error: any) { + console.error("API Error:", error.response || error.message); + throw error; + } +}; diff --git a/utils/cloneFm.ts b/utils/cloneFm.ts index 09e0c34..1208bfc 100644 --- a/utils/cloneFm.ts +++ b/utils/cloneFm.ts @@ -1,7 +1,7 @@ export const cloneFM = (fm: any, row: any) => { - // const result - - return { - ...fm, - data: row - } -} \ No newline at end of file + return { + ...fm, + data: row, + render: fm.render, + }; +}; diff --git a/utils/document_type.ts b/utils/document_type.ts new file mode 100644 index 0000000..97e3a4f --- /dev/null +++ b/utils/document_type.ts @@ -0,0 +1,42 @@ +export const labelDocumentType = (value?: string) => { + switch (value) { + case "ADMINISTRATIVE_SELECTION": + return "Administrative"; + break; + case "TEST": + return "Test"; + break; + case "INTERVIEW": + return "Interview"; + break; + case "FGD": + return "FGD"; + break; + case "SURAT_PENGANTAR_MASUK": + return "Surat Pengantar Masuk"; + break; + case "SURAT_IZIN_ORANG_TUA": + return "Surat Izin Orang Tua"; + break; + case "FINAL_INTERVIEW": + return "Final Interview"; + break; + + case "KARYAWAN_TETAP": + return "Karyawan Tetap"; + case "OFFERING_LETTER": + return "Offering Letter"; + break; + + case "CONTRACT_DOCUMENT": + return "Contract Document"; + break; + + case "DOCUMENT_CHECKING": + return "Document Checking"; + break; + + default: + return value; + } +}; diff --git a/utils/event.ts b/utils/event.ts index 2050a7a..c7a8893 100644 --- a/utils/event.ts +++ b/utils/event.ts @@ -1,25 +1,34 @@ import get from "lodash.get"; import { generateQueryString } from "./generateQueryString"; -type EventActions = "before-onload" | "onload-param" | string; +type EventActions = "before-onload" | "onload-param" | string; export const events = async (action: EventActions, data: any) => { - switch (action) { - case "onload-param": + switch (action) { + case "onload-param": + let params = { + ...data, + page: get(data, "paging"), + page_size: get(data, "take"), + search: get(data, "search"), + }; + params = { + ...params, + }; + if (params?.sort) { + params = { + ...params, + ...params?.sort, + }; + } + delete params["sort"]; + delete params["paging"]; + delete params["take"]; + return generateQueryString(params); + return; + break; - const params = { - ...data, - page: get(data, "paging"), - page_size: get(data, "take"), - search: get(data, "search") - }; - delete params["paging"] - delete params["take"] - return generateQueryString(params) - return - break; - - default: - break; - } - return null -} \ No newline at end of file + default: + break; + } + return null; +};