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/TASK-326-327-slicing-and-integration-marketing-closing-report' into 'feat/FE/US-285/marketing-closing-report'
[FEAT/FE][US#285/TASK#326-327] Add Feature Marketing Closing Report (Sales/Penjualan) See merge request mbugroup/lti-web-client!70
This commit is contained in:
@@ -42,3 +42,6 @@ next-env.d.ts
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
# claude
|
||||
.claude
|
||||
|
||||
+21
-3
@@ -15,8 +15,24 @@ stages:
|
||||
script:
|
||||
- echo "Installing dependencies..."
|
||||
- npm ci --no-audit --no-fund
|
||||
- echo "Build env used:"
|
||||
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||
- echo "Building Next.js static export..."
|
||||
- npx next build
|
||||
- |
|
||||
mkdir -p out
|
||||
cat <<EOF > out/build-info.json
|
||||
{
|
||||
"commit": "$CI_COMMIT_SHORT_SHA",
|
||||
"pipeline": "$CI_PIPELINE_ID",
|
||||
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
||||
}
|
||||
EOF
|
||||
artifacts:
|
||||
name: 'out-$CI_COMMIT_SHORT_SHA'
|
||||
paths:
|
||||
@@ -106,8 +122,11 @@ build:dev:
|
||||
environment:
|
||||
name: development
|
||||
variables:
|
||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||
# NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||
# NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
|
||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||
|
||||
deploy:dev:
|
||||
<<: *deploy_template
|
||||
@@ -142,5 +161,4 @@ deploy:dev:
|
||||
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
|
||||
# environment:
|
||||
# name: production
|
||||
# url: https://royalgoldcapital.com
|
||||
|
||||
|
||||
Generated
+51
-41
@@ -15,7 +15,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.5.3",
|
||||
"next": "^15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -1082,9 +1082,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz",
|
||||
"integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
|
||||
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1098,9 +1098,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
|
||||
"integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
|
||||
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1114,9 +1114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
|
||||
"integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
|
||||
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1130,9 +1130,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
|
||||
"integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1146,9 +1146,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
|
||||
"integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1162,9 +1162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
|
||||
"integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
|
||||
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1178,9 +1178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
|
||||
"integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
|
||||
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1194,9 +1194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
|
||||
"integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1210,9 +1210,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
|
||||
"integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
|
||||
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1855,6 +1855,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1924,6 +1925,7 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -2447,6 +2449,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3060,7 +3063,8 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.3.10",
|
||||
@@ -3516,6 +3520,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3689,6 +3694,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -5654,12 +5660,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz",
|
||||
"integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
|
||||
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.3",
|
||||
"@next/env": "15.5.7",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -5672,14 +5678,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.3",
|
||||
"@next/swc-darwin-x64": "15.5.3",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.3",
|
||||
"@next/swc-linux-arm64-musl": "15.5.3",
|
||||
"@next/swc-linux-x64-gnu": "15.5.3",
|
||||
"@next/swc-linux-x64-musl": "15.5.3",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.3",
|
||||
"@next/swc-win32-x64-msvc": "15.5.3",
|
||||
"@next/swc-darwin-arm64": "15.5.7",
|
||||
"@next/swc-darwin-x64": "15.5.7",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.7",
|
||||
"@next/swc-linux-arm64-musl": "15.5.7",
|
||||
"@next/swc-linux-x64-gnu": "15.5.7",
|
||||
"@next/swc-linux-x64-musl": "15.5.7",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.7",
|
||||
"@next/swc-win32-x64-msvc": "15.5.7",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -6167,6 +6173,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -6197,6 +6204,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -7083,6 +7091,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7250,6 +7259,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.5.3",
|
||||
"next": "^15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
|
||||
@@ -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,55 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess, isResponseError } 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: string) => {
|
||||
const numericId = parseInt(id, 10);
|
||||
if (isNaN(numericId) || numericId <= 0) {
|
||||
throw new Error('Invalid closing ID');
|
||||
}
|
||||
return ClosingApi.getPenjualan(numericId);
|
||||
}
|
||||
);
|
||||
|
||||
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'>
|
||||
{isLoadingClosing && (
|
||||
<div className='w-full flex flex-row justify-center items-center'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingClosing && isResponseSuccess(closing) && (
|
||||
<SalesReportTable type='detail' initialValues={closing.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingDetailPage;
|
||||
@@ -4,7 +4,7 @@ import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
import Image from 'next/image';
|
||||
import Collapse from './Collapse';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
export interface CardProps
|
||||
|
||||
@@ -31,6 +31,9 @@ interface TableClassNames {
|
||||
tableBodyClassName?: string;
|
||||
bodyRowClassName?: string;
|
||||
bodyColumnClassName?: string;
|
||||
tableFooterClassName?: string;
|
||||
footerRowClassName?: string;
|
||||
footerColumnClassName?: string;
|
||||
paginationClassName?: string;
|
||||
}
|
||||
|
||||
@@ -52,6 +55,9 @@ export interface TableProps<TData extends object> {
|
||||
rowSelection?: Record<string, boolean>;
|
||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||
renderFooter?: boolean;
|
||||
footerContent?: ReactNode;
|
||||
footerData?: TData[];
|
||||
}
|
||||
|
||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||
@@ -84,6 +90,9 @@ const Table = <TData extends object>({
|
||||
tableBodyClassName: '',
|
||||
bodyRowClassName: '',
|
||||
bodyColumnClassName: '',
|
||||
tableFooterClassName: '',
|
||||
footerRowClassName: '',
|
||||
footerColumnClassName: '',
|
||||
paginationClassName: '',
|
||||
},
|
||||
emptyContent = emptyContentDefaultValue,
|
||||
@@ -93,6 +102,9 @@ const Table = <TData extends object>({
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
enableRowSelection,
|
||||
renderFooter = false,
|
||||
footerContent,
|
||||
footerData = [],
|
||||
}: TableProps<TData>) => {
|
||||
const isServerSideTable =
|
||||
totalItems !== undefined &&
|
||||
@@ -160,6 +172,14 @@ const Table = <TData extends object>({
|
||||
const table = useReactTable(tableOptions);
|
||||
const { setPageSize } = table;
|
||||
|
||||
const footerTableOptions: TableOptions<TData> = {
|
||||
columns,
|
||||
data: footerData,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
};
|
||||
|
||||
const footerTable = useReactTable(footerTableOptions);
|
||||
|
||||
const prevPageClickHandler = () => {
|
||||
table.previousPage();
|
||||
|
||||
@@ -262,6 +282,26 @@ const Table = <TData extends object>({
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot className={cn(className.tableFooterClassName)}>
|
||||
{renderFooter &&
|
||||
(footerData && footerData.length > 0
|
||||
? footerTable.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className={className.footerRowClassName}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={className.footerColumnClassName}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
: footerContent)}
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
'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/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;
|
||||
}
|
||||
|
||||
interface FooterSalesRow extends BaseSales {
|
||||
_isFooter: true;
|
||||
}
|
||||
|
||||
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 footerData = useMemo((): FooterSalesRow[] => {
|
||||
if (salesData.length === 0) return [];
|
||||
|
||||
const footerRow: FooterSalesRow = {
|
||||
id: -999,
|
||||
realization_date: 'Total Penjualan',
|
||||
age: 0,
|
||||
do_number: '',
|
||||
product: {} as Product,
|
||||
customer: {} as Customer,
|
||||
qty: totals.totalQuantity,
|
||||
weight: totals.totalWeight,
|
||||
avg_weight: totals.avgWeight,
|
||||
price: totals.avgPricePartner,
|
||||
total_price: totals.totalPartner,
|
||||
kandang: {} as Kandang,
|
||||
payment_status: '',
|
||||
_isFooter: true,
|
||||
};
|
||||
|
||||
return [footerRow];
|
||||
}, [salesData, totals]);
|
||||
|
||||
const salesColumns: ColumnDef<BaseSales>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'realization_date',
|
||||
accessorKey: 'realization_date',
|
||||
header: 'Tanggal Realisasi',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
if (isFooter) {
|
||||
return (
|
||||
<div className='font-semibold text-gray-900 col-span-5'>
|
||||
{props.row.original.realization_date}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const date = props.row.original.realization_date;
|
||||
return date ? formatDate(date, 'DD MMM YYYY') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'age',
|
||||
accessorKey: 'age',
|
||||
header: 'Umur',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return isFooter ? null : props.getValue() || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'do_number',
|
||||
accessorKey: 'do_number',
|
||||
header: 'No. DO',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return isFooter ? null : props.getValue() || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
accessorKey: 'product',
|
||||
header: 'Produk',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
if (isFooter) return null;
|
||||
const product = props.getValue() as Product;
|
||||
return product?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'customer',
|
||||
accessorKey: 'customer',
|
||||
header: 'Customer',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
if (isFooter) return null;
|
||||
const customer = props.getValue() as Customer;
|
||||
return customer?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qty',
|
||||
accessorKey: 'qty',
|
||||
header: 'Kuantitas',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
|
||||
}
|
||||
>
|
||||
{formatNumber(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'weight',
|
||||
accessorKey: 'weight',
|
||||
header: 'Kg',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
|
||||
}
|
||||
>
|
||||
{formatNumber(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'avg_weight',
|
||||
accessorKey: 'avg_weight',
|
||||
header: 'AVG (Kg)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
|
||||
}
|
||||
>
|
||||
{formatNumber(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'price_partner',
|
||||
accessorKey: 'price',
|
||||
header: 'Harga Mitra (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter
|
||||
? 'text-right font-semibold text-gray-900'
|
||||
: 'text-right'
|
||||
}
|
||||
>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'total_mitra',
|
||||
accessorKey: 'total_price',
|
||||
header: 'Total Mitra (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter
|
||||
? 'text-right font-semibold text-gray-900'
|
||||
: 'text-right'
|
||||
}
|
||||
>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'price_act',
|
||||
accessorKey: 'price',
|
||||
header: 'Harga Act (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter
|
||||
? 'text-right font-semibold text-gray-900'
|
||||
: 'text-right'
|
||||
}
|
||||
>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'total_act',
|
||||
accessorKey: 'total_price',
|
||||
header: 'Total Act (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isFooter
|
||||
? 'text-right font-semibold text-gray-900'
|
||||
: 'text-right'
|
||||
}
|
||||
>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'kandang',
|
||||
accessorKey: 'kandang',
|
||||
header: 'Kandang',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
if (isFooter) return null;
|
||||
const kandang = props.getValue() as Kandang;
|
||||
return kandang?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'payment_status',
|
||||
accessorKey: 'payment_status',
|
||||
header: 'Status Pembayaran',
|
||||
cell: (props) => {
|
||||
const isFooter = '_isFooter' in props.row.original;
|
||||
if (isFooter) return null;
|
||||
|
||||
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}
|
||||
footerData={footerData}
|
||||
renderFooter={salesData.length > 0}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 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 [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [approvalNotes, setApprovalNotes] = useState('');
|
||||
const [, setApprovalNotes] = useState('');
|
||||
|
||||
const singleDeleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
getRecordingLayingFormInitialValues,
|
||||
UpdateRecordingGrowingFormSchema,
|
||||
UpdateRecordingLayingFormSchema,
|
||||
} from './RecordingForm.schema';
|
||||
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
||||
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
@@ -2924,8 +2924,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating recording:', error);
|
||||
} catch {
|
||||
toast.error(
|
||||
'Gagal membuat recording. Silakan coba lagi.'
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
RecordingGradingFormValues,
|
||||
UpdateRecordingGradingFormSchema,
|
||||
getRecordingGradingFormInitialValues,
|
||||
} from '../../form/RecordingForm.schema';
|
||||
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
||||
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -173,8 +173,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
|
||||
deleteModal.closeModal();
|
||||
toast.success(res?.message || 'Successfully delete Grading!');
|
||||
router.push('/production/recording');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} catch {
|
||||
setGradingFormErrorMessage('Failed to delete Grading');
|
||||
} finally {
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
PurchaseRequestAcceptApprovalFormDefaultValues,
|
||||
PurchaseRequestAcceptApprovalFormInitialValues,
|
||||
PurchaseRequestAcceptApprovalFormSchema,
|
||||
} from './PurchaseOrderForm.schema';
|
||||
} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
PurchaseRequestStaffApprovalFormInitialValues,
|
||||
PurchaseRequestStaffApprovalFormSchema,
|
||||
PurchaseStaffApprovalItemSchema,
|
||||
} from './PurchaseOrderForm.schema';
|
||||
} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
@@ -241,9 +241,8 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
);
|
||||
formik.setFieldValue('items', updatedPurchaseItems);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Terjadi kesalahan saat menghapus item pembelian');
|
||||
console.error('Delete item error:', error);
|
||||
}
|
||||
}, [
|
||||
initialValues?.id,
|
||||
|
||||
@@ -22,13 +22,12 @@ import {
|
||||
PurchaseRequestFormValues,
|
||||
getPurchaseRequestFormInitialValues,
|
||||
UpdatePurchaseRequestFormSchema,
|
||||
} from './PurchaseRequestForm.schema';
|
||||
} from '@/components/pages/purchase/form/request/PurchaseRequestForm.schema';
|
||||
import {
|
||||
SupplierApi,
|
||||
AreaApi,
|
||||
LocationApi,
|
||||
WarehouseApi,
|
||||
ProductApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier';
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import { Icon } from '@iconify/react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
@@ -251,7 +252,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!purchaseData) {
|
||||
alert('No purchase order data available');
|
||||
toast.error('No purchase order data available');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -502,9 +503,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} catch {
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BaseApiService } from './base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { ClosingSales } from '@/types/api/closing/closing';
|
||||
|
||||
export class ClosingApiService extends BaseApiService<
|
||||
ClosingSales,
|
||||
unknown,
|
||||
unknown
|
||||
> {
|
||||
constructor(basePath: string) {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
async getPenjualan(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<ClosingSales> | undefined> {
|
||||
try {
|
||||
const getPenjualanPath = `${id}/penjualan`;
|
||||
return await this.customRequest<BaseApiResponse<ClosingSales>>(
|
||||
getPenjualanPath
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ClosingApi = new ClosingApiService('/closings');
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { Product } from '@type/api/master-data/product';
|
||||
import { Customer } from '@type/api/master-data/customer';
|
||||
import { Kandang } from '@type/api/master-data/kandang';
|
||||
|
||||
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 ClosingSales = BaseMetadata & BaseClosingSales;
|
||||
Reference in New Issue
Block a user