diff --git a/package-lock.json b/package-lock.json index 6b7c09e6..e1f28d3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -30,6 +31,7 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1636,6 +1638,13 @@ "@types/react": "*" } }, + "node_modules/@types/inputmask": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz", + "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2793,9 +2802,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.9.tgz", - "integrity": "sha512-741x1pGGSGHcrBYtdE7iKbqW1OoiijYdAZ8oJPZR9MhSKLcMBlHjKfN3YlM2/K7t5jd7O0sg4SqkVNPylalRFw==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz", + "integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==", "dev": true, "license": "MIT", "funding": { @@ -4193,6 +4202,12 @@ "node": ">=0.8.19" } }, + "node_modules/inputmask": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz", + "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", diff --git a/package.json b/package.json index 2e806ddd..b371e4e7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", + "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -32,6 +33,7 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", + "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/production/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx index af834b37..3ef73396 100644 --- a/src/app/production/chickin/add/page.tsx +++ b/src/app/production/chickin/add/page.tsx @@ -141,8 +141,8 @@ const AddChickin = () => { options={options} isLoading={isLoadingListProjectFlock} value={{ - label: `${projectFlock.data.flock.name} - ${projectFlock.data.category} - Periode ${projectFlock.data.period}`, - value: projectFlock.data.id, + label: `${projectFlock.data?.flock?.name} - ${projectFlock.data?.category} - Periode ${projectFlock.data?.period}`, + value: projectFlock.data?.id, }} onChange={(val) => router.push( @@ -159,7 +159,7 @@ const AddChickin = () => { - data={projectFlock.data.kandangs} + data={projectFlock.data?.kandangs} columns={[ { header: '#', @@ -202,7 +202,7 @@ const AddChickin = () => { containerClassName: cn({ 'mb-20': isResponseSuccess(projectFlock) && - projectFlock.data.kandangs?.length === 0, + projectFlock.data?.kandangs?.length === 0, }), tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', @@ -239,10 +239,10 @@ const AddChickin = () => { diff --git a/src/app/production/chickin/detail/page.tsx b/src/app/production/chickin/detail/page.tsx index aef7c4b7..96647c55 100644 --- a/src/app/production/chickin/detail/page.tsx +++ b/src/app/production/chickin/detail/page.tsx @@ -18,6 +18,11 @@ import { useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; +/** + * TODO: Refactor code - pindahin detail ke reuseable component + * setelah implement approval and reject + */ + const DetailChickin = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -112,6 +117,7 @@ const DetailChickin = () => { if (isResponseError(deleteProjectFlockRes)) { toast.error(deleteProjectFlockRes?.message as string); } + deleteModal.closeModal(); setIsDeleteLoading(false); }; diff --git a/src/app/production/recording/add/page.tsx b/src/app/production/recording/add/page.tsx new file mode 100644 index 00000000..d41fc183 --- /dev/null +++ b/src/app/production/recording/add/page.tsx @@ -0,0 +1,11 @@ +import RecordingForm from '@/components/pages/production/recording/form/RecordingForm'; + +const AddRecording = () => { + return ( +
+ +
+ ); +}; + +export default AddRecording; diff --git a/src/app/production/recording/detail/edit/page.tsx b/src/app/production/recording/detail/edit/page.tsx new file mode 100644 index 00000000..de53a354 --- /dev/null +++ b/src/app/production/recording/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import RecordingForm from '@/components/pages/production/recording/form/RecordingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const RecordingEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recordingId'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId, + (id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi + ); + + if (!recordingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingRecording && (!recording || isResponseError(recording))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingRecording && ( + + )} + {!isLoadingRecording && isResponseSuccess(recording) && ( + + )} +
+ ); +}; + +export default RecordingEdit; diff --git a/src/app/production/recording/detail/layout.tsx b/src/app/production/recording/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/production/recording/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/production/recording/detail/page.tsx b/src/app/production/recording/detail/page.tsx new file mode 100644 index 00000000..77b82a68 --- /dev/null +++ b/src/app/production/recording/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import RecordingForm from '@/components/pages/production/recording/form/RecordingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const RecordingDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recordingId'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId, + (id: number) => RecordingApi.getSingle(id) + ); + + if (!recordingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingRecording && (!recording || isResponseError(recording))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingRecording && ( + + )} + {!isLoadingRecording && isResponseSuccess(recording) && ( + + )} +
+ ); +}; + +export default RecordingDetail; diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx new file mode 100644 index 00000000..f31ac19a --- /dev/null +++ b/src/app/production/recording/page.tsx @@ -0,0 +1,11 @@ +import RecordingTable from '@/components/pages/production/recording/RecordingTable'; + +const Recording = () => { + return ( +
+ +
+ ); +}; + +export default Recording; diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx new file mode 100644 index 00000000..5dc5022d --- /dev/null +++ b/src/components/Badge.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { HTMLAttributes, ReactNode } from 'react'; + +import { cn } from '@/lib/helper'; + +export interface BadgeProps + extends Omit, 'className'> { + children?: ReactNode; + className?: { + badge?: string; + }; + variant?: 'default' | 'outline' | 'ghost' | 'soft' | 'dash'; + color?: + | 'neutral' + | 'primary' + | 'secondary' + | 'accent' + | 'info' + | 'success' + | 'warning' + | 'error'; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +} + +const Badge = ({ + children, + className, + variant = 'default', + color, + size = 'md', + ...props +}: BadgeProps) => { + const getBadgeClasses = () => { + const baseClasses = 'badge'; + + const variantClasses = { + default: '', + outline: 'badge-outline', + ghost: 'badge-ghost', + soft: 'badge-soft', + dash: 'badge-dash', + }; + + const colorClasses = { + neutral: 'badge-neutral', + primary: 'badge-primary', + secondary: 'badge-secondary', + accent: 'badge-accent', + info: 'badge-info', + success: 'badge-success', + warning: 'badge-warning', + error: 'badge-error', + }; + + const sizeClasses = { + xs: 'badge-xs', + sm: 'badge-sm', + md: 'badge-md', + lg: 'badge-lg', + xl: 'badge-xl', + }; + + return cn( + baseClasses, + variantClasses[variant], + color && colorClasses[color], + sizeClasses[size], + className?.badge + ); + }; + + return ( + + {children} + + ); +}; + +export default Badge; diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 1895c2c5..06438390 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,18 +1,17 @@ -"use client"; +'use client'; -import { HTMLAttributes, ReactNode } from "react"; +import { + HTMLAttributes, + ReactNode, +} from 'react'; -import { cn } from "@/lib/helper"; -import Image from "next/image"; +import { cn } from '@/lib/helper'; -export interface CardProps - extends Omit, "className"> { +export interface CardProps extends Omit, 'className'> { title?: string; subtitle?: string; image?: string; imageAlt?: string; - imageWidth?: number; - imageHeight?: number; actions?: ReactNode; footer?: ReactNode; className?: { @@ -24,8 +23,8 @@ export interface CardProps actions?: string; footer?: string; }; - variant?: "default" | "compact" | "bordered" | "shadow" | "image-full"; - size?: "sm" | "md" | "lg"; + variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full'; + size?: 'sm' | 'md' | 'lg'; } const Card = ({ @@ -33,110 +32,85 @@ const Card = ({ subtitle, image, imageAlt, - imageWidth, - imageHeight, actions, footer, className, - variant = "default", - size = "md", + variant = 'default', + size = 'md', children, ...props }: CardProps) => { const getCardClasses = () => { - const baseClasses = "card bg-base-100"; + const baseClasses = 'card bg-base-100'; const variantClasses = { - default: "", - compact: "card-compact", - bordered: "border border-base-300", - shadow: "shadow-xl", - "image-full": "card-side card-compact shadow-xl", + 'default': '', + 'compact': 'card-compact', + 'bordered': 'border border-base-300', + 'shadow': 'shadow-xl', + 'image-full': 'card-side card-compact shadow-xl', }; const sizeClasses = { - sm: "w-64", - md: "w-96", - lg: "w-[28rem]", + 'sm': 'w-64', + 'md': 'w-96', + 'lg': 'w-[28rem]', }; return cn( baseClasses, variantClasses[variant], - variant !== "image-full" ? sizeClasses[size] : "", - className?.wrapper, + variant !== 'image-full' ? sizeClasses[size] : '', + className?.wrapper ); }; - const getImageDimensions = () => { - if (variant === "image-full") { - return { - width: imageWidth || 128, - height: imageHeight || 128, - }; - } - - const cardWidths = { - sm: 256, // w-64 - md: 384, // w-96 - lg: 448, // w-[28rem] - }; - - return { - width: imageWidth || cardWidths[size], - height: imageHeight || 192, - }; - }; - const getImageClasses = () => { - if (variant === "image-full") { - return cn("object-cover", className?.image); + if (variant === 'image-full') { + return cn('w-32 h-32 object-cover', className?.image); } - return cn("w-full object-cover", className?.image); + return cn('h-48 object-cover', className?.image); }; const getBodyClasses = () => { - const baseClasses = "card-body"; + const baseClasses = 'card-body'; - if (variant === "compact" || variant === "image-full") { - return cn(baseClasses, "p-4", className?.body); + if (variant === 'compact' || variant === 'image-full') { + return cn(baseClasses, 'p-4', className?.body); } - return cn(baseClasses, "p-6", className?.body); + return cn(baseClasses, 'p-6', className?.body); }; const getTitleClasses = () => { const sizeClasses = { - sm: "text-lg", - md: "text-xl", - lg: "text-2xl", + 'sm': 'text-lg', + 'md': 'text-xl', + 'lg': 'text-2xl', }; - return cn("card-title font-bold", sizeClasses[size], className?.title); + return cn('card-title font-bold', sizeClasses[size], className?.title); }; const getSubtitleClasses = () => { - return cn("text-base-content/70 text-sm mt-1", className?.subtitle); + return cn('text-base-content/70 text-sm mt-1', className?.subtitle); }; const getActionsClasses = () => { - return cn("card-actions justify-end mt-4", className?.actions); + return cn('card-actions justify-end mt-4', className?.actions); }; const getFooterClasses = () => { - return cn("border-t border-base-300 mt-4 pt-4", className?.footer); + return cn('border-t border-base-300 mt-4 pt-4', className?.footer); }; - if (variant === "image-full" && image) { - const imageDimensions = getImageDimensions(); + if (variant === 'image-full' && image) { return (
- {imageAlt
@@ -155,11 +129,9 @@ const Card = ({
{image && (
- {imageAlt
diff --git a/src/components/Collapse.tsx b/src/components/Collapse.tsx index cb05d5b0..8506f65c 100644 --- a/src/components/Collapse.tsx +++ b/src/components/Collapse.tsx @@ -68,7 +68,7 @@ export const Collapse = ({ 'collapse', variant === 'arrow' && 'collapse-arrow', variant === 'plus' && 'collapse-plus', - bordered && 'border base-content/20 border-opacity-20 rounded-box', + bordered && 'border base-content/20 border-opacity-20 rounded', disabled && 'opacity-60 pointer-events-none', !open && 'w-fit', className diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index 4375ca20..89b02845 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -1,10 +1,10 @@ -'use client'; +"use client"; -import { ChangeEvent } from 'react'; -import { NumericFormat, OnValueChange } from 'react-number-format'; -import TextInput, { TextInputProps } from '@/components/input/TextInput'; +import { ChangeEvent, ReactNode } from "react"; +import { NumericFormat, OnValueChange } from "react-number-format"; +import TextInput, { TextInputProps } from "@/components/input/TextInput"; -interface NumberInputProps extends Omit { +interface NumberInputProps extends Omit { thousandSeparator?: string; decimalSeparator?: string; decimalScale?: number; @@ -12,19 +12,23 @@ interface NumberInputProps extends Omit { prefix?: string; suffix?: string; fixedDecimalScale?: boolean; + inputPrefix?: ReactNode; + inputSuffix?: ReactNode; } const NumberInput = ({ - thousandSeparator = ',', - decimalSeparator = '.', + thousandSeparator = ",", + decimalSeparator = ".", decimalScale = 5, allowNegative = true, onChange, + inputPrefix, + inputSuffix, ...restProps }: NumberInputProps) => { const valueChangeHandler: OnValueChange = ( numberFormatValues, - sourceInfo + sourceInfo, ) => { const newChangeEvent = sourceInfo.event as | ChangeEvent @@ -45,6 +49,8 @@ const NumberInput = ({ onValueChange={valueChangeHandler} decimalScale={decimalScale} allowNegative={allowNegative} + startAdornment={inputPrefix} + endAdornment={inputSuffix} {...restProps} /> ); diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index cbb0c9db..9b57cabc 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -104,7 +104,7 @@ const SelectInput = (props: SelectInputProps) => { const SelectComponent = createables ? CreatableSelect : Select; /** 🎯 handleChange tanpa any */ - const handleChange = (val: MultiValue | SingleValue | null): void => { + const handleChange = (val: MultiValue | SingleValue): void => { if (!val) { onChange?.(null); return; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx index d8d58929..0e7891f7 100644 --- a/src/components/input/TextArea.tsx +++ b/src/components/input/TextArea.tsx @@ -1,10 +1,6 @@ 'use client'; -import { - ChangeEventHandler, - FocusEventHandler, - ReactNode, -} from 'react'; +import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; import { cn } from '@/lib/helper'; @@ -52,7 +48,7 @@ const TextArea = ({ onBlur, readOnly = false, isLoading = false, - rows = 3 + rows = 3, }: TextAreaProps) => { return (
)} - {startAdornment && startAdornment} + {startAdornment && startAdornment} -