diff --git a/src/app/production/transfer-to-laying/page.tsx b/src/app/production/transfer-to-laying/page.tsx index 5d790345..be8ff454 100644 --- a/src/app/production/transfer-to-laying/page.tsx +++ b/src/app/production/transfer-to-laying/page.tsx @@ -1,15 +1,25 @@ import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable'; import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal'; import TransferToLayingDetailModal from '@/components/pages/production/transfer-to-laying/TransferToLayingDetailModal'; +import RequirePermission from '@/components/helper/RequirePermission'; const TransferToLaying = () => { return (
- + + + - + + +
); }; diff --git a/src/components/Card.tsx b/src/components/Card.tsx index c78766e1..ce7c1c57 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -123,6 +123,10 @@ const Card = ({ return cn(baseClasses, 'p-6', className?.body); }; + const getCollapsibleClasses = () => { + return cn('', className?.collapsible); + }; + const getTitleClasses = () => { const sizeClasses = { sm: 'text-lg', @@ -213,6 +217,7 @@ const Card = ({ titleClassName='w-full cursor-pointer' contentClassName='p-0' fullWidth={true} + className={getCollapsibleClasses()} > {cardContent} diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx index bbc36782..fc0af9fb 100644 --- a/src/components/Drawer.tsx +++ b/src/components/Drawer.tsx @@ -16,7 +16,6 @@ interface DrawerProps { onBackdropClick?: () => void; closeOnBackdropClick?: boolean; expandedContent?: ReactNode; - expandedWidth?: string; } type DrawerClassName = { @@ -25,6 +24,7 @@ type DrawerClassName = { drawerSide?: string; drawerOverlay?: string; drawerSidebarContent?: string; + drawerExpandedContent?: string; }; const Drawer = ({ @@ -39,7 +39,6 @@ const Drawer = ({ onBackdropClick, closeOnBackdropClick = true, expandedContent, - expandedWidth = 'w-[400px]', }: DrawerProps) => { const getDrawerClassNames = (): DrawerClassName => { const baseClassNames = { @@ -56,6 +55,9 @@ const Drawer = ({ ? 'w-full lg:min-w-[600px] lg:max-w-[600px]' : 'w-full max-w-[300px] lg:w-[300px]'; } + if (className?.drawerSidebarContent) { + return ''; + } return 'w-full sm:min-w-120 sm:w-fit'; }; @@ -174,7 +176,7 @@ const Drawer = ({
diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 0be39fb5..b40d9db5 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -42,6 +42,7 @@ interface TableClassNames { footerRowClassName?: string; footerColumnClassName?: string; paginationClassName?: string; + skeletonCellClassName?: string; } export interface TableProps { @@ -79,7 +80,9 @@ export interface TableProps { getSubRows?: (originalRow: TData, index: number) => TData[] | undefined; } -const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; +const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ + id: index, +})); const emptyContentDefaultValue = (
@@ -414,7 +417,14 @@ const Table = ({ cell.getContext() )} - {isLoading &&
} + {isLoading && ( +
+ )} ))} diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 8f685452..8a06f9ed 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -25,8 +25,10 @@ export interface TabsProps wrapper?: string; tab?: string; content?: string; + tabHeaderWrapper?: string; }; onTabChange?: (tabId: string) => void; + sideContent?: ReactNode; } const Tabs = ({ @@ -38,6 +40,7 @@ const Tabs = ({ activeTabId: controlledActiveId, className, onTabChange, + sideContent, ...props }: TabsProps) => { // State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset @@ -59,6 +62,7 @@ const Tabs = ({ wrapper: wrapperClassName, tab: tabClassName, content: contentClassName, + tabHeaderWrapper: tabHeaderWrapperClassName, } = typeof className === 'object' ? className : { wrapper: className, tab: undefined }; @@ -102,6 +106,10 @@ const Tabs = ({ tabClassName ); + const getSideContentClasses = () => { + return cn('flex flex-row', tabHeaderWrapperClassName); + }; + const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content; return ( @@ -112,18 +120,21 @@ const Tabs = ({ typeof className === 'string' ? className : containerClassName )} > -
- {tabs.map(({ id, label, disabled }) => ( - - ))} +
+
+ {tabs.map(({ id, label, disabled }) => ( + + ))} +
+ {sideContent && sideContent}
{activeContent && ( diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index aca86a88..cff1d167 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -19,11 +19,11 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { variant='outline' color='none' className={cn( - 'padding-[12px] rounded-[8px] max-h-[40px] font-semibold text-[14px] gap-[6px]', + 'rounded-lg max-h-10 font-semibold text-sm gap-1.5', 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', getFilledFormikValuesCount(values) > 0 - ? 'border-primary-gradient !rounded-[8px]' - : '!rounded-[8px]', + ? 'border-primary-gradient text-primary rounded-lg!' + : 'rounded-lg', props.className )} > @@ -37,7 +37,7 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { /> Filter {getFilledFormikValuesCount(values) > 0 && ( - + {getFilledFormikValuesCount(values)} )} diff --git a/src/components/helper/PermissionNotFound.tsx b/src/components/helper/PermissionNotFound.tsx index 75e48c62..e2823b8b 100644 --- a/src/components/helper/PermissionNotFound.tsx +++ b/src/components/helper/PermissionNotFound.tsx @@ -1,10 +1,17 @@ +import Button from '@/components/Button'; + const PermissionNotFound = () => { return (
-

Permission Not Found

+

+ Hak Akses Tidak Ditemukan +

- You do not have permission to access this page. + Anda tidak memiliki hak akses untuk mengakses halaman ini.

+
); }; diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx index c4f99593..f9725fff 100644 --- a/src/components/helper/StatusBadge.tsx +++ b/src/components/helper/StatusBadge.tsx @@ -27,6 +27,7 @@ const StatusBadge = ({ 'bg-success/30': color === 'success', 'bg-error/20': color === 'error', 'bg-primary/20': color === 'info', + 'bg-[#FF9A20]/12': color === 'warning', }, className?.badge ), @@ -43,6 +44,7 @@ const StatusBadge = ({ 'text-[#008000]': color === 'success', 'text-error': color === 'error', 'text-primary': color === 'info', + 'text-[#FF9A20]': color === 'warning', })} > diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx index f9d70a04..8bb635ae 100644 --- a/src/components/helper/drawer/DrawerHeader.tsx +++ b/src/components/helper/drawer/DrawerHeader.tsx @@ -58,6 +58,7 @@ const DrawerHeader = ({ if (leftIconOnClick) { return ( {isRange && ( - )} diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index 2085d943..674f3719 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -226,7 +226,7 @@ const DashboardProduction = () => { variant='outline' color='none' className={cn( - 'p-2 rounded-lg font-semibold text-sm gap-1.5', + 'rounded-lg font-semibold text-sm gap-1.5', 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft' )} > diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index 092e4bca..58749cf2 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; import { OptionType } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; @@ -721,24 +722,18 @@ const DashboardLineChart = ({ return (
{/* Chart icon */} -
-
+ -
-
- - {/* Empty state text */} -

- Data Not Yet Available -

-

- Please change your filters to get the data. -

+ } + title='Data Not Yet Available' + description='Please change your filters to get the data.' + />
); } diff --git a/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx index 6e4e4b97..7f19a6d0 100644 --- a/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx +++ b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx @@ -1,5 +1,6 @@ import { Icon } from '@iconify/react'; import { DashboardMeta } from '@/types/api/dashboard/dashboard'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => { return ( @@ -24,50 +25,35 @@ const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => { {!meta?.filters && ( <> {/* Filter icon */} -
-
-
- -
-
-
- - {/* Empty state text */} -

- No Filters Selected -

-

- Please choose filters to narrow down your results and make - your search easier. -

+ + } + title='No Filters Selected' + description='Please choose filters to narrow down your results and make your search easier.' + /> )} {meta?.filters && ( <> {/* Filter icon */} -
-
+ -
-
- - {/* Empty state text */} -

- Data Not Yet Available -

-

- Please change your filters to get the data. -

+ } + title='Data Not Yet Available' + description='Please change your filters to get the data.' + /> )}
diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index b2f50361..dbb30314 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -86,6 +86,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { } // ===== USE SELECT HOOKS ===== + const { + setInputValue: setSourceWarehouseSelectInputValue, + isLoadingOptions: isLoadingSourceWarehouses, + loadMore: loadMoreSourceWarehouses, + rawData: sourceWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { + transfer_context: 'inventory_transfer', + }); + const { setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, @@ -136,6 +145,25 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { return stockMap; }, [allProductWarehouses]); + const sourceWarehouseOptions = useMemo(() => { + if (!isResponseSuccess(sourceWarehouses)) return []; + + return ( + sourceWarehouses?.data.map((w) => { + warehouseStockMap.get(w.id); + return { + value: w.id, + label: w.name, + area: w.area?.name, + location: + 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG') + ? w.location?.name + : undefined, + }; + }) || [] + ); + }, [sourceWarehouses, warehouseStockMap]); + const warehouseOptions = useMemo(() => { if (!isResponseSuccess(warehouses)) return []; @@ -1354,10 +1382,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { placeholder='Pilih gudang asal...' value={formik.values.source_warehouse} onChange={handleSourceWarehouseChange} - options={warehouseOptions} - onInputChange={setWarehouseSelectInputValue} - onMenuScrollToBottom={loadMoreWarehouses} - isLoading={isLoadingWarehouses} + options={sourceWarehouseOptions} + onInputChange={setSourceWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreSourceWarehouses} + isLoading={isLoadingSourceWarehouses} isError={ formik.touched.source_warehouse_id && Boolean(formik.errors.source_warehouse_id) diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index 87541d8c..399468c7 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -98,6 +98,7 @@ const TransferToLayingFormModal = () => { 'search', { category: 'GROWING', + transfer_context: 'transfer_to_laying', } ); diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 6ddf50d3..8c9eab84 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -5,6 +5,7 @@ import UniformityGaugeChart from '@/components/pages/production/uniformity/chart import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; import { Uniformity, type ChartData } from '@/types/api/production/uniformity'; +import { Icon } from '@iconify/react'; interface UniformityChartProps { uniformityData?: Uniformity | null; @@ -101,15 +102,26 @@ const UniformityChart = ({ const shouldShowEmptyState = !isFiltered; return ( -
+
+
+
+ Performance Overview{' '} + +
+
{shouldShowEmptyState || !uniformityData || @@ -120,26 +132,31 @@ const UniformityChart = ({ )}
- {shouldShowEmptyState || !uniformityData || !gaugeChartData ? ( - + +
+
+ Weekly Performance{' '} + +
+
+ {shouldShowEmptyState || !uniformityData || !gaugeChartData ? ( -
- ) : ( - + ) : ( - - )} + )} +
); }; diff --git a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx index ac14ebb5..5c8f1313 100644 --- a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx @@ -3,7 +3,7 @@ import { usePathname, useRouter } from 'next/navigation'; import Drawer from '@/components/Drawer'; import React, { ReactNode } from 'react'; -import UniformityTable from '@/components/pages/production/uniformity/UniformityTable'; +import Uniformity from '@/app/production/uniformity/page'; import { useUiStore } from '@/stores/ui/ui.store'; export default function UniformityPageWrapper({ @@ -40,8 +40,8 @@ export default function UniformityPageWrapper({ return ( <> -
- +
+
{children}
: null} expandedContent={expandedDrawerOpen ? expandedDrawerContent : null} - expandedWidth='w-[500px]' + className={{ + drawerSidebarContent: 'w-[446px]', + drawerExpandedContent: 'w-[446px]', + }} /> ); diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 29d1155a..a3530032 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -19,11 +19,11 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { type BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; -import Card from '@/components/Card'; import UniformityTableSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; @@ -42,15 +42,12 @@ import { ProjectFlock, } from '@/types/api/production/project-flock'; import { - getStatusColor, - getStatusIndicatorColor, getStatusText, + getStatusBadgeColor, } from '@/components/pages/production/uniformity/uniformity-utils'; import { generateUniformityPDF } from '@/components/pages/production/uniformity/export/UniformityExportPDF'; import { generateUniformityExcel } from '@/components/pages/production/uniformity/export/UniformityExportExcel'; import Dropdown from '@/components/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import { useFormik } from 'formik'; import { UniformityTableFilterSchema, @@ -114,12 +111,18 @@ const UniformityConfirmationPreview = ({ const columns: ColumnDef[] = [ { accessorKey: 'label', - header: 'Label', + enableSorting: false, + header: () => ( + Label + ), cell: (props) => props.row.original.label, }, { accessorKey: 'value', - header: 'Value', + enableSorting: false, + header: () => ( + Value + ), cell: (props) => { const id = props.row.original.id; const value = props.row.original.value; @@ -127,16 +130,10 @@ const UniformityConfirmationPreview = ({ if (id === 'status') { return (
- - {getStatusText(value)} - +
); } @@ -217,7 +214,6 @@ const UniformityTable = () => { const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; const singleDeleteModal = useModal(); const successModal = useModal(); @@ -241,7 +237,7 @@ const UniformityTable = () => { const [filterEndDate, setFilterEndDate] = useState(''); const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] = useState(''); - const [filterErrors, setFilterErrors] = useState>({}); + const [, setFilterErrors] = useState>({}); const { setInputValue: setFilterLocationInputValue, @@ -312,7 +308,6 @@ const UniformityTable = () => { ? projectFlockKandangLookupData.data : undefined; - // Update filterProjectFlockKandangId when lookup changes useEffect(() => { if (projectFlockKandangLookup?.id) { setFilterProjectFlockKandangId(projectFlockKandangLookup.id); @@ -486,12 +481,6 @@ const UniformityTable = () => { [filterFormik] ); - const handleApplyFilters = useCallback(() => { - handleFormSubmit( - new Event('submit') as unknown as React.FormEvent - ); - }, [handleFormSubmit]); - const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) .filter((key) => rowSelection[key]) @@ -819,7 +808,7 @@ const UniformityTable = () => { ); return ( -
+
{ }, cell: ({ row }) => { return ( -
+
{ { accessorKey: 'week', header: 'Tanggal (Week)', - cell: (props) => - `${formatDate(props.row.original.applied_at, 'DD MMM YYYY')} (${props.row.original.week})`, + cell: (props) => ( + + {`${formatDate(props.row.original.applied_at, 'DD MMM YYYY')} (${props.row.original.week})`} + + ), }, { accessorKey: 'status', @@ -872,20 +864,11 @@ const UniformityTable = () => { const uniformity = props.row.original; const status = uniformity.latest_approval?.action ?? uniformity.status; - return ( -
- - {getStatusText(status)} - -
- ); + + const badgeColor = getStatusBadgeColor(status); + const statusText = getStatusText(status); + + return ; }, }, { @@ -900,334 +883,410 @@ const UniformityTable = () => { [] ); + // ===== CALCULATE FILTER COUNT ===== + const filterCount = useMemo(() => { + let count = 0; + + if (filterStartDate && filterEndDate) { + count += 1; + } + + if (filterLocation) { + count += 1; + } + + if (filterProjectFlock) { + count += 1; + } + + if (filterKandang) { + count += 1; + } + + return count; + }, [ + filterStartDate, + filterEndDate, + filterLocation, + filterProjectFlock, + filterKandang, + ]); + + const isFilterActive = filterCount > 0; + return ( <> -
-
- - - -
- -
- - - - - Export +
+
+
+ + - } - align='end' - > - - - - - + +
+ +
+ + + +
+ + + Export + +
+ + +
+ + } + > + + + +
-
-
+
+ +
+
-
- -
- - + data={isResponseSuccess(uniformities) ? uniformities?.data : []} + columns={uniformityColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0} + totalItems={ + isResponseSuccess(uniformities) + ? uniformities?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} className={{ - wrapper: 'my-4 w-full relative', + containerClassName: cn('p-3 pt-0', { + 'mb-20': + isResponseSuccess(uniformities) && + uniformities?.data?.length === 0, + }), + headerColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + bodyColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + }} + emptyContent={} + /> + + - - data={isResponseSuccess(uniformities) ? uniformities?.data : []} - columns={uniformityColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0} - totalItems={ - isResponseSuccess(uniformities) - ? uniformities?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(uniformities) && - uniformities?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full ', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - emptyContent={} - /> - - -
- {createdUniformity ? ( - - ) : selectedRowIds.length === 1 ? ( - - ) : ( -
- {selectedRowIds.length} data dipilih -
- )} -
-
- - -
- -
-
- - -
- {selectedRowIds.length === 1 ? ( - - ) : ( -
- {selectedRowIds.length} data dipilih -
- )} -
-
- - -
- -
-
- - -
- {selectedRowIds.length === 1 ? ( - - ) : ( -
- {selectedRowIds.length} data dipilih -
- )} -
-
- - -
- {selectedRowIds.length === 1 ? ( - - ) : ( -
- {selectedRowIds.length} data dipilih -
- )} -
-
- - {/* Filter Modal */} - -
- {/* Modal Header */} -
-
- -

Filter Data

-
- +
+ {createdUniformity ? ( + + ) : selectedRowIds.length === 1 ? ( + + ) : ( +
+ {selectedRowIds.length} data dipilih
+ )} +
+ - {/* Error List Alert */} - {formErrorList.length > 0 && ( -
- -
- )} + +
+ +
+
-
-
-
+ +
+ {selectedRowIds.length === 1 ? ( + + ) : ( +
+ {selectedRowIds.length} data dipilih +
+ )} +
+
+ + +
+ +
+
+ + +
+ {selectedRowIds.length === 1 ? ( + + ) : ( +
+ {selectedRowIds.length} data dipilih +
+ )} +
+
+ + +
+ {selectedRowIds.length === 1 ? ( + + ) : ( +
+ {selectedRowIds.length} data dipilih +
+ )} +
+
+ + {/* Filter Modal */} + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+
+ {/* Rentang Waktu */} +
+ +
-
- -
+
@@ -1299,74 +1358,83 @@ const UniformityTable = () => { className={{ wrapper: 'w-full' }} />
+ + {formErrorList.length > 0 && ( +
+ +
+ )}
{/* Action Buttons */} -
+
-
-
+ +
+ - {/* Floating Actions Button */} - - + permissions: 'lti.production.uniformity.detail', + }, + { + action: 'DELETE', + icon: 'mdi:delete-outline', + label: 'Delete', + hidden: selectedRowIds.length !== 1, + onClick: handleDelete, + permissions: 'lti.production.uniformity.delete', + }, + ]} + approvals={[ + { + action: 'APPROVED', + icon: 'mdi:check-circle-outline', + label: 'Approve', + onClick: handleBulkApprove, + permissions: 'lti.production.uniformity.approve', + disabled: !canApproveReject, + }, + { + action: 'REJECTED', + icon: 'mdi:close-circle-outline', + label: 'Reject', + onClick: handleBulkReject, + permissions: 'lti.production.uniformity.approve', + disabled: !canApproveReject, + }, + ]} + selectedRowIds={selectedRowIds} + onClose={handleCloseFab} + /> ); }; diff --git a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx index 82f0085c..f43aa244 100644 --- a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx @@ -164,7 +164,7 @@ const UniformityBarChart: React.FC = ({ data }) => { const margin = { top: 20, right: 30, - left: 20, + left: 0, bottom: 5, }; @@ -172,7 +172,7 @@ const UniformityBarChart: React.FC = ({ data }) => { diff --git a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx index 54f8e4ec..8249e1d5 100644 --- a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx @@ -65,12 +65,12 @@ const UniformityGaugeChart: React.FC = ({ -
- +
+ {value}% -
- +
+ {label}
@@ -81,7 +81,7 @@ const UniformityGaugeChart: React.FC = ({ @@ -610,8 +611,8 @@ const UniformityForm = ({ > @@ -621,7 +622,7 @@ const UniformityForm = ({
{!isNextStep && ( - - - + <> +
+ + + + )}
diff --git a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx index 63ba34fd..3cc120fd 100644 --- a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx @@ -50,12 +50,16 @@ const UniformityPreviewForm = () => { () => [ { accessorKey: 'number', - header: 'No', + header: () => ( + Number + ), cell: (props) => props.row.original.number, }, { accessorKey: 'weight', - header: 'Weight (g)', + header: () => ( + Weight (g) + ), cell: (props) => {props.row.original.weight}, }, ], @@ -68,19 +72,18 @@ const UniformityPreviewForm = () => { {/* Form Section */} -
-
+
{verifyUniformityResult ? (
@@ -90,7 +93,11 @@ const UniformityPreviewForm = () => { className={{ containerClassName: 'mb-5' }} /> - diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index df144c64..eaf51103 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -14,12 +14,11 @@ import { useRouter } from 'next/navigation'; import toast from 'react-hot-toast'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError } from '@/lib/api-helper'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { formatNumber } from '@/lib/helper'; import { - getWeightStatusColor, - getWeightStatusIndicatorColor, getWeightStatusText, + getWeightStatusBadgeColor, } from '@/components/pages/production/uniformity/uniformity-utils'; import { DetailOptionType } from '@/types/api/production/uniformity'; import { @@ -121,13 +120,19 @@ const UniformityResultForm = () => { () => [ { accessorKey: 'label', - header: 'Label', + header: () => ( + Label + ), cell: (props) => props.row.original.label, + enableSorting: false, }, { accessorKey: 'value', - header: 'Value', + header: () => ( + Value + ), cell: (props) => {props.row.original.value}, + enableSorting: false, }, ], [] @@ -161,13 +166,19 @@ const UniformityResultForm = () => { () => [ { accessorKey: 'label', - header: 'Label', + header: () => ( + Label + ), cell: (props) => props.row.original.label, + enableSorting: false, }, { accessorKey: 'value', - header: 'Value', + header: () => ( + Value + ), cell: (props) => {props.row.original.value}, + enableSorting: false, }, ], [] @@ -190,7 +201,7 @@ const UniformityResultForm = () => { () => [ { accessorKey: 'number', - header: 'No', + header: 'Number', cell: (props) => props.row.original.number, }, { @@ -204,30 +215,12 @@ const UniformityResultForm = () => { cell: (props) => { const status = props.row.original.status; return status ? ( -
- - {getWeightStatusText(status)} - -
+ ) : ( - - Unknown - + ); }, }, @@ -241,23 +234,24 @@ const UniformityResultForm = () => { {/* Form Section */} -
-
+
{verifyUniformityResult ? ( -
+
-

Sampling and Range

+

+ Sampling and Range +

data={samplingTableData} columns={columnsSampling} @@ -270,7 +264,9 @@ const UniformityResultForm = () => {
-

Result

+

+ Result +

data={resultTableData} columns={resultColumns} @@ -281,7 +277,7 @@ const UniformityResultForm = () => { }} />
-
+
data={tableData} columns={columnsUniformity} @@ -296,7 +292,7 @@ const UniformityResultForm = () => { onClick={handleSubmit} isLoading={isSubmitting} disabled={!uniformityFormData} - className='mb-10' + className='mb-5 px-3 py-2.5 text-sm text-base-100 rounded-lg shadow-sm' > Submit diff --git a/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx index 123a456a..3ce40a3a 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx @@ -1,4 +1,3 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; const LeftLegend = () => { @@ -45,11 +44,11 @@ const ChartArea = () => { ))}
-
+
{ranges.map((range) => (
))}
@@ -65,28 +64,38 @@ const ChartArea = () => { const EmptyState = () => { return ( - <> -
-
- +
+
+ {/* Filter icon */} +
+
+
+ +
+
- + + {/* Empty state text */} +

No Filters Selected - - +

+

Please choose filters to narrow down your results and make your search easier. - +

- +
); }; const UniformityBarChartSkeleton = () => { return ( -
+
diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 17ed7ee9..2ff7155b 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -1,4 +1,3 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import React from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; @@ -55,22 +54,29 @@ const UniformityGaugeChartSkeleton: React.FC< -
-
- +
+ {/* Filter icon */} +
+
+
+ +
+
- + + {/* Empty state text */} +

No Filters Selected - - +

+

Please choose filters to narrow down your results and make your search easier. - +

diff --git a/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx index b90048a1..e1bbd4a9 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx @@ -1,24 +1,30 @@ -import Button from '@/components/Button'; import { Icon } from '@iconify/react'; const UniformityTableSkeleton = () => { return ( -
-
- +
+ {/* Document icon */} +
+
+
+ +
+
- + + {/* Empty state text */} +

No Data Available - - +

+

There is no uniformity data displayed. Enter uniformity check data to get started. - +

); }; diff --git a/src/components/pages/production/uniformity/uniformity-utils.ts b/src/components/pages/production/uniformity/uniformity-utils.ts index 340bb555..f25f9293 100644 --- a/src/components/pages/production/uniformity/uniformity-utils.ts +++ b/src/components/pages/production/uniformity/uniformity-utils.ts @@ -1,46 +1,24 @@ -export const weightStatusColorMap: Record = { - ideal: 'bg-[#00D39033]', - outside: 'bg-error/10', -}; - -export const weightStatusIndicatorColorMap: Record = { - ideal: 'bg-[#008000]', - outside: 'bg-error', -}; - export const weightStatusTextMap: Record = { ideal: 'Ideal', outside: 'Outside', }; -export const getWeightStatusColor = (status: string): string => { - return weightStatusColorMap[status] || 'bg-info'; -}; - -export const getWeightStatusIndicatorColor = (status: string): string => { - return weightStatusIndicatorColorMap[status] || 'bg-info'; -}; - export const getWeightStatusText = (status: string): string => { return weightStatusTextMap[status] || status; }; -export const statusColorMap: Record = { - APPROVED: 'bg-[#00D39033]', - Disetujui: 'bg-[#00D39033]', - REJECTED: 'bg-error/10', - Ditolak: 'bg-error/10', - CREATED: 'bg-[#f3f3f4]', - Pengajuan: 'bg-[#f3f3f4]', +export const weightStatusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' +> = { + ideal: 'success', + outside: 'error', }; -export const statusIndicatorColorMap: Record = { - APPROVED: 'bg-[#008000]', - Disetujui: 'bg-[#008000]', - REJECTED: 'bg-error', - Ditolak: 'bg-error', - CREATED: 'bg-[#D9D9D9]', - Pengajuan: 'bg-[#D9D9D9]', +export const getWeightStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' => { + return weightStatusBadgeColorMap[status] || 'neutral'; }; export const statusTextMap: Record = { @@ -52,14 +30,32 @@ export const statusTextMap: Record = { Pengajuan: 'Pengajuan', }; -export const getStatusColor = (status: string): string => { - return statusColorMap[status] || 'bg-info'; -}; - -export const getStatusIndicatorColor = (status: string): string => { - return statusIndicatorColorMap[status] || 'bg-info'; -}; - export const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; + +export const statusBadgeColorMap: Record< + string, + 'success' | 'error' | 'neutral' | 'info' +> = { + APPROVED: 'success', + Disetujui: 'success', + approved: 'success', + disetujui: 'success', + REJECTED: 'error', + Ditolak: 'error', + rejected: 'error', + ditolak: 'error', + CREATED: 'neutral', + Pengajuan: 'neutral', + created: 'neutral', + pengajuan: 'neutral', + PENDING: 'neutral', + pending: 'neutral', +}; + +export const getStatusBadgeColor = ( + status: string +): 'success' | 'error' | 'neutral' | 'info' => { + return statusBadgeColorMap[status] || 'neutral'; +}; diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index 67702035..2ed5a9cb 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -10,7 +10,13 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; import Collapse from '@/components/Collapse'; -import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + cn, + formatCurrency, + formatDate, + formatNumber, + formatVechicleNumber, +} from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { DailyMarketingRow } from '@/types/api/report/marketing'; import { MarketingReportApi } from '@/services/api/report/marketing-report'; @@ -94,7 +100,9 @@ const DailyMarketingsTable = ({ accessorKey: 'vehicle_number', header: 'No. Polisi', cell: (props) => ( - {props.row.original.vehicle_number} + + {formatVechicleNumber(props.row.original.vehicle_number)} + ), }, { diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index 58d1e78b..ffb0d3f1 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -1,28 +1,43 @@ 'use client'; +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; const FinanceTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useFinanceTabStore((state) => state.tabActions); + const tabs = [ { id: '1', label: 'Rekapitulasi Hutang Ke Supplier', - - content: , + content: , }, { id: '2', label: 'Kontrol Pembayaran Customer', - - content: , + content: , }, ]; return ( -
- +
+
); }; diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index 869430b0..edcd360f 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -281,16 +281,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { No - + No. PR - + No. PO - + Tgl Terima/Bayar - + Tgl PO @@ -320,7 +320,12 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Status - + No. Perjalanan @@ -330,16 +335,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* NO */} - + {/* No. PR */} - + {/* No. PO */} - + {/* Tgl Terima/Bayar */} - + {/* Tgl PO */} @@ -381,8 +386,13 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Status */} - - {/* No. Perjalanan */} + + @@ -400,13 +410,13 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {index + 1} - + {item.pr_number || '-'} - + {item.po_number || '-'} - + {item.received_date ? item.received_date != '-' @@ -415,7 +425,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { : '-'} - + {item.po_date ? item.po_date != '-' @@ -526,7 +536,12 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { - )} - + {item.travel_number || '-'} @@ -538,18 +553,18 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Total - - - - - - + + + + + + {formatNumber(supplierReport.total.aging)} Hari diff --git a/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx new file mode 100644 index 00000000..8ab0ffd3 --- /dev/null +++ b/src/components/pages/report/finance/skeleton/CustomerSupplierSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { CustomerPaymentRow } from '@/types/api/report/customer-payment'; +import { ColumnDef } from '@tanstack/react-table'; + +const CustomerSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default CustomerSupplierSkeleton; diff --git a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx new file mode 100644 index 00000000..b9397f8f --- /dev/null +++ b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx @@ -0,0 +1,38 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { DebtRow } from '@/types/api/report/debt-supplier'; +import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; + +const DebtSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default DebtSupplierSkeleton; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index dc1705ed..2987455a 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; @@ -11,9 +11,10 @@ import { FinanceApi } from '@/services/api/report/finance-report'; import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { CustomerPaymentReport, + CustomerPaymentRow, CustomerPaymentSummary, } from '@/types/api/report/customer-payment'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -26,8 +27,14 @@ import { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; -const CustomerPaymentTab = () => { +interface CustomerPaymentTabProps { + tabId: string; +} + +const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -111,6 +118,10 @@ const CustomerPaymentTab = () => { }; // ===== FILTER HANDLERS ===== + const handleFilterModalOpen = useCallback(() => { + filterModal.openModal(); + }, [filterModal]); + const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); @@ -298,6 +309,92 @@ const CustomerPaymentTab = () => { } }, [customerPaymentExport]); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useFinanceTabStore((state) => state.setTabActions); + const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
+ + + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + handleExportExcel, + handleExportPdf, + filterModal.open, + setTabActions, + ]); + + // Cleanup on unmount + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + const getTableColumns = ( summary: CustomerPaymentSummary ): ColumnDef[] => { @@ -552,192 +649,40 @@ const CustomerPaymentTab = () => { }; return ( -
- -
- - - - - Export - - } - align='end' - > - - - - - -
- - {/* Filter Modal */} - -
- {/* Modal Header */} -
-
- -

Filter Data

-
- -
-
-
-
- { - setFilterStartDate(e.target.value); - }} - className={{ wrapper: 'w-full' }} - /> -
- -
- { - setFilterEndDate(e.target.value); - }} - className={{ wrapper: 'w-full' }} - /> -
-
- -
- { - setFilterCustomer( - Array.isArray(val) ? val : val ? [val] : [] - ); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> -
- - {/* TODO: Uncomment when BE is ready */} - {/*
- { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
*/} - - {/* TODO: Uncomment when BE is ready */} - {/*
- -
*/} -
- - {/* Action Buttons */} -
- - -
-
-
- + <> +
{!isSubmitted ? ( -
- Silakan klik tombol Filter untuk mengatur filter dan menampilkan - data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? (
) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((customerReport) => { const summary = customerReport.summary || { @@ -757,15 +702,17 @@ const CustomerPaymentTab = () => { title={customerReport.customer.name} subtitle={`(${customerReport.customer.address})`} className={{ - wrapper: 'w-full rounded-2xl', + wrapper: 'w-full rounded-lg border-none', body: 'p-0', title: - 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', subtitle: - 'px-3 pb-1 bg-primary text-white text-sm font-normal', + 'px-2 pb-1.5 bg-primary text-white text-xs font-normal', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} + defaultCollapsed={true} >
{ renderFooter={customerReport.rows.length > 0} className={{ containerClassName: 'w-full mb-0!', - tableWrapperClassName: 'overflow-x-auto', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -799,8 +747,8 @@ const CustomerPaymentTab = () => { if (row.index === 0) { return ( - {selectedEmployees.map((emp) => ( + {sortedSelectedEmployees.map((emp) => ( {activity.employees.length > 0 && - activity.employees[0].note ? ( + activity.employees[ + activity.employees.length - 1 + ].note ? (

- {activity.employees[0].note} + { + activity.employees[ + activity.employees.length - 1 + ].note + }

) : (

diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx index 9fa75c33..33ad2608 100644 --- a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; @@ -404,7 +404,22 @@ export function MasterConfigurationContent() { {/* Add/Edit Modal */} -

+ { + if (!open) { + setIsFormInvalid(false); + setConfigurationForm({ + id: 0, + date: '', + percentage_threshold_bad: '', + percentage_threshold_enough: '', + }); + } + + setShowModal(open); + }} + > diff --git a/src/stores/finance-tab/finance-tab.store.ts b/src/stores/finance-tab/finance-tab.store.ts new file mode 100644 index 00000000..9b5cf096 --- /dev/null +++ b/src/stores/finance-tab/finance-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type FinanceTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useFinanceTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'FinanceTabStore', + } + ) +);
{ ); }) )} - - + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ { + setFilterStartDate(e.target.value); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + /> +
+ + { + setFilterEndDate(e.target.value); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + /> +
+
+ + { + setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); + }} + onInputChange={setCustomerInputValue} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + {/* TODO: Uncomment when BE is ready */} + {/*
+ { + setFilterSales(Array.isArray(val) ? val : val ? [val] : []); + }} + onInputChange={setSalesInputValue} + isLoading={isLoadingSales} + isClearable + onMenuScrollToBottom={loadMoreSales} + className={{ wrapper: 'w-full' }} + /> +
*/} + + {/* TODO: Uncomment when BE is ready */} + {/*
+ +
*/} + + {/* Action Buttons */} +
+
+ + +
+
+ ); }; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index c5065d29..646cfb15 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -2,10 +2,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; @@ -22,7 +19,7 @@ import { generateDebtSupplierExcel } from '@/components/pages/report/finance/exp import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; @@ -32,11 +29,14 @@ import { DebtSupplierFilterType, } from '@/components/pages/report/finance/filter/DebtSupplierFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; -import Badge from '@/components/Badge'; import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import StatusBadge from '@/components/helper/StatusBadge'; +import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; const dueStatus: Record = { 'Sudah Jatuh Tempo': 'error', @@ -60,22 +60,14 @@ const getPillBadge = ( ? dueStatus[statusText] || 'neutral' : paymentStatus[statusText] || 'neutral'; - return ( - - {statusText} - - ); + return ; }; -const DebtSupplierTab = () => { +interface DebtSupplierTabProps { + tabId: string; +} + +const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -271,7 +263,78 @@ const DebtSupplierTab = () => { } }, [debtSupplierExport]); - const getTableColumns = (supplier: DebtSupplier): ColumnDef[] => [ + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useFinanceTabStore((state) => state.setTabActions); + const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
+ + + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + formik.values, + isAnyExportLoading, + handleExportExcel, + handleExportPdf, + setTabActions, + ]); + + // Cleanup on unmount + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { id: 'no', header: 'No', @@ -337,8 +400,10 @@ const DebtSupplierTab = () => { return
{formatNumber(value)} Hari
; }, footer: () => { - const value = supplier.total.aging; - return
{formatNumber(value)} Hari
; + const value = supplier?.total.aging; + return ( +
{formatNumber(value || 0)} Hari
+ ); }, }, { @@ -399,10 +464,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.total_price; + const value = supplier?.total.total_price; return ( -
- {formatCurrency(value)} +
+ {formatCurrency(value || 0)}
); }, @@ -421,10 +486,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.payment_price; + const value = supplier?.total.payment_price; return ( -
- {formatCurrency(value)} +
+ {formatCurrency(value || 0)}
); }, @@ -443,10 +508,10 @@ const DebtSupplierTab = () => { ); }, footer: () => { - const value = supplier.total.debt_price; + const value = supplier?.total.debt_price; return ( -
- {formatCurrency(value)} +
+ {formatCurrency(value || 0)}
); }, @@ -478,52 +543,39 @@ const DebtSupplierTab = () => { ]; return ( <> -
- -
- - - - - Export - - } - align='end' - > - - - - - -
-
- +
{!isSubmitted ? ( -
- Silakan klik tombol Filter untuk mengatur filter dan menampilkan - data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? (
) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((supplierReport) => { return ( @@ -531,10 +583,11 @@ const DebtSupplierTab = () => { key={supplierReport.supplier.id} title={supplierReport.supplier.name} className={{ - wrapper: 'w-full !rounded-lg', - body: 'p-0 rounded-lg', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', title: - 'ps-2 pt-1 pb-1 font-normal text-md bg-primary text-white', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} @@ -551,8 +604,9 @@ const DebtSupplierTab = () => { pageSize={supplierReport.rows.length + 1} renderFooter={supplierReport.rows.length > 0} className={{ - containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto', + containerClassName: 'w-full mb-0', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', headerColumnClassName: cn( TABLE_DEFAULT_STYLING.headerColumnClassName, 'whitespace-nowrap' @@ -617,33 +671,34 @@ const DebtSupplierTab = () => { ref={filterModal.ref} className={{ modal: 'p-0', - modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm', + modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm', }} > -
+ {/* Modal Header */} -
+
-

Filter Data

+

Filter Data

-
-
-
+ + {/* Modal Body */} +
+
+ +
{ @@ -654,12 +709,10 @@ const DebtSupplierTab = () => { formik.touched.startDate && !!formik.errors.startDate } errorMessage={formik.errors.startDate} + isNestedModal /> -
- -
+
{ @@ -668,6 +721,7 @@ const DebtSupplierTab = () => { className={{ wrapper: 'w-full' }} isError={formik.touched.endDate && !!formik.errors.endDate} errorMessage={formik.errors.endDate} + isNestedModal />
@@ -730,15 +784,19 @@ const DebtSupplierTab = () => {
{/* Action Buttons */} -
+
-
diff --git a/src/config/constant.ts b/src/config/constant.ts index 5eb5df38..3c336df9 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -5,6 +5,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Dashboard', link: '/dashboard', icon: 'heroicons-outline:chart-bar-square', + permission: ['lti.dashboard.list'], }, { text: 'Daily Checklist', @@ -81,6 +82,8 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ permission: [ 'lti.production.project_flocks.list', 'lti.production.recording.list', + 'lti.production.transfer_to_laying.list', + 'lti.production.uniformity.list', ], submenu: [ { @@ -96,6 +99,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { text: 'Transfer ke Laying', link: '/production/transfer-to-laying', + permission: ['lti.production.transfer_to_laying.list'], }, { text: 'Uniformity', @@ -114,11 +118,13 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Penjualan', link: '/marketing', icon: 'heroicons-outline:currency-dollar', + permission: ['lti.marketing.delivery_order.list'], }, { text: 'Keuangan', link: '/finance', icon: 'heroicons-outline:banknotes', + permission: ['lti.finance.transactions.list'], }, { text: 'Biaya', @@ -136,26 +142,46 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Laporan', link: '/report', icon: 'mdi:chart-box-outline', + permission: [ + 'lti.repport.debtsupplier.list', + 'lti.repport.customerpayment.list', + 'lti.repport.purchasesupplier.list', + 'lti.repport.expense.list', + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + 'lti.repport.production_result.list', + ], submenu: [ { text: 'Keuangan', link: '/report/finance', + permission: [ + 'lti.repport.debtsupplier.list', + 'lti.repport.customerpayment.list', + ], }, { text: 'Logistik & Persediaan', link: '/report/logistic-stock', + permission: ['lti.repport.purchasesupplier.list'], }, { text: 'Biaya Operasional', link: '/report/expense', + permission: ['lti.repport.expense.list'], }, { text: 'Penjualan', link: '/report/marketing', + permission: [ + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + ], }, { text: 'Hasil Produksi', link: '/report/production-result', + permission: ['lti.repport.production_result.list'], }, ], }, @@ -204,6 +230,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ 'lti.master.suppliers.list', 'lti.master.uoms.list', 'lti.master.warehouses.list', + 'lti.master.production_standards.list', ], submenu: [ { @@ -274,6 +301,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { text: 'Standar Produksi', link: '/master-data/production-standard', + permission: ['lti.master.production_standards.list'], }, ], }, diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 44f3728e..20ee5292 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -116,7 +116,10 @@ export const ROUTE_PERMISSIONS: Record = { // Report '/report/logistic-stock/': ['lti.repport.purchasesupplier.list'], '/report/expense/': ['lti.repport.expense.list'], - '/report/marketing/': ['lti.repport.delivery.list'], + '/report/marketing/': [ + 'lti.repport.delivery.list', + 'lti.repport.gethppperkandang.list', + ], '/report/production-result/': ['lti.repport.production_result.list'], '/report/finance/': [ 'lti.repport.finance.list', diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 37430354..266b8740 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -127,6 +127,10 @@ export function DailyChecklistContent() { { id: number; name: string }[] >([]); + const sortedSelectedEmployees = selectedEmployees.toSorted((a, b) => + a.name.localeCompare(b.name) + ); + const [dailyChecklistId, setDailyChecklistId] = useState(null); const [checklistStatus, setChecklistStatus] = useState('DRAFT'); // const [isEditMode, setIsEditMode] = useState(false); @@ -486,6 +490,11 @@ export function DailyChecklistContent() { return; } + if (!tempSelectedPhaseIds.length) { + toast.error('Pilih minimal satu fase'); + return; + } + try { // Insert new phase links const setDailyChecklistPhaseRes = @@ -535,14 +544,6 @@ export function DailyChecklistContent() { } }; - const toggleSelectAllAbk = () => { - if (tempSelectedEmployees.length === employees.length) { - setTempSelectedEmployees([]); - } else { - setTempSelectedEmployees([...employees]); - } - }; - const applyAbkSelection = async () => { if (!dailyChecklistId) { toast.error('Checklist belum tersedia'); @@ -853,10 +854,34 @@ export function DailyChecklistContent() { ); const isAllAbkSelected = - tempSelectedEmployees.length === employees.length && employees.length > 0; + tempSelectedEmployees.length === filteredEmployees.length && + filteredEmployees.length > 0 && + tempSelectedEmployees.every((tempSelectedEmployee) => { + return ( + filteredEmployees.findIndex( + (filteredEmployee) => filteredEmployee.id === tempSelectedEmployee.id + ) >= 0 + ); + }); const isAllPhasesSelected = - tempSelectedPhaseIds.length === availablePhases.length && - availablePhases.length > 0; + tempSelectedPhaseIds.length === filteredPhases.length && + filteredPhases.length > 0 && + tempSelectedPhaseIds.every((tempSelectedPhaseId) => { + return ( + filteredPhases.findIndex( + (filteredPhase) => + String(filteredPhase.id) === String(tempSelectedPhaseId) + ) >= 0 + ); + }); + + const toggleSelectAllAbk = () => { + if (isAllAbkSelected) { + setTempSelectedEmployees([]); + } else { + setTempSelectedEmployees([...filteredEmployees]); + } + }; // Group activities by PHASE → TIME_TYPE → ACTIVITIES const groupActivitiesByPhase = () => { @@ -1130,7 +1155,7 @@ export function DailyChecklistContent() {
Aktivitas + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + console.log(activities); + activities.forEach((activity, index) => { const taskId = taskIdsByPhaseActivityId[activity.id]; @@ -1244,7 +1277,7 @@ export function DailyChecklistContent() {

)} - {selectedEmployees.map((emp) => ( + {sortedSelectedEmployees.map((emp) => (
)} @@ -1519,14 +1554,14 @@ export function DailyChecklistContent() { setTempSelectedPhaseIds([]); } else { setTempSelectedPhaseIds( - availablePhases.map((p) => String(p.id)) + filteredPhases.map((p) => String(p.id)) ); } }} className='checkbox-clean' /> - Pilih Semua ({availablePhases.length} Fase) + Pilih Semua ({filteredPhases.length} Fase) @@ -1621,7 +1656,7 @@ export function DailyChecklistContent() { /> - {employees.length > 0 && ( + {filteredEmployees.length > 0 && (
diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 0b3ece27..d8723df0 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -275,6 +275,13 @@ export function DetailDailyChecklistContent() { ]) ).values() ); + + uniqueEmployees.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + setEmployees(uniqueEmployees); // Group data by Phase → Time Type → Activity @@ -779,11 +786,23 @@ export function DetailDailyChecklistContent() { } // ACTIVITY rows - timeGroup.activities.forEach((activity, index) => { + const activities = timeGroup.activities; + + activities.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + activities.forEach((activity, index) => { const indentClass = hasMultipleTimeTypes ? 'pl-12' : 'pl-8'; + console.log({ + activity, + }); + rows.push(