From a4275f4b663826b350f59186e1d0463a5abdd521 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 10:46:05 +0700 Subject: [PATCH 01/99] refactor(FE): Support UniformityDetail in confirmation preview --- .../production/uniformity/UniformityTable.tsx | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index e031e2c4..3df04a97 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -51,41 +51,54 @@ import MenuItem from '@/components/menu/MenuItem'; const UniformityConfirmationPreview = ({ uniformity, + uniformityDetail, }: { uniformity?: Uniformity; + uniformityDetail?: UniformityDetail; }) => { + const fileName = uniformityDetail?.info_umum?.file_name || '-'; + const data: DetailOptionType[] = [ { id: 'tanggal', label: 'Tanggal', value: uniformity ? formatDate(uniformity.applied_at, 'DD MMM YYYY') - : '-', + : uniformityDetail + ? formatDate(uniformityDetail.info_umum.tanggal, 'DD MMM YYYY') + : '-', }, { id: 'lokasi-farm', label: 'Lokasi Farm', - value: uniformity?.location_name || '-', + value: + uniformity?.location_name || + uniformityDetail?.info_umum?.lokasi_farm || + '-', }, { id: 'project-flock', label: 'Project Flock', - value: uniformity?.flock_name || '-', + value: + uniformity?.flock_name || + uniformityDetail?.info_umum?.project_flock || + '-', }, { id: 'kandang', label: 'Kandang', - value: uniformity?.kandang_name || '-', + value: + uniformity?.kandang_name || uniformityDetail?.info_umum?.kandang || '-', }, { id: 'file-uniformity', label: 'File Uniformity', - value: '-', + value: fileName, }, { id: 'status', label: 'Status', - value: uniformity?.status || '-', + value: uniformity?.status || (uniformityDetail ? 'CREATED' : '-'), }, ]; @@ -938,34 +951,7 @@ const UniformityTable = () => {
{createdUniformity ? ( ) : selectedRowIds.length === 1 ? ( Date: Tue, 6 Jan 2026 11:04:54 +0700 Subject: [PATCH 02/99] refactor(FE): Check delete API response before showing toast --- .../pages/expense/ExpenseRequestContent.tsx | 12 ++++++------ src/components/pages/expense/ExpensesTable.tsx | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 2b9086e0..657c5e5c 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -140,17 +140,17 @@ const ExpenseRequestContent = ({ const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - try { - await ExpenseApi.delete(initialValues?.id as number); + const deleteResponse = await ExpenseApi.delete(initialValues?.id as number); + if (isResponseSuccess(deleteResponse)) { toast.success('Berhasil menghapus data biaya operasional!'); router.push('/expense'); - } catch (error) { + } else { toast.error('Gagal menghapus data biaya operasional!'); - } finally { - deleteModal.closeModal(); - setIsDeleteLoading(false); } + + deleteModal.closeModal(); + setIsDeleteLoading(false); }; const confirmationModalCompleteClickHandler = async () => { diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 9ae3ed34..1f3e9df5 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -420,11 +420,19 @@ const ExpensesTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ExpenseApi.delete(selectedExpense?.id as number); - refreshExpenses(); + const deleteResponse = await ExpenseApi.delete( + selectedExpense?.id as number + ); + + if (isResponseSuccess(deleteResponse)) { + refreshExpenses(); + deleteModal.closeModal(); + toast.success('Berhasil menghapus biaya operasional!'); + } else { + deleteModal.closeModal(); + toast.error('Gagal menghapus biaya operasional!'); + } - deleteModal.closeModal(); - toast.success('Berhasil menghapus biaya operasional!'); setIsDeleteLoading(false); }; From 841aadc107d9f3675641f170fe9bc25418fc9b58 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 6 Jan 2026 13:29:05 +0700 Subject: [PATCH 03/99] fix(FE): fixing issue reject modal show up when creating project flock --- .../project-flock/ProjectFlockTable.tsx | 5 +- .../project-flock/form/ProjectFlockForm.tsx | 133 +----------------- 2 files changed, 5 insertions(+), 133 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7d9ce7da..f6888c3d 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -308,7 +308,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
)} - {approvals && !approvalsLoading && formType == 'detail' && ( - - )} - {formType == 'detail' && ( -
- - - - - - -
- )} +
- {/*
-
- {JSON.stringify(formik.values)} -
-
- {JSON.stringify(formik.errors)} -
-
*/} {formType !== 'detail' && ( - - { - confirmApprovalHandler(notes, approvalAction); - }, - }} - /> ); }; From f22c4e4798b45386fb2e77725dce400859db1181 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 13:32:59 +0700 Subject: [PATCH 04/99] refactor(FE): Adjust expense status badge colors --- src/components/pages/expense/ExpenseStatusBadge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index 3a84f6bc..a70b6454 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -21,7 +21,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { switch (latestApprovalStepNumber) { case 1: - expenseStatusPillBadgeColor = 'yellow'; + expenseStatusPillBadgeColor = 'gray'; break; case 2: @@ -33,7 +33,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { break; case 4: - expenseStatusPillBadgeColor = 'red'; + expenseStatusPillBadgeColor = 'yellow'; break; case 5: From 0af612703a5066a0d7a8eaf96350b9d0d28a1955 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 6 Jan 2026 13:46:18 +0700 Subject: [PATCH 05/99] fix(FE): remove pullet table, change doc table to conditional data base on project flock category --- .../pages/closing/ClosingDetail.tsx | 7 ++- .../ClosingSapronakCalculationTabContent.tsx | 10 +++-- .../ClosingSapronakCalculationTable.tsx | 45 +++++++------------ 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 94647f87..3de2ffe9 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -45,7 +45,12 @@ const ClosingDetail: React.FC = ({ { id: 'perhitunganSapronak', label: 'Perhitungan Sapronak', - content: , + content: ( + + ), }, { id: 'penjualan', diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx index 15e43bbc..b8add15b 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx @@ -1,21 +1,25 @@ 'use client'; -import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; -import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; +import { ClosingGeneralInformation } from '@/types/api/closing'; interface ClosingSapronakCalculationTabContentProps { projectFlockId?: number; + closingGeneralInformation?: ClosingGeneralInformation; } const ClosingSapronakCalculationTabContent = ({ projectFlockId, + closingGeneralInformation, }: ClosingSapronakCalculationTabContentProps) => { return (
{projectFlockId && ( <> - + )}
diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index 22b4d2e2..6e3b1a95 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -13,15 +13,16 @@ import { useMemo } from 'react'; import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; +import { ClosingGeneralInformation } from '@/types/api/closing'; interface ClosingSapronakCalculationTableProps { - type?: 'detail'; projectFlockId: number; + closingGeneralInformation?: ClosingGeneralInformation; } const ClosingSapronakCalculationTable = ({ - type, projectFlockId, + closingGeneralInformation, }: ClosingSapronakCalculationTableProps) => { const { data: sapronakCalculation, isLoading } = useSWR( `/closing/sapronak-calculation/${projectFlockId}`, @@ -182,8 +183,13 @@ const ClosingSapronakCalculationTable = ({ return (
+ {/* Table DOC jika kategori Project Flock Growing */} data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) + ? ((closingGeneralInformation?.project_category === 'GROWING' + ? sapronakCalculation.data?.doc?.rows + : sapronakCalculation.data?.pullet?.rows) ?? []) : [] } - columns={docColumns} + columns={ + closingGeneralInformation?.project_category === 'GROWING' + ? docColumns + : pulletColumns + } className={{ containerClassName: 'my-4', }} @@ -250,29 +262,6 @@ const ClosingSapronakCalculationTable = ({ renderFooter={isResponseSuccess(sapronakCalculation)} /> - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pullet?.rows ?? []) - : [] - } - columns={pulletColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={isResponseSuccess(sapronakCalculation)} - /> -
); }; From 2fa086bb3232692846e20679b67af2e6b10d224d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 14:01:01 +0700 Subject: [PATCH 06/99] refactor(FE): Prefer latest_approval action and add file_name --- .../production/uniformity/UniformityTable.tsx | 21 ++++++++++++------- src/types/api/production/uniformity.d.ts | 4 +--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 3df04a97..0c0c3f70 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -56,8 +56,6 @@ const UniformityConfirmationPreview = ({ uniformity?: Uniformity; uniformityDetail?: UniformityDetail; }) => { - const fileName = uniformityDetail?.info_umum?.file_name || '-'; - const data: DetailOptionType[] = [ { id: 'tanggal', @@ -93,7 +91,8 @@ const UniformityConfirmationPreview = ({ { id: 'file-uniformity', label: 'File Uniformity', - value: fileName, + value: + uniformity?.file_name || uniformityDetail?.info_umum?.file_name || '-', }, { id: 'status', @@ -461,9 +460,15 @@ const UniformityTable = () => { const canApproveReject = useMemo(() => { return ( selectedUniformities.length > 0 && - selectedUniformities.every( - (u) => u.status === 'CREATED' || u.status === 'Pengajuan' - ) + selectedUniformities.every((u) => { + const approvalAction = u.latest_approval?.action; + return ( + approvalAction === 'CREATED' || + approvalAction === 'Pengajuan' || + (!approvalAction && + (u.status === 'CREATED' || u.status === 'Pengajuan')) + ); + }) ); }, [selectedUniformities]); @@ -818,7 +823,9 @@ const UniformityTable = () => { accessorKey: 'status', header: 'Status', cell: (props) => { - const status = props.row.original.status; + const uniformity = props.row.original; + const status = + uniformity.latest_approval?.action ?? uniformity.status; return (
Date: Tue, 6 Jan 2026 14:42:52 +0700 Subject: [PATCH 07/99] feat(FE): adding stok information in form repeater SO and DO --- .../pages/marketing/MarketingTable.tsx | 3 +- .../delivery-order/DeliverOrderProduct.tsx | 17 +++++++++-- .../sales-order/SalesOrderProductForm.tsx | 30 +++++++++++++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 507819e3..1c37dbbb 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -682,7 +682,7 @@ const MarketingTable = () => { @@ -724,6 +724,7 @@ const MarketingTable = () => { }, ]} className={{ + containerClassName: 'p-6', tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', headerRowClassName: 'border-b border-b-gray-200', diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index a0eed811..5c81396e 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -15,6 +15,7 @@ import { BaseSalesOrder } from '@/types/api/marketing/marketing'; import Badge from '@/components/Badge'; import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; import * as Yup from 'yup'; +import { isResponseSuccess } from '@/lib/api-helper'; const DeliveryOrderProductForm = ({ formState, @@ -208,7 +209,7 @@ const DeliveryOrderProductForm = ({ ...formik.values, marketing_product_id: undefined, marketing_product: null, - qty: formik.values.qty || '', + qty: '', unit_price: '', total_price: '', avg_weight: '', @@ -222,7 +223,7 @@ const DeliveryOrderProductForm = ({ ...formik.values, marketing_product_id: selected.value as number, marketing_product: SalesProductToFieldValues(so), - qty: formik.values.qty || so.qty, + qty: so.qty, unit_price: so.unit_price, total_price: so.total_price, avg_weight: so.avg_weight, @@ -298,8 +299,18 @@ const DeliveryOrderProductForm = ({ isError={Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' + bottomLabel={ + formik.values.marketing_product_id + ? 'Stok dijual: ' + + salesOrders?.find( + (item) => item.id === formik.values.marketing_product_id + )?.qty + : '' + } /> - +
+
+
(''); + // ============ Formik ============ const formik = useFormik({ enableReinitialize: true, initialValues: { @@ -58,6 +63,7 @@ const SalesOrderProductForm = ({ isInitialValid: false, }); + // ===== Options ===== const { options: kandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions, @@ -86,12 +92,13 @@ const SalesOrderProductForm = ({ ); }, [warehouseSourceOptions, exisitingValues]); + // ===== Handler ===== const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldValue('kandang', val as OptionType); formik.setFieldValue('kandang_id', (val as OptionType)?.value); formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse', null); - formik.setFieldValue('qty', null); + formik.setFieldValue('qty', ''); }; const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -106,7 +113,7 @@ const SalesOrderProductForm = ({ formik.setFieldValue('qty', productWarehouse?.quantity); handleBlurField('qty'); } else { - formik.setFieldValue('qty', null); + formik.setFieldValue('qty', ''); } }; @@ -248,7 +255,24 @@ const SalesOrderProductForm = ({ isError={formik.touched.qty && Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' + bottomLabel={ + isResponseSuccess(warehouseSourceRawData) && + formik.values.product_warehouse_id + ? `Stok tersedia: ${formatNumber( + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.quantity ?? 0 + )} ${ + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.product?.uom?.name ?? '' + }` + : '' + } /> +
+
+
Date: Tue, 6 Jan 2026 16:06:26 +0700 Subject: [PATCH 08/99] fix(FE): shows delivery number when status marketing is delivery in marketing detail page --- .../marketing/detail/MarketingDetail.tsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/pages/marketing/detail/MarketingDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx index 677ea422..12ebda20 100644 --- a/src/components/pages/marketing/detail/MarketingDetail.tsx +++ b/src/components/pages/marketing/detail/MarketingDetail.tsx @@ -124,7 +124,10 @@ const MarketingDetail = ({ return ( <>
- + 2 ? 'Delivery Order' : 'Sales Order'}`} + backUrl='/marketing' + /> {!isLoadingApproval && approvals && ( )} @@ -202,8 +205,23 @@ const MarketingDetail = ({ No. Sales Order : - {initialValues?.so_number} + + {initialValues?.so_number} + + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + + No. Delivery Order + + : + + {initialValues?.delivery_order + ?.map((item) => item.do_number) + .join(', ')} + + + )} Nama Pelanggan : @@ -230,12 +248,27 @@ const MarketingDetail = ({ {initialValues?.notes ?? '-'} - Dokumen + Dokumen Penjualan : + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + Dokumen Pengiriman + : + + {initialValues?.delivery_order?.map((item, index) => ( + + ))} + + + )}
From 8dfccf25d8703db1b92b35809aed03161eeb4a1c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 19:32:13 +0700 Subject: [PATCH 09/99] refactor(FE): Truncate delivery document name in MovementForm --- src/components/pages/inventory/movement/form/MovementForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 3c49295e..64c87717 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -1562,7 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { width={20} height={20} /> - {delivery.document.name} + + {delivery.document.name} + ) : ( + ); +} + +function CarouselNext({ + className, + variant = 'outline', + size = 'icon', + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/src/figma-make/components/base/checkbox.tsx b/src/figma-make/components/base/checkbox.tsx new file mode 100644 index 00000000..930b49fe --- /dev/null +++ b/src/figma-make/components/base/checkbox.tsx @@ -0,0 +1,32 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from 'lucide-react'; + +import { cn } from '@/lib/helper'; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/src/figma-make/components/base/collapsible.tsx b/src/figma-make/components/base/collapsible.tsx new file mode 100644 index 00000000..3ed0ecd2 --- /dev/null +++ b/src/figma-make/components/base/collapsible.tsx @@ -0,0 +1,33 @@ +'use client'; + +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +function Collapsible({ + ...props +}: React.ComponentProps) { + return ; +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/src/figma-make/components/base/command.tsx b/src/figma-make/components/base/command.tsx new file mode 100644 index 00000000..90478b0e --- /dev/null +++ b/src/figma-make/components/base/command.tsx @@ -0,0 +1,177 @@ +'use client'; + +import * as React from 'react'; +import { Command as CommandPrimitive } from 'cmdk'; +import { SearchIcon } from 'lucide-react'; + +import { cn } from '@/lib/helper'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/figma-make/components/base/dialog'; + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandDialog({ + title = 'Command Palette', + description = 'Search for a command to run...', + children, + ...props +}: React.ComponentProps & { + title?: string; + description?: string; +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ); +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ); +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/figma-make/components/base/context-menu.tsx b/src/figma-make/components/base/context-menu.tsx new file mode 100644 index 00000000..c0b924fa --- /dev/null +++ b/src/figma-make/components/base/context-menu.tsx @@ -0,0 +1,252 @@ +'use client'; + +import * as React from 'react'; +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@/lib/helper'; + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function ContextMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function ContextMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/src/figma-make/components/base/date-picker.tsx b/src/figma-make/components/base/date-picker.tsx new file mode 100644 index 00000000..abd3414f --- /dev/null +++ b/src/figma-make/components/base/date-picker.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { + ChevronLeft, + ChevronRight, + Calendar as CalendarIcon, +} from 'lucide-react'; +import { Button } from '@/figma-make/components/base/button'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/figma-make/components/base/popover'; +import { Input } from '@/figma-make/components/base/input'; +import { Label } from '@/figma-make/components/base/label'; + +interface DatePickerProps { + date: string; + onDateChange: (date: string) => void; + disabled?: boolean; + placeholder?: string; + formatDisplay?: (date: string) => string; +} + +export function DatePicker({ + date, + onDateChange, + disabled = false, + placeholder = 'Select date', + formatDisplay, +}: DatePickerProps) { + const [open, setOpen] = useState(false); + const [currentMonth, setCurrentMonth] = useState(() => { + const d = date ? new Date(date) : new Date(); + return { year: d.getFullYear(), month: d.getMonth() }; + }); + + const defaultFormatDisplay = (dateStr: string) => { + if (!dateStr) return placeholder; + const d = new Date(dateStr + 'T00:00:00'); + return d.toLocaleDateString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + const formatDateInput = (dateStr: string) => { + if (!dateStr) return ''; + const d = new Date(dateStr + 'T00:00:00'); + return d.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); + }; + + const displayFormatter = formatDisplay || defaultFormatDisplay; + + const navigateMonth = (direction: 'prev' | 'next') => { + const newDate = new Date( + currentMonth.year, + currentMonth.month + (direction === 'next' ? 1 : -1) + ); + setCurrentMonth({ year: newDate.getFullYear(), month: newDate.getMonth() }); + }; + + const handleDateSelect = (dateStr: string) => { + onDateChange(dateStr); + setOpen(false); + }; + + const handleManualInput = (value: string) => { + onDateChange(value); + setOpen(false); + }; + + const renderCalendar = () => { + const { year, month } = currentMonth; + const firstDay = new Date(year, month, 1).getDay(); + const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1; // Monday = 0 + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const monthName = new Date(year, month).toLocaleDateString('en-US', { + month: 'long', + }); + + const days = []; + + // Empty cells before first day + for (let i = 0; i < adjustedFirstDay; i++) { + days.push(
); + } + + // Days of the month + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const isSelected = dateStr === date; + const isToday = dateStr === new Date().toISOString().split('T')[0]; + + days.push( + + ); + } + + return ( +
+
+ +
+ {monthName} {year} +
+ +
+
+ {['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].map((day) => ( +
+ {day} +
+ ))} +
+
{days}
+
+ ); + }; + + return ( + + + + + + {renderCalendar()} +
+ + handleManualInput(e.target.value)} + className='text-sm border-gray-200' + /> +
+
+
+ ); +} diff --git a/src/figma-make/components/base/date-range-picker.tsx b/src/figma-make/components/base/date-range-picker.tsx new file mode 100644 index 00000000..d1bca47e --- /dev/null +++ b/src/figma-make/components/base/date-range-picker.tsx @@ -0,0 +1,352 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { + ChevronLeft, + ChevronRight, + Calendar as CalendarIcon, +} from 'lucide-react'; +import { Button } from '@/figma-make/components/base/button'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/figma-make/components/base/popover'; +import { Input } from '@/figma-make/components/base/input'; + +interface DateRange { + from: string; + to: string; +} + +interface DateRangePickerProps { + dateFrom: string; + dateTo: string; + onDateChange: (from: string, to: string) => void; + disabled?: boolean; +} + +const PRESET_OPTIONS = [ + { label: 'Last 7 days', value: 'last_7_days' }, + { label: 'Last 14 Days', value: 'last_14_days' }, + { label: 'Last 30 Days', value: 'last_30_days' }, + { label: 'Last 3 months', value: 'last_3_months' }, + { label: 'Last 12 months', value: 'last_12_months' }, + { label: 'Month to date', value: 'month_to_date' }, + { label: 'Quarter to date', value: 'quarter_to_date' }, + { label: 'All time', value: 'all_time' }, + { label: 'Custom', value: 'custom' }, +]; + +export function DateRangePicker({ + dateFrom, + dateTo, + onDateChange, + disabled = false, +}: DateRangePickerProps) { + const [open, setOpen] = useState(false); + const [selectedPreset, setSelectedPreset] = useState('custom'); + const [tempDateFrom, setTempDateFrom] = useState(dateFrom); + const [tempDateTo, setTempDateTo] = useState(dateTo); + const [currentMonth1, setCurrentMonth1] = useState(() => { + const date = dateFrom ? new Date(dateFrom) : new Date(); + return { year: date.getFullYear(), month: date.getMonth() }; + }); + const [currentMonth2, setCurrentMonth2] = useState(() => { + const date = dateFrom ? new Date(dateFrom) : new Date(); + const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1); + return { year: nextMonth.getFullYear(), month: nextMonth.getMonth() }; + }); + + const formatDateDisplay = (dateStr: string) => { + if (!dateStr) return 'Select date'; + const date = new Date(dateStr); + return date.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); + }; + + const handlePresetClick = (preset: string) => { + setSelectedPreset(preset); + const today = new Date(); + let from = ''; + let to = today.toISOString().split('T')[0]; + + switch (preset) { + case 'last_7_days': + from = new Date(today.getTime() - 6 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + break; + case 'last_14_days': + from = new Date(today.getTime() - 13 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + break; + case 'last_30_days': + from = new Date(today.getTime() - 29 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0]; + break; + case 'last_3_months': + from = new Date( + today.getFullYear(), + today.getMonth() - 3, + today.getDate() + ) + .toISOString() + .split('T')[0]; + break; + case 'last_12_months': + from = new Date( + today.getFullYear(), + today.getMonth() - 12, + today.getDate() + ) + .toISOString() + .split('T')[0]; + break; + case 'month_to_date': + from = new Date(today.getFullYear(), today.getMonth(), 1) + .toISOString() + .split('T')[0]; + break; + case 'quarter_to_date': + const quarter = Math.floor(today.getMonth() / 3); + from = new Date(today.getFullYear(), quarter * 3, 1) + .toISOString() + .split('T')[0]; + break; + case 'all_time': + from = '2020-01-01'; + break; + case 'custom': + from = tempDateFrom; + to = tempDateTo; + break; + } + + setTempDateFrom(from); + setTempDateTo(to); + }; + + const handleSetDate = () => { + onDateChange(tempDateFrom, tempDateTo); + setOpen(false); + }; + + const handleCancel = () => { + setTempDateFrom(dateFrom); + setTempDateTo(dateTo); + setOpen(false); + }; + + const navigateMonth = (direction: 'prev' | 'next', calendar: 1 | 2) => { + if (calendar === 1) { + const newDate = new Date( + currentMonth1.year, + currentMonth1.month + (direction === 'next' ? 1 : -1) + ); + setCurrentMonth1({ + year: newDate.getFullYear(), + month: newDate.getMonth(), + }); + } else { + const newDate = new Date( + currentMonth2.year, + currentMonth2.month + (direction === 'next' ? 1 : -1) + ); + setCurrentMonth2({ + year: newDate.getFullYear(), + month: newDate.getMonth(), + }); + } + }; + + const renderCalendar = ( + year: number, + month: number, + onNavigate: (dir: 'prev' | 'next') => void + ) => { + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const monthName = new Date(year, month).toLocaleDateString('en-US', { + month: 'long', + }); + + const days = []; + for (let i = 0; i < firstDay; i++) { + days.push(
); + } + + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const isInRange = + tempDateFrom && + tempDateTo && + dateStr >= tempDateFrom && + dateStr <= tempDateTo; + const isStart = dateStr === tempDateFrom; + const isEnd = dateStr === tempDateTo; + const isToday = dateStr === new Date().toISOString().split('T')[0]; + + days.push( + + ); + } + + return ( +
+
+ +
+ {monthName} {year} +
+ +
+
+ {['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].map((day) => ( +
+ {day} +
+ ))} +
+
{days}
+
+ ); + }; + + return ( + + + + + +
+ {/* Preset Sidebar */} +
+ {PRESET_OPTIONS.map((preset) => ( + + ))} +
+ + {/* Calendar Section */} +
+
+ {renderCalendar(currentMonth1.year, currentMonth1.month, (dir) => + navigateMonth(dir, 1) + )} + {renderCalendar(currentMonth2.year, currentMonth2.month, (dir) => + navigateMonth(dir, 2) + )} +
+ + {/* Date Input & Actions */} +
+
+ + + +
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/figma-make/components/base/dialog.tsx b/src/figma-make/components/base/dialog.tsx new file mode 100644 index 00000000..ca74ac69 --- /dev/null +++ b/src/figma-make/components/base/dialog.tsx @@ -0,0 +1,140 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; + +import { cn } from '@/lib/helper'; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogOverlay.displayName = 'DialogOverlay'; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, children, ...props }, ref) => { + return ( + + + + {children} + + + Close + + + + ); +}); + +DialogContent.displayName = 'DialogContent'; + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/src/figma-make/components/base/drawer.tsx b/src/figma-make/components/base/drawer.tsx new file mode 100644 index 00000000..1a4f2993 --- /dev/null +++ b/src/figma-make/components/base/drawer.tsx @@ -0,0 +1,132 @@ +'use client'; + +import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; + +import { cn } from '@/lib/helper'; + +function Drawer({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ); +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/src/figma-make/components/base/dropdown-menu.tsx b/src/figma-make/components/base/dropdown-menu.tsx new file mode 100644 index 00000000..19154ca2 --- /dev/null +++ b/src/figma-make/components/base/dropdown-menu.tsx @@ -0,0 +1,257 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@/lib/helper'; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/src/figma-make/components/base/form.tsx b/src/figma-make/components/base/form.tsx new file mode 100644 index 00000000..436cd098 --- /dev/null +++ b/src/figma-make/components/base/form.tsx @@ -0,0 +1,168 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form'; + +import { cn } from '@/lib/helper'; +import { Label } from '@/figma-make/components/base/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue +); + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId(); + + return ( + +
+ + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( +