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