mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-285/marketing-closing-report' into 'development'
[FEAT/FE][US#285] Add Feature Marketing Closing Report (Sales/Penjualan) See merge request mbugroup/lti-web-client!77
This commit is contained in:
@@ -42,3 +42,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# idea
|
# idea
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# claude
|
||||||
|
.claude
|
||||||
|
|||||||
Generated
+11
-1
@@ -1855,6 +1855,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1924,6 +1925,7 @@
|
|||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
@@ -2447,6 +2449,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3060,7 +3063,8 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "5.5.8",
|
"version": "5.5.8",
|
||||||
@@ -3516,6 +3520,7 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3689,6 +3694,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -6167,6 +6173,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6197,6 +6204,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -7083,6 +7091,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -7250,6 +7259,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
||||||
|
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||||
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -19,6 +20,11 @@ const ClosingDetailPage = () => {
|
|||||||
(id: number) => ClosingApi.getGeneralInfo(id)
|
(id: number) => ClosingApi.getGeneralInfo(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR(
|
||||||
|
closingId,
|
||||||
|
(id: number) => ClosingApi.getPenjualan(id)
|
||||||
|
);
|
||||||
|
|
||||||
if (!closingId) {
|
if (!closingId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
@@ -43,6 +49,9 @@ const ClosingDetailPage = () => {
|
|||||||
{!isLoadingClosing && isResponseSuccess(closing) && (
|
{!isLoadingClosing && isResponseSuccess(closing) && (
|
||||||
<ClosingDetail id={Number(closingId)} initialValue={closing.data} />
|
<ClosingDetail id={Number(closingId)} initialValue={closing.data} />
|
||||||
)}
|
)}
|
||||||
|
{!isLoadingSalesReport && isResponseSuccess(salesReport) && (
|
||||||
|
<SalesReportTable type='detail' initialValues={salesReport.data} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
SortingState,
|
SortingState,
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
Row,
|
Row,
|
||||||
|
HeaderContext,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils';
|
import { rankItem } from '@tanstack/match-sorter-utils';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -31,6 +32,9 @@ interface TableClassNames {
|
|||||||
tableBodyClassName?: string;
|
tableBodyClassName?: string;
|
||||||
bodyRowClassName?: string;
|
bodyRowClassName?: string;
|
||||||
bodyColumnClassName?: string;
|
bodyColumnClassName?: string;
|
||||||
|
tableFooterClassName?: string;
|
||||||
|
footerRowClassName?: string;
|
||||||
|
footerColumnClassName?: string;
|
||||||
paginationClassName?: string;
|
paginationClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ 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);
|
||||||
|
renderFooter?: boolean;
|
||||||
withCheckbox?: boolean;
|
withCheckbox?: boolean;
|
||||||
rowOptions?: number[];
|
rowOptions?: number[];
|
||||||
}
|
}
|
||||||
@@ -67,18 +72,22 @@ const emptyContentDefaultValue = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TABLE_DEFAULT_STYLING = {
|
export const TABLE_DEFAULT_STYLING = {
|
||||||
containerClassName: 'w-full mb-20',
|
containerClassName: 'w-full mb-20',
|
||||||
tableWrapperClassName:
|
tableWrapperClassName:
|
||||||
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
|
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
|
||||||
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
|
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
|
||||||
tableHeaderClassName: '',
|
tableHeaderClassName: '',
|
||||||
headerRowClassName: '',
|
headerRowClassName: '',
|
||||||
headerColumnClassName: 'px-4 py-3 text-base-content/50',
|
headerColumnClassName:
|
||||||
|
'px-4 py-3 border-base-content/10 text-base-content/50',
|
||||||
tableBodyClassName: '',
|
tableBodyClassName: '',
|
||||||
bodyRowClassName: 'border-t border-t-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',
|
||||||
paginationClassName: '',
|
paginationClassName: '',
|
||||||
|
tableFooterClassName: 'font-semibold border-base-content/10',
|
||||||
|
footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
|
||||||
|
footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Table = <TData extends object>({
|
const Table = <TData extends object>({
|
||||||
@@ -100,6 +109,7 @@ const Table = <TData extends object>({
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
|
renderFooter = false,
|
||||||
withCheckbox = false,
|
withCheckbox = false,
|
||||||
rowOptions = [10, 20, 50, 100],
|
rowOptions = [10, 20, 50, 100],
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
@@ -214,10 +224,26 @@ const Table = <TData extends object>({
|
|||||||
key={headerGroup.id}
|
key={headerGroup.id}
|
||||||
className={tableClassNames.headerRowClassName}
|
className={tableClassNames.headerRowClassName}
|
||||||
>
|
>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => {
|
||||||
|
const columnRelativeDepth =
|
||||||
|
header.depth - header.column.depth;
|
||||||
|
if (
|
||||||
|
!header.isPlaceholder &&
|
||||||
|
columnRelativeDepth > 1 &&
|
||||||
|
header.id === header.column.id
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let rowSpan = 1;
|
||||||
|
if (header.isPlaceholder) {
|
||||||
|
const leafs = header.getLeafHeaders();
|
||||||
|
rowSpan = leafs[leafs.length - 1].depth - header.depth;
|
||||||
|
}
|
||||||
|
return (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
|
rowSpan={rowSpan}
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
className={cn(
|
className={cn(
|
||||||
header.column.getCanSort()
|
header.column.getCanSort()
|
||||||
@@ -226,10 +252,17 @@ const Table = <TData extends object>({
|
|||||||
{
|
{
|
||||||
'first:w-9 first:pr-0': withCheckbox,
|
'first:w-9 first:pr-0': withCheckbox,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'border-b': header.colSpan > 1,
|
||||||
|
},
|
||||||
tableClassNames.headerColumnClassName
|
tableClassNames.headerColumnClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-1'>
|
<div
|
||||||
|
className={cn('flex items-center gap-1 min-h-full', {
|
||||||
|
'justify-center': header.colSpan > 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
@@ -265,7 +298,8 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
@@ -290,6 +324,28 @@ const Table = <TData extends object>({
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot className={cn(tableClassNames.tableFooterClassName)}>
|
||||||
|
{renderFooter && (
|
||||||
|
<tr className={cn(tableClassNames.footerRowClassName)}>
|
||||||
|
{table.getAllLeafColumns().map((column) => (
|
||||||
|
<td
|
||||||
|
key={column.id}
|
||||||
|
className={cn(
|
||||||
|
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||||
|
tableClassNames.footerColumnClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{column.columnDef.footer &&
|
||||||
|
flexRender(column.columnDef.footer, {
|
||||||
|
column,
|
||||||
|
header: column.columnDef,
|
||||||
|
table,
|
||||||
|
} as HeaderContext<TData, unknown>)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
||||||
|
import { BaseClosingSales, BaseSales } from '@/types/api/closing';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { Customer } from '@/types/api/master-data/customer';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
|
||||||
|
interface SalesReportTableProps {
|
||||||
|
type?: 'detail';
|
||||||
|
initialValues?: BaseClosingSales;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SalesReportTable = ({
|
||||||
|
type = 'detail',
|
||||||
|
initialValues,
|
||||||
|
}: SalesReportTableProps) => {
|
||||||
|
const salesData: BaseSales[] = useMemo(() => {
|
||||||
|
return initialValues?.sales || [];
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const totals = useMemo(() => {
|
||||||
|
if (salesData.length === 0) {
|
||||||
|
return {
|
||||||
|
totalQuantity: 0,
|
||||||
|
totalWeight: 0,
|
||||||
|
avgWeight: 0,
|
||||||
|
avgPricePartner: 0,
|
||||||
|
totalPartner: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalQuantity = salesData.reduce(
|
||||||
|
(sum, item) => sum + (item.qty || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalWeight = salesData.reduce(
|
||||||
|
(sum, item) => sum + (item.weight || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0;
|
||||||
|
|
||||||
|
const validPriceItems = salesData.filter(
|
||||||
|
(item) => item.price != null && item.price > 0
|
||||||
|
);
|
||||||
|
const avgPricePartner =
|
||||||
|
validPriceItems.length > 0
|
||||||
|
? validPriceItems.reduce((sum, item) => sum + item.price, 0) /
|
||||||
|
validPriceItems.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const totalPartner = salesData.reduce(
|
||||||
|
(sum, item) => sum + (item.total_price || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalQuantity,
|
||||||
|
totalWeight,
|
||||||
|
avgWeight,
|
||||||
|
avgPricePartner,
|
||||||
|
totalPartner,
|
||||||
|
};
|
||||||
|
}, [salesData]);
|
||||||
|
|
||||||
|
const salesColumns: ColumnDef<BaseSales>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'realization_date',
|
||||||
|
accessorKey: 'realization_date',
|
||||||
|
header: 'Tanggal Realisasi',
|
||||||
|
cell: (props) => {
|
||||||
|
const date = props.row.original.realization_date;
|
||||||
|
return date ? formatDate(date, 'DD MMM YYYY') : '-';
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'age',
|
||||||
|
accessorKey: 'age',
|
||||||
|
header: 'Umur',
|
||||||
|
cell: (props) => props.getValue() || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'do_number',
|
||||||
|
accessorKey: 'do_number',
|
||||||
|
header: 'No. DO',
|
||||||
|
cell: (props) => props.getValue() || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'product',
|
||||||
|
accessorKey: 'product',
|
||||||
|
header: 'Produk',
|
||||||
|
cell: (props) => {
|
||||||
|
const product = props.getValue() as Product;
|
||||||
|
return product?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'customer',
|
||||||
|
accessorKey: 'customer',
|
||||||
|
header: 'Customer',
|
||||||
|
cell: (props) => {
|
||||||
|
const customer = props.getValue() as Customer;
|
||||||
|
return customer?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jumlah',
|
||||||
|
header: 'Jumlah',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'qty',
|
||||||
|
accessorKey: 'qty',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-left font-semibold text-gray-900'>
|
||||||
|
{formatNumber(totals.totalQuantity)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'weight',
|
||||||
|
accessorKey: 'weight',
|
||||||
|
header: 'Kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-left font-semibold text-gray-900'>
|
||||||
|
{formatNumber(totals.totalWeight)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'avg_weight',
|
||||||
|
accessorKey: 'avg_weight',
|
||||||
|
header: 'AVG (Kg)',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-left font-semibold text-gray-900'>
|
||||||
|
{formatNumber(totals.avgWeight)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price_partner',
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: 'Harga Mitra (Rp)',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(totals.avgPricePartner)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total_mitra',
|
||||||
|
accessorKey: 'total_price',
|
||||||
|
header: 'Total Mitra (Rp)',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(totals.totalPartner)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price_act',
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: 'Harga Act (Rp)',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total_act',
|
||||||
|
accessorKey: 'total_price',
|
||||||
|
header: 'Total Act (Rp)',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kandang',
|
||||||
|
accessorKey: 'kandang',
|
||||||
|
header: 'Kandang',
|
||||||
|
cell: (props) => {
|
||||||
|
const kandang = props.getValue() as Kandang;
|
||||||
|
return kandang?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'payment_status',
|
||||||
|
accessorKey: 'payment_status',
|
||||||
|
header: 'Status Pembayaran',
|
||||||
|
cell: (props) => {
|
||||||
|
const status = props.getValue() as string;
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
if (!status) return 'neutral';
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'paid':
|
||||||
|
return 'success';
|
||||||
|
case 'tempo':
|
||||||
|
return 'warning';
|
||||||
|
default:
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant='soft' size='sm' color={getStatusColor(status)}>
|
||||||
|
{status || '-'}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full'>
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full bg-base-100',
|
||||||
|
body: 'p-0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={salesData}
|
||||||
|
columns={salesColumns}
|
||||||
|
renderFooter={salesData.length > 0}
|
||||||
|
className={{
|
||||||
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||||
|
bodyRowClassName:
|
||||||
|
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
|
tableFooterClassName:
|
||||||
|
'bg-gray-100 font-semibold border border-gray-200',
|
||||||
|
footerRowClassName: 'border-t-2 border-gray-300',
|
||||||
|
footerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SalesReportTable;
|
||||||
@@ -370,7 +370,7 @@ const RecordingTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
const [approvalNotes, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
|
|||||||
@@ -2924,8 +2924,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Error creating recording:', error);
|
|
||||||
toast.error(
|
toast.error(
|
||||||
'Gagal membuat recording. Silakan coba lagi.'
|
'Gagal membuat recording. Silakan coba lagi.'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -173,8 +173,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
|||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success(res?.message || 'Successfully delete Grading!');
|
toast.success(res?.message || 'Successfully delete Grading!');
|
||||||
router.push('/production/recording');
|
router.push('/production/recording');
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error(err);
|
|
||||||
setGradingFormErrorMessage('Failed to delete Grading');
|
setGradingFormErrorMessage('Failed to delete Grading');
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|||||||
@@ -241,9 +241,8 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
);
|
);
|
||||||
formik.setFieldValue('items', updatedPurchaseItems);
|
formik.setFieldValue('items', updatedPurchaseItems);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
toast.error('Terjadi kesalahan saat menghapus item pembelian');
|
toast.error('Terjadi kesalahan saat menghapus item pembelian');
|
||||||
console.error('Delete item error:', error);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
initialValues?.id,
|
initialValues?.id,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
AreaApi,
|
AreaApi,
|
||||||
LocationApi,
|
LocationApi,
|
||||||
WarehouseApi,
|
WarehouseApi,
|
||||||
ProductApi,
|
|
||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier';
|
import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier';
|
||||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
pdf,
|
pdf,
|
||||||
} from '@react-pdf/renderer';
|
} from '@react-pdf/renderer';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { Purchase } from '@/types/api/purchase/purchase';
|
import { Purchase } from '@/types/api/purchase/purchase';
|
||||||
@@ -251,7 +252,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!purchaseData) {
|
if (!purchaseData) {
|
||||||
alert('No purchase order data available');
|
toast.error('No purchase order data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,9 +503,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Error generating PDF:', error);
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
alert('Failed to generate PDF. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,29 @@ import {
|
|||||||
} from '@/types/api/closing';
|
} from '@/types/api/closing';
|
||||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { ClosingSales } from '@/types/api/closing';
|
||||||
|
|
||||||
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||||
constructor(basePath: string) {
|
constructor(basePath: string) {
|
||||||
super(basePath);
|
super(basePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPenjualan(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseApiResponse<ClosingSales> | undefined> {
|
||||||
|
try {
|
||||||
|
const getPenjualanPath = `${id}/penjualan`;
|
||||||
|
return await this.customRequest<BaseApiResponse<ClosingSales>>(
|
||||||
|
getPenjualanPath
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<ClosingSales>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAllIncomingSapronakFetcher(
|
async getAllIncomingSapronakFetcher(
|
||||||
endpoint: string
|
endpoint: string
|
||||||
): Promise<BaseApiResponse<ClosingIncomingSapronak[]>> {
|
): Promise<BaseApiResponse<ClosingIncomingSapronak[]>> {
|
||||||
@@ -51,4 +68,4 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClosingApi = new ClosingApiService('/closing');
|
export const ClosingApi = new ClosingApiService('/closings');
|
||||||
|
|||||||
Vendored
+28
-2
@@ -1,9 +1,34 @@
|
|||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { Fcr } from '@/types/api/master-data/fcr';
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
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 { Location } from '@/types/api/master-data/location';
|
||||||
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Product } from '@type/api/master-data/product';
|
||||||
|
import { Customer } from '@type/api/master-data/customer';
|
||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
project_type: string;
|
||||||
|
flock_id: number;
|
||||||
|
period: number;
|
||||||
|
sales: BaseSales[];
|
||||||
|
};
|
||||||
|
|
||||||
export type BaseClosing = {
|
export type BaseClosing = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -53,3 +78,4 @@ export type ClosingIncomingSapronak = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
||||||
|
export type ClosingSales = BaseMetadata & BaseClosingSales;
|
||||||
|
|||||||
Reference in New Issue
Block a user