mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
Merge branch 'feat/FE/US-283/TASK-320-321-322-323-sapronak-closing-report' into 'feat/FE/US-283/sapronak-closing-report'
[FEAT/FE][US#283/TASK#320-321-322-323] Sapronak Closing Report See merge request mbugroup/lti-web-client!73
This commit is contained in:
Generated
+4
-4
@@ -36,7 +36,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^5.5.5",
|
"daisyui": "^5.5.8",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^15.5.7",
|
"eslint-config-next": "^15.5.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
@@ -3063,9 +3063,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "5.5.5",
|
"version": "5.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz",
|
||||||
"integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==",
|
"integrity": "sha512-6psL9jIEOFOw68V10j/BKCWcRgx8dh81mmNxShr+g7HDM6UHNoPharlp9zq/PQkHNuGU1ZQsajR3HgpvavbRKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^5.5.5",
|
"daisyui": "^5.5.8",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^15.5.7",
|
"eslint-config-next": "^15.5.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
||||||
|
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ClosingDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const closingId = searchParams.get('closingId');
|
||||||
|
|
||||||
|
const { data: closing, isLoading: isLoadingClosing } = useSWR(
|
||||||
|
closingId,
|
||||||
|
(id: number) => ClosingApi.getGeneralInfo(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!closingId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingClosing && (!closing || isResponseError(closing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingClosing && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingClosing && isResponseSuccess(closing) && (
|
||||||
|
<ClosingDetail id={Number(closingId)} initialValue={closing.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
||||||
|
|
||||||
|
const Closing = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ClosingsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Closing;
|
||||||
@@ -43,6 +43,12 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-inter: var(--font-inter);
|
--font-inter: var(--font-inter);
|
||||||
|
|
||||||
|
--container-sm: 40rem;
|
||||||
|
--container-md: 48rem;
|
||||||
|
--container-lg: 64rem;
|
||||||
|
--container-xl: 80rem;
|
||||||
|
--container-2xl: 96rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const PaginationButton = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0'
|
'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -52,7 +52,7 @@ const EtcPaginationButton = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role='button'
|
role='button'
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
@@ -61,7 +61,7 @@ const EtcPaginationButton = ({
|
|||||||
<div className='dropdown-content'>
|
<div className='dropdown-content'>
|
||||||
<ul
|
<ul
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className='menu bg-base-100 rounded-lg z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
|
className='menu bg-base-100 rounded-lg! z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
|
||||||
>
|
>
|
||||||
{pages.map((pageNumber) => (
|
{pages.map((pageNumber) => (
|
||||||
<li key={pageNumber}>
|
<li key={pageNumber}>
|
||||||
@@ -80,7 +80,7 @@ const EtcPaginationButton = ({
|
|||||||
<button
|
<button
|
||||||
disabled
|
disabled
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
@@ -129,7 +129,7 @@ const Pagination = ({
|
|||||||
<select
|
<select
|
||||||
defaultValue={itemsPerPage}
|
defaultValue={itemsPerPage}
|
||||||
onChange={rowChangeHandler}
|
onChange={rowChangeHandler}
|
||||||
className='select select-xs w-fit text-base-content/50'
|
className='select select-xs w-fit pl-3 pr-7 text-base-content/50'
|
||||||
>
|
>
|
||||||
{rowOptions.map((rowOption, rowOptionIdx) => (
|
{rowOptions.map((rowOption, rowOptionIdx) => (
|
||||||
<option
|
<option
|
||||||
@@ -151,7 +151,7 @@ const Pagination = ({
|
|||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -171,7 +171,7 @@ const Pagination = ({
|
|||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -191,7 +191,7 @@ const Pagination = ({
|
|||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={lastPageClickHandler}
|
onClick={lastPageClickHandler}
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -211,7 +211,7 @@ const Pagination = ({
|
|||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={onNextPage}
|
onClick={onNextPage}
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -233,16 +233,16 @@ const Pagination = ({
|
|||||||
return (
|
return (
|
||||||
<div className='@container'>
|
<div className='@container'>
|
||||||
<div className='flex flex-row justify-center items-center'>
|
<div className='flex flex-row justify-center items-center'>
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<DisplayedRowCountSelect />
|
<DisplayedRowCountSelect />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='join w-full justify-end @lg:justify-center items-center gap-0.5'>
|
<div className='join w-full justify-end @md:justify-center items-center gap-0.5'>
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<GoToFirstPageButton />
|
<GoToFirstPageButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<PrevPageButton />
|
<PrevPageButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -390,21 +390,21 @@ const Pagination = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<NextPageButton />
|
<NextPageButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<GoToLastPageButton />
|
<GoToLastPageButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='hidden @lg:block'>
|
<div className='hidden @md:block'>
|
||||||
<PageInfo />
|
<PageInfo />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex @lg:hidden flex-col justify-center items-end gap-2'>
|
<div className='flex @md:hidden flex-col justify-center items-end gap-2'>
|
||||||
<div className='flex flex-row items-center gap-0.5'>
|
<div className='flex flex-row items-center gap-0.5'>
|
||||||
<GoToFirstPageButton />
|
<GoToFirstPageButton />
|
||||||
<PrevPageButton />
|
<PrevPageButton />
|
||||||
|
|||||||
+59
-29
@@ -38,6 +38,7 @@ export interface TableProps<TData extends object> {
|
|||||||
data: TData[];
|
data: TData[];
|
||||||
columns: ColumnDef<TData, unknown>[];
|
columns: ColumnDef<TData, unknown>[];
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
totalItems?: number;
|
totalItems?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
onPageChange?: (page: number) => void;
|
onPageChange?: (page: number) => void;
|
||||||
@@ -52,6 +53,8 @@ export interface TableProps<TData extends object> {
|
|||||||
rowSelection?: Record<string, boolean>;
|
rowSelection?: Record<string, boolean>;
|
||||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
|
withCheckbox?: boolean;
|
||||||
|
rowOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -64,28 +67,32 @@ const emptyContentDefaultValue = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TABLE_DEFAULT_STYLING = {
|
||||||
|
containerClassName: 'w-full mb-20',
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
|
||||||
|
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
|
||||||
|
tableHeaderClassName: '',
|
||||||
|
headerRowClassName: '',
|
||||||
|
headerColumnClassName: 'px-4 py-3 text-base-content/50',
|
||||||
|
tableBodyClassName: '',
|
||||||
|
bodyRowClassName: 'border-t border-t-base-content/10',
|
||||||
|
bodyColumnClassName: 'px-4 py-3 text-base-content',
|
||||||
|
paginationClassName: '',
|
||||||
|
};
|
||||||
|
|
||||||
const Table = <TData extends object>({
|
const Table = <TData extends object>({
|
||||||
data = [],
|
data = [],
|
||||||
columns = [],
|
columns = [],
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
|
onPageSizeChange,
|
||||||
totalItems,
|
totalItems,
|
||||||
page,
|
page,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
fuzzySearchValue,
|
fuzzySearchValue,
|
||||||
onFuzzySearchValueChange,
|
onFuzzySearchValueChange,
|
||||||
className = {
|
className = TABLE_DEFAULT_STYLING,
|
||||||
containerClassName: '',
|
|
||||||
tableWrapperClassName: '',
|
|
||||||
tableClassName: '',
|
|
||||||
tableHeaderClassName: '',
|
|
||||||
headerRowClassName: '',
|
|
||||||
headerColumnClassName: '',
|
|
||||||
tableBodyClassName: '',
|
|
||||||
bodyRowClassName: '',
|
|
||||||
bodyColumnClassName: '',
|
|
||||||
paginationClassName: '',
|
|
||||||
},
|
|
||||||
emptyContent = emptyContentDefaultValue,
|
emptyContent = emptyContentDefaultValue,
|
||||||
sorting,
|
sorting,
|
||||||
setSorting,
|
setSorting,
|
||||||
@@ -93,12 +100,19 @@ const Table = <TData extends object>({
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
|
withCheckbox = false,
|
||||||
|
rowOptions = [10, 20, 50, 100],
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
const isServerSideTable =
|
const isServerSideTable =
|
||||||
totalItems !== undefined &&
|
totalItems !== undefined &&
|
||||||
page !== undefined &&
|
page !== undefined &&
|
||||||
onPageChange !== undefined;
|
onPageChange !== undefined;
|
||||||
|
|
||||||
|
const tableClassNames = {
|
||||||
|
...TABLE_DEFAULT_STYLING,
|
||||||
|
...className,
|
||||||
|
};
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
@@ -191,12 +205,15 @@ const Table = <TData extends object>({
|
|||||||
}, [pageSize, setPageSize]);
|
}, [pageSize, setPageSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className.containerClassName}>
|
<div className={tableClassNames.containerClassName}>
|
||||||
<div className={className.tableWrapperClassName}>
|
<div className={tableClassNames.tableWrapperClassName}>
|
||||||
<table className={className.tableClassName}>
|
<table className={tableClassNames.tableClassName}>
|
||||||
<thead className={className.tableHeaderClassName}>
|
<thead className={tableClassNames.tableHeaderClassName}>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id} className={className.headerRowClassName}>
|
<tr
|
||||||
|
key={headerGroup.id}
|
||||||
|
className={tableClassNames.headerRowClassName}
|
||||||
|
>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
@@ -206,7 +223,10 @@ const Table = <TData extends object>({
|
|||||||
header.column.getCanSort()
|
header.column.getCanSort()
|
||||||
? 'cursor-pointer select-none'
|
? 'cursor-pointer select-none'
|
||||||
: '',
|
: '',
|
||||||
className.headerColumnClassName
|
{
|
||||||
|
'first:w-9 first:pr-0': withCheckbox,
|
||||||
|
},
|
||||||
|
tableClassNames.headerColumnClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1'>
|
||||||
@@ -216,12 +236,13 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{header.column.getCanSort() && (
|
{header.column.getCanSort() && (
|
||||||
<div className='flex items-center'>
|
<div className='w-4 h-4 relative flex flex-col items-center'>
|
||||||
<Icon
|
<Icon
|
||||||
icon='lucide:arrow-up'
|
icon='heroicons:chevron-up-16-solid'
|
||||||
width={12}
|
width={18}
|
||||||
height={12}
|
height={18}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'absolute -top-1',
|
||||||
'transition-all ease-in-out duration-200',
|
'transition-all ease-in-out duration-200',
|
||||||
header.column.getIsSorted() === 'asc'
|
header.column.getIsSorted() === 'asc'
|
||||||
? 'text-black'
|
? 'text-black'
|
||||||
@@ -229,10 +250,11 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
icon='lucide:arrow-down'
|
icon='heroicons:chevron-down-16-solid'
|
||||||
width={12}
|
width={18}
|
||||||
height={12}
|
height={18}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'absolute -bottom-1.5',
|
||||||
'transition-all ease-in-out duration-200',
|
'transition-all ease-in-out duration-200',
|
||||||
header.column.getIsSorted() === 'desc'
|
header.column.getIsSorted() === 'desc'
|
||||||
? 'text-black'
|
? 'text-black'
|
||||||
@@ -248,11 +270,17 @@ const Table = <TData extends object>({
|
|||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className={className.tableBodyClassName}>
|
<tbody className={tableClassNames.tableBodyClassName}>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr key={row.id} className={className.bodyRowClassName}>
|
<tr key={row.id} className={tableClassNames.bodyRowClassName}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td key={cell.id} className={className.bodyColumnClassName}>
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(
|
||||||
|
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||||
|
tableClassNames.bodyColumnClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
flexRender(cell.column.columnDef.cell, cell.getContext())}
|
flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
|
||||||
@@ -270,7 +298,7 @@ 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', className.paginationClassName)}>
|
<div className={cn('mt-5', tableClassNames.paginationClassName)}>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
|
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
|
||||||
itemsPerPage={table.getState().pagination.pageSize}
|
itemsPerPage={table.getState().pagination.pageSize}
|
||||||
@@ -282,6 +310,8 @@ const Table = <TData extends object>({
|
|||||||
onPrevPage={prevPageClickHandler}
|
onPrevPage={prevPageClickHandler}
|
||||||
onNextPage={nextPageClickHandler}
|
onNextPage={nextPageClickHandler}
|
||||||
onPageChange={pageChangeHandler}
|
onPageChange={pageChangeHandler}
|
||||||
|
rowOptions={rowOptions}
|
||||||
|
onRowChange={onPageSizeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||||
|
|
||||||
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
|
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
|
||||||
|
|
||||||
|
interface ClosingDetailProps {
|
||||||
|
id: number;
|
||||||
|
initialValue?: ClosingGeneralInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({ id, initialValue }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||||
|
|
||||||
|
const closingDetailTabs = useMemo(() => {
|
||||||
|
const validTabs = [
|
||||||
|
{
|
||||||
|
id: 'sapronak',
|
||||||
|
label: 'Sapronak',
|
||||||
|
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perhitunganSapronak',
|
||||||
|
label: 'Perhitungan Sapronak',
|
||||||
|
content: 'Perhitungan Sapronak',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'penjualan',
|
||||||
|
label: 'Penjualan',
|
||||||
|
content: 'Penjualan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overhead',
|
||||||
|
label: 'Overhead',
|
||||||
|
content: 'Overhead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hppEkspedisi',
|
||||||
|
label: 'HPP Ekspedisi',
|
||||||
|
content: 'HPP Ekspedisi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dataProduksi',
|
||||||
|
label: 'Data Produksi',
|
||||||
|
content: 'Data Produksi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'keuangan',
|
||||||
|
label: 'Keuangan',
|
||||||
|
content: 'Keuangan',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return validTabs;
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
|
<header className='flex flex-col gap-4'>
|
||||||
|
<Button
|
||||||
|
href='/closing'
|
||||||
|
variant='link'
|
||||||
|
className='w-fit p-0 text-primary'
|
||||||
|
>
|
||||||
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h1 className='text-2xl font-bold text-center'>Detail Closing</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ClosingGeneralInformationTable initialValue={initialValue} />
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
activeTabId={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
tabs={closingDetailTabs}
|
||||||
|
variant='lifted'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full mt-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingDetail;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingGeneralInformationProps {
|
||||||
|
initialValue?: ClosingGeneralInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingGeneralInformationTable = ({
|
||||||
|
initialValue,
|
||||||
|
}: ClosingGeneralInformationProps) => {
|
||||||
|
return (
|
||||||
|
<div className='w-full my-4 @container'>
|
||||||
|
<div className='flex flex-col @sm:flex-row gap-4'>
|
||||||
|
<div className='w-full'>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table table-zebra table-sm'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Lokasi</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.location_name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Periode</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.period}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Kategori</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.project_category}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Populasi</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.population} Ekor</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Jenis Project</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.project_type}</td>
|
||||||
|
</tr>
|
||||||
|
<tr className='table-row @sm:hidden'>
|
||||||
|
<td>Kandang Aktif</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.active_house_count} Kandang</td>
|
||||||
|
</tr>
|
||||||
|
<tr className='table-row @sm:hidden'>
|
||||||
|
<td>Status Pembayaran Penjualan</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.sales_payment_status}</td>
|
||||||
|
</tr>
|
||||||
|
<tr className='table-row @sm:hidden'>
|
||||||
|
<td>Status Project</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.project_status}</td>
|
||||||
|
</tr>
|
||||||
|
<tr className='table-row @sm:hidden'>
|
||||||
|
<td>Status Closing</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.closing_status}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full hidden @sm:block'>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table table-zebra table-sm'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Kandang Aktif</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.active_house_count} Kandang</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status Pembayaran Penjualan</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.sales_payment_status}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status Project</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.project_status}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status Closing</td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>{initialValue?.closing_status}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingGeneralInformationTable;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingIncomingSapronaksTableProps {
|
||||||
|
projectFlockId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingIncomingSapronaksTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingIncomingSapronaksTableProps) => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
||||||
|
useSWR(
|
||||||
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak/incoming${getTableFilterQueryString()}`,
|
||||||
|
ClosingApi.getAllIncomingSapronakFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'reference_number',
|
||||||
|
header: 'No. Referensi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transaction_type',
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_name',
|
||||||
|
header: 'Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_category',
|
||||||
|
header: 'Kategori Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'source_warehouse',
|
||||||
|
header: 'Gudang Asal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'destination_warehouse',
|
||||||
|
header: 'Gudang Tujuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'quantity',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
cell: (props) =>
|
||||||
|
`${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'notes',
|
||||||
|
header: 'Keterangan',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(incomingSapronaks)
|
||||||
|
? incomingSapronaks.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [incomingSapronaks, isResponseSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Sapronak Masuk</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Sapronak Masuk'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<ClosingIncomingSapronak>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(incomingSapronaks)
|
||||||
|
? incomingSapronaks?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={incomingSapronaksColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(incomingSapronaks)
|
||||||
|
? incomingSapronaks?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(incomingSapronaks)
|
||||||
|
? incomingSapronaks?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingIncomingSapronaks}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(incomingSapronaks) &&
|
||||||
|
incomingSapronaks?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingIncomingSapronaksTable;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingOutgoingSapronaksTableProps {
|
||||||
|
projectFlockId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingOutgoingSapronaksTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingOutgoingSapronaksTableProps) => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||||
|
useSWR(
|
||||||
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak/outgoing${getTableFilterQueryString()}`,
|
||||||
|
ClosingApi.getAllOutgoingSapronakFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'reference_number',
|
||||||
|
header: 'No. Referensi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transaction_type',
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_name',
|
||||||
|
header: 'Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_category',
|
||||||
|
header: 'Kategori Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'source_warehouse',
|
||||||
|
header: 'Gudang Asal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'destination_warehouse',
|
||||||
|
header: 'Gudang Tujuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'quantity',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
cell: (props) =>
|
||||||
|
`${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'notes',
|
||||||
|
header: 'Keterangan',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [outgoingSapronaks, isResponseSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Sapronak Keluar</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Sapronak Keluar'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<ClosingOutgoingSapronak>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={outgoingSapronaksColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingOutgoingSapronaks}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(outgoingSapronaks) &&
|
||||||
|
outgoingSapronaks?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingOutgoingSapronaksTable;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||||
|
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||||
|
|
||||||
|
interface ClosingSapronakTableProps {
|
||||||
|
projectFlockId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingSapronakTabContent = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingSapronakTableProps) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{projectFlockId && (
|
||||||
|
<>
|
||||||
|
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
|
||||||
|
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingSapronakTabContent;
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { Closing } from '@/types/api/closing';
|
||||||
|
|
||||||
|
const PROJECT_STATUS_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 'Pengajuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: 'Aktif',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Closing, unknown>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
||||||
|
<Button
|
||||||
|
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RowOptionsMenuWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClosingsTable = () => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
transactionDate: '',
|
||||||
|
realizationDate: '',
|
||||||
|
locationId: '',
|
||||||
|
projectStatus: '',
|
||||||
|
userId: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
transactionDate: 'transaction_date',
|
||||||
|
realizationDate: 'realization_date',
|
||||||
|
locationId: 'location_id',
|
||||||
|
projectStatus: 'project_status',
|
||||||
|
userId: 'user_id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: closings, isLoading: isLoadingClosings } = useSWR(
|
||||||
|
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
ClosingApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const closingsColumns: ColumnDef<Closing>[] = [
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'location_name',
|
||||||
|
header: 'Lokasi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'project_category',
|
||||||
|
header: 'Kategori',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'period',
|
||||||
|
header: 'Periode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'closing_date',
|
||||||
|
header: 'Periode',
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.closing_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'shed_label',
|
||||||
|
header: 'Jumlah Kandang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sales_paid_amount',
|
||||||
|
header: 'Jumlah Sudah Bayar',
|
||||||
|
cell: (props) => (
|
||||||
|
<span className='text-success'>
|
||||||
|
{formatCurrency(props.row.original.sales_paid_amount)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sales_remaining_amount',
|
||||||
|
header: 'Jumlah Sisa Bayar',
|
||||||
|
cell: (props) => (
|
||||||
|
<span className='text-error'>
|
||||||
|
{formatCurrency(props.row.original.sales_remaining_amount)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sales_payment_status',
|
||||||
|
header: 'Status Pembayaran',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'project_status',
|
||||||
|
header: 'Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props) => {
|
||||||
|
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||||
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
|
const currentRowRelativeIndex =
|
||||||
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 3 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu type='dropdown' props={props} />
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 3 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu type='collapse' props={props} />
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedLocation(val as OptionType);
|
||||||
|
updateFilter(
|
||||||
|
'locationId',
|
||||||
|
val ? ((val as OptionType).value as string) : ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedProjectStatus, setSelectedProjectStatus] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
|
||||||
|
const projectStatusChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
setSelectedProjectStatus(val as OptionType);
|
||||||
|
updateFilter(
|
||||||
|
'projectStatus',
|
||||||
|
val ? ((val as OptionType).value as string) : ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
<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 sm:flex-row justify-end items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Closing'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
value={selectedLocation}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Project'
|
||||||
|
placeholder='Pilih Status'
|
||||||
|
options={PROJECT_STATUS_OPTIONS}
|
||||||
|
value={selectedProjectStatus}
|
||||||
|
onChange={projectStatusChangeHandler}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<Closing>
|
||||||
|
data={isResponseSuccess(closings) ? closings?.data : []}
|
||||||
|
columns={closingsColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingClosings}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(closings) && closings?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingsTable;
|
||||||
@@ -40,6 +40,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/expense',
|
link: '/expense',
|
||||||
icon: 'heroicons:wallet',
|
icon: 'heroicons:wallet',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Closing',
|
||||||
|
link: '/closing',
|
||||||
|
icon: 'heroicons-outline:presentation-chart-bar',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Persediaan',
|
text: 'Persediaan',
|
||||||
link: '/inventory',
|
link: '/inventory',
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const sleep = (ms: number = 1000) =>
|
|||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export const formatDate = (date: moment.MomentInput, format?: string) => {
|
export const formatDate = (date: moment.MomentInput, format?: string) => {
|
||||||
|
if (!date) return '-';
|
||||||
|
|
||||||
return moment(date).format(format);
|
return moment(date).format(format);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import {
|
||||||
|
Closing,
|
||||||
|
ClosingGeneralInformation,
|
||||||
|
ClosingIncomingSapronak,
|
||||||
|
ClosingOutgoingSapronak,
|
||||||
|
} from '@/types/api/closing';
|
||||||
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||||
|
constructor(basePath: string) {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllIncomingSapronakFetcher(
|
||||||
|
endpoint: string
|
||||||
|
): Promise<BaseApiResponse<ClosingIncomingSapronak[]>> {
|
||||||
|
return await httpClientFetcher<BaseApiResponse<ClosingIncomingSapronak[]>>(
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllOutgoingSapronakFetcher(
|
||||||
|
endpoint: string
|
||||||
|
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
|
||||||
|
return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>(
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGeneralInfo(id: number) {
|
||||||
|
try {
|
||||||
|
const getGeneralInfoPath = `${this.basePath}/${id}`;
|
||||||
|
const getGeneralInfoRes =
|
||||||
|
await httpClient<BaseApiResponse<ClosingGeneralInformation>>(
|
||||||
|
getGeneralInfoPath
|
||||||
|
);
|
||||||
|
|
||||||
|
return getGeneralInfoRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
axios.isAxiosError<BaseApiResponse<ClosingGeneralInformation>>(error)
|
||||||
|
) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClosingApi = new ClosingApiService('/closings');
|
||||||
Vendored
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
export type BaseClosing = {
|
||||||
|
id: number;
|
||||||
|
location_id: number;
|
||||||
|
location_name: string;
|
||||||
|
project_category: 'GROWING' | 'LAYING';
|
||||||
|
period: number;
|
||||||
|
closing_date?: string;
|
||||||
|
shed_label: string;
|
||||||
|
shed_count: number;
|
||||||
|
sales_paid_amount: number;
|
||||||
|
sales_remaining_amount: number;
|
||||||
|
sales_payment_status: string;
|
||||||
|
project_status: 'Pengajuan' | 'Aktif' | 'Selesai';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Closing = BaseMetadata & BaseClosing;
|
||||||
|
|
||||||
|
export type BaseClosingGeneralInformation = BaseClosing & {
|
||||||
|
flock_id: number;
|
||||||
|
period: number;
|
||||||
|
project_type: 'GROWING' | 'LAYING';
|
||||||
|
population: number;
|
||||||
|
active_house_count: number;
|
||||||
|
sales_payment_status: string;
|
||||||
|
project_status: 'Pengajuan' | 'Aktif' | 'Selesai';
|
||||||
|
closing_status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClosingGeneralInformation = BaseMetadata &
|
||||||
|
BaseClosingGeneralInformation;
|
||||||
|
|
||||||
|
export type ClosingIncomingSapronak = {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
reference_number: string;
|
||||||
|
transaction_type: string;
|
||||||
|
product_name: string;
|
||||||
|
product_category: string;
|
||||||
|
product_sub_category: string;
|
||||||
|
source_warehouse: string;
|
||||||
|
destination_warehouse: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
formatted_quantity: string;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
||||||
Reference in New Issue
Block a user