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