Merge branch 'staging' into 'production'

Staging

See merge request mbugroup/lti-web-client!229
This commit is contained in:
Adnan Zahir
2026-01-21 15:15:04 +07:00
38 changed files with 2036 additions and 657 deletions
+927 -14
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -19,6 +19,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"exceljs": "^4.4.0",
"formik": "^2.4.6", "formik": "^2.4.6",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
@@ -55,7 +56,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"daisyui": "^5.5.8", "daisyui": "^5.5.14",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "^15.5.7",
"husky": "^9.1.7", "husky": "^9.1.7",
+1
View File
@@ -57,6 +57,7 @@
@theme { @theme {
--font-inter: var(--font-inter); --font-inter: var(--font-inter);
--font-roboto: var(--font-roboto);
--container-sm: 40rem; --container-sm: 40rem;
--container-md: 48rem; --container-md: 48rem;
+10 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google'; import { Inter, Roboto } from 'next/font/google';
import '@/app/globals.css'; import '@/app/globals.css';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
@@ -12,6 +12,12 @@ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
}); });
const roboto = Roboto({
variable: '--font-roboto',
subsets: ['latin'],
weight: ['200', '300', '400', '500', '600', '700', '900'],
});
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: '#1f74bf', themeColor: '#1f74bf',
colorScheme: 'light', colorScheme: 'light',
@@ -30,7 +36,9 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang='en' data-theme='lti'> <html lang='en' data-theme='lti'>
<body className={`${inter.variable} antialiased font-inter`}> <body
className={`${inter.variable} ${roboto.variable} antialiased font-inter`}
>
<RequireAuth> <RequireAuth>
<MainDrawer>{children}</MainDrawer> <MainDrawer>{children}</MainDrawer>
</RequireAuth> </RequireAuth>
+1 -1
View File
@@ -162,7 +162,7 @@ const Drawer = ({
<div <div
className={cn( className={cn(
varianClassName?.drawerSidebarContent, varianClassName?.drawerSidebarContent,
className?.drawerContent, className?.drawerSidebarContent,
'overflow-y-auto' 'overflow-y-auto'
)} )}
> >
+22 -13
View File
@@ -26,29 +26,34 @@ const MainDrawerContent = () => {
}; };
return ( return (
<div className='w-full p-4 flex flex-col gap-4'> <div className='w-full flex flex-col'>
<div className='flex flex-row items-center gap-4'> <div className='p-3 flex flex-row items-center gap-4 border-b border-base-content/10'>
<Image <div className='flex flex-row items-center gap-2'>
src='/assets/img/lti-logo.png' <Image
alt='MBU Logo' src='/assets/img/lti-logo.png'
width={256} alt='LTI Logo'
height={256} width={40}
className='w-full max-w-16 h-auto' height={40}
/> className='w-full max-w-10 h-auto'
/>
<h1 className='text-xl font-bold'>LTI ERP</h1> <div className='font-roboto'>
<h1 className='text-sm font-semibold'>LTI ERP</h1>
<p className='text-sm text-black/50'>Lumbung Telur Indonesia</p>
</div>
</div>
<div className='grow flex flex-row justify-end sm:hidden'> <div className='grow flex flex-row justify-end sm:hidden'>
<Button <Button
variant='soft' variant='soft'
color='error' color='error'
onClick={closeMainDrawerHandler} onClick={closeMainDrawerHandler}
className='rounded-full' className='p-1 rounded-full'
> >
<Icon <Icon
icon='material-symbols:close-rounded' icon='material-symbols:close-rounded'
width={24} width={16}
height={24} height={16}
/> />
</Button> </Button>
</div> </div>
@@ -121,6 +126,10 @@ const MainDrawer = ({
setOpen={setMainDrawerOpen} setOpen={setMainDrawerOpen}
openOnLarge openOnLarge
sidebarContent={<MainDrawerContent />} sidebarContent={<MainDrawerContent />}
className={{
drawerSide: 'border-r border-base-content/10',
drawerSidebarContent: 'min-w-[244px] lg:w-[244px]',
}}
> >
<main className='w-full h-full flex flex-col'> <main className='w-full h-full flex flex-col'>
<Navbar title={pageTitle as string} toggleSidebar={toggleSidebar} /> <Navbar title={pageTitle as string} toggleSidebar={toggleSidebar} />
+64 -11
View File
@@ -86,7 +86,7 @@ export const TABLE_DEFAULT_STYLING = {
tableHeaderClassName: '', tableHeaderClassName: '',
headerRowClassName: '', headerRowClassName: '',
headerColumnClassName: headerColumnClassName:
'px-4 py-3 border-base-content/10 text-base-content/50', 'px-4 py-3 border-base-content/10 text-base-content/50 text-sm font-medium',
tableBodyClassName: '', tableBodyClassName: '',
bodyRowClassName: 'border-t border-base-content/10', bodyRowClassName: 'border-t border-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content', bodyColumnClassName: 'px-4 py-3 text-base-content',
@@ -222,14 +222,37 @@ const Table = <TData extends object>({
}, [pageSize, setPageSize]); }, [pageSize, setPageSize]);
return ( return (
<div className={tableClassNames.containerClassName}> <div
<div className={tableClassNames.tableWrapperClassName}> className={cn(
<table className={tableClassNames.tableClassName}> TABLE_DEFAULT_STYLING.containerClassName,
<thead className={tableClassNames.tableHeaderClassName}> tableClassNames.containerClassName
)}
>
<div
className={cn(
TABLE_DEFAULT_STYLING.tableWrapperClassName,
tableClassNames.tableWrapperClassName
)}
>
<table
className={cn(
TABLE_DEFAULT_STYLING.tableClassName,
tableClassNames.tableClassName
)}
>
<thead
className={cn(
TABLE_DEFAULT_STYLING.tableHeaderClassName,
tableClassNames.tableHeaderClassName
)}
>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr <tr
key={headerGroup.id} key={headerGroup.id}
className={tableClassNames.headerRowClassName} className={cn(
TABLE_DEFAULT_STYLING.headerRowClassName,
tableClassNames.headerRowClassName
)}
> >
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
const columnRelativeDepth = const columnRelativeDepth =
@@ -262,6 +285,7 @@ const Table = <TData extends object>({
{ {
'border-b': header.colSpan > 1, 'border-b': header.colSpan > 1,
}, },
TABLE_DEFAULT_STYLING.headerColumnClassName,
tableClassNames.headerColumnClassName tableClassNames.headerColumnClassName
)} )}
> >
@@ -311,7 +335,12 @@ const Table = <TData extends object>({
))} ))}
</thead> </thead>
<tbody className={tableClassNames.tableBodyClassName}> <tbody
className={cn(
TABLE_DEFAULT_STYLING.tableBodyClassName,
tableClassNames.tableBodyClassName
)}
>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
const customRowContent = renderCustomRow?.(row); const customRowContent = renderCustomRow?.(row);
@@ -320,12 +349,19 @@ const Table = <TData extends object>({
} }
return ( return (
<tr key={row.id} className={tableClassNames.bodyRowClassName}> <tr
key={row.id}
className={cn(
TABLE_DEFAULT_STYLING.bodyRowClassName,
tableClassNames.bodyRowClassName
)}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <td
key={cell.id} key={cell.id}
className={cn( className={cn(
{ 'first:w-9 first:pr-0': withCheckbox }, { 'first:w-9 first:pr-0': withCheckbox },
TABLE_DEFAULT_STYLING.bodyColumnClassName,
tableClassNames.bodyColumnClassName tableClassNames.bodyColumnClassName
)} )}
> >
@@ -342,14 +378,25 @@ const Table = <TData extends object>({
); );
})} })}
</tbody> </tbody>
<tfoot className={cn(tableClassNames.tableFooterClassName)}> <tfoot
className={cn(
TABLE_DEFAULT_STYLING.tableFooterClassName,
tableClassNames.tableFooterClassName
)}
>
{renderFooter && ( {renderFooter && (
<tr className={cn(tableClassNames.footerRowClassName)}> <tr
className={cn(
TABLE_DEFAULT_STYLING.footerRowClassName,
tableClassNames.footerRowClassName
)}
>
{table.getAllLeafColumns().map((column) => ( {table.getAllLeafColumns().map((column) => (
<td <td
key={column.id} key={column.id}
className={cn( className={cn(
{ 'first:w-9 first:pr-0': withCheckbox }, { 'first:w-9 first:pr-0': withCheckbox },
TABLE_DEFAULT_STYLING.footerColumnClassName,
tableClassNames.footerColumnClassName tableClassNames.footerColumnClassName
)} )}
> >
@@ -372,7 +419,13 @@ const Table = <TData extends object>({
emptyContent} emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', tableClassNames.paginationClassName)}> <div
className={cn(
'mt-5',
TABLE_DEFAULT_STYLING.paginationClassName,
tableClassNames.paginationClassName
)}
>
<Pagination <Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()} totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize} itemsPerPage={table.getState().pagination.pageSize}
+9 -9
View File
@@ -39,16 +39,15 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
<li> <li>
<Link <Link
href={item.link} href={item.link}
className={cn( className={cn('px-3 py-1.5', {
{ 'text-base-content/60': !isItemActive,
'menu-active border-2 border-solid border-base-300': isItemActive, 'menu-active border-[1.5px] border-solid border-base-300':
}, isItemActive,
'px-3 py-1.5' })}
)}
> >
{item.icon && <Icon icon={item.icon} width={20} height={20} />} {item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span> <span className='text-sm'>{item.text}</span>
</Link> </Link>
</li> </li>
); );
@@ -62,12 +61,13 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
<details open={isItemActive}> <details open={isItemActive}>
<summary <summary
className={cn({ className={cn({
'text-base-content/60': !isItemActive,
'text-primary': isItemActive, 'text-primary': isItemActive,
})} })}
> >
{item.icon && <Icon icon={item.icon} width={20} height={20} />} {item.icon && <Icon icon={item.icon} width={20} height={20} />}
<span className='text-base'>{item.text}</span> <span className='text-sm'>{item.text}</span>
</summary> </summary>
<ul> <ul>
@@ -88,7 +88,7 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => { const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
return ( return (
<Menu> <Menu className='p-3'>
{menu.map((menuItem, menuIdx) => { {menu.map((menuItem, menuIdx) => {
return ( return (
<SidebarMenuItem <SidebarMenuItem
@@ -104,6 +104,10 @@ const ClosingsTable = () => {
header: '#', header: '#',
cell: (props) => props.row.index + 1, cell: (props) => props.row.index + 1,
}, },
{
accessorKey: 'project_name',
header: 'Flock',
},
{ {
accessorKey: 'location_name', accessorKey: 'location_name',
header: 'Lokasi', header: 'Lokasi',
@@ -6,7 +6,11 @@ import Table from '@/components/Table';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import { BaseClosingSales, BaseSales } from '@/types/api/closing'; import {
BaseClosingSales,
BaseSales,
ClosingSalesSummary,
} from '@/types/api/closing';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { Customer } from '@/types/api/master-data/customer'; import { Customer } from '@/types/api/master-data/customer';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
@@ -24,14 +28,20 @@ const SalesReportTable = ({
return initialValues?.sales || []; return initialValues?.sales || [];
}, [initialValues]); }, [initialValues]);
const summary: ClosingSalesSummary | undefined = useMemo(() => {
return initialValues?.summary;
}, [initialValues]);
const totals = useMemo(() => { const totals = useMemo(() => {
if (salesData.length === 0) { if (salesData.length === 0) {
return { return {
totalQuantity: 0, totalQuantity: 0,
totalWeight: 0, totalWeight: 0,
avgWeight: 0, avgWeight: 0,
avgPricePartner: 0, avgSalesPrice: 0,
totalPartner: 0, totalSalesPrice: 0,
avgActualPrice: 0,
totalActualPrice: 0,
}; };
} }
@@ -45,26 +55,46 @@ const SalesReportTable = ({
); );
const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0;
const validPriceItems = salesData.filter( const totalSalesPrice = salesData.reduce(
(item) => item.price != null && item.price > 0 (sum, item) => sum + (item.total_sales_price || 0),
);
const avgPricePartner =
validPriceItems.length > 0
? validPriceItems.reduce((sum, item) => sum + item.price, 0) /
validPriceItems.length
: 0;
const totalPartner = salesData.reduce(
(sum, item) => sum + (item.total_price || 0),
0 0
); );
const validSalesPriceItems = salesData.filter(
(item) => item.sales_price != null && item.sales_price > 0
);
const avgSalesPrice =
validSalesPriceItems.length > 0
? validSalesPriceItems.reduce(
(sum, item) => sum + item.sales_price,
0
) / validSalesPriceItems.length
: 0;
const totalActualPrice = salesData.reduce(
(sum, item) => sum + (item.total_actual_price || 0),
0
);
const validActualPriceItems = salesData.filter(
(item) => item.actual_price != null && item.actual_price > 0
);
const avgActualPrice =
validActualPriceItems.length > 0
? validActualPriceItems.reduce(
(sum, item) => sum + item.actual_price,
0
) / validActualPriceItems.length
: 0;
return { return {
totalQuantity, totalQuantity,
totalWeight, totalWeight,
avgWeight, avgWeight,
avgPricePartner, avgSalesPrice,
totalPartner, totalSalesPrice,
avgActualPrice,
totalActualPrice,
}; };
}, [salesData]); }, [salesData]);
@@ -161,50 +191,68 @@ const SalesReportTable = ({
), ),
}, },
{ {
id: 'price_partner', id: 'sales_price',
accessorKey: 'price', accessorKey: 'sales_price',
header: 'Harga Mitra (Rp)', header: 'Harga Sales (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; return <div className='text-right'>{formatCurrency(value)}</div>;
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.avgPricePartner)} {summary
? formatCurrency(summary.avg_sales_price)
: formatCurrency(totals.avgSalesPrice)}
</div> </div>
), ),
}, },
{ {
id: 'total_mitra', id: 'total_sales_price',
accessorKey: 'total_price', accessorKey: 'total_sales_price',
header: 'Total Mitra (Rp)', header: 'Total Sales (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; return <div className='text-right'>{formatCurrency(value)}</div>;
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalPartner)} {summary
? formatCurrency(summary.total_sales_price)
: formatCurrency(totals.totalSalesPrice)}
</div> </div>
), ),
}, },
{ {
id: 'price_act', id: 'actual_price',
accessorKey: 'price', accessorKey: 'actual_price',
header: 'Harga Act (Rp)', header: 'Harga Act (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; return <div className='text-right'>{formatCurrency(value)}</div>;
}, },
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{summary
? formatCurrency(summary.avg_actual_price)
: formatCurrency(totals.avgActualPrice)}
</div>
),
}, },
{ {
id: 'total_act', id: 'total_actual_price',
accessorKey: 'total_price', accessorKey: 'total_actual_price',
header: 'Total Act (Rp)', header: 'Total Act (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; return <div className='text-right'>{formatCurrency(value)}</div>;
}, },
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{summary
? formatCurrency(summary.total_actual_price)
: formatCurrency(totals.totalActualPrice)}
</div>
),
}, },
{ {
id: 'kandang', id: 'kandang',
@@ -64,7 +64,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
}, },
{ {
label: 'Nominal', label: 'Nominal',
value: formatCurrency(finance.nominal), value: formatCurrency(Math.abs(finance.nominal)),
}, },
].filter((item) => { ].filter((item) => {
// Hide party account number row if transaction type is INJECTION // Hide party account number row if transaction type is INJECTION
+111 -80
View File
@@ -19,6 +19,7 @@ import {
FINANCE_INITIAL_BALANCE_STATUS, FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_INJECTION_STATUS, FINANCE_INJECTION_STATUS,
FINANCE_TRANSACTION_STATUS, FINANCE_TRANSACTION_STATUS,
FINANCE_TRANSACTION_TYPE_OPTIONS,
} from '@/config/constant'; } from '@/config/constant';
import { FinanceApi } from '@/services/api/finance'; import { FinanceApi } from '@/services/api/finance';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
@@ -65,24 +66,19 @@ const RowOptionsMenu = ({
{FINANCE_TRANSACTION_STATUS.includes( {FINANCE_TRANSACTION_STATUS.includes(
props.row.original.transaction_type props.row.original.transaction_type
) && ) && (
props.row.original.party?.type !== 'SUPPLIER' && ( <RequirePermission permissions='lti.finance.payments.update'>
<RequirePermission permissions='lti.finance.payments.update'> <Button
<Button href={`/finance/detail/edit?financeId=${props.row.original.id}`}
href={`/finance/detail/edit?financeId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='warning'
color='warning' className='justify-start text-sm'
className='justify-start text-sm' >
> <Icon icon='material-symbols:edit-outline' width={16} height={16} />
<Icon Edit
icon='material-symbols:edit-outline' </Button>
width={16} </RequirePermission>
height={16} )}
/>
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes( {FINANCE_INITIAL_BALANCE_STATUS.includes(
props.row.original.transaction_type props.row.original.transaction_type
@@ -148,7 +144,8 @@ const FinanceTable = () => {
search: '', search: '',
transactionType: '', transactionType: '',
bankId: '', bankId: '',
partyType: '', customerId: '',
supplierId: '',
sortBy: '', sortBy: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
@@ -158,7 +155,8 @@ const FinanceTable = () => {
pageSize: 'limit', pageSize: 'limit',
transactionType: 'transaction_type', transactionType: 'transaction_type',
bankId: 'bank_id', bankId: 'bank_id',
partyType: 'party_type', customerId: 'customer_id',
supplierId: 'supplier_id',
sortBy: 'sort_date', sortBy: 'sort_date',
startDate: 'start_date', startDate: 'start_date',
endDate: 'end_date', endDate: 'end_date',
@@ -172,17 +170,24 @@ const FinanceTable = () => {
search: '', search: '',
transactionType: '', transactionType: '',
bankId: '', bankId: '',
partyType: '', customerId: '',
supplierId: '',
sortBy: '', sortBy: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
}); });
const [selectedTransactionType, setSelectedTransactionType] = const [selectedTransactionType, setSelectedTransactionType] = useState<
useState<OptionType | null>(null); OptionType | OptionType[] | null
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null); >(null);
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>( const [selectedBank, setSelectedBank] = useState<
null OptionType | OptionType[] | null
); >(null);
const [selectedCustomerId, setSelectedCustomerId] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedSupplierId, setSelectedSupplierId] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null); const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null); const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@@ -197,27 +202,18 @@ const FinanceTable = () => {
FinanceApi.getAllFetcher FinanceApi.getAllFetcher
); );
// ===== Options =====
const transactionTypeOptions = useMemo(() => {
return [
{ label: 'Customer', value: 'CUSTOMER' },
{ label: 'Supplier', value: 'SUPPLIER' },
];
}, []);
const { const {
options: partyTypeOptions, options: customerOptions,
isLoadingOptions: partyTypeIsLoadingOptions, isLoadingOptions: customerIsLoadingOptions,
setInputValue: partyTypeInputValue, setInputValue: customerInputValue,
loadMore: partyTypeLoadMore, loadMore: customerLoadMore,
} = useSelect( } = useSelect(CustomerApi.basePath, 'id', 'name');
selectedTransactionType const {
? selectedTransactionType.value === 'CUSTOMER' options: supplierOptions,
? CustomerApi.basePath isLoadingOptions: supplierIsLoadingOptions,
: SupplierApi.basePath setInputValue: supplierInputValue,
: '', loadMore: supplierLoadMore,
'id', } = useSelect(SupplierApi.basePath, 'id', 'name');
'name'
);
const sortByOptions = useMemo(() => { const sortByOptions = useMemo(() => {
return [ return [
{ label: 'Tanggal Pembayaran', value: 'payment_date' }, { label: 'Tanggal Pembayaran', value: 'payment_date' },
@@ -238,24 +234,47 @@ const FinanceTable = () => {
const transactionTypeChangeHandler = ( const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null val: OptionType | OptionType[] | null
) => { ) => {
setSelectedTransactionType(val as OptionType); setSelectedTransactionType(val);
setPendingFilters((prev) => ({ setPendingFilters((prev) => ({
...prev, ...prev,
transactionType: val ? ((val as OptionType).value as string) : '', transactionType: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
})); }));
}; };
const bankChangeHandler = (val: OptionType | OptionType[] | null) => { const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val as OptionType); setSelectedBank(val);
setPendingFilters((prev) => ({ setPendingFilters((prev) => ({
...prev, ...prev,
bankId: val ? ((val as OptionType).value as string) : '', bankId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
})); }));
}; };
const partyTypeChangeHandler = (val: OptionType | OptionType[] | null) => { const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedPartyType(val as OptionType); setSelectedCustomerId(val);
setPendingFilters((prev) => ({ setPendingFilters((prev) => ({
...prev, ...prev,
partyType: val ? ((val as OptionType).value as string) : '', customerId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
};
const supplierIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSupplierId(val);
setPendingFilters((prev) => ({
...prev,
supplierId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
})); }));
}; };
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => { const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -279,7 +298,8 @@ const FinanceTable = () => {
updateFilter('search', pendingFilters.search); updateFilter('search', pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType); updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId); updateFilter('bankId', pendingFilters.bankId);
updateFilter('partyType', pendingFilters.partyType); updateFilter('customerId', pendingFilters.customerId);
updateFilter('supplierId', pendingFilters.supplierId);
updateFilter('sortBy', pendingFilters.sortBy); updateFilter('sortBy', pendingFilters.sortBy);
updateFilter('startDate', pendingFilters.startDate); updateFilter('startDate', pendingFilters.startDate);
updateFilter('endDate', pendingFilters.endDate); updateFilter('endDate', pendingFilters.endDate);
@@ -287,14 +307,16 @@ const FinanceTable = () => {
const resetFilterHandler = () => { const resetFilterHandler = () => {
setSelectedTransactionType(null); setSelectedTransactionType(null);
setSelectedBank(null); setSelectedBank(null);
setSelectedPartyType(null); setSelectedCustomerId(null);
setSelectedSupplierId(null);
setSelectedSortBy(null); setSelectedSortBy(null);
const emptyFilters = { const emptyFilters = {
search: '', search: '',
transactionType: '', transactionType: '',
bankId: '', bankId: '',
partyType: '', customerId: '',
supplierId: '',
sortBy: '', sortBy: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
@@ -304,7 +326,8 @@ const FinanceTable = () => {
updateFilter('search', ''); updateFilter('search', '');
updateFilter('transactionType', ''); updateFilter('transactionType', '');
updateFilter('bankId', ''); updateFilter('bankId', '');
updateFilter('partyType', ''); updateFilter('customerId', '');
updateFilter('supplierId', '');
updateFilter('sortBy', ''); updateFilter('sortBy', '');
updateFilter('startDate', ''); updateFilter('startDate', '');
updateFilter('endDate', ''); updateFilter('endDate', '');
@@ -477,27 +500,34 @@ const FinanceTable = () => {
> >
<div className='grid grid-cols-4 gap-6'> <div className='grid grid-cols-4 gap-6'>
<SelectInput <SelectInput
options={transactionTypeOptions} options={FINANCE_TRANSACTION_TYPE_OPTIONS}
label='Tipe Transaksi' label='Jenis Transaksi'
value={selectedTransactionType} value={selectedTransactionType}
onChange={transactionTypeChangeHandler} onChange={transactionTypeChangeHandler}
isClearable isClearable
isMulti
/> />
<SelectInput <SelectInput
options={partyTypeOptions} options={customerOptions}
label={ label={'Customer'}
selectedTransactionType value={selectedCustomerId}
? selectedTransactionType.value === 'CUSTOMER' onChange={customerIdChangeHandler}
? 'Pelanggan' onInputChange={customerInputValue}
: 'Supplier' onMenuScrollToBottom={customerLoadMore}
: 'Pihak' isLoading={customerIsLoadingOptions}
}
value={selectedPartyType}
onChange={partyTypeChangeHandler}
onInputChange={partyTypeInputValue}
onMenuScrollToBottom={partyTypeLoadMore}
isLoading={partyTypeIsLoadingOptions}
isClearable isClearable
isMulti
/>
<SelectInput
options={supplierOptions}
label={'Supplier'}
value={selectedSupplierId}
onChange={supplierIdChangeHandler}
onInputChange={supplierInputValue}
onMenuScrollToBottom={supplierLoadMore}
isLoading={supplierIsLoadingOptions}
isClearable
isMulti
/> />
<SelectInput <SelectInput
options={ options={
@@ -522,13 +552,7 @@ const FinanceTable = () => {
onInputChange={bankInputValue} onInputChange={bankInputValue}
onMenuScrollToBottom={bankLoadMore} onMenuScrollToBottom={bankLoadMore}
isClearable isClearable
/> isMulti
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
/> />
<SelectInput <SelectInput
options={sortByOptions} options={sortByOptions}
@@ -549,6 +573,13 @@ const FinanceTable = () => {
value={pendingFilters.endDate} value={pendingFilters.endDate}
onChange={endDateChangeHandler} onChange={endDateChangeHandler}
/> />
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
/>
</div> </div>
</Card> </Card>
<Table<Finance> <Table<Finance>
@@ -245,7 +245,11 @@ const FormFinanceAddInitialBalance = ({
} }
required required
isClearable isClearable
isDisabled={!formik.values.party_type_option?.value} isDisabled={
!formik.values.party_type_option?.value ||
(type === 'edit' &&
formik.values.party_type_option?.value == 'SUPPLIER')
}
/> />
<SelectInput <SelectInput
label='Bank' label='Bank'
@@ -323,6 +327,7 @@ const FormFinanceAddInitialBalance = ({
} }
required required
isClearable isClearable
isDisabled={type == 'edit'}
/> />
<NumberInput <NumberInput
label='Nominal' label='Nominal'
@@ -89,7 +89,6 @@ const MarketingDetail = ({
deleteModal.closeModal(); deleteModal.closeModal();
router.push('/marketing'); router.push('/marketing');
toast.success(res?.message as string); toast.success(res?.message as string);
refresh?.();
setIsLoading(false); setIsLoading(false);
}; };
@@ -507,7 +507,7 @@ const MarketingForm = ({
addSOModal.openModal(); addSOModal.openModal();
}, [addSOModal]); }, [addSOModal]);
const handleAddSubmitSO = useCallback( const handleAddSubmitSO = useCallback(
async (values: SalesOrderProductFormValues) => { async (values: SalesOrderProductFormValues, id?: number) => {
const currentProducts = formik.values.sales_order; const currentProducts = formik.values.sales_order;
const newValues = { const newValues = {
@@ -515,18 +515,12 @@ const MarketingForm = ({
id: values.id ?? Date.now(), id: values.id ?? Date.now(),
}; };
const existingIndex = currentProducts.findIndex(
(item) =>
item.kandang_id === newValues.kandang_id &&
item.product_warehouse_id === newValues.product_warehouse_id
);
let updatedProducts = []; let updatedProducts = [];
if (existingIndex !== -1) { if (id) {
// Overwrite // Overwrite
updatedProducts = currentProducts.map((item, index) => updatedProducts = currentProducts.map((item) =>
index === existingIndex ? newValues : item item.id === id ? newValues : item
); );
} else { } else {
// Add new item // Add new item
@@ -39,7 +39,10 @@ const SalesOrderProductForm = ({
initialValues?: SalesOrderProductFormValues; initialValues?: SalesOrderProductFormValues;
exisitingValues?: SalesOrderProductFormValues[]; exisitingValues?: SalesOrderProductFormValues[];
modalRef?: RefObject<HTMLDialogElement | null>; modalRef?: RefObject<HTMLDialogElement | null>;
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>; onSubmitForm?: (
value: SalesOrderProductFormValues,
id?: number
) => Promise<void>;
}) => { }) => {
const [formErrorMessage, setFormErrorMessage] = useState(''); const [formErrorMessage, setFormErrorMessage] = useState('');
const [currentInput, setCurrentInput] = useState<string>(''); const [currentInput, setCurrentInput] = useState<string>('');
@@ -76,7 +79,7 @@ const SalesOrderProductForm = ({
validationSchema: SalesOrderProductSchema, validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
setFormErrorMessage(''); setFormErrorMessage('');
onSubmitForm?.(values); onSubmitForm?.(values, initialValues?.id);
handleResetForm(); handleResetForm();
}, },
validateOnBlur: true, validateOnBlur: true,
@@ -414,7 +417,9 @@ const SalesOrderProductForm = ({
/> />
</div> </div>
<AlertErrorList formErrorList={formErrorList} onClose={close} /> <div className='mt-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
<div className='flex flex-row justify-end gap-3 mt-4'> <div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning' onClick={handleResetForm}> <Button type='reset' color='warning' onClick={handleResetForm}>
@@ -367,7 +367,7 @@ const ProductionStandardForm = ({
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_hen_house_production, row.production_standard_details?.target_hen_house_production,
cell: ({ row }) => cell: ({ row }) =>
`${row.original.production_standard_details?.target_hen_house_production} pc`, `${row.original.production_standard_details?.target_hen_house_production} btr`,
enableSorting: false, enableSorting: false,
}, },
{ {
@@ -383,7 +383,7 @@ const ProductionStandardForm = ({
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_egg_mass, row.production_standard_details?.target_egg_mass,
cell: ({ row }) => cell: ({ row }) =>
`${row.original.production_standard_details?.target_egg_mass} g`, `${row.original.production_standard_details?.target_egg_mass} kg`,
enableSorting: false, enableSorting: false,
}, },
{ {
@@ -958,7 +958,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
bottomLabel='Butir (pc)' bottomLabel='Butir (btr)'
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -1015,7 +1015,7 @@ const ProductionStandardForm = ({
name='production_standard_details.target_egg_mass' name='production_standard_details.target_egg_mass'
label='Egg Mass' label='Egg Mass'
placeholder='1' placeholder='1'
bottomLabel='Gram (g)' bottomLabel='Kg (kg)'
value={ value={
repeaterFormik.values repeaterFormik.values
.production_standard_details?.target_egg_mass .production_standard_details?.target_egg_mass
@@ -1176,7 +1176,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
bottomLabel='Gram/Ekor (g)' bottomLabel='Gram (g)'
endAdornment endAdornment
errorMessage={ errorMessage={
repeaterFormik.errors repeaterFormik.errors
@@ -156,39 +156,39 @@ const productionStandardColumns: ColumnDef<StandardDetails>[] = [
}, },
{ {
accessorKey: 'egg_production_standard_detail.target_hen_house_production', accessorKey: 'egg_production_standard_detail.target_hen_house_production',
header: 'Target Hen House (%)', header: 'Target Hen House (btr)',
cell: (props) => cell: (props) =>
`${ formatNumber(
(props.row.original.egg_production_standard_detail (props.row.original.egg_production_standard_detail
?.target_hen_house_production as number) || 0 ?.target_hen_house_production as number) || 0
}%`, ),
}, },
{ {
accessorKey: 'egg_production_standard_detail.target_egg_weight', accessorKey: 'egg_production_standard_detail.target_egg_weight',
header: 'Target Egg Weight (gram)', header: 'Target Egg Weight (g)',
cell: (props) => cell: (props) =>
formatNumber( `${
(props.row.original.egg_production_standard_detail (props.row.original.egg_production_standard_detail
?.target_egg_weight as number) || 0 ?.target_egg_weight as number) || 0
), } g`,
}, },
{ {
accessorKey: 'egg_production_standard_detail.target_egg_mass', accessorKey: 'egg_production_standard_detail.target_egg_mass',
header: 'Target Egg Mass (gram)', header: 'Target Egg Mass (kg)',
cell: (props) => cell: (props) =>
formatNumber( `${
(props.row.original.egg_production_standard_detail (props.row.original.egg_production_standard_detail
?.target_egg_mass as number) || 0 ?.target_egg_mass as number) || 0
), } kg`,
}, },
{ {
accessorKey: 'egg_production_standard_detail.standard_fcr', accessorKey: 'egg_production_standard_detail.standard_fcr',
header: 'Standard FCR', header: 'Standard FCR (g)',
cell: (props) => cell: (props) =>
formatNumber( `${
(props.row.original.egg_production_standard_detail (props.row.original.egg_production_standard_detail
?.standard_fcr as number) || 0 ?.standard_fcr as number) || 0
), } g`,
}, },
]; ];
@@ -552,23 +552,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
const nextDayRecordingUrl = useMemo(() => { const nextDayRecordingUrl = useMemo(() => {
if (!selectedProjectFlock) return null; if (!projectFlockKandangLookup) return null;
const projectFlockId = const projectFlockKandangId = projectFlockKandangLookup.id;
typeof selectedProjectFlock.value === 'string' return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}`;
? parseInt(selectedProjectFlock.value, 10) }, [projectFlockKandangLookup]);
: selectedProjectFlock.value;
return `${RecordingApi.basePath}/next-day?project_flock_id=${projectFlockId}`;
}, [selectedProjectFlock]);
const { data: nextDayRecordingData } = useSWR( const { data: nextDayRecordingData } = useSWR(
nextDayRecordingUrl, nextDayRecordingUrl,
nextDayRecordingUrl nextDayRecordingUrl
? () => { ? () => {
const projectFlockId = const projectFlockKandangId = projectFlockKandangLookup!.id;
typeof selectedProjectFlock!.value === 'string' return RecordingApi.nextDayRecording(projectFlockKandangId);
? parseInt(selectedProjectFlock!.value, 10)
: selectedProjectFlock!.value;
return RecordingApi.nextDayRecording(projectFlockId);
} }
: null : null
); );
@@ -1180,6 +1174,47 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[stockProducts] [stockProducts]
); );
const getProductUomSuffix = useCallback(
(productWarehouseId: number, dataSource: 'stock' | 'depletion' | 'egg') => {
if (type !== 'add' && initialValues) {
let items;
if (dataSource === 'stock') {
items = initialValues.stocks;
} else if (dataSource === 'depletion') {
items = initialValues.depletions;
} else if (dataSource === 'egg') {
items = initialValues.eggs;
}
if (items) {
const item = items.find(
(i) => i.product_warehouse_id === productWarehouseId
);
if (item?.product_warehouse?.product?.uom?.name) {
return item.product_warehouse.product.uom.name;
}
}
}
let rawData;
if (dataSource === 'stock') {
rawData = stockProducts;
} else if (dataSource === 'depletion') {
rawData = depletionProductsData;
} else if (dataSource === 'egg') {
rawData = eggProductsData;
}
if (!isResponseSuccess(rawData)) return null;
const data = rawData.data as unknown as ProductWarehouse[];
const productWarehouse = data.find((pw) => pw.id === productWarehouseId);
return productWarehouse?.product.uom.name || null;
},
[stockProducts, depletionProductsData, eggProductsData, initialValues, type]
);
const hasExceededStock = useMemo(() => { const hasExceededStock = useMemo(() => {
if ((type as 'add' | 'edit' | 'detail') === 'detail') return false; if ((type as 'add' | 'edit' | 'detail') === 'detail') return false;
return ( return (
@@ -2113,38 +2148,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className='py-3 font-medium'>FCR</td> <td className='py-3 font-medium'>FCR (g)</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.fcr_value != null {initialValues.fcr_value != null
? formatNumber(initialValues.fcr_value) ? `${formatNumber(initialValues.fcr_value)} g`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.fcr?.fcr_std != null {initialValues.project_flock?.fcr?.fcr_std != null
? formatNumber( ? `${formatNumber(initialValues.project_flock?.fcr?.fcr_std)} g`
initialValues.project_flock?.fcr?.fcr_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='py-3 font-medium'>Feed Intake (KG)</td> <td className='py-3 font-medium'>Feed Intake (g)</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.feed_intake != null {initialValues.feed_intake != null
? formatNumber(initialValues.feed_intake) ? `${formatNumber(initialValues.feed_intake)} g`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.production_standart {initialValues.project_flock?.production_standart
?.feed_intake_std != null ?.feed_intake_std != null
? formatNumber( ? `${formatNumber(initialValues.project_flock?.production_standart?.feed_intake_std)} g`
initialValues.project_flock?.production_standart
?.feed_intake_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -2224,51 +2254,43 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className='py-3 font-medium'>Egg Mass</td> <td className='py-3 font-medium'>Egg Mass (kg)</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.egg_mass != null {initialValues.egg_mass != null
? formatNumber(initialValues.egg_mass) ? `${formatNumber(initialValues.egg_mass)} kg`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.production_standart {initialValues.project_flock?.production_standart
?.egg_mass_std != null ?.egg_mass_std != null
? formatNumber( ? `${formatNumber(initialValues.project_flock?.production_standart?.egg_mass_std)} kg`
initialValues.project_flock
?.production_standart?.egg_mass_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='py-3 font-medium'> <td className='py-3 font-medium'>Egg Weight (g)</td>
Egg Weight (KG)
</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.egg_weight != null {initialValues.egg_weight != null
? formatNumber(initialValues.egg_weight) ? `${formatNumber(initialValues.egg_weight)} g`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.production_standart {initialValues.project_flock?.production_standart
?.egg_weight_std != null ?.egg_weight_std != null
? formatNumber( ? `${formatNumber(initialValues.project_flock?.production_standart?.egg_weight_std)} g`
initialValues.project_flock
?.production_standart?.egg_weight_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='py-3 font-medium'>Hen Day</td> <td className='py-3 font-medium'>Hen Day (%)</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.hen_day != null {initialValues.hen_day != null
? formatNumber(initialValues.hen_day) ? `${formatNumber(initialValues.hen_day)}%`
: '-'} : '-'}
</span> </span>
</td> </td>
@@ -2280,18 +2302,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='py-3 font-medium'>Hen House</td> <td className='py-3 font-medium'>
Hen House (btr)
</td>
<td className='text-center py-3'> <td className='text-center py-3'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.hen_house != null {initialValues.hen_house != null
? formatNumber(initialValues.hen_house) ? `${formatNumber(initialValues.hen_house)} btr`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.production_standart {initialValues.project_flock?.production_standart
?.hen_house_std != null ?.hen_house_std != null
? `${initialValues.project_flock?.production_standart?.hen_house_std}%` ? `${formatNumber(initialValues.project_flock?.production_standart?.hen_house_std)} btr`
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -2468,6 +2492,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Masukkan jumlah pakai' placeholder='Masukkan jumlah pakai'
inputSuffix={
stock.product_warehouse_id
? getProductUomSuffix(
stock.product_warehouse_id,
'stock'
)
: null
}
/> />
{(type as 'add' | 'edit' | 'detail') !== 'detail' && {(type as 'add' | 'edit' | 'detail') !== 'detail' &&
getStockUsageAdornment(idx)} getStockUsageAdornment(idx)}
@@ -2663,6 +2695,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Masukkan jumlah deplesi' placeholder='Masukkan jumlah deplesi'
inputSuffix={
depletion.product_warehouse_id
? getProductUomSuffix(
depletion.product_warehouse_id,
'depletion'
)
: null
}
/> />
</td> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
@@ -2759,7 +2799,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<th>Kondisi Telur</th> <th>Kondisi Telur</th>
<th>Jumlah</th> <th>Jumlah</th>
<th>Berat (gram)</th> <th>Total Berat (Kilogram)</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th> <th>Action</th>
)} )}
@@ -2856,6 +2896,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Masukkan jumlah telur' placeholder='Masukkan jumlah telur'
inputSuffix={'Butir'}
/> />
</td> </td>
<td> <td>
@@ -2880,7 +2921,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Masukkan berat telur (gram)...' placeholder='Masukkan total berat telur (Kilogram)...'
inputSuffix='Kilogram'
/> />
</td> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useState } from 'react'; import { ChangeEventHandler, useEffect, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import {
CellContext, CellContext,
@@ -20,33 +20,32 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import DateInput from '@/components/input/DateInput';
import PopoverButton from '@/components/popover/PopoverButton';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production';
import PillBadge from '@/components/PillBadge'; import Badge from '@/components/Badge';
import { Color } from '@/types/theme';
import PopoverContent from '@/components/popover/PopoverContent';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown',
props, props,
popoverPosition = 'bottom',
approveClickHandler, approveClickHandler,
rejectClickHandler, rejectClickHandler,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse';
props: CellContext<TransferToLaying, unknown>; props: CellContext<TransferToLaying, unknown>;
popoverPosition: 'bottom' | 'top';
approveClickHandler: () => void; approveClickHandler: () => void;
rejectClickHandler: () => void; rejectClickHandler: () => void;
deleteClickHandler: () => void; deleteClickHandler: () => void;
@@ -60,80 +59,99 @@ const RowOptionsMenu = ({
const showApproveButton = showEditButton; const showApproveButton = showEditButton;
const showRejectButton = showEditButton; const showRejectButton = showEditButton;
const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<RequirePermission permissions='lti.production.transfer_to_laying.detail'> <PopoverButton
<Button tabIndex={0}
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
{showEditButton && ( <PopoverContent
<RequirePermission permissions='lti.production.transfer_to_laying.update'> id={popoverId}
<Button anchorName={popoverAnchorName}
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`} position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
variant='ghost' className='rounded-xl border border-base-content/5 shadow-sm'
color='warning' >
className='justify-start text-sm' <div className='flex flex-col bg-base-100 rounded-xl'>
> <RequirePermission permissions='lti.production.transfer_to_laying.detail'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Button
Edit href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
</Button> variant='ghost'
</RequirePermission> color='none'
)} className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
{/* TODO: apply RBAC */} {showEditButton && (
{showApproveButton && ( <RequirePermission permissions='lti.production.transfer_to_laying.update'>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'> <Button
<Button href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='success' color='none'
onClick={approveClickHandler} className='p-3 justify-start text-sm font-semibold w-full'
className='justify-start text-sm' >
> <Icon icon='heroicons:pencil-square' width={20} height={20} />
<Icon icon='material-symbols:check' width={24} height={24} /> Edit
Approve </Button>
</Button> </RequirePermission>
</RequirePermission> )}
)}
{showRejectButton && ( {showApproveButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.approve'> <RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button <Button
variant='ghost' variant='ghost'
color='error' color='success'
onClick={rejectClickHandler} onClick={approveClickHandler}
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
> >
<Icon icon='material-symbols:close' width={24} height={24} /> <Icon icon='heroicons:check' width={20} height={20} />
Reject Approve
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{showDeleteButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.delete'> {showRejectButton && (
<Button <RequirePermission permissions='lti.production.transfer_to_laying.approve'>
onClick={deleteClickHandler} <Button
variant='ghost' variant='ghost'
color='error' color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' onClick={rejectClickHandler}
> className='p-3 justify-start text-sm font-semibold w-full'
<Icon >
icon='material-symbols:delete-outline-rounded' <Icon icon='heroicons:x-mark' width={20} height={20} />
width={16} Reject
height={16} </Button>
className='justify-start text-sm' </RequirePermission>
/> )}
Delete
</Button> {showDeleteButton && (
</RequirePermission> <RequirePermission permissions='lti.production.transfer_to_laying.delete'>
)} <hr className='mx-3 border-base-content/10 h-px' />
</RowOptionsMenuWrapper> <Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
)}
</div>
</PopoverContent>
</div>
); );
}; };
@@ -150,6 +168,8 @@ const TransferToLayingsTable = () => {
transferDate: '', transferDate: '',
flockSource: '', flockSource: '',
flockDestination: '', flockDestination: '',
filter_by: '',
sort_by: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -157,6 +177,8 @@ const TransferToLayingsTable = () => {
transferDate: 'transfer_date', transferDate: 'transfer_date',
flockSource: 'flock_source', flockSource: 'flock_source',
flockDestination: 'flock_destination', flockDestination: 'flock_destination',
filter_by: 'filter_by',
sort_by: 'sort_by',
}, },
}); });
@@ -181,7 +203,7 @@ const TransferToLayingsTable = () => {
isLoadingOptions: isLoadingFlockSourceOptions, isLoadingOptions: isLoadingFlockSourceOptions,
loadMore: loadMoreFlockSource, loadMore: loadMoreFlockSource,
hasMore: hasMoreFlockSource, hasMore: hasMoreFlockSource,
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name'); } = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
const { const {
setInputValue: setFlockDestinationInputValue, setInputValue: setFlockDestinationInputValue,
@@ -189,7 +211,7 @@ const TransferToLayingsTable = () => {
isLoadingOptions: isLoadingFlockDestinationOptions, isLoadingOptions: isLoadingFlockDestinationOptions,
loadMore: loadMoreFlockDestination, loadMore: loadMoreFlockDestination,
hasMore: hasMoreFlockDestination, hasMore: hasMoreFlockDestination,
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name'); } = useSelect<Flock>(ProjectFlockApi.basePath, 'id', 'flock_name');
// Flocks value // Flocks value
const [selectedFlockSource, setSelectedFlockSource] = const [selectedFlockSource, setSelectedFlockSource] =
@@ -244,13 +266,6 @@ const TransferToLayingsTable = () => {
); );
}, },
}, },
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{ {
accessorKey: 'transfer_date', accessorKey: 'transfer_date',
header: 'Tanggal Transfer', header: 'Tanggal Transfer',
@@ -274,6 +289,7 @@ const TransferToLayingsTable = () => {
{ {
accessorKey: 'notes', accessorKey: 'notes',
header: 'Alasan Transfer', header: 'Alasan Transfer',
enableSorting: false,
}, },
{ {
header: 'Status', header: 'Status',
@@ -282,34 +298,39 @@ const TransferToLayingsTable = () => {
props.row.original.approval.action === 'REJECTED'; props.row.original.approval.action === 'REJECTED';
let latestApprovalStepName = props.row.original.approval.step_name; let latestApprovalStepName = props.row.original.approval.step_name;
let pillBadgeColor: 'yellow' | 'green' | 'gray' | 'red' = 'gray'; let badgeColor: Color = 'neutral';
switch (latestApprovalStepName.toLowerCase()) { switch (latestApprovalStepName.toLowerCase()) {
case 'pengajuan': case 'pengajuan':
pillBadgeColor = 'yellow'; badgeColor = 'neutral';
break; break;
case 'disetujui': case 'disetujui':
pillBadgeColor = 'green'; badgeColor = 'success';
break; break;
} }
if (isLatestApprovalRejected) { if (isLatestApprovalRejected) {
pillBadgeColor = 'red'; badgeColor = 'error';
latestApprovalStepName = 'Ditolak'; latestApprovalStepName = 'Ditolak';
} }
return ( return (
<PillBadge <Badge
content={latestApprovalStepName} variant='soft'
color={pillBadgeColor} className={{
className='text-sm' badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
/> }}
color={badgeColor}
>
<Icon icon='mdi:circle' width={12} height={12} color={badgeColor} />
{latestApprovalStepName}
</Badge>
); );
}, },
}, },
{ {
header: 'Aksi', id: 'actions',
cell: (props) => { cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentPageRows = props.table.getPaginationRowModel().flatRows;
@@ -346,31 +367,13 @@ const TransferToLayingsTable = () => {
}; };
return ( return (
<> <RowOptionsMenu
{currentPageSize > 3 && ( props={props}
<RowDropdownOptions isLast2Rows={isLast2Rows}> approveClickHandler={approveClickHandler}
<RowOptionsMenu rejectClickHandler={rejectClickHandler}
type='dropdown' deleteClickHandler={deleteClickHandler}
props={props} popoverPosition={isLast2Rows ? 'top' : 'bottom'}
approveClickHandler={approveClickHandler} />
rejectClickHandler={rejectClickHandler}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
); );
}, },
}, },
@@ -397,17 +400,21 @@ const TransferToLayingsTable = () => {
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
try { const deleteResponse = await TransferToLayingApi.delete(
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number); selectedTransferToLaying?.id as number
);
toast.success('Berhasil menghapus data transfer ke laying!'); if (isResponseError(deleteResponse)) {
refreshTransferToLayings(); toast.error(deleteResponse.message);
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false); setIsDeleteLoading(false);
return;
} }
refreshTransferToLayings();
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
setIsDeleteLoading(false);
}; };
const confirmationModalApproveClickHandler = async (notes: string) => { const confirmationModalApproveClickHandler = async (notes: string) => {
@@ -499,20 +506,19 @@ const TransferToLayingsTable = () => {
); );
}; };
// track sorting useEffect(() => {
// useEffect(() => { if (sorting.length === 1) {
// const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); updateFilter('filter_by', sorting[0].id);
updateFilter('sort_by', sorting[0].desc ? 'desc' : 'asc');
// if (!isNameSorted) { } else {
// updateFilter('nameSort', ''); updateFilter('filter_by', '');
// } else { updateFilter('sort_by', '');
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); }
// } }, [sorting]);
// }, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'> <div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'> <div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
@@ -579,12 +585,10 @@ const TransferToLayingsTable = () => {
</div> </div>
<div className='grid grid-cols-12 justify-end gap-4'> <div className='grid grid-cols-12 justify-end gap-4'>
<TextInput <DateInput
required
type='date'
label='Tanggal Transfer'
name='transfer_date' name='transfer_date'
placeholder='Masukkan tanggal transfer' label='Tanggal Transfer'
placeholder='Tanggal Transfer'
value={tableFilterState.transferDate} value={tableFilterState.transferDate}
onChange={transferDateChangeHandler} onChange={transferDateChangeHandler}
className={{ className={{
@@ -619,20 +623,6 @@ const TransferToLayingsTable = () => {
wrapper: 'col-span-12 sm:col-span-3', wrapper: 'col-span-12 sm:col-span-3',
}} }}
/> />
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{
wrapper:
'col-span-6 sm:col-span-3 max-w-28 sm:justify-self-end',
}}
/>
</div> </div>
</div> </div>
@@ -653,26 +643,21 @@ const TransferToLayingsTable = () => {
: 0 : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler} enableRowSelection={tableEnableRowSelectionHandler}
withCheckbox
className={{ className={{
containerClassName: cn({ containerClassName: cn({
'mb-20': 'w-full mb-20':
isResponseSuccess(transferToLayings) && isResponseSuccess(transferToLayings) &&
transferToLayings?.data?.length === 0, transferToLayings?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
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',
}} }}
/> />
</div> </div>
@@ -80,7 +80,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
) )
.required('Kuantitas wajib diisi!'), .required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field maxQuantity: Yup.number().min(0).required(), // internal helper field
}) })
) )
.min(1, 'Minimal 1 kandang terisi!') .min(1, 'Minimal 1 kandang terisi!')
@@ -102,7 +102,7 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
) )
.required('Kuantitas wajib diisi!'), .required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field maxQuantity: Yup.number().min(0).required(), // internal helper field
}) })
) )
.min(1, 'Minimal 1 kandang terisi!') .min(1, 'Minimal 1 kandang terisi!')
@@ -123,6 +123,13 @@ const DailyMarketingsTable = ({
accessorKey: 'average_weight', accessorKey: 'average_weight',
header: 'Bobot Rata-Rata (Kg)', header: 'Bobot Rata-Rata (Kg)',
cell: (props) => formatNumber(props.row.original.average_weight_kg), cell: (props) => formatNumber(props.row.original.average_weight_kg),
footer: () => {
const totalAverageWeightKg = isResponseSuccess(dailyMarketings)
? dailyMarketings?.total?.average_weight_kg
: 0;
return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-';
},
}, },
{ {
accessorKey: 'total_weight', accessorKey: 'total_weight',
@@ -140,6 +147,13 @@ const DailyMarketingsTable = ({
accessorKey: 'sales_price', accessorKey: 'sales_price',
header: 'Harga Jual (Rp)', header: 'Harga Jual (Rp)',
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
footer: () => {
const totalSalesPrice = isResponseSuccess(dailyMarketings)
? dailyMarketings?.total?.average_sales_price
: 0;
return totalSalesPrice ? formatNumber(totalSalesPrice) : '-';
},
}, },
{ {
accessorKey: 'hpp_price', accessorKey: 'hpp_price',
@@ -1,6 +1,6 @@
'use client'; 'use client';
import * as XLSX from 'xlsx'; import ExcelJS from 'exceljs';
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
@@ -8,104 +8,130 @@ interface CustomerPaymentExportExcelParams {
data: CustomerPaymentReport[]; data: CustomerPaymentReport[];
} }
export const generateCustomerPaymentExcel = ( export const generateCustomerPaymentExcel = async (
params: CustomerPaymentExportExcelParams params: CustomerPaymentExportExcelParams
): void => { ): Promise<void> => {
if (!params.data || params.data.length === 0) { if (!params.data || params.data.length === 0) {
return; return;
} }
const workbook = XLSX.utils.book_new(); const workbook = new ExcelJS.Workbook();
params.data.forEach((customerReport) => { const columns = [
{ header: 'No', key: 'no', width: 5 },
{ header: 'Tanggal DO/Bayar', key: 'transDate', width: 15 },
{ header: 'Tanggal Realisasi', key: 'deliveryDate', width: 15 },
{ header: 'Aging', key: 'aging', width: 8 },
{ header: 'Referensi', key: 'reference', width: 12 },
{ header: 'Nomor Polisi', key: 'vehicleNumbers', width: 15 },
{ header: 'Ekor/Qty', key: 'qty', width: 10 },
{ header: 'Berat (Kg)', key: 'weight', width: 12 },
{ header: 'AVG', key: 'avgWeight', width: 10 },
{ header: 'Harga/Unit', key: 'unitPrice', width: 15 },
{ header: 'Harga Akhir', key: 'finalPrice', width: 15 },
{ header: 'Total', key: 'totalPrice', width: 15 },
{ header: 'Pembayaran', key: 'paymentAmount', width: 15 },
{ header: 'Saldo Piutang', key: 'accountsReceivable', width: 15 },
{ header: 'Keterangan', key: 'status', width: 20 },
{ header: 'Pengambilan', key: 'pickupInfo', width: 15 },
{ header: 'Sales/Marketing', key: 'salesPerson', width: 20 },
];
for (const customerReport of params.data) {
const customerData = customerReport.rows; const customerData = customerReport.rows;
const customerName = customerReport.customer.name || 'Unknown Customer'; const customerName = customerReport.customer.name || 'Unknown Customer';
const excelData: { [key: string]: string | number }[] = customerData.map( const worksheet = workbook.addWorksheet(customerName.substring(0, 31));
(item, index) => ({ worksheet.columns = columns;
No: index + 1,
'Tanggal DO/Bayar': item.trans_date customerData.forEach((item, index) => {
const row = worksheet.addRow({
no: index + 1,
transDate: item.trans_date
? formatDate(item.trans_date, 'DD MMM YYYY') ? formatDate(item.trans_date, 'DD MMM YYYY')
: '', : '',
'Tanggal Realisasi': item.delivery_date deliveryDate: item.delivery_date
? formatDate(item.delivery_date, 'DD MMM YYYY') ? formatDate(item.delivery_date, 'DD MMM YYYY')
: '', : '',
Aging: formatNumber(item.aging_day || 0), aging: formatNumber(item.aging_day || 0),
Referensi: item.reference || '', reference: item.reference || '',
'Nomor Polisi': Array.isArray(item.vehicle_numbers) vehicleNumbers: Array.isArray(item.vehicle_numbers)
? item.vehicle_numbers.join(', ') ? item.vehicle_numbers.join(', ')
: '', : '',
'Ekor/Qty': formatNumber(item.qty || 0), qty: formatNumber(item.qty || 0),
'Berat (Kg)': formatNumber(item.weight || 0), weight: formatNumber(item.weight || 0),
AVG: formatNumber(item.average_weight || 0), avgWeight: formatNumber(item.average_weight || 0),
'Harga/Unit': formatCurrency(item.unit_price || 0), unitPrice: formatCurrency(item.unit_price || 0),
'Harga Akhir': formatCurrency(item.final_price || 0), finalPrice: formatCurrency(item.final_price || 0),
Total: formatCurrency(item.total_price || 0), totalPrice: formatCurrency(item.total_price || 0),
Pembayaran: formatCurrency(item.payment_amount || 0), paymentAmount: formatCurrency(item.payment_amount || 0),
'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), accountsReceivable: formatCurrency(item.accounts_receivable || 0),
Keterangan: item.status || '', status: item.status || '',
Pengambilan: Array.isArray(item.pickup_info) pickupInfo: Array.isArray(item.pickup_info)
? item.pickup_info.join(', ') ? item.pickup_info.join(', ')
: '', : '',
'Sales/Marketing': item.sales_person || '', salesPerson: item.sales_person || '',
}) });
);
const accountsReceivableCell = row.getCell('accountsReceivable');
if (
accountsReceivableCell.value &&
accountsReceivableCell.value.toString().startsWith('-Rp')
) {
accountsReceivableCell.font = { color: { argb: 'FFFF0000' } };
}
});
if (customerReport.summary) { if (customerReport.summary) {
excelData.push({ const summaryRow = worksheet.addRow({
No: 'Total', no: 'Total',
'Tanggal DO/Bayar': '', transDate: '',
'Tanggal Realisasi': '', deliveryDate: '',
Aging: '', aging: '',
Referensi: '', reference: '',
'Nomor Polisi': '', vehicleNumbers: '',
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), qty: formatNumber(customerReport.summary.total_qty || 0),
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), weight: formatNumber(customerReport.summary.total_weight || 0),
AVG: '', avgWeight: '',
'Harga/Unit': '', unitPrice: '',
'Harga Akhir': formatCurrency( finalPrice: formatCurrency(
customerReport.summary.total_final_amount || 0 customerReport.summary.total_final_amount || 0
), ),
Total: formatCurrency(customerReport.summary.total_grand_amount || 0), totalPrice: formatCurrency(
Pembayaran: formatCurrency(customerReport.summary.total_payment || 0), customerReport.summary.total_grand_amount || 0
'Saldo Piutang': formatCurrency( ),
paymentAmount: formatCurrency(
customerReport.summary.total_payment || 0
),
accountsReceivable: formatCurrency(
customerReport.summary.total_accounts_receivable || 0 customerReport.summary.total_accounts_receivable || 0
), ),
Keterangan: '', status: '',
Pengambilan: '', pickupInfo: '',
'Sales/Marketing': '', salesPerson: '',
}); });
const summaryAccountsReceivableCell =
summaryRow.getCell('accountsReceivable');
if (
summaryAccountsReceivableCell.value &&
summaryAccountsReceivableCell.value.toString().startsWith('-Rp')
) {
summaryAccountsReceivableCell.font = { color: { argb: 'FFFF0000' } };
}
} }
}
const worksheet = XLSX.utils.json_to_sheet(excelData);
const colWidths = [
{ wch: 5 }, // No
{ wch: 15 }, // Tanggal DO/Bayar
{ wch: 15 }, // Tanggal Realisasi
{ wch: 8 }, // Aging
{ wch: 12 }, // Referensi
{ wch: 15 }, // Nomor Polisi
{ wch: 10 }, // Ekor/Qty
{ wch: 12 }, // Berat
{ wch: 10 }, // AVG
{ wch: 15 }, // Harga/Unit
{ wch: 15 }, // Harga Akhir
{ wch: 15 }, // Total
{ wch: 15 }, // Pembayaran
{ wch: 15 }, // Saldo Piutang
{ wch: 20 }, // Keterangan
{ wch: 15 }, // Pengambilan
{ wch: 20 }, // Sales/Marketing
];
worksheet['!cols'] = colWidths;
const sheetName =
customerName.length > 31 ? customerName.substring(0, 31) : customerName;
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
const filename = `laporan-kontrol-pembayaran-customer-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; const filename = `laporan-kontrol-pembayaran-customer-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
XLSX.writeFile(workbook, filename); const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
}; };
@@ -252,7 +252,7 @@ const CustomerPaymentTab = () => {
return; return;
} }
generateCustomerPaymentExcel({ data: allDataForExport }); await generateCustomerPaymentExcel({ data: allDataForExport });
toast.success('Excel berhasil dibuat dan diunduh.'); toast.success('Excel berhasil dibuat dan diunduh.');
} catch { } catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.'); toast.error('Gagal membuat Excel. Silakan coba lagi.');
+29
View File
@@ -0,0 +1,29 @@
import Button, { ButtonProps } from '@/components/Button';
export interface PopoverButtonProps extends ButtonProps {
popoverTarget: string;
anchorName: string;
}
const PopoverButton = ({
children,
popoverTarget,
anchorName,
...props
}: PopoverButtonProps) => {
return (
<Button
{...props}
popoverTarget={popoverTarget}
style={
{
anchorName: anchorName,
} as React.CSSProperties
}
>
{children}
</Button>
);
};
export default PopoverButton;
+71
View File
@@ -0,0 +1,71 @@
import { cn } from '@/lib/helper';
export interface PopoverContentProps {
children: React.ReactNode;
id: string;
anchorName: string; // Must include `--` like "--menu-anchor"
popover?: 'auto' | 'hint' | 'manual';
position?:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end';
className?: string;
}
const positionAreaMap: Record<
NonNullable<PopoverContentProps['position']>,
string
> = {
top: 'top center',
bottom: 'bottom center',
left: 'left center',
right: 'right center',
'top-start': 'top left',
'top-end': 'top right',
'bottom-start': 'bottom left',
'bottom-end': 'bottom right',
'left-start': 'left top',
'left-end': 'left bottom',
'right-start': 'right top',
'right-end': 'right bottom',
};
const PopoverContent = ({
children,
id,
anchorName,
popover = 'auto',
position = 'bottom-start',
className,
}: PopoverContentProps) => {
return (
<div
className={cn(className)}
id={id}
popover={popover}
style={
{
inset: 'unset',
positionAnchor: anchorName,
positionArea: positionAreaMap[position],
} as React.CSSProperties
}
>
{children}
</div>
);
};
export default PopoverContent;
+1 -1
View File
@@ -12,7 +12,7 @@ const RowCollapseOptions = ({ children }: RowCollapseOptionsProps) => {
return ( return (
<Collapse <Collapse
title={ title={
<Button> <Button variant='ghost' color='none'>
<Icon icon='material-symbols:more-vert' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
</Button> </Button>
} }
+1 -1
View File
@@ -21,7 +21,7 @@ const RowDropdownOptions = ({
'dropdown-end': isLast2Rows, 'dropdown-end': isLast2Rows,
})} })}
> >
<Button tabIndex={0}> <Button tabIndex={0} variant='ghost' color='none'>
<Icon icon='material-symbols:more-vert' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
</Button> </Button>
+10 -2
View File
@@ -28,7 +28,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
permission: ['lti.daily_checklist.dashboard.list'], permission: ['lti.daily_checklist.dashboard.list'],
}, },
{ {
text: 'Daily Checklist', text: 'Formulir',
link: '/daily-checklist/daily-checklist', link: '/daily-checklist/daily-checklist',
icon: 'lucide:clipboard-check', icon: 'lucide:clipboard-check',
permission: ['lti.daily_checklist.create'], permission: ['lti.daily_checklist.create'],
@@ -94,7 +94,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
permission: ['lti.production.recording.list'], permission: ['lti.production.recording.list'],
}, },
{ {
text: 'Transfer to Laying', text: 'Transfer ke Laying',
link: '/production/transfer-to-laying', link: '/production/transfer-to-laying',
}, },
{ {
@@ -389,6 +389,14 @@ export const FINANCE_INITIAL_BALANCE_TYPE_OPTIONS = [
{ label: 'Saldo Awal Negatif', value: 'NEGATIVE' }, { label: 'Saldo Awal Negatif', value: 'NEGATIVE' },
]; ];
export const FINANCE_TRANSACTION_TYPE_OPTIONS = [
{ label: 'Pembelian', value: 'PEMBELIAN' },
{ label: 'Penjualan', value: 'PENJUALAN' },
{ label: 'Biaya', value: 'BIAYA' },
{ label: 'Saldo Awal', value: 'SALDO_AWAL' },
{ label: 'Injection', value: 'INJECTION' },
];
export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN', 'BIAYA']; export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN', 'BIAYA'];
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL']; export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
@@ -41,6 +41,7 @@ import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import DebouncedTextArea from '@/components/input/DebouncedTextArea'; import DebouncedTextArea from '@/components/input/DebouncedTextArea';
import DropFileInput from '@/components/input/DropFileInput'; import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
// Static categories // Static categories
@@ -51,7 +52,7 @@ const CATEGORIES = [
{ value: 'produksi_close', label: 'Produksi Close' }, { value: 'produksi_close', label: 'Produksi Close' },
]; ];
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam']; const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = { const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum', Umum: 'Umum',
Pagi: 'Pagi', Pagi: 'Pagi',
@@ -67,7 +68,23 @@ interface Phase {
} }
export function DailyChecklistContent() { export function DailyChecklistContent() {
const [kandangId, setKandangId] = useState(''); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [kandangId, setKandangId] = useState(
searchParams.get('kandang_id') || ''
);
const [date, setDate] = useState(() => {
const paramDate = searchParams.get('date');
if (paramDate) return paramDate;
const today = new Date();
return today.toISOString().split('T')[0];
});
const [selectedCategory, setSelectedCategory] = useState(
searchParams.get('category') || ''
);
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', { useSelect(KandangApi.basePath, 'id', 'name', 'search', {
@@ -104,12 +121,6 @@ export function DailyChecklistContent() {
? employeesRes.data || [] ? employeesRes.data || []
: []; : [];
const [date, setDate] = useState(() => {
const today = new Date();
return today.toISOString().split('T')[0];
});
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]); const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
const [selectedEmployees, setSelectedEmployees] = useState< const [selectedEmployees, setSelectedEmployees] = useState<
@@ -118,7 +129,7 @@ export function DailyChecklistContent() {
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null); const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT'); const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
const [isEditMode, setIsEditMode] = useState(false); // const [isEditMode, setIsEditMode] = useState(false);
// Activities grouped by phase // Activities grouped by phase
const [activitiesByPhase, setActivitiesByPhase] = useState<{ const [activitiesByPhase, setActivitiesByPhase] = useState<{
@@ -148,13 +159,57 @@ export function DailyChecklistContent() {
const [searchAbk, setSearchAbk] = useState(''); const [searchAbk, setSearchAbk] = useState('');
const [searchPhase, setSearchPhase] = useState(''); const [searchPhase, setSearchPhase] = useState('');
const [loading, setLoading] = useState(false); const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const [isLoadingDraft, setIsLoadingDraft] = useState(false);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]); const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]); const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
// Sync state to URL query params
useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
let pendingUpdate = false;
// Sync date
if (date) {
if (params.get('date') !== date) {
params.set('date', date);
pendingUpdate = true;
}
} else if (params.has('date')) {
params.delete('date');
pendingUpdate = true;
}
// Sync kandang_id
if (kandangId) {
if (params.get('kandang_id') !== kandangId) {
params.set('kandang_id', kandangId);
pendingUpdate = true;
}
} else if (params.has('kandang_id')) {
params.delete('kandang_id');
pendingUpdate = true;
}
// Sync category
if (selectedCategory) {
if (params.get('category') !== selectedCategory) {
params.set('category', selectedCategory);
pendingUpdate = true;
}
} else if (params.has('category')) {
params.delete('category');
pendingUpdate = true;
}
if (pendingUpdate) {
router.replace(`${pathname}?${params.toString()}`);
}
}, [date, kandangId, selectedCategory, pathname, router, searchParams]);
// Format date for display // Format date for display
const formatDateForDisplay = (dateStr: string) => { const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal'; if (!dateStr) return 'Pilih tanggal';
@@ -179,7 +234,7 @@ export function DailyChecklistContent() {
if (!date || !kandangId || !selectedCategory) { if (!date || !kandangId || !selectedCategory) {
setDailyChecklistId(null); setDailyChecklistId(null);
setChecklistStatus('DRAFT'); setChecklistStatus('DRAFT');
setIsEditMode(false); // setIsEditMode(false);
setSelectedPhaseIds([]); setSelectedPhaseIds([]);
setActivitiesByPhase({}); setActivitiesByPhase({});
setTaskIdsByPhaseActivityId({}); setTaskIdsByPhaseActivityId({});
@@ -216,7 +271,7 @@ export function DailyChecklistContent() {
existingPhases.data.phases.length > 0 existingPhases.data.phases.length > 0
) { ) {
// Existing checklist - EDIT MODE // Existing checklist - EDIT MODE
setIsEditMode(true); // setIsEditMode(true);
const phaseIds = existingPhases.data.phases.map((p) => const phaseIds = existingPhases.data.phases.map((p) =>
String(p.phase_id) String(p.phase_id)
); );
@@ -234,7 +289,7 @@ export function DailyChecklistContent() {
} }
} else { } else {
// New checklist - CREATE MODE // New checklist - CREATE MODE
setIsEditMode(false); // setIsEditMode(false);
setSelectedPhaseIds([]); setSelectedPhaseIds([]);
} }
} catch (error) { } catch (error) {
@@ -608,7 +663,7 @@ export function DailyChecklistContent() {
// taskId, // taskId,
// hasTaskId: !!taskId, // hasTaskId: !!taskId,
// checklistStatus, // checklistStatus,
// isEditable, // isChecklistStatusDraft,
// }); // });
if (!taskId) { if (!taskId) {
@@ -618,7 +673,7 @@ export function DailyChecklistContent() {
return; return;
} }
if (!isEditable) { if (!isChecklistStatusDraft) {
console.warn( console.warn(
'[CHECKBOX] Checklist is not editable, status:', '[CHECKBOX] Checklist is not editable, status:',
checklistStatus checklistStatus
@@ -736,7 +791,7 @@ export function DailyChecklistContent() {
return; return;
} }
setLoading(true); setIsLoadingSubmit(true);
try { try {
const submitRes = await DailyChecklistApi.submit( const submitRes = await DailyChecklistApi.submit(
@@ -757,13 +812,15 @@ export function DailyChecklistContent() {
console.error('Error submitting:', error); console.error('Error submitting:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
} finally { } finally {
setLoading(false); setIsLoadingSubmit(false);
} }
}; };
const handleSaveDraft = async () => { const handleSaveDraft = async () => {
if (!dailyChecklistId) return; if (!dailyChecklistId) return;
setIsLoadingDraft(true);
const uploadImageRes = await DailyChecklistApi.uploadImage( const uploadImageRes = await DailyChecklistApi.uploadImage(
Number(dailyChecklistId), Number(dailyChecklistId),
'DRAFT', 'DRAFT',
@@ -774,10 +831,12 @@ export function DailyChecklistContent() {
if (isResponseError(uploadImageRes)) { if (isResponseError(uploadImageRes)) {
console.error('Error saving draft:', uploadImageRes.message); console.error('Error saving draft:', uploadImageRes.message);
toast.error('Gagal menyimpan draft'); toast.error('Gagal menyimpan draft');
setIsLoadingDraft(false);
return; return;
} }
toast.success('Draft tersimpan otomatis'); setIsLoadingDraft(false);
toast.success('Draft tersimpan!');
}; };
// Filter functions // Filter functions
@@ -825,7 +884,7 @@ export function DailyChecklistContent() {
// Group activities by time_type within this phase // Group activities by time_type within this phase
phaseActivities.forEach((activity) => { phaseActivities.forEach((activity) => {
const timeType = activity.time_type || 'umum'; const timeType = activity.time_type || 'Umum';
if (!grouped[phase.id].timeGroups[timeType]) { if (!grouped[phase.id].timeGroups[timeType]) {
grouped[phase.id].timeGroups[timeType] = []; grouped[phase.id].timeGroups[timeType] = [];
@@ -838,7 +897,7 @@ export function DailyChecklistContent() {
return grouped; return grouped;
}; };
const isEditable = checklistStatus === 'DRAFT'; const isChecklistStatusDraft = checklistStatus === 'DRAFT';
if (initialLoading) { if (initialLoading) {
return ( return (
@@ -871,7 +930,7 @@ export function DailyChecklistContent() {
<h1 className='text-2xl font-semibold text-gray-900'> <h1 className='text-2xl font-semibold text-gray-900'>
Daily Checklist Daily Checklist
</h1> </h1>
{isEditMode && ( {isChecklistStatusDraft && (
<Badge <Badge
variant='outline' variant='outline'
className='border-amber-300 text-amber-700 bg-white' className='border-amber-300 text-amber-700 bg-white'
@@ -907,7 +966,7 @@ export function DailyChecklistContent() {
<DatePicker <DatePicker
date={date} date={date}
onDateChange={setDate} onDateChange={setDate}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
placeholder='Pilih tanggal' placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay} formatDisplay={formatDateForDisplay}
/> />
@@ -921,7 +980,7 @@ export function DailyChecklistContent() {
<Select <Select
value={kandangId} value={kandangId}
onValueChange={setKandangId} onValueChange={setKandangId}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
> >
<SelectTrigger <SelectTrigger
id='kandang' id='kandang'
@@ -949,7 +1008,7 @@ export function DailyChecklistContent() {
<Select <Select
value={selectedCategory} value={selectedCategory}
onValueChange={setSelectedCategory} onValueChange={setSelectedCategory}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
> >
<SelectTrigger <SelectTrigger
id='category' id='category'
@@ -971,19 +1030,21 @@ export function DailyChecklistContent() {
{/* Phase Selection Section */} {/* Phase Selection Section */}
{dailyChecklistId && ( {dailyChecklistId && (
<div className='mb-6 pb-6 border-b border-gray-200'> <div className='mb-6 pb-6 border-b border-gray-200'>
<div className='flex items-center justify-between mb-3'> {isChecklistStatusDraft && (
<Label>Fase / Tahap</Label> <div className='flex items-center justify-between mb-3'>
<Button <Label>Fase / Tahap</Label>
onClick={handleAddPhase} <Button
size='sm' onClick={handleAddPhase}
variant='outline' size='sm'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50' variant='outline'
disabled={!selectedCategory || !isEditable} className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
> disabled={!selectedCategory || !isChecklistStatusDraft}
<Plus className='w-4 h-4 mr-1' /> >
Pilih Fase <Plus className='w-4 h-4 mr-1' />
</Button> Pilih Fase
</div> </Button>
</div>
)}
{selectedPhaseIds.length > 0 ? ( {selectedPhaseIds.length > 0 ? (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
@@ -1010,19 +1071,21 @@ export function DailyChecklistContent() {
{/* ABK Assignment Section */} {/* ABK Assignment Section */}
{dailyChecklistId && selectedPhaseIds.length > 0 && ( {dailyChecklistId && selectedPhaseIds.length > 0 && (
<div className='mb-6 pb-6 border-b border-gray-200'> <div className='mb-6 pb-6 border-b border-gray-200'>
<div className='flex items-center justify-between mb-3'> {isChecklistStatusDraft && (
<Label>ABK Assignment</Label> <div className='flex items-center justify-between mb-3'>
<Button <Label>ABK Assignment</Label>
onClick={handleAddAbk} <Button
size='sm' onClick={handleAddAbk}
variant='outline' size='sm'
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50' variant='outline'
disabled={!kandangId || !isEditable} className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
> disabled={!kandangId || !isChecklistStatusDraft}
<Plus className='w-4 h-4 mr-1' /> >
Tambah ABK <Plus className='w-4 h-4 mr-1' />
</Button> Tambah ABK
</div> </Button>
</div>
)}
{selectedEmployees.length > 0 ? ( {selectedEmployees.length > 0 ? (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
@@ -1033,7 +1096,7 @@ export function DailyChecklistContent() {
className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg' className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg'
> >
{emp.name} {emp.name}
{isEditable && ( {isChecklistStatusDraft && (
<button <button
onClick={() => handleRemoveAbk(String(emp.id))} onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900' className='ml-2 hover:text-gray-900'
@@ -1084,6 +1147,7 @@ export function DailyChecklistContent() {
(phaseId) => { (phaseId) => {
const phaseData = groupActivitiesByPhase()[phaseId]; const phaseData = groupActivitiesByPhase()[phaseId];
const { phase, timeGroups } = phaseData; const { phase, timeGroups } = phaseData;
const timeTypes = Object.keys(timeGroups).sort( const timeTypes = Object.keys(timeGroups).sort(
(a, b) => (a, b) =>
TIME_TYPE_ORDER.indexOf(a) - TIME_TYPE_ORDER.indexOf(a) -
@@ -1197,7 +1261,7 @@ export function DailyChecklistContent() {
e.target.checked e.target.checked
) )
} }
disabled={!isEditable} disabled={!isChecklistStatusDraft}
className='checkbox-clean' className='checkbox-clean'
/> />
</td> </td>
@@ -1224,7 +1288,7 @@ export function DailyChecklistContent() {
); );
} }
}} }}
disabled={!isEditable} disabled={!isChecklistStatusDraft}
/> />
</td> </td>
</tr> </tr>
@@ -1320,61 +1384,68 @@ export function DailyChecklistContent() {
/> />
</Link> </Link>
<Button {isChecklistStatusDraft && (
type='button' <Button
variant='ghost' type='button'
color='error' variant='ghost'
onClick={() => { color='error'
setDeletedDocumentIds((prevIds) => [ onClick={() => {
...prevIds, setDeletedDocumentIds((prevIds) => [
existingDocument.id, ...prevIds,
]); existingDocument.id,
]);
setExistingDocuments((prevExistingDocument) => { setExistingDocuments(
const newExistingDocuments = [ (prevExistingDocument) => {
...prevExistingDocument, const newExistingDocuments = [
]; ...prevExistingDocument,
newExistingDocuments.splice( ];
existingDocumentIdx, newExistingDocuments.splice(
1 existingDocumentIdx,
1
);
return newExistingDocuments;
}
); );
return newExistingDocuments; }}
}); className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
}} >
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content' <Icon
> icon='fluent:delete-12-regular'
<Icon width={20}
icon='fluent:delete-12-regular' height={20}
width={20} />
height={20} </Button>
/> )}
</Button>
</div> </div>
) )
)} )}
</div> </div>
)} )}
<DropFileInput {isChecklistStatusDraft && (
name='Dokumen' <DropFileInput
label='Dokumen' name='Dokumen'
values={documents} label='Dokumen'
onChange={(files) => { values={documents}
setDocuments(files); onChange={(files) => {
}} setDocuments(files);
onDelete={(deletedFileIdx: number) => { }}
const newRequestDocuments = [...documents]; onDelete={(deletedFileIdx: number) => {
const newRequestDocuments = [...documents];
newRequestDocuments?.splice(deletedFileIdx, 1); newRequestDocuments?.splice(deletedFileIdx, 1);
setDocuments(newRequestDocuments); setDocuments(newRequestDocuments);
}} }}
className={{ disabled={!isChecklistStatusDraft}
wrapper: 'mt-6', className={{
inputWrapper: 'flex items-center', wrapper: 'mt-6',
label: 'font-semibold text-gray-900', inputWrapper: 'flex items-center',
}} label: 'font-semibold text-gray-900',
/> }}
/>
)}
</> </>
)} )}
@@ -1382,24 +1453,30 @@ export function DailyChecklistContent() {
{dailyChecklistId && {dailyChecklistId &&
selectedPhaseIds.length > 0 && selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && selectedEmployees.length > 0 &&
isEditable && ( isChecklistStatusDraft && (
<div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'> <div className='flex justify-end gap-3 mt-6 pt-6 border-t border-gray-200'>
<Button <Button
onClick={handleSaveDraft} onClick={handleSaveDraft}
variant='outline' variant='outline'
disabled={loading} disabled={isLoadingDraft}
className='border-gray-200' className='border-gray-200'
> >
<Save className='w-4 h-4 mr-2' /> <Save className='w-4 h-4 mr-2' />
Simpan Draft {isLoadingDraft ? (
<span className='loading loading-spinner loading-sm' />
) : (
'Simpan Draft'
)}
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={isLoadingSubmit}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white' className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
> >
<Send className='w-4 h-4 mr-2' /> <Send className='w-4 h-4 mr-2' />
Submit Checklist {isLoadingSubmit
? 'Mengirim Checklist...'
: 'Submit Checklist'}
</Button> </Button>
</div> </div>
)} )}
@@ -1440,7 +1517,9 @@ export function DailyChecklistContent() {
if (isAllPhasesSelected) { if (isAllPhasesSelected) {
setTempSelectedPhaseIds([]); setTempSelectedPhaseIds([]);
} else { } else {
setTempSelectedPhaseIds(availablePhases.map((p) => p.id)); setTempSelectedPhaseIds(
availablePhases.map((p) => String(p.id))
);
} }
}} }}
className='checkbox-clean' className='checkbox-clean'
@@ -83,10 +83,7 @@ export function Dashboard() {
dateFrom && dateTo dateFrom && dateTo
? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}` ? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}`
: '', : '',
httpClientFetcher, httpClientFetcher
{
keepPreviousData: true,
}
); );
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
@@ -311,14 +308,14 @@ export function Dashboard() {
<Bar <Bar
dataKey='remaining' dataKey='remaining'
stackId='a' stackId='a'
fill='#E5E7EB' fill='#878c96'
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
> >
{chartData?.map((entry, index) => ( {chartData?.map((entry, index) => (
<Cell <Cell
key={`cell-remaining-${index}`} key={`cell-remaining-${index}`}
fill={`${entry.color}33`} fill={`${entry.color}70`}
opacity={0.3} opacity={0.7}
/> />
))} ))}
</Bar> </Bar>
@@ -370,7 +367,7 @@ export function Dashboard() {
<tbody> <tbody>
{employeePerformance?.map((emp, index) => ( {employeePerformance?.map((emp, index) => (
<tr <tr
key={emp.employee_id} key={index}
className={ className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50' index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
} }
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react'; import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
@@ -121,6 +121,16 @@ export function ListDailyChecklistContent() {
); );
}; };
const handleEdit = (item: DailyChecklist) => {
const formattedDate = new Date(item.date).toISOString().split('T')[0];
const kandangId = item.kandang.id;
const category = item.category;
router.push(
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
);
};
const handleApprove = (item: DailyChecklist) => { const handleApprove = (item: DailyChecklist) => {
setSelectedItem(item); setSelectedItem(item);
setShowApproveModal(true); setShowApproveModal(true);
@@ -357,7 +367,7 @@ export function ListDailyChecklistContent() {
}, },
{ {
accessorKey: 'updated_at', accessorKey: 'updated_at',
header: 'Update At', header: 'Diperbarui',
enableSorting: false, enableSorting: false,
cell: ({ row }) => formatDateTime(row.original.updated_at), cell: ({ row }) => formatDateTime(row.original.updated_at),
}, },
@@ -377,6 +387,19 @@ export function ListDailyChecklistContent() {
<Eye className='w-4 h-4 mr-1' /> <Eye className='w-4 h-4 mr-1' />
Detail Detail
</Button> </Button>
{row.original.status === 'DRAFT' && (
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(row.original)}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Edit className='w-4 h-4 mr-1' />
Edit
</Button>
)}
{row.original.status === 'SUBMITTED' && ( {row.original.status === 'SUBMITTED' && (
<> <>
<Button <Button
@@ -398,15 +421,18 @@ export function ListDailyChecklistContent() {
</Button> </Button>
</> </>
)} )}
<Button
size='sm' {row.original.status === 'DRAFT' && (
variant='destructive' <Button
onClick={() => handleDelete(row.original)} size='sm'
className='bg-red-600 hover:bg-red-700 text-white' variant='destructive'
> onClick={() => handleDelete(row.original)}
<Trash2 className='w-4 h-4 mr-1' /> className='bg-red-600 hover:bg-red-700 text-white'
Hapus >
</Button> <Trash2 className='w-4 h-4 mr-1' />
Hapus
</Button>
)}
</div> </div>
), ),
}, },
@@ -389,7 +389,7 @@ export function DetailDailyChecklistContent() {
} = {}; } = {};
phaseData.activities.forEach((activityData) => { phaseData.activities.forEach((activityData) => {
const timeType = activityData.time_type || 'umum'; const timeType = activityData.time_type || 'Umum';
if (!timeGroups[timeType]) { if (!timeGroups[timeType]) {
timeGroups[timeType] = { activities: [] }; timeGroups[timeType] = { activities: [] };
@@ -144,13 +144,9 @@ export function MasterAktivitasContent() {
id: '', id: '',
name: '', name: '',
description: '', description: '',
time_type: 'umum', time_type: '',
}); });
useEffect(() => {
setInitialLoading(false);
}, []);
// Phase handlers // Phase handlers
const handleAddPhase = () => { const handleAddPhase = () => {
if (!selectedCategory) { if (!selectedCategory) {
@@ -277,7 +273,7 @@ export function MasterAktivitasContent() {
return; return;
} }
setActivityModalMode('create'); setActivityModalMode('create');
setActivityForm({ id: '', name: '', description: '', time_type: 'umum' }); setActivityForm({ id: '', name: '', description: '', time_type: '' });
setShowActivityModal(true); setShowActivityModal(true);
}; };
@@ -293,16 +289,25 @@ export function MasterAktivitasContent() {
}; };
const handleSaveActivity = async () => { const handleSaveActivity = async () => {
if (!activityForm.name.trim()) { const isTimeTypeValid = Boolean(activityForm.time_type);
const isNameValid = Boolean(activityForm.name.trim());
const isNameLengthValid = activityForm.name.trim().length >= 3;
if (!isNameValid) {
toast.error('Nama aktivitas harus diisi'); toast.error('Nama aktivitas harus diisi');
return; return;
} }
if (activityForm.name.trim().length < 3) { if (!isNameLengthValid) {
toast.error('Nama aktivitas minimal 3 karakter!'); toast.error('Nama aktivitas minimal 3 karakter!');
return; return;
} }
if (!isTimeTypeValid) {
toast.error('Tipe waktu harus diisi');
return;
}
if (!selectedPhase) { if (!selectedPhase) {
toast.error('Pilih phase terlebih dahulu'); toast.error('Pilih phase terlebih dahulu');
return; return;
@@ -356,7 +361,7 @@ export function MasterAktivitasContent() {
} }
setShowActivityModal(false); setShowActivityModal(false);
setActivityForm({ id: '', name: '', description: '', time_type: 'umum' }); setActivityForm({ id: '', name: '', description: '', time_type: '' });
} catch (error) { } catch (error) {
console.error('Error saving activity:', error); console.error('Error saving activity:', error);
toast.error('Terjadi kesalahan saat menyimpan aktivitas'); toast.error('Terjadi kesalahan saat menyimpan aktivitas');
@@ -423,6 +428,14 @@ export function MasterAktivitasContent() {
} }
}; };
useEffect(() => {
setInitialLoading(false);
}, []);
useEffect(() => {
setSelectedPhase(null);
}, [selectedCategory]);
if (initialLoading) { if (initialLoading) {
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
@@ -69,6 +69,7 @@ export function MasterConfigurationContent() {
} }
); );
const [isFormInvalid, setIsFormInvalid] = useState(false);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [configurationToDelete, setConfigurationToDelete] = useState< const [configurationToDelete, setConfigurationToDelete] = useState<
@@ -173,6 +174,26 @@ export function MasterConfigurationContent() {
return; return;
} }
if (
Number(configurationForm.percentage_threshold_enough) >= 100 ||
Number(configurationForm.percentage_threshold_bad) >= 100
) {
setIsFormInvalid(true);
toast.error('Persentase threshold tidak boleh lebih dari 100');
return;
}
if (
Number(configurationForm.percentage_threshold_enough) <=
Number(configurationForm.percentage_threshold_bad) + 1
) {
setIsFormInvalid(true);
toast.error(
'Persentase threshold "kurang" harus lebih kecil dari persentase threshold "cukup"'
);
return;
}
setLoading(true); setLoading(true);
try { try {
@@ -443,14 +464,18 @@ export function MasterConfigurationContent() {
type='number' type='number'
id='percentageThresholdBad' id='percentageThresholdBad'
value={configurationForm.percentage_threshold_bad} value={configurationForm.percentage_threshold_bad}
onChange={(e) => onChange={(e) => {
setIsFormInvalid(false);
setConfigurationForm({ setConfigurationForm({
...configurationForm, ...configurationForm,
percentage_threshold_bad: e.target.value, percentage_threshold_bad: e.target.value,
}) });
} }}
placeholder='Kurang' placeholder='Kurang'
className='w-20' className={cn('w-20', {
'border-red-500': isFormInvalid,
})}
disabled={loading} disabled={loading}
max={100} max={100}
/> />
@@ -476,14 +501,17 @@ export function MasterConfigurationContent() {
type='number' type='number'
id='percentageThresholdEnough' id='percentageThresholdEnough'
value={configurationForm.percentage_threshold_enough} value={configurationForm.percentage_threshold_enough}
onChange={(e) => onChange={(e) => {
setIsFormInvalid(false);
setConfigurationForm({ setConfigurationForm({
...configurationForm, ...configurationForm,
percentage_threshold_enough: e.target.value, percentage_threshold_enough: e.target.value,
}) });
} }}
placeholder='Cukup' placeholder='Cukup'
className='w-20' className={cn('w-20', {
'border-red-500': isFormInvalid,
})}
disabled={loading} disabled={loading}
min={Number(configurationForm.percentage_threshold_bad) + 1} min={Number(configurationForm.percentage_threshold_bad) + 1}
max={100} max={100}
@@ -292,7 +292,7 @@ export function DailyChecklistReportsContent() {
}; };
const phaseChangeHandler = (value: string) => { const phaseChangeHandler = (value: string) => {
updateFilter('phase_id', value); updateFilter('phase_id', value === 'ALL' ? '' : value);
}; };
const employeeChangeHandler = (value: string) => { const employeeChangeHandler = (value: string) => {
+12 -26
View File
@@ -23,33 +23,18 @@ export type BaseSales = {
qty: number; qty: number;
weight: number; weight: number;
avg_weight: number; avg_weight: number;
price: number; sales_price: number;
total_price: number; total_sales_price: number;
actual_price: number;
total_actual_price: number;
kandang: Kandang; kandang: Kandang;
payment_status: string; };
};
export type ClosingSalesSummary = {
export type BaseClosingSales = { total_sales_price: number;
project_type: string; avg_sales_price: number;
flock_id: number; total_actual_price: number;
period: number; avg_actual_price: number;
sales: BaseSales[];
};
export type BaseSales = {
id: number;
realization_date: string;
age: number;
do_number: string;
product: Product;
customer: Customer;
qty: number;
weight: number;
avg_weight: number;
price: number;
total_price: number;
kandang: Kandang;
payment_status: string;
}; };
export type BaseClosingSales = { export type BaseClosingSales = {
@@ -57,6 +42,7 @@ export type BaseClosingSales = {
flock_id: number; flock_id: number;
period: number; period: number;
sales: BaseSales[]; sales: BaseSales[];
summary: ClosingSalesSummary;
}; };
export type BaseClosing = { export type BaseClosing = {
+2
View File
@@ -1,11 +1,13 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { Uom } from '@/types/api/master-data/uom';
export type BaseProductWarehouse = { export type BaseProductWarehouse = {
id: number; id: number;
product_id: number; product_id: number;
warehouse_id: number; warehouse_id: number;
uom: Uom;
quantity: number; quantity: number;
product: Product; product: Product;
warehouse: Warehouse; warehouse: Warehouse;
+2
View File
@@ -41,7 +41,9 @@ export type DailyMarketingRow = BaseMetadata & BaseDailyMarketingRow;
export interface SalesSummary { export interface SalesSummary {
total_qty: number; total_qty: number;
average_weight_kg: number;
total_weight_kg: number; total_weight_kg: number;
average_sales_price: number;
total_sales_amount: number; total_sales_amount: number;
total_hpp_amount: number; total_hpp_amount: number;
total_hpp_price_per_kg: number; total_hpp_price_per_kg: number;