mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
Merge branch 'dev/randy' into 'feat/FE/US-159/marketing-sales-order'
[FE/FE][US#159/TASK#166-167-168-169-176-177-271] Adding Feature Sales Order See merge request mbugroup/lti-web-client!56
This commit is contained in:
@@ -40,8 +40,5 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# prettier
|
|
||||||
.prettierrc
|
|
||||||
|
|
||||||
# idea
|
# idea
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
+132
-62
@@ -1,76 +1,146 @@
|
|||||||
stages: [notify]
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
# --- Notify when MR is opened/updated ---
|
.build_template: &build_template
|
||||||
notify_discord_mr:
|
stage: build
|
||||||
stage: notify
|
image: node:20-alpine
|
||||||
image: alpine:3.20
|
cache:
|
||||||
rules:
|
key: npm-cache
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
paths:
|
||||||
|
- node_modules/
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
NPM_CONFIG_PRODUCTION: 'false'
|
||||||
before_script:
|
NODE_ENV: ''
|
||||||
- apk add --no-cache curl jq
|
script:
|
||||||
script: |
|
- echo "Installing dependencies..."
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
- npm ci --no-audit --no-fund
|
||||||
|
- echo "Building Next.js static export..."
|
||||||
|
- npx next build
|
||||||
|
artifacts:
|
||||||
|
name: 'out-$CI_COMMIT_SHORT_SHA'
|
||||||
|
paths:
|
||||||
|
- out/
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
.deploy_template: &deploy_template
|
||||||
|
stage: deploy
|
||||||
|
image:
|
||||||
|
name: amazon/aws-cli:latest
|
||||||
|
entrypoint: ['/bin/sh', '-c']
|
||||||
|
script:
|
||||||
|
- set -e
|
||||||
|
- aws --version
|
||||||
|
- echo "Cleaning up newline characters in AWS credentials..."
|
||||||
|
- export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
|
||||||
|
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
|
||||||
|
- echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
|
||||||
|
- aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION"
|
||||||
|
- aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
|
||||||
|
|
||||||
|
# CloudFront invalidation
|
||||||
|
- |
|
||||||
|
STATUS="success"
|
||||||
|
if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
|
||||||
|
echo "Invalidating CloudFront cache..."
|
||||||
|
if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then
|
||||||
|
echo "CloudFront invalidation failed."
|
||||||
|
STATUS="failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No CloudFront distribution specified — skipping invalidation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Notifikasi Discord
|
||||||
|
- |
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
|
||||||
|
|
||||||
|
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
|
||||||
|
ENVIRONMENT_NAME="WEB-LTI-DEV"
|
||||||
|
elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
|
||||||
|
ENVIRONMENT_NAME="WEB-LTI-PROD"
|
||||||
|
else
|
||||||
|
ENVIRONMENT_NAME="UNKNOWN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993
|
||||||
|
TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."
|
||||||
|
else
|
||||||
|
COLOR=15158332
|
||||||
|
TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues."
|
||||||
|
fi
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
|
--arg title "$TITLE" \
|
||||||
|
--arg desc "$DESC" \
|
||||||
|
--arg color "$COLOR" \
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
--arg repo "$CI_PROJECT_PATH" \
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
--arg actor "$GITLAB_USER_LOGIN" \
|
||||||
--arg url "$MR_URL" \
|
--arg commit "$CI_COMMIT_SHA" \
|
||||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
--arg run_url "$RUN_URL" \
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
|
||||||
'{
|
'{
|
||||||
username: "CI Bot - FE",
|
username: "CI Bot - LTI WEB",
|
||||||
embeds: [{
|
embeds: [{
|
||||||
title: "📣 [LTI WEB CLIENT] Merge Request Opened/Updated",
|
title: $title,
|
||||||
description: ($mr + " in " + $repo),
|
description: $desc,
|
||||||
url: $url,
|
color: ($color|tonumber),
|
||||||
color: 3447003,
|
|
||||||
fields: [
|
fields: [
|
||||||
{name: "Author", value: $requestor, inline: true},
|
{name: "Repository", value: $repo, inline: true},
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
{name: "Actor", value: $actor, inline: true},
|
||||||
{name: "Title", value: $title}
|
{name: "Commit", value: $commit, inline: false},
|
||||||
|
{name: "Pipeline", value: ("[Open run](" + $run_url + ")"), inline: false}
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}' \
|
}' > payload.json
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
|
|
||||||
# --- Notify when MR is merged ---
|
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
|
||||||
notify_discord_merge:
|
|
||||||
stage: notify
|
# ====== DEVELOPMENT (Branch development) ======
|
||||||
image: alpine:3.20
|
build:dev:
|
||||||
|
<<: *build_template
|
||||||
rules:
|
rules:
|
||||||
# Only run for merge request pipelines that are in merged state
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
environment:
|
||||||
|
name: development
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||||
before_script:
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||||
- apk add --no-cache curl jq
|
|
||||||
script: |
|
deploy:dev:
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
<<: *deploy_template
|
||||||
|
needs: ['build:dev']
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
|
variables:
|
||||||
|
S3_BUCKET: 'dev-lti-erp.mbugroup.id'
|
||||||
|
AWS_REGION: 'ap-southeast-3'
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
|
url: https://dev-lti-erp.mbugroup.id
|
||||||
|
# ====== PRODUCTION ======
|
||||||
|
# build:production:
|
||||||
|
# <<: *build_template
|
||||||
|
# rules:
|
||||||
|
# # pilih salah satu: pakai branch master ATAU pakai tags rilis
|
||||||
|
# - if: '$CI_COMMIT_BRANCH == "master"'
|
||||||
|
# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas
|
||||||
|
# environment:
|
||||||
|
# name: production
|
||||||
|
|
||||||
|
# deploy:production:
|
||||||
|
# <<: *deploy_template
|
||||||
|
# needs: ["build:production"]
|
||||||
|
# rules:
|
||||||
|
# - if: '$CI_COMMIT_BRANCH == "master"'
|
||||||
|
# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production
|
||||||
|
# variables:
|
||||||
|
# S3_BUCKET: "lti-erp.mbugroup.id"
|
||||||
|
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
|
||||||
|
# environment:
|
||||||
|
# name: production
|
||||||
|
# url: https://royalgoldcapital.com
|
||||||
|
|
||||||
jq -n \
|
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
|
||||||
--arg url "$MR_URL" \
|
|
||||||
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
|
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
|
||||||
'{
|
|
||||||
username: "CI Bot - FE",
|
|
||||||
embeds: [{
|
|
||||||
title: "✅ [LTI WEB CLIENT] Merge Request Merged",
|
|
||||||
description: ($mr + " has been merged into " + $repo),
|
|
||||||
url: $url,
|
|
||||||
color: 3066993,
|
|
||||||
fields: [
|
|
||||||
{name: "Author", value: $requestor, inline: true},
|
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
|
||||||
{name: "Title", value: $title}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}' \
|
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run build
|
npm run build
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Buat config agar Next tahu output: export
|
||||||
|
RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs
|
||||||
|
|
||||||
|
# Build project (Next.js 15 otomatis static export)
|
||||||
|
RUN NEXT_DISABLE_TURBOPACK=1 npx next build
|
||||||
|
|
||||||
|
# Copy static assets dan hasil build agar bisa diakses
|
||||||
|
RUN mkdir -p .next/server/app/_next && \
|
||||||
|
cp -r .next/static .next/server/app/_next/static && \
|
||||||
|
cp -r public/* .next/server/app/
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
dev-web-lti:
|
||||||
|
container_name: dev-web-lti
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- '3002:3000'
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
APP_ENV: production
|
||||||
|
networks:
|
||||||
|
- dev-lti-network
|
||||||
|
restart: always
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '3.0'
|
||||||
|
memory: 3G
|
||||||
|
reservations:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 512M
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
# Optional: aktifkan healthcheck jika punya endpoint
|
||||||
|
# healthcheck:
|
||||||
|
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
|
||||||
|
# interval: 10s
|
||||||
|
# timeout: 3s
|
||||||
|
# retries: 10
|
||||||
|
# start_period: 15s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev-lti-network:
|
||||||
|
external: true
|
||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
import { dirname } from "path";
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from 'url';
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -10,14 +10,14 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
"node_modules/**",
|
'node_modules/**',
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Generated
+619
-19
@@ -8,16 +8,18 @@
|
|||||||
"name": "lti-web-client",
|
"name": "lti-web-client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"inputmask": "^5.0.9",
|
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-number-format": "^5.4.4",
|
"react-number-format": "^5.4.4",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
@@ -31,7 +33,6 @@
|
|||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/inputmask": "^5.0.7",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "15.5.3",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,12 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@date-fns/tz": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
|
||||||
@@ -1265,6 +1273,180 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-pdf/fns": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/font": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/pdfkit": "^4.0.4",
|
||||||
|
"@react-pdf/types": "^2.9.1",
|
||||||
|
"fontkit": "^2.0.2",
|
||||||
|
"is-url": "^1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/image": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/png-js": "^3.0.0",
|
||||||
|
"jay-peg": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/layout": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/fns": "3.1.2",
|
||||||
|
"@react-pdf/image": "^3.0.3",
|
||||||
|
"@react-pdf/primitives": "^4.1.1",
|
||||||
|
"@react-pdf/stylesheet": "^6.1.1",
|
||||||
|
"@react-pdf/textkit": "^6.0.0",
|
||||||
|
"@react-pdf/types": "^2.9.1",
|
||||||
|
"emoji-regex-xs": "^1.0.0",
|
||||||
|
"queue": "^6.0.1",
|
||||||
|
"yoga-layout": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/pdfkit": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@react-pdf/png-js": "^3.0.0",
|
||||||
|
"browserify-zlib": "^0.2.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"fontkit": "^2.0.2",
|
||||||
|
"jay-peg": "^1.1.1",
|
||||||
|
"linebreak": "^1.1.0",
|
||||||
|
"vite-compatible-readable-stream": "^3.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/png-js": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browserify-zlib": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/primitives": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/reconciler": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"scheduler": "0.25.0-rc-603e6108-20241029"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/reconciler/node_modules/scheduler": {
|
||||||
|
"version": "0.25.0-rc-603e6108-20241029",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
|
||||||
|
"integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/render": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@react-pdf/fns": "3.1.2",
|
||||||
|
"@react-pdf/primitives": "^4.1.1",
|
||||||
|
"@react-pdf/textkit": "^6.0.0",
|
||||||
|
"@react-pdf/types": "^2.9.1",
|
||||||
|
"abs-svg-path": "^0.1.1",
|
||||||
|
"color-string": "^1.9.1",
|
||||||
|
"normalize-svg-path": "^1.1.0",
|
||||||
|
"parse-svg-path": "^0.1.2",
|
||||||
|
"svg-arc-to-cubic-bezier": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/renderer": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@react-pdf/fns": "3.1.2",
|
||||||
|
"@react-pdf/font": "^4.0.3",
|
||||||
|
"@react-pdf/layout": "^4.4.1",
|
||||||
|
"@react-pdf/pdfkit": "^4.0.4",
|
||||||
|
"@react-pdf/primitives": "^4.1.1",
|
||||||
|
"@react-pdf/reconciler": "^1.1.4",
|
||||||
|
"@react-pdf/render": "^4.3.1",
|
||||||
|
"@react-pdf/types": "^2.9.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"queue": "^6.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/stylesheet": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/fns": "3.1.2",
|
||||||
|
"@react-pdf/types": "^2.9.1",
|
||||||
|
"color-string": "^1.9.1",
|
||||||
|
"hsl-to-hex": "^1.0.0",
|
||||||
|
"media-engine": "^1.0.3",
|
||||||
|
"postcss-value-parser": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/textkit": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/fns": "3.1.2",
|
||||||
|
"bidi-js": "^1.0.2",
|
||||||
|
"hyphen": "^1.6.4",
|
||||||
|
"unicode-properties": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-pdf/types": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-pdf/font": "^4.0.3",
|
||||||
|
"@react-pdf/primitives": "^4.1.1",
|
||||||
|
"@react-pdf/stylesheet": "^6.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -1638,13 +1820,6 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/inputmask": {
|
|
||||||
"version": "5.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz",
|
|
||||||
"integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -2260,6 +2435,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/abs-svg-path": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -2516,6 +2697,15 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/attr-accept": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -2585,6 +2775,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -2609,6 +2828,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brotli": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-zlib": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "~1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
@@ -2710,6 +2947,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -2736,9 +2982,18 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -2795,6 +3050,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"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",
|
||||||
@@ -2872,6 +3133,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-fns-jalali": {
|
||||||
|
"version": "4.1.0-0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||||
|
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2969,6 +3246,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dfa": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -3013,6 +3296,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex-xs": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
@@ -3646,11 +3935,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
@@ -3720,6 +4017,18 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-selector": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -3797,6 +4106,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fontkit": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.12",
|
||||||
|
"brotli": "^1.3.2",
|
||||||
|
"clone": "^2.1.2",
|
||||||
|
"dfa": "^1.2.0",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"restructure": "^3.0.0",
|
||||||
|
"tiny-inflate": "^1.0.3",
|
||||||
|
"unicode-properties": "^1.4.0",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -4150,6 +4476,21 @@
|
|||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hsl-to-hex": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hsl-to-rgb-for-reals": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hsl-to-rgb-for-reals": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/husky": {
|
"node_modules/husky": {
|
||||||
"version": "9.1.7",
|
"version": "9.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||||
@@ -4166,6 +4507,12 @@
|
|||||||
"url": "https://github.com/sponsors/typicode"
|
"url": "https://github.com/sponsors/typicode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyphen": {
|
||||||
|
"version": "1.10.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz",
|
||||||
|
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4202,11 +4549,11 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inputmask": {
|
"node_modules/inherits": {
|
||||||
"version": "5.0.9",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "MIT"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -4584,6 +4931,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-url": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-weakmap": {
|
"node_modules/is-weakmap": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||||
@@ -4662,6 +5015,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jay-peg": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"restructure": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
@@ -4679,9 +5041,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5064,6 +5426,25 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linebreak": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "0.0.8",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linebreak/node_modules/base64-js": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
@@ -5136,6 +5517,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/media-engine": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
@@ -5346,6 +5733,15 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-svg-path": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"svg-arc-to-cubic-bezier": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5536,6 +5932,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -5566,6 +5968,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-svg-path": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -5659,6 +6067,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -5669,6 +6083,22 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5702,6 +6132,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/queue": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -5732,6 +6171,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-day-picker": {
|
||||||
|
"version": "9.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
|
||||||
|
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@date-fns/tz": "^1.4.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-jalali": "^4.1.0-0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/gpbl"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"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",
|
||||||
@@ -5744,6 +6204,23 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dropzone": {
|
||||||
|
"version": "14.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
|
||||||
|
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"attr-accept": "^2.2.4",
|
||||||
|
"file-selector": "^2.1.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8 || 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-fast-compare": {
|
"node_modules/react-fast-compare": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
@@ -5870,6 +6347,15 @@
|
|||||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -5909,6 +6395,12 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restructure": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -5964,6 +6456,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-push-apply": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||||
@@ -6209,6 +6721,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
@@ -6248,6 +6775,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -6438,6 +6974,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-arc-to-cubic-bezier": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
||||||
@@ -6488,6 +7030,12 @@
|
|||||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-inflate": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -6736,6 +7284,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/unicode-properties": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-trie": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^0.2.5",
|
||||||
|
"tiny-inflate": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-trie/node_modules/pako": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
||||||
@@ -6816,6 +7390,26 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vite-compatible-readable-stream": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -6953,6 +7547,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yoga-layout": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yup": {
|
"node_modules/yup": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
|
||||||
|
|||||||
+6
-3
@@ -7,19 +7,22 @@
|
|||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"inputmask": "^5.0.9",
|
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-number-format": "^5.4.4",
|
"react-number-format": "^5.4.4",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
@@ -33,7 +36,6 @@
|
|||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/inputmask": "^5.0.7",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "15.5.3",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ['@tailwindcss/postcss'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ExpenseRequestForm from '@/components/pages/expense/form/ExpenseRequestForm';
|
||||||
|
|
||||||
|
const AddExpense = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<ExpenseRequestForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddExpense;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ExpenseRequestForm from '@/components/pages/expense/form/ExpenseRequestForm';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseEditPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpenseRejectedOrApproved =
|
||||||
|
!isLoadingExpense &&
|
||||||
|
isResponseSuccess(expense) &&
|
||||||
|
(expense.data.approval.action === 'REJECTED' ||
|
||||||
|
expense.data.approval.step_number === 5);
|
||||||
|
|
||||||
|
if (isExpenseRejectedOrApproved) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseRequestForm type='edit' initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseEditPage;
|
||||||
@@ -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 ExpenseDetail from '@/components/pages/expense/ExpenseDetail';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseDetail initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ExpensesTable from '@/components/pages/expense/ExpensesTable';
|
||||||
|
|
||||||
|
const Expense = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ExpensesTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Expense;
|
||||||
+7
-4
@@ -3,10 +3,10 @@
|
|||||||
@import '../styles/daisyui.css';
|
@import '../styles/daisyui.css';
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
name: "lti";
|
name: 'lti';
|
||||||
default: false;
|
default: false;
|
||||||
prefersdark: false;
|
prefersdark: false;
|
||||||
color-scheme: "light";
|
color-scheme: 'light';
|
||||||
--color-base-100: oklch(98% 0.001 106.423);
|
--color-base-100: oklch(98% 0.001 106.423);
|
||||||
--color-base-200: oklch(97% 0.001 106.424);
|
--color-base-200: oklch(97% 0.001 106.424);
|
||||||
--color-base-300: oklch(92% 0.003 48.717);
|
--color-base-300: oklch(92% 0.003 48.717);
|
||||||
@@ -37,8 +37,6 @@
|
|||||||
--noise: 0;
|
--noise: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-primary: #1f74bf;
|
--color-primary: #1f74bf;
|
||||||
}
|
}
|
||||||
@@ -50,3 +48,8 @@
|
|||||||
html {
|
html {
|
||||||
scrollbar-gutter: initial;
|
scrollbar-gutter: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-select__menu-portal {
|
||||||
|
position: relative;
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import InventoryAdjustmentForm from "@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm";
|
import InventoryAdjustmentForm from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm';
|
||||||
|
|
||||||
const CreateInventoryAdjustment = () => {
|
const CreateInventoryAdjustment = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<InventoryAdjustmentForm />
|
<InventoryAdjustmentForm />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateInventoryAdjustment;
|
export default CreateInventoryAdjustment;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import SuspenseHelper from "@/components/helper/SuspenseHelper"
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
@@ -7,11 +7,12 @@ import type { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
|||||||
|
|
||||||
const DetailInventoryAdjustment = () => {
|
const DetailInventoryAdjustment = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [inventoryAdjustment, setInventoryAdjustment] = useState<InventoryAdjustment | null>(null);
|
const [inventoryAdjustment, setInventoryAdjustment] =
|
||||||
|
useState<InventoryAdjustment | null>(null);
|
||||||
|
|
||||||
// Ambil data dari router state
|
// Ambil data dari router state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Router State");
|
console.log('Router State');
|
||||||
console.log(window.history.state);
|
console.log(window.history.state);
|
||||||
const state = window.history.state?.usr as
|
const state = window.history.state?.usr as
|
||||||
| { inventoryAdjustment?: InventoryAdjustment }
|
| { inventoryAdjustment?: InventoryAdjustment }
|
||||||
@@ -25,19 +26,19 @@ const DetailInventoryAdjustment = () => {
|
|||||||
|
|
||||||
const finalData = inventoryAdjustment;
|
const finalData = inventoryAdjustment;
|
||||||
|
|
||||||
console.log("Final Data");
|
console.log('Final Data');
|
||||||
console.log(finalData);
|
console.log(finalData);
|
||||||
|
|
||||||
if (!finalData) {
|
if (!finalData) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-center items-center p-4">
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<InventoryAdjustmentForm initialValues={finalData} />
|
<InventoryAdjustmentForm initialValues={finalData} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='add_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
|
||||||
|
const AddSalesOrder = () => {
|
||||||
|
return (
|
||||||
|
<div className='size-full p-4'>
|
||||||
|
<MarketingForm formType='add' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddSalesOrder;
|
||||||
@@ -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,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(marketing) &&
|
||||||
|
marketing.data.latest_approval.step_number != 3
|
||||||
|
) {
|
||||||
|
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -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,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const DetailMarketing = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingDetail
|
||||||
|
initialValues={marketing.data}
|
||||||
|
refresh={refreshMarketing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailMarketing;
|
||||||
@@ -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,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditSalesOrder = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditSalesOrder;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
||||||
|
|
||||||
|
const Marketing = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<MarketingTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Marketing;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
|
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
|
||||||
|
|
||||||
const AddCustomer = () => {
|
const AddCustomer = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<CustomerForm />
|
<CustomerForm />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddCustomer;
|
export default AddCustomer;
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
|
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
|
||||||
|
|
||||||
const CustomerDetail = () => {
|
const CustomerDetail = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const costumerId = searchParams.get("customerId");
|
const costumerId = searchParams.get('customerId');
|
||||||
|
|
||||||
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
|
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
|
||||||
costumerId,
|
costumerId,
|
||||||
@@ -21,25 +21,27 @@ const CustomerDetail = () => {
|
|||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-center items-center p-4">
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) {
|
if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) {
|
||||||
router.replace("/404");
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
|
{isLoadingCostumer && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
{!isLoadingCostumer && isResponseSuccess(costumer) && (
|
{!isLoadingCostumer && isResponseSuccess(costumer) && (
|
||||||
<CustomerForm formType="detail" initialValues={costumer.data} />
|
<CustomerForm formType='detail' initialValues={costumer.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerDetail;
|
export default CustomerDetail;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import CustomersTable from "@/components/pages/master-data/customer/CustomersTable";
|
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
||||||
|
|
||||||
const Customer = () => {
|
const Customer = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<CustomersTable />
|
<CustomersTable />
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Customer;
|
export default Customer;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
|
|
||||||
const AddFlock = () => {
|
const AddFlock = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<FlockForm />
|
<FlockForm />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddFlock;
|
export default AddFlock;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { FlockApi } from "@/services/api/master-data";
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const FlockEdit = () => {
|
const FlockEdit = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,6 +44,6 @@ const FlockEdit = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FlockEdit;
|
export default FlockEdit;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import SuspenseHelper from "@/components/helper/SuspenseHelper"
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { FlockApi } from "@/services/api/master-data";
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const FlockDetail = () => {
|
const FlockDetail = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -14,14 +14,17 @@ const FlockDetail = () => {
|
|||||||
const flockId = searchParams.get('flockId');
|
const flockId = searchParams.get('flockId');
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const { data: flock, isLoading: isLoadingFlock } = useSWR(flockId, (id: number) => FlockApi.getSingle(id));
|
const { data: flock, isLoading: isLoadingFlock } = useSWR(
|
||||||
|
flockId,
|
||||||
|
(id: number) => FlockApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
if (!flockId) {
|
if (!flockId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-center items-center p-4">
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -32,15 +35,15 @@ const FlockDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingFlock && (
|
{isLoadingFlock && (
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{!isLoadingFlock && isResponseSuccess(flock) && (
|
{!isLoadingFlock && isResponseSuccess(flock) && (
|
||||||
<FlockForm formType="detail" initialValues={flock.data} />
|
<FlockForm formType='detail' initialValues={flock.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FlockDetail;
|
export default FlockDetail;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import FlockTable from "@/components/pages/master-data/flock/FlocksTable";
|
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
||||||
|
|
||||||
const Flock = () => {
|
const Flock = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<FlockTable />
|
<FlockTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Flock;
|
export default Flock;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ProductCategoryForm from "@/components/pages/master-data/product-category/form/ProductCategoryForm";
|
import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm';
|
||||||
|
|
||||||
const AddProductCategory = () => {
|
const AddProductCategory = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
<ProductCategoryForm />
|
<ProductCategoryForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,19 +29,24 @@ const ProductCategoryEdit = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
|
if (
|
||||||
|
!isLoadingProductCategory &&
|
||||||
|
(!productCategory || isResponseError(productCategory))
|
||||||
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
|
{isLoadingProductCategory && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
||||||
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
|
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProductCategoryEdit;
|
export default ProductCategoryEdit;
|
||||||
@@ -29,16 +29,24 @@ const ProductCategoryDetail = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
|
if (
|
||||||
|
!isLoadingProductCategory &&
|
||||||
|
(!productCategory || isResponseError(productCategory))
|
||||||
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
|
{isLoadingProductCategory && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
||||||
<ProductCategoryForm type='detail' initialValues={productCategory.data} />
|
<ProductCategoryForm
|
||||||
|
type='detail'
|
||||||
|
initialValues={productCategory.data}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ProductCategoryTable from "@/components/pages/master-data/product-category/ProductCategoryTable";
|
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
||||||
|
|
||||||
const ProductCategory = () => {
|
const ProductCategory = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<ProductCategoryTable />
|
<ProductCategoryTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ProductForm from '@/components/pages/master-data/product/form/ProductForm
|
|||||||
|
|
||||||
const AddProduct = () => {
|
const AddProduct = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
<ProductForm />
|
<ProductForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ const ProductEdit = () => {
|
|||||||
|
|
||||||
const productId = searchParams.get('productId');
|
const productId = searchParams.get('productId');
|
||||||
|
|
||||||
const { data: product, isLoading } = useSWR(
|
const { data: product, isLoading } = useSWR(productId, (id: number) =>
|
||||||
productId,
|
ProductApi.getSingle(id)
|
||||||
(id: number) => ProductApi.getSingle(id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ const ProductDetail = () => {
|
|||||||
|
|
||||||
const productId = searchParams.get('productId');
|
const productId = searchParams.get('productId');
|
||||||
|
|
||||||
const { data: product, isLoading } = useSWR(
|
const { data: product, isLoading } = useSWR(productId, (id: number) =>
|
||||||
productId,
|
ProductApi.getSingle(id)
|
||||||
(id: number) => ProductApi.getSingle(id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ProductsTable from "@/components/pages/master-data/product/ProductTable";
|
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
||||||
|
|
||||||
const Product = () => {
|
const Product = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<ProductsTable />
|
<ProductsTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable";
|
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
||||||
|
|
||||||
const Supplier = () => {
|
const Supplier = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from "@/components/helper/SuspenseHelper"
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { cn } from '@/lib/helper';
|
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const AddChickin = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
|
||||||
|
|
||||||
// Tables Props
|
|
||||||
const { state: tableFilterState } = useTableFilter({
|
|
||||||
initial: { search: '' },
|
|
||||||
paramMap: { page: 'page', pageSize: 'limit' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// States
|
|
||||||
const [selectedKandang, setSelectedKandang] = useState<Kandang | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [projectFlockKandang, setProjectFlockKandang] =
|
|
||||||
useState<BaseApiResponse<ProjectFlockKandang>>();
|
|
||||||
const [isLoadingProjectFlockKandang, setIsLoadingProjectFlockKandang] =
|
|
||||||
useState(false);
|
|
||||||
const [searchProjectFlock, setSearchProjectFlock] = useState('');
|
|
||||||
|
|
||||||
// Fetch Data
|
|
||||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
|
||||||
projectFlockId,
|
|
||||||
(id: number) => ProjectFlockApi.getSingle(id)
|
|
||||||
);
|
|
||||||
const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } =
|
|
||||||
useSWR(
|
|
||||||
`${ProjectFlockApi.basePath}?${new URLSearchParams({
|
|
||||||
search: searchProjectFlock,
|
|
||||||
}).toString()}`,
|
|
||||||
ProjectFlockApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const getProjectFlockKandangUrl = `/kandangs/lookup`;
|
|
||||||
// Mapping Options
|
|
||||||
const options = isResponseSuccess(listProjectFlock)
|
|
||||||
? listProjectFlock?.data.map((projectFlock) => {
|
|
||||||
return {
|
|
||||||
value: projectFlock.id,
|
|
||||||
label: `${projectFlock?.flock?.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const chickinModal = useModal();
|
|
||||||
const alertModal = useModal();
|
|
||||||
|
|
||||||
if (!projectFlockId) {
|
|
||||||
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 (
|
|
||||||
!isLoadingProjectFlock &&
|
|
||||||
(!projectFlock || isResponseError(projectFlock))
|
|
||||||
) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Function
|
|
||||||
const handleChickinClick = async (kandang: Kandang) => {
|
|
||||||
setIsLoadingProjectFlockKandang(true);
|
|
||||||
setSelectedKandang(kandang);
|
|
||||||
const ProjectFlockKandangRes = await ProjectFlockApi.customRequest<
|
|
||||||
BaseApiResponse<ProjectFlockKandang>,
|
|
||||||
'GET'
|
|
||||||
>(getProjectFlockKandangUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
project_flock_id: projectFlockId ?? 0,
|
|
||||||
kandang_id: kandang.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (isResponseSuccess(ProjectFlockKandangRes)) {
|
|
||||||
setProjectFlockKandang(ProjectFlockKandangRes);
|
|
||||||
setIsLoadingProjectFlockKandang(false);
|
|
||||||
if (
|
|
||||||
ProjectFlockKandangRes.data.available_quantity &&
|
|
||||||
ProjectFlockKandangRes.data.available_quantity > 0
|
|
||||||
) {
|
|
||||||
chickinModal.openModal();
|
|
||||||
} else {
|
|
||||||
alertModal.openModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleAfterSubmit = () => {
|
|
||||||
chickinModal.closeModal();
|
|
||||||
router.push('/production/chickin');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isResponseSuccess(projectFlock) && (
|
|
||||||
<>
|
|
||||||
<section className='w-full p-4'>
|
|
||||||
<header className='flex flex-col gap-4'>
|
|
||||||
<Button
|
|
||||||
href='/production/project-flock'
|
|
||||||
variant='link'
|
|
||||||
className='w-fit p-0 text-primary'
|
|
||||||
>
|
|
||||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
|
||||||
Kembali
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 w-full my-4'>
|
|
||||||
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
|
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
isSearchable
|
|
||||||
label='Project Flock'
|
|
||||||
options={options}
|
|
||||||
isLoading={isLoadingListProjectFlock}
|
|
||||||
value={{
|
|
||||||
label: `${projectFlock.data?.flock?.name} - ${projectFlock.data?.category} - Periode ${projectFlock.data?.period}`,
|
|
||||||
value: projectFlock.data?.id,
|
|
||||||
}}
|
|
||||||
onChange={(val) =>
|
|
||||||
router.push(
|
|
||||||
`/production/chickin/add?projectFlockId=${
|
|
||||||
(val as OptionType | null)?.value
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onInputChange={(val) => {
|
|
||||||
setSearchProjectFlock(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<Table<Kandang>
|
|
||||||
data={projectFlock.data?.kandangs}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: '#',
|
|
||||||
cell: (props) =>
|
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'name',
|
|
||||||
header: 'Nama Kandang',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
handleChickinClick(props.row.original);
|
|
||||||
}}
|
|
||||||
disabled={isLoadingProjectFlockKandang}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:home-import-outline'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
Chickin
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
page={undefined}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20':
|
|
||||||
isResponseSuccess(projectFlock) &&
|
|
||||||
projectFlock.data?.kandangs?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<Modal ref={chickinModal.ref}>
|
|
||||||
<div className='flex flex-row justify-between items-center'>
|
|
||||||
<h1 className='text-xl font-semibold text-center mb-6'>
|
|
||||||
Chickin Kandang - {selectedKandang?.name}
|
|
||||||
</h1>
|
|
||||||
<Button
|
|
||||||
color='error'
|
|
||||||
variant='link'
|
|
||||||
onClick={chickinModal.closeModal}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className='text-black'
|
|
||||||
icon='uil:times'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isResponseSuccess(projectFlockKandang) &&
|
|
||||||
!isLoadingProjectFlockKandang && (
|
|
||||||
<ChickinForm
|
|
||||||
initialValues={{
|
|
||||||
project_flock_kandang: projectFlockKandang.data,
|
|
||||||
created_user: projectFlock.data?.created_user,
|
|
||||||
created_at: projectFlock.data?.created_at,
|
|
||||||
updated_at: projectFlock.data?.updated_at,
|
|
||||||
approval: projectFlock.data?.approval,
|
|
||||||
}}
|
|
||||||
afterSubmit={handleAfterSubmit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={alertModal.ref}
|
|
||||||
type='info'
|
|
||||||
text={`Persediaan Day Old Chick pada kandang (${selectedKandang?.name}) belum ada, mohon isi terlebih dahulu di bagian Persediaan!`}
|
|
||||||
secondaryButton={undefined}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'info',
|
|
||||||
onClick: () => {
|
|
||||||
alertModal.closeModal();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddChickin;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from "@/components/helper/SuspenseHelper"
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { ChickinApi } from '@/services/api/production';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import {
|
|
||||||
Chickin,
|
|
||||||
ChickinApprovalPayload,
|
|
||||||
} from '@/types/api/production/chickin';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Refactor code - pindahin detail ke reuseable component
|
|
||||||
* setelah implement approval and reject
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DetailChickin = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const chickinId = searchParams.get('chickinId');
|
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const confirmModal = useModal();
|
|
||||||
const deleteModal = useModal();
|
|
||||||
const chickinModal = useModal();
|
|
||||||
const {
|
|
||||||
data: chickin,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshChickin,
|
|
||||||
} = useSWR(chickinId, (id: number) => ChickinApi.getSingle(id));
|
|
||||||
|
|
||||||
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
|
|
||||||
// chickin.data?.approval.step_number == 1 ? false : true
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const [isRejectedDisabled, setIsRejectedDisabled] = useState(
|
|
||||||
!isApprovedDisabled
|
|
||||||
);
|
|
||||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
|
||||||
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!chickinId) {
|
|
||||||
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 (!isLoading && (!chickin || isResponseError(chickin))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isResponseSuccess(chickin)) {
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmationModalClickHandler = async ({
|
|
||||||
action = 'APPROVED',
|
|
||||||
}: {
|
|
||||||
action: 'APPROVED' | 'REJECTED';
|
|
||||||
}) => {
|
|
||||||
if (chickin?.data.id === undefined) return;
|
|
||||||
setIsApproveLoading(true);
|
|
||||||
const approveChickinRes = await ChickinApi.customRequest<
|
|
||||||
BaseApiResponse<Chickin>,
|
|
||||||
ChickinApprovalPayload
|
|
||||||
>(`/approvals`, {
|
|
||||||
method: 'POST',
|
|
||||||
payload: {
|
|
||||||
action: action,
|
|
||||||
approvable_ids: [chickin.data.id],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isResponseSuccess(approveChickinRes)) {
|
|
||||||
if (refreshChickin) {
|
|
||||||
await refreshChickin();
|
|
||||||
}
|
|
||||||
toast.success(approveChickinRes.message as string);
|
|
||||||
}
|
|
||||||
if (isResponseError(approveChickinRes)) {
|
|
||||||
toast.error(approveChickinRes?.message as string);
|
|
||||||
}
|
|
||||||
confirmModal.closeModal();
|
|
||||||
setIsApproveLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
|
||||||
setIsDeleteLoading(true);
|
|
||||||
const deleteProjectFlockRes = await ChickinApi.delete(
|
|
||||||
chickin.data?.id as number
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseSuccess(deleteProjectFlockRes)) {
|
|
||||||
toast.success(deleteProjectFlockRes?.message as string);
|
|
||||||
router.push('/production/chickin');
|
|
||||||
}
|
|
||||||
if (isResponseError(deleteProjectFlockRes)) {
|
|
||||||
toast.error(deleteProjectFlockRes?.message as string);
|
|
||||||
}
|
|
||||||
deleteModal.closeModal();
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='w-full p-4 flex flex-col justify-center gap-4'>
|
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoading && isResponseSuccess(chickin) && (
|
|
||||||
<>
|
|
||||||
{/* <div className='w-full flex flex-col sm:flex-row gap-2'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='success'
|
|
||||||
onClick={(() => {
|
|
||||||
if (chickin?.data.id) {
|
|
||||||
setApprovalAction('APPROVED');
|
|
||||||
confirmModal.openModal();
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
disabled={!chickin?.data.id || isApprovedDisabled}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='error'
|
|
||||||
onClick={() => {
|
|
||||||
if (chickin?.data.id) {
|
|
||||||
setApprovalAction('REJECTED');
|
|
||||||
confirmModal.openModal();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!chickin?.data.id || isRejectedDisabled}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:times' width={24} height={24} />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
<Card
|
|
||||||
title='Informasi Umum'
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-4'>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Flock</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{
|
|
||||||
chickin.data.project_flock_kandang?.project_flock.flock
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Area</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{
|
|
||||||
chickin.data.project_flock_kandang?.project_flock.area
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Kategori</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{chickin.data.project_flock_kandang?.project_flock.category}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Lokasi</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{
|
|
||||||
chickin.data.project_flock_kandang?.project_flock.location
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Periode</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{chickin.data.project_flock_kandang?.project_flock.period}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Kandang</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{chickin.data.project_flock_kandang?.kandang.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
title='Detail Chickin'
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='grid grid-cols-2 gap-4 mt-4'>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Flock Kandang</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{
|
|
||||||
chickin.data.project_flock_kandang?.project_flock.flock
|
|
||||||
.name
|
|
||||||
}{' '}
|
|
||||||
- {chickin.data.project_flock_kandang?.kandang.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Tanggal Chickin</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{chickin.data.chick_in_date
|
|
||||||
? new Date(chickin.data.chick_in_date).toLocaleDateString(
|
|
||||||
'id-ID'
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Jumlah (Ekor)</div>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{chickin.data.quantity?.toLocaleString('id-ID')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<div className='font-semibold text-sm'>Catatan</div>
|
|
||||||
<div className='text-sm'>{chickin.data.note}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<div className='w-full flex flex-col sm:flex-row gap-2'>
|
|
||||||
<Button
|
|
||||||
color='error'
|
|
||||||
onClick={() => {
|
|
||||||
deleteModal.openModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:times' width={24} height={24} />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
<Button color='warning'
|
|
||||||
onClick={() => {
|
|
||||||
chickinModal.openModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={deleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${chickin?.data.project_flock_kandang?.project_flock.flock.name} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
onClick: confirmationModalDeleteClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal ref={chickinModal.ref}>
|
|
||||||
<div className='flex flex-row justify-between items-center'>
|
|
||||||
<h1 className='text-xl font-semibold text-center mb-6'>
|
|
||||||
Chickin Kandang -{' '}
|
|
||||||
{chickin?.data?.project_flock_kandang &&
|
|
||||||
chickin?.data?.project_flock_kandang.kandang?.name}
|
|
||||||
</h1>
|
|
||||||
<Button
|
|
||||||
color='error'
|
|
||||||
variant='link'
|
|
||||||
onClick={chickinModal.closeModal}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className='text-black'
|
|
||||||
icon='uil:times'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ChickinForm
|
|
||||||
initialValues={chickin?.data}
|
|
||||||
formType='edit'
|
|
||||||
afterSubmit={() => {
|
|
||||||
refreshChickin();
|
|
||||||
chickinModal.closeModal();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={confirmModal.ref}
|
|
||||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
|
||||||
text={`Apakah anda yakin ingin ${
|
|
||||||
approvalAction == 'APPROVED' ? 'approve' : 'reject'
|
|
||||||
} chickin berikut? (${
|
|
||||||
chickin?.data.project_flock_kandang?.project_flock.flock.name
|
|
||||||
} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: () => {
|
|
||||||
confirmationModalClickHandler({
|
|
||||||
action: approvalAction,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DetailChickin;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import ChickinTable from "@/components/pages/production/chickin/ChickinTable";
|
|
||||||
|
|
||||||
const Chickin = () => {
|
|
||||||
return (
|
|
||||||
<section className="w-full p-4">
|
|
||||||
<ChickinTable/>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default Chickin;
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
|
|
||||||
const AddProjectFlock = () => {
|
const AddProjectFlock = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<ProjectFlockForm formType="add"/>
|
<ProjectFlockForm formType='add' />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddProjectFlock;
|
export default AddProjectFlock;
|
||||||
@@ -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,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
export default function AddChickinKandang() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: projectFlockKandang,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshProjectFlockKandang,
|
||||||
|
} = useSWR(
|
||||||
|
`get-single-project-flock-kandang/${projectFlockKandangId}`,
|
||||||
|
async () =>
|
||||||
|
ProjectFlockKandangApi.getSingle(
|
||||||
|
parseInt(projectFlockKandangId as string)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!projectFlockKandangId) {
|
||||||
|
router.push(`/production/chickin/add?projectFlockId=${projectFlockId}`);
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !projectFlockKandang) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterSubmit = () => {
|
||||||
|
refreshProjectFlockKandang();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading &&
|
||||||
|
isResponseSuccess(projectFlockKandang) &&
|
||||||
|
projectFlockId && (
|
||||||
|
<ChickinForm
|
||||||
|
initialValues={projectFlockKandang.data}
|
||||||
|
afterSubmit={handleAfterSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
const AddChickin = () => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddChickin;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
|
||||||
|
|
||||||
|
const Chickin = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ChickinTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Chickin;
|
||||||
@@ -1,46 +1,51 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { ProjectFlockApi } from "@/services/api/production";
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import useSWR from 'swr';
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
const ProjectFlockEdit = () => {
|
const ProjectFlockEdit = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const projectFlockId = searchParams.get("projectFlockId");
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
|
const {
|
||||||
projectFlockId,
|
data: projectFlock,
|
||||||
(id: number) => ProjectFlockApi.getSingle(id)
|
isLoading: isLoadingProjectFlock,
|
||||||
);
|
mutate: refreshProjectFlocks,
|
||||||
|
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||||
|
|
||||||
if (!projectFlockId) {
|
if (!projectFlockId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-center items-center p-4">
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))){
|
if (
|
||||||
router.replace("/404");
|
!isLoadingProjectFlock &&
|
||||||
|
(!projectFlock || isResponseError(projectFlock))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-col justify-center'>
|
||||||
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
|
{isLoadingProjectFlock && (
|
||||||
{!isLoadingCostumer && isResponseSuccess(projectFlock) && (
|
<span className='loading loading-spinner loading-xl' />
|
||||||
<ProjectFlockForm formType="edit" initialValues={projectFlock.data} />
|
)}
|
||||||
|
{!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
|
||||||
|
<ProjectFlockForm formType='edit' initialValues={projectFlock.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProjectFlockEdit;
|
export default ProjectFlockEdit;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import SuspenseHelper from "@/components/helper/SuspenseHelper"
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -37,12 +37,16 @@ const ProjectFlockDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-col justify-center'>
|
||||||
{isLoadingProjectFlock && (
|
{isLoadingProjectFlock && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
|
{isResponseSuccess(projectFlock) && (
|
||||||
<ProjectFlockForm formType='detail' initialValues={projectFlock.data} refreshProjectFlocks={refreshProjectFlock} />
|
<ProjectFlockForm
|
||||||
|
formType='detail'
|
||||||
|
initialValues={projectFlock.data}
|
||||||
|
refreshProjectFlocks={refreshProjectFlock}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import ProjectFlockTable from "@/components/pages/production/project-flock/ProjectFlockTable";
|
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||||
|
|
||||||
const ProjectFlock = () => {
|
const ProjectFlock = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<ProjectFlockTable />
|
<ProjectFlockTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProjectFlock;
|
export default ProjectFlock;
|
||||||
|
|||||||
@@ -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,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const AddGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recording_id');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId && recordingId !== 'new' ? [recordingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
recordingId &&
|
||||||
|
recordingId !== 'new' &&
|
||||||
|
!isLoadingRecording &&
|
||||||
|
(!recording || !isResponseSuccess(recording))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{recordingId && recordingId !== 'new' && isLoadingRecording && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{(!recordingId ||
|
||||||
|
recordingId === 'new' ||
|
||||||
|
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
|
||||||
|
<GradingForm
|
||||||
|
type='add'
|
||||||
|
initialValues={
|
||||||
|
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddGrading;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const EditGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
const gradingId = searchParams.get('gradingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId ? [recordingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!recordingId) {
|
||||||
|
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 (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingRecording && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
|
||||||
|
<GradingForm
|
||||||
|
type='edit'
|
||||||
|
initialValues={recording.data.eggs?.find(
|
||||||
|
(egg) => egg.id === parseInt(gradingId || '0')
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditGrading;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const DetailGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const gradingId = searchParams.get('gradingId');
|
||||||
|
|
||||||
|
const { data: grading, isLoading: isLoadingGrading } = useSWR(
|
||||||
|
gradingId ? [gradingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gradingId) {
|
||||||
|
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 (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingGrading && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
|
||||||
|
<GradingForm
|
||||||
|
type='detail'
|
||||||
|
initialValues={grading.data.eggs?.find(
|
||||||
|
(egg) => egg.id === parseInt(gradingId)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailGrading;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
|
|||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
|
||||||
|
|
||||||
// TODO: delete dummy data
|
|
||||||
const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = {
|
|
||||||
id: 1,
|
|
||||||
transfer_date: '2025-10-14',
|
|
||||||
flock_source: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Flock asal test',
|
|
||||||
},
|
|
||||||
flock_destination: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Flock tujuan destination',
|
|
||||||
},
|
|
||||||
quantity: 10,
|
|
||||||
kandangs: [
|
|
||||||
{
|
|
||||||
kandang: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kandang test',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
location: {
|
|
||||||
id: 1,
|
|
||||||
name: 'test location',
|
|
||||||
address: 'test address 1',
|
|
||||||
area: { id: 1, name: 'test area 1' },
|
|
||||||
},
|
|
||||||
pic: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
},
|
|
||||||
quantity: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kandang: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kandang test 2',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
location: {
|
|
||||||
id: 1,
|
|
||||||
name: 'test location',
|
|
||||||
address: 'test address 1',
|
|
||||||
area: { id: 1, name: 'test area 1' },
|
|
||||||
},
|
|
||||||
pic: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
},
|
|
||||||
quantity: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reason: 'Test alasan',
|
|
||||||
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
};
|
|
||||||
|
|
||||||
const TransferToLayingEdit = () => {
|
const TransferToLayingEdit = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -114,33 +29,33 @@ const TransferToLayingEdit = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove dummy data and integrate with real API
|
|
||||||
if (
|
if (
|
||||||
!isLoadingTransferToLaying &&
|
!isLoadingTransferToLaying &&
|
||||||
(!transferToLaying ||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
|
|
||||||
) {
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(transferToLaying) &&
|
||||||
|
transferToLaying.data.approval.step_number === 2
|
||||||
|
) {
|
||||||
|
router.replace('/production/transfer-to-laying');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingTransferToLaying && (
|
{isLoadingTransferToLaying && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
<TransferToLayingForm
|
|
||||||
type='detail'
|
|
||||||
initialValues={transferToLaying.data}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* TODO: remove this dummy data and integrate to real API */}
|
|
||||||
<TransferToLayingForm
|
<TransferToLayingForm
|
||||||
type='edit'
|
type='edit'
|
||||||
initialValues={DUMMY_TRANSFER_TO_LAYING_EDIT}
|
initialValues={transferToLaying.data}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
|
|||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
|
||||||
|
|
||||||
// TODO: delete dummy data
|
|
||||||
const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
|
|
||||||
id: 1,
|
|
||||||
transfer_date: '2025-10-14',
|
|
||||||
flock_source: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Flock asal test',
|
|
||||||
},
|
|
||||||
flock_destination: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Flock tujuan destination',
|
|
||||||
},
|
|
||||||
quantity: 10,
|
|
||||||
kandangs: [
|
|
||||||
{
|
|
||||||
kandang: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kandang test',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
location: {
|
|
||||||
id: 1,
|
|
||||||
name: 'test location',
|
|
||||||
address: 'test address 1',
|
|
||||||
area: { id: 1, name: 'test area 1' },
|
|
||||||
},
|
|
||||||
pic: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
},
|
|
||||||
quantity: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kandang: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kandang test 2',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
location: {
|
|
||||||
id: 1,
|
|
||||||
name: 'test location',
|
|
||||||
address: 'test address 1',
|
|
||||||
area: { id: 1, name: 'test area 1' },
|
|
||||||
},
|
|
||||||
pic: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
},
|
|
||||||
quantity: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reason: 'Test alasan',
|
|
||||||
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 2,
|
|
||||||
email: 'test@gmail.com',
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
created_at: '14-10-2025',
|
|
||||||
updated_at: '14-10-2025',
|
|
||||||
};
|
|
||||||
|
|
||||||
const TransferToLayingDetail = () => {
|
const TransferToLayingDetail = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -114,11 +29,9 @@ const TransferToLayingDetail = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove dummy data and integrate with real API
|
|
||||||
if (
|
if (
|
||||||
!isLoadingTransferToLaying &&
|
!isLoadingTransferToLaying &&
|
||||||
(!transferToLaying ||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
|
|
||||||
) {
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
@@ -129,18 +42,13 @@ const TransferToLayingDetail = () => {
|
|||||||
{isLoadingTransferToLaying && (
|
{isLoadingTransferToLaying && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
|
||||||
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
<TransferToLayingForm
|
<TransferToLayingForm
|
||||||
type='detail'
|
type='detail'
|
||||||
initialValues={transferToLaying.data}
|
initialValues={transferToLaying.data}
|
||||||
/>
|
/>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
{/* TODO: remove this dummy data and integrate to real API */}
|
|
||||||
<TransferToLayingForm
|
|
||||||
type='detail'
|
|
||||||
initialValues={DUMMY_TRANSFER_TO_LAYING_DETAIL}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Link from 'next/link';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
interface ButtonProps extends react.ComponentProps<'button'> {
|
export interface ButtonProps extends react.ComponentProps<'button'> {
|
||||||
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
||||||
color?: Color;
|
color?: Color;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
|||||||
+16
-17
@@ -1,13 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
HTMLAttributes,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
export interface CardProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
@@ -44,17 +43,17 @@ const Card = ({
|
|||||||
const baseClasses = 'card bg-base-100';
|
const baseClasses = 'card bg-base-100';
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
'default': '',
|
default: '',
|
||||||
'compact': 'card-compact',
|
compact: 'card-compact',
|
||||||
'bordered': 'border border-base-300',
|
bordered: 'border border-base-300',
|
||||||
'shadow': 'shadow-xl',
|
shadow: 'shadow-xl',
|
||||||
'image-full': 'card-side card-compact shadow-xl',
|
'image-full': 'card-side card-compact shadow-xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
'sm': 'w-64',
|
sm: 'w-64',
|
||||||
'md': 'w-96',
|
md: 'w-96',
|
||||||
'lg': 'w-[28rem]',
|
lg: 'w-[28rem]',
|
||||||
};
|
};
|
||||||
|
|
||||||
return cn(
|
return cn(
|
||||||
@@ -84,9 +83,9 @@ const Card = ({
|
|||||||
|
|
||||||
const getTitleClasses = () => {
|
const getTitleClasses = () => {
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
'sm': 'text-lg',
|
sm: 'text-lg',
|
||||||
'md': 'text-xl',
|
md: 'text-xl',
|
||||||
'lg': 'text-2xl',
|
lg: 'text-2xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
return cn('card-title font-bold', sizeClasses[size], className?.title);
|
return cn('card-title font-bold', sizeClasses[size], className?.title);
|
||||||
@@ -108,7 +107,7 @@ const Card = ({
|
|||||||
return (
|
return (
|
||||||
<div className={getCardClasses()} {...props}>
|
<div className={getCardClasses()} {...props}>
|
||||||
<figure>
|
<figure>
|
||||||
<img
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={imageAlt || title || 'Card image'}
|
alt={imageAlt || title || 'Card image'}
|
||||||
className={getImageClasses()}
|
className={getImageClasses()}
|
||||||
@@ -129,7 +128,7 @@ const Card = ({
|
|||||||
<div className={getCardClasses()} {...props}>
|
<div className={getCardClasses()} {...props}>
|
||||||
{image && (
|
{image && (
|
||||||
<figure>
|
<figure>
|
||||||
<img
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={imageAlt || title || 'Card image'}
|
alt={imageAlt || title || 'Card image'}
|
||||||
className={getImageClasses()}
|
className={getImageClasses()}
|
||||||
|
|||||||
+37
-23
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
import {
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export const useModal = () => {
|
export const useModal = () => {
|
||||||
@@ -8,31 +15,34 @@ export const useModal = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
const openModal = useCallback(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
ref.current.show();
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
|
||||||
ref.current?.showModal();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
ref.current.close();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
ref.current?.close();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (open) {
|
open ? closeModal() : openModal();
|
||||||
closeModal();
|
|
||||||
} else {
|
|
||||||
openModal();
|
|
||||||
}
|
|
||||||
}, [open, closeModal, openModal]);
|
}, [open, closeModal, openModal]);
|
||||||
|
|
||||||
if (ref.current) {
|
useEffect(() => {
|
||||||
ref.current.addEventListener('close', () => {
|
const dialog = ref.current;
|
||||||
closeModal();
|
if (!dialog) return;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ref, open, setOpen, openModal, closeModal, toggle } as const;
|
const handleClose = () => setOpen(false);
|
||||||
|
dialog.addEventListener('close', handleClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener('close', handleClose);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ref, open, openModal, closeModal, toggle } as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
@@ -46,15 +56,19 @@ interface ModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
||||||
return (
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
||||||
<dialog ref={ref} className={cn('modal', className?.modal)}>
|
if (closeOnBackdrop && e.target === ref.current) {
|
||||||
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
ref.current?.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
{closeOnBackdrop && (
|
return (
|
||||||
<form method='dialog' className='modal-backdrop'>
|
<dialog
|
||||||
<button>close</button>
|
ref={ref}
|
||||||
</form>
|
className={cn('modal', className?.modal)}
|
||||||
)}
|
onClick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
FilterFn,
|
FilterFn,
|
||||||
SortingState,
|
SortingState,
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
|
Row,
|
||||||
} 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';
|
||||||
@@ -50,6 +51,7 @@ export interface TableProps<TData extends object> {
|
|||||||
manualSorting?: boolean;
|
manualSorting?: boolean;
|
||||||
rowSelection?: Record<string, boolean>;
|
rowSelection?: Record<string, boolean>;
|
||||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -90,6 +92,7 @@ const Table = <TData extends object>({
|
|||||||
manualSorting = false,
|
manualSorting = false,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
|
enableRowSelection,
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
const isServerSideTable =
|
const isServerSideTable =
|
||||||
totalItems !== undefined &&
|
totalItems !== undefined &&
|
||||||
@@ -150,6 +153,10 @@ const Table = <TData extends object>({
|
|||||||
tableOptions.getRowId = (row) => (row as { id: string }).id;
|
tableOptions.getRowId = (row) => (row as { id: string }).id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableRowSelection !== undefined) {
|
||||||
|
tableOptions.enableRowSelection = enableRowSelection;
|
||||||
|
}
|
||||||
|
|
||||||
const table = useReactTable(tableOptions);
|
const table = useReactTable(tableOptions);
|
||||||
const { setPageSize } = table;
|
const { setPageSize } = table;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
id: string;
|
||||||
|
label: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||||
|
tabs: TabItem[];
|
||||||
|
variant?: 'bordered' | 'lifted' | 'boxed';
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
placement?: 'top' | 'bottom';
|
||||||
|
/** Tab yang aktif secara default (uncontrolled mode) */
|
||||||
|
defaultActiveId?: string;
|
||||||
|
/** Tab yang aktif (controlled mode, dikontrol parent) */
|
||||||
|
activeTabId?: string;
|
||||||
|
className?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
wrapper?: string;
|
||||||
|
tab?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
onTabChange?: (tabId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tabs = ({
|
||||||
|
tabs,
|
||||||
|
variant,
|
||||||
|
size = 'md',
|
||||||
|
placement = 'top',
|
||||||
|
defaultActiveId,
|
||||||
|
activeTabId: controlledActiveId,
|
||||||
|
className,
|
||||||
|
onTabChange,
|
||||||
|
...props
|
||||||
|
}: TabsProps) => {
|
||||||
|
// State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset
|
||||||
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(
|
||||||
|
defaultActiveId || tabs[0]?.id || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const isControlled = controlledActiveId !== undefined;
|
||||||
|
const activeTabId = isControlled ? controlledActiveId : uncontrolledActiveId;
|
||||||
|
|
||||||
|
const handleTabChange = (tabId: string) => {
|
||||||
|
if (tabId === activeTabId) return;
|
||||||
|
if (!isControlled) setUncontrolledActiveId(tabId);
|
||||||
|
onTabChange?.(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { wrapper: wrapperClassName, tab: tabClassName } =
|
||||||
|
typeof className === 'object'
|
||||||
|
? className
|
||||||
|
: { wrapper: className, tab: undefined };
|
||||||
|
|
||||||
|
const getTabsClasses = () => {
|
||||||
|
const variantClasses: Record<string, string> = {
|
||||||
|
bordered: 'tabs-bordered',
|
||||||
|
lifted: 'tabs-lift',
|
||||||
|
boxed: 'tabs-box',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<string, string> = {
|
||||||
|
xs: 'tabs-xs',
|
||||||
|
sm: 'tabs-sm',
|
||||||
|
md: '',
|
||||||
|
lg: 'tabs-lg',
|
||||||
|
xl: 'tabs-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const placementClasses: Record<string, string> = {
|
||||||
|
top: '',
|
||||||
|
bottom: 'tabs-bottom',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'tabs',
|
||||||
|
variant && variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
placementClasses[placement],
|
||||||
|
wrapperClassName
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabClasses = (isActive: boolean, isDisabled?: boolean) =>
|
||||||
|
cn(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
'tab-active': isActive,
|
||||||
|
'tab-disabled': isDisabled,
|
||||||
|
},
|
||||||
|
tabClassName
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'w-full',
|
||||||
|
typeof className === 'string' ? className : undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
|
{tabs.map(({ id, label, disabled }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
role='tab'
|
||||||
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
@@ -9,6 +9,11 @@ interface FormActionsProps<T> {
|
|||||||
editUrl?: string;
|
editUrl?: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
disableSubmit?: boolean;
|
disableSubmit?: boolean;
|
||||||
|
onApprove?: () => void;
|
||||||
|
onReject?: () => void;
|
||||||
|
isApproveLoading?: boolean;
|
||||||
|
isRejectLoading?: boolean;
|
||||||
|
showApproveReject?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormActions = <T,>({
|
export const FormActions = <T,>({
|
||||||
@@ -17,11 +22,17 @@ export const FormActions = <T,>({
|
|||||||
editUrl,
|
editUrl,
|
||||||
onDelete,
|
onDelete,
|
||||||
disableSubmit = false,
|
disableSubmit = false,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
isApproveLoading = false,
|
||||||
|
isRejectLoading = false,
|
||||||
|
showApproveReject = false,
|
||||||
}: FormActionsProps<T>) => {
|
}: FormActionsProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && onDelete && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
{onDelete && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -36,6 +47,7 @@ export const FormActions = <T,>({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{type !== 'edit' && editUrl && (
|
{type !== 'edit' && editUrl && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -52,6 +64,46 @@ export const FormActions = <T,>({
|
|||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{type === 'detail' &&
|
||||||
|
showApproveReject &&
|
||||||
|
(onApprove || onReject) && (
|
||||||
|
<>
|
||||||
|
{onApprove && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={onApprove}
|
||||||
|
className='px-4'
|
||||||
|
isLoading={isApproveLoading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onReject && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={onReject}
|
||||||
|
className='px-4'
|
||||||
|
isLoading={isRejectLoading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:cancel-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
|
|||||||
@@ -2,15 +2,27 @@ import Button from '@/components/Button';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface FormHeaderProps {
|
interface FormHeaderProps {
|
||||||
type: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
title: string;
|
title: string;
|
||||||
backUrl: string;
|
backUrl?: string;
|
||||||
|
onBackClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormHeader = ({ type, title, backUrl }: FormHeaderProps) => {
|
export const FormHeader = ({
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
backUrl,
|
||||||
|
onBackClick,
|
||||||
|
}: FormHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button href={backUrl} variant='link' className='w-fit p-0 text-primary'>
|
<Button
|
||||||
|
type='button'
|
||||||
|
href={!onBackClick ? backUrl : undefined}
|
||||||
|
onClick={onBackClick}
|
||||||
|
variant='link'
|
||||||
|
className='w-fit p-0 text-primary'
|
||||||
|
>
|
||||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
@@ -18,6 +30,7 @@ export const FormHeader = ({ type, title, backUrl }: FormHeaderProps) => {
|
|||||||
{type === 'add' && `Tambah ${title}`}
|
{type === 'add' && `Tambah ${title}`}
|
||||||
{type === 'edit' && `Edit ${title}`}
|
{type === 'edit' && `Edit ${title}`}
|
||||||
{type === 'detail' && `Detail ${title}`}
|
{type === 'detail' && `Detail ${title}`}
|
||||||
|
{!type && title}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,21 @@
|
|||||||
import {
|
import {
|
||||||
ChangeEventHandler,
|
ChangeEventHandler,
|
||||||
FocusEventHandler,
|
FocusEventHandler,
|
||||||
ReactNode,
|
useEffect,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { cn } from '@/lib/helper';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
|
||||||
|
import 'react-day-picker/dist/style.css';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
export interface DateInputProps {
|
export interface DateInputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
bottomLabel?: string;
|
bottomLabel?: string;
|
||||||
name: string;
|
name: string;
|
||||||
value?: string;
|
value?: string | { from?: string; to?: string };
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
min?: string;
|
min?: string;
|
||||||
max?: string;
|
max?: string;
|
||||||
@@ -28,9 +33,8 @@ export interface DateInputProps {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
isRange?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
startAdornment?: ReactNode;
|
|
||||||
endAdornment?: ReactNode;
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
@@ -40,22 +44,147 @@ const DateInput = ({
|
|||||||
bottomLabel,
|
bottomLabel,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder = 'dd/mm/yyyy',
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
className,
|
className,
|
||||||
isError,
|
isError: externalError,
|
||||||
isValid,
|
isValid: externalValid,
|
||||||
errorMessage,
|
errorMessage: externalErrorMessage,
|
||||||
startAdornment,
|
|
||||||
endAdornment,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
isRange = false,
|
||||||
}: DateInputProps) => {
|
}: DateInputProps) => {
|
||||||
|
const [internalError, setInternalError] = useState<string | null>(null);
|
||||||
|
const [selected, setSelected] = useState<Date | undefined>();
|
||||||
|
const [selectedRange, setSelectedRange] = useState<{
|
||||||
|
from?: Date;
|
||||||
|
to?: Date;
|
||||||
|
}>({});
|
||||||
|
const [displayValue, setDisplayValue] = useState<string>('');
|
||||||
|
|
||||||
|
const minDate = min
|
||||||
|
? new Date(min.split('/').reverse().join('-'))
|
||||||
|
: undefined;
|
||||||
|
const maxDate = max
|
||||||
|
? new Date(max.split('/').reverse().join('-'))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const calendarModal = useModal();
|
||||||
|
|
||||||
|
// --- Sync value props ---
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
setDisplayValue('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isRange && typeof value === 'object') {
|
||||||
|
const from = value.from ? new Date(value.from) : undefined;
|
||||||
|
const to = value.to ? new Date(value.to) : undefined;
|
||||||
|
setSelectedRange({ from, to });
|
||||||
|
setDisplayValue(
|
||||||
|
`${from ? formatDate(from, 'DD/MM/YYYY') : ''} ${
|
||||||
|
to ? '- ' + formatDate(to, 'DD/MM/YYYY') : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
const iso = value.includes('/')
|
||||||
|
? value.split('/').reverse().join('-')
|
||||||
|
: value;
|
||||||
|
const date = new Date(iso);
|
||||||
|
setSelected(date);
|
||||||
|
setDisplayValue(formatDate(iso, 'DD/MM/YYYY'));
|
||||||
|
}
|
||||||
|
}, [value, isRange]);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!disabled && !readOnly) calendarModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onBlur?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSingle = (selectedDate?: Date) => {
|
||||||
|
if (!selectedDate) return;
|
||||||
|
if (minDate && selectedDate < minDate) {
|
||||||
|
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (maxDate && selectedDate > maxDate) {
|
||||||
|
setInternalError(`Tanggal tidak boleh setelah ${max}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInternalError(null);
|
||||||
|
setSelected(selectedDate);
|
||||||
|
const formattedDisplay = formatDate(selectedDate, 'DD/MM/YYYY');
|
||||||
|
const formattedISO = formatDate(selectedDate, 'YYYY-MM-DD');
|
||||||
|
setDisplayValue(formattedDisplay);
|
||||||
|
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: { name, value: formattedISO },
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange?.(syntheticEvent);
|
||||||
|
calendarModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
|
||||||
|
if (!range) return;
|
||||||
|
setSelectedRange(range);
|
||||||
|
|
||||||
|
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
|
||||||
|
const toStr = range.to ? formatDate(range.to, 'DD/MM/YYYY') : '';
|
||||||
|
setDisplayValue(`${fromStr}${toStr ? ' - ' + toStr : ''}`);
|
||||||
|
|
||||||
|
// Jika kedua tanggal sudah terpilih
|
||||||
|
if (range.from && range.to) {
|
||||||
|
if (minDate && range.from < minDate) {
|
||||||
|
setInternalError(`Tanggal mulai tidak boleh sebelum ${min}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (maxDate && range.to > maxDate) {
|
||||||
|
setInternalError(`Tanggal akhir tidak boleh setelah ${max}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInternalError(null);
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name,
|
||||||
|
value: {
|
||||||
|
from: formatDate(range.from, 'YYYY-MM-DD'),
|
||||||
|
to: formatDate(range.to, 'YYYY-MM-DD'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange?.(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetDate = () => {
|
||||||
|
setSelected(undefined);
|
||||||
|
setSelectedRange({});
|
||||||
|
setDisplayValue('');
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: { name, value: isRange ? { from: '', to: '' } : '' },
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange?.(syntheticEvent);
|
||||||
|
calendarModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDate = () => {
|
||||||
|
if (internalError) return;
|
||||||
|
calendarModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalIsError = externalError || !!internalError;
|
||||||
|
const finalErrorMessage = internalError || externalErrorMessage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -68,68 +197,136 @@ const DateInput = ({
|
|||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-sm font-normal leading-5',
|
'w-full text-sm font-normal leading-5',
|
||||||
{
|
{ 'text-error': finalIsError },
|
||||||
'text-error': isError,
|
|
||||||
},
|
|
||||||
className?.label
|
className?.label
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && (
|
{required && (
|
||||||
<>
|
<span className='text-error' title='required'>
|
||||||
{' '}
|
{' '}
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
*
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200 flex items-center',
|
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded transition-all duration-200 flex items-center border',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': finalIsError,
|
||||||
'border-success!': isValid,
|
'border-success': externalValid && !finalIsError,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{startAdornment && startAdornment}
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type='date'
|
type='text'
|
||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
placeholder={placeholder}
|
placeholder={isRange ? 'dd/mm/yyyy - dd/mm/yyyy' : placeholder}
|
||||||
value={value}
|
value={displayValue}
|
||||||
onChange={onChange}
|
onBlur={handleBlur}
|
||||||
onBlur={onBlur}
|
onClick={handleClick}
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
readOnly // ✅ tidak bisa diketik manual
|
||||||
className={cn(
|
className={cn(
|
||||||
'grow bg-transparent cursor-pointer',
|
'grow bg-transparent cursor-pointer focus:outline-none',
|
||||||
className?.input
|
className?.input
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isLoading || endAdornment) && (
|
{isLoading && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
{isLoading && <span className='loading loading-spinner' />}
|
<span className='loading loading-spinner' />
|
||||||
{endAdornment && endAdornment}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Icon
|
||||||
|
icon='uil:calendar'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='cursor-pointer text-dark'
|
||||||
|
onClick={(e) =>
|
||||||
|
handleClick(e as unknown as React.MouseEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!finalIsError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
{isError && errorMessage && (
|
{finalIsError && finalErrorMessage && (
|
||||||
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
<p className='w-full text-sm text-error'>{finalErrorMessage}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={calendarModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'rounded',
|
||||||
|
modalBox: `w-fit min-h-${isRange ? '124' : '110'} flex flex-col`,
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
{isRange ? (
|
||||||
|
<DayPicker
|
||||||
|
required={required}
|
||||||
|
mode='range'
|
||||||
|
captionLayout='dropdown-years'
|
||||||
|
navLayout='around'
|
||||||
|
reverseYears
|
||||||
|
defaultMonth={selectedRange.from ?? new Date()}
|
||||||
|
startMonth={minDate ?? new Date(1999, 1)}
|
||||||
|
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
|
||||||
|
selected={selectedRange as DateRange}
|
||||||
|
onSelect={handleSelectRange}
|
||||||
|
footer={<div className='text-center mt-3'>{displayValue}</div>}
|
||||||
|
disabled={
|
||||||
|
[
|
||||||
|
minDate ? { before: minDate } : undefined,
|
||||||
|
maxDate ? { after: maxDate } : undefined,
|
||||||
|
].filter(Boolean) as Matcher[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DayPicker
|
||||||
|
required={required}
|
||||||
|
mode='single'
|
||||||
|
captionLayout='dropdown-years'
|
||||||
|
navLayout='around'
|
||||||
|
reverseYears
|
||||||
|
defaultMonth={selected ?? new Date()}
|
||||||
|
startMonth={minDate ?? new Date(1999, 1)}
|
||||||
|
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
|
||||||
|
selected={selected}
|
||||||
|
onSelect={handleSelectSingle}
|
||||||
|
disabled={
|
||||||
|
[
|
||||||
|
minDate ? { before: minDate } : undefined,
|
||||||
|
maxDate ? { after: maxDate } : undefined,
|
||||||
|
].filter(Boolean) as Matcher[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='mt-auto flex flex-col gap-2'>
|
||||||
|
{isRange && (
|
||||||
|
<small className='text-secondary'>
|
||||||
|
Tekan dua kali untuk memilih tanggal awal
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='flex h-full justify-end items-end gap-2'>
|
||||||
|
<Button type='button' color='warning' onClick={handleResetDate}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
{isRange && (
|
||||||
|
<Button type='button' onClick={handleSaveDate}>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDropzone, type Accept } from 'react-dropzone';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface DropFileInputProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
bottomLabel?: string;
|
||||||
|
caption?: string;
|
||||||
|
values?: File[];
|
||||||
|
accept?: Accept;
|
||||||
|
required?: boolean;
|
||||||
|
maxFiles?: number; // defaults to 1
|
||||||
|
maxSize?: number; // defaults to 2097152 (2 MB)
|
||||||
|
isError?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (files: File[]) => void;
|
||||||
|
onDelete?: (index: number) => void;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
inputContainer?: string;
|
||||||
|
label?: string;
|
||||||
|
inputWrapper?: string;
|
||||||
|
caption?: string;
|
||||||
|
bottomLabel?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
fileItemContainer?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropFileInput: React.FC<DropFileInputProps> = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
bottomLabel,
|
||||||
|
caption = 'Seret atau Pilih Dokumen',
|
||||||
|
values,
|
||||||
|
accept,
|
||||||
|
required,
|
||||||
|
maxFiles = Infinity,
|
||||||
|
maxSize,
|
||||||
|
isError,
|
||||||
|
errorMessage,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
onDelete,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const isDisabled =
|
||||||
|
Boolean(values && maxFiles && values.length >= maxFiles) || disabled;
|
||||||
|
|
||||||
|
const {
|
||||||
|
acceptedFiles,
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
isFocused,
|
||||||
|
isDragAccept,
|
||||||
|
isDragReject,
|
||||||
|
} = useDropzone({
|
||||||
|
maxSize,
|
||||||
|
maxFiles,
|
||||||
|
accept: accept,
|
||||||
|
disabled: isDisabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values && maxFiles && values.length <= maxFiles) {
|
||||||
|
onChange?.([...values, ...acceptedFiles]);
|
||||||
|
}
|
||||||
|
}, [acceptedFiles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('w-full', className?.wrapper)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full flex flex-col gap-2 text-start',
|
||||||
|
className?.inputContainer
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-sm font-normal leading-5',
|
||||||
|
className?.label
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
{...getRootProps({
|
||||||
|
'aria-disabled': isDisabled,
|
||||||
|
className: cn(
|
||||||
|
'dropzone w-full px-4 py-2 border border-dashed border-gray-300 rounded cursor-pointer transition-all',
|
||||||
|
'hover:border-primary hover:bg-primary/10',
|
||||||
|
{
|
||||||
|
'border-success bg-success/10': isDragAccept,
|
||||||
|
'border-error bg-error/10': isDragReject || isError,
|
||||||
|
'border-primary bg-primary/10': isFocused,
|
||||||
|
'bg-gray-200/20 cursor-not-allowed': isDisabled,
|
||||||
|
},
|
||||||
|
className?.inputWrapper
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
{...getInputProps({
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
disabled: isDisabled,
|
||||||
|
'aria-disabled': isDisabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{caption && (
|
||||||
|
<p className={cn('text-gray-500 text-sm', className?.caption)}>
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isError && bottomLabel && (
|
||||||
|
<p
|
||||||
|
className={cn('w-full text-sm opacity-60', className?.bottomLabel)}
|
||||||
|
>
|
||||||
|
{bottomLabel}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p
|
||||||
|
className={cn('w-full text-sm text-error', className?.errorMessage)}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{values && values.length > 0 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full mt-1.5 flex flex-col gap-1.5',
|
||||||
|
className?.fileItemContainer
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{values.map((file, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={cn('w-full flex flex-row items-center gap-2')}
|
||||||
|
>
|
||||||
|
<div className='p-2 rounded-full bg-primary/10'>
|
||||||
|
<Icon
|
||||||
|
icon='basil:file-solid'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='text-blue-500'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full text-sm'>
|
||||||
|
<p>{file.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
onClick={() => {
|
||||||
|
onDelete?.(idx);
|
||||||
|
}}
|
||||||
|
className='rounded-full text-error focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon icon='fluent:delete-12-regular' width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropFileInput;
|
||||||
@@ -69,10 +69,7 @@ const FileInput = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn('grow file-input w-full h-12 rounded', className?.input)}
|
||||||
'grow file-input w-full h-12 rounded',
|
|
||||||
className?.input
|
|
||||||
)}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { ChangeEvent, ReactNode } from "react";
|
import { ChangeEvent, ReactNode } from 'react';
|
||||||
import { NumericFormat, OnValueChange } from "react-number-format";
|
import { NumericFormat, OnValueChange } from 'react-number-format';
|
||||||
import TextInput, { TextInputProps } from "@/components/input/TextInput";
|
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||||
|
|
||||||
interface NumberInputProps extends Omit<TextInputProps, "type"> {
|
interface NumberInputProps extends Omit<TextInputProps, 'type'> {
|
||||||
thousandSeparator?: string;
|
thousandSeparator?: string;
|
||||||
decimalSeparator?: string;
|
decimalSeparator?: string;
|
||||||
decimalScale?: number;
|
decimalScale?: number;
|
||||||
@@ -17,8 +17,8 @@ interface NumberInputProps extends Omit<TextInputProps, "type"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NumberInput = ({
|
const NumberInput = ({
|
||||||
thousandSeparator = ",",
|
thousandSeparator = ',',
|
||||||
decimalSeparator = ".",
|
decimalSeparator = '.',
|
||||||
decimalScale = 5,
|
decimalScale = 5,
|
||||||
allowNegative = true,
|
allowNegative = true,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -28,7 +28,7 @@ const NumberInput = ({
|
|||||||
}: NumberInputProps) => {
|
}: NumberInputProps) => {
|
||||||
const valueChangeHandler: OnValueChange = (
|
const valueChangeHandler: OnValueChange = (
|
||||||
numberFormatValues,
|
numberFormatValues,
|
||||||
sourceInfo,
|
sourceInfo
|
||||||
) => {
|
) => {
|
||||||
const newChangeEvent = sourceInfo.event as
|
const newChangeEvent = sourceInfo.event as
|
||||||
| ChangeEvent<HTMLInputElement>
|
| ChangeEvent<HTMLInputElement>
|
||||||
@@ -49,8 +49,8 @@ const NumberInput = ({
|
|||||||
onValueChange={valueChangeHandler}
|
onValueChange={valueChangeHandler}
|
||||||
decimalScale={decimalScale}
|
decimalScale={decimalScale}
|
||||||
allowNegative={allowNegative}
|
allowNegative={allowNegative}
|
||||||
startAdornment={inputPrefix}
|
inputPrefix={inputPrefix}
|
||||||
endAdornment={inputSuffix}
|
inputSuffix={inputSuffix}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import {
|
||||||
|
PatternFormat,
|
||||||
|
NumberFormatBase,
|
||||||
|
NumberFormatBaseProps,
|
||||||
|
OnValueChange,
|
||||||
|
} from 'react-number-format';
|
||||||
|
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||||
|
|
||||||
|
interface PatternInputProps extends Omit<TextInputProps, 'type'> {
|
||||||
|
/**
|
||||||
|
* Format pattern, contoh: "##/##/####", "(###) ###-####", "####-####-####"
|
||||||
|
*/
|
||||||
|
format: string;
|
||||||
|
/** Mask karakter kosong, misal "_" */
|
||||||
|
mask?: string;
|
||||||
|
/** Menampilkan mask walau value kosong */
|
||||||
|
allowEmptyFormatting?: boolean;
|
||||||
|
/** Placeholder karakter format, default: "#" */
|
||||||
|
patternChar?: string;
|
||||||
|
/** Jika true, izinkan huruf (A-Z) selain angka */
|
||||||
|
inputVehicleNumber?: boolean;
|
||||||
|
type?: 'text' | 'password' | 'tel';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PatternInput – tetap backward-compatible dengan Storybook
|
||||||
|
* tapi bisa menerima huruf jika `allowCharacters={true}`
|
||||||
|
*/
|
||||||
|
const PatternInput = ({
|
||||||
|
type = 'text',
|
||||||
|
format,
|
||||||
|
mask = '_',
|
||||||
|
allowEmptyFormatting = false,
|
||||||
|
patternChar = '#',
|
||||||
|
inputVehicleNumber = false,
|
||||||
|
onChange,
|
||||||
|
...restProps
|
||||||
|
}: PatternInputProps) => {
|
||||||
|
const handleValueChange: OnValueChange = (values, { event }) => {
|
||||||
|
const newEvent = event as ChangeEvent<HTMLInputElement> | undefined;
|
||||||
|
if (newEvent) {
|
||||||
|
newEvent.target.value = values.value.toUpperCase();
|
||||||
|
onChange?.(newEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inputVehicleNumber) {
|
||||||
|
return (
|
||||||
|
<NumberFormatBase
|
||||||
|
{...restProps}
|
||||||
|
type={type}
|
||||||
|
customInput={TextInput}
|
||||||
|
format={(value) => {
|
||||||
|
const clean = value.replace(/[^a-z0-9]/gi, '').toUpperCase();
|
||||||
|
|
||||||
|
const match = clean.match(/^([A-Z]{0,2})(\d{0,4})([A-Z]{0,3})$/);
|
||||||
|
if (!match) return clean;
|
||||||
|
const [, prefix, number, suffix] = match;
|
||||||
|
return [prefix, number, suffix].filter(Boolean).join(' ');
|
||||||
|
}}
|
||||||
|
removeFormatting={(val) => val.replace(/\s+/g, '')}
|
||||||
|
isValidInputCharacter={(char) => /^[a-z0-9]$/i.test(char)}
|
||||||
|
getCaretBoundary={(val) =>
|
||||||
|
Array(val.length + 1)
|
||||||
|
.fill(true)
|
||||||
|
.map(Boolean)
|
||||||
|
}
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PatternFormat
|
||||||
|
{...restProps}
|
||||||
|
type={type}
|
||||||
|
format={format}
|
||||||
|
mask={mask}
|
||||||
|
allowEmptyFormatting={allowEmptyFormatting}
|
||||||
|
patternChar={patternChar}
|
||||||
|
customInput={TextInput}
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PatternInput;
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
|
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import Select, {
|
import Select, {
|
||||||
OptionProps,
|
OptionProps,
|
||||||
GroupBase,
|
GroupBase,
|
||||||
InputActionMeta,
|
InputActionMeta,
|
||||||
MultiValue,
|
MultiValue,
|
||||||
SingleValue,
|
SingleValue,
|
||||||
|
components as ReactSelectComponents,
|
||||||
|
ControlProps,
|
||||||
} from 'react-select';
|
} from 'react-select';
|
||||||
import CreatableSelect from 'react-select/creatable';
|
import CreatableSelect from 'react-select/creatable';
|
||||||
import makeAnimated from 'react-select/animated';
|
import makeAnimated from 'react-select/animated';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { cn, getByPath } from '@/lib/helper';
|
import { cn, getByPath } from '@/lib/helper';
|
||||||
|
import useSWR from 'swr';
|
||||||
import { httpClientFetcher } from '@/services/http/client';
|
import { httpClientFetcher } from '@/services/http/client';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -53,6 +54,8 @@ interface SelectInputBaseProps<T = OptionType> {
|
|||||||
openMenu?: boolean;
|
openMenu?: boolean;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
onInputChange?: (search: string) => void;
|
onInputChange?: (search: string) => void;
|
||||||
|
startAdornment?: ReactNode;
|
||||||
|
menuPortalTarget?: HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
||||||
@@ -63,6 +66,33 @@ interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
|||||||
|
|
||||||
const animatedComponents = makeAnimated();
|
const animatedComponents = makeAnimated();
|
||||||
|
|
||||||
|
const CustomControl = <
|
||||||
|
Option,
|
||||||
|
IsMulti extends boolean,
|
||||||
|
Group extends GroupBase<Option>,
|
||||||
|
>(
|
||||||
|
props: ControlProps<Option, IsMulti, Group>
|
||||||
|
) => {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
const customProps = props.selectProps as unknown as {
|
||||||
|
shouldShowAdornment?: boolean;
|
||||||
|
startAdornment?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowAdornment = customProps.shouldShowAdornment ?? false;
|
||||||
|
const startAdornment = customProps.startAdornment;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactSelectComponents.Control {...props}>
|
||||||
|
<div className='flex-1 px-4! py-1.5 gap-1 flex items-center'>
|
||||||
|
{shouldShowAdornment && startAdornment}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</ReactSelectComponents.Control>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@@ -87,15 +117,25 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
delay = 300,
|
delay = 300,
|
||||||
createables = false,
|
createables = false,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
|
startAdornment,
|
||||||
|
menuPortalTarget,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [internalInputValue, setInternalInputValue] = useState('');
|
const [internalInputValue, setInternalInputValue] = useState('');
|
||||||
const [debouncedInputValue] = useDebounce(internalInputValue, delay);
|
const [debouncedInputValue] = useDebounce(internalInputValue, delay);
|
||||||
|
|
||||||
|
const shouldShowAdornment = startAdornment && !internalInputValue;
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
const base = isAnimated ? animatedComponents : {};
|
const base = isAnimated ? animatedComponents : {};
|
||||||
return { ...base, IndicatorSeparator: () => null };
|
const customComponents = { ...base, IndicatorSeparator: () => null };
|
||||||
}, [isAnimated]);
|
|
||||||
|
if (startAdornment) {
|
||||||
|
customComponents.Control = CustomControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return customComponents;
|
||||||
|
}, [isAnimated, startAdornment]);
|
||||||
|
|
||||||
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
|
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
|
||||||
if (meta.action === 'input-change') setInternalInputValue(val);
|
if (meta.action === 'input-change') setInternalInputValue(val);
|
||||||
@@ -139,9 +179,12 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && (
|
{required && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -149,11 +192,12 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
<SelectComponent<T, boolean, GroupBase<T>>
|
<SelectComponent<T, boolean, GroupBase<T>>
|
||||||
instanceId='select'
|
instanceId='select'
|
||||||
value={value ?? (isMulti ? [] : null)}
|
value={value ?? (isMulti ? [] : null)}
|
||||||
onChange={handleChange}
|
onChange={onChange ? handleChange : undefined}
|
||||||
options={options}
|
options={options}
|
||||||
menuIsOpen={openMenu}
|
menuIsOpen={openMenu}
|
||||||
inputValue={internalInputValue}
|
inputValue={internalInputValue}
|
||||||
onInputChange={internalInputChangeHandler}
|
onInputChange={internalInputChangeHandler}
|
||||||
|
onMenuClose={() => setInternalInputValue('')}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -163,6 +207,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={cn('w-full', className?.select)}
|
className={cn('w-full', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
|
...(!startAdornment && {
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn(
|
cn(
|
||||||
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
|
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
|
||||||
@@ -174,6 +219,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
|
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
|
||||||
|
}),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
|
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
@@ -190,7 +236,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
|
cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
|
||||||
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
menuList: () => cn('p-2! max-h-60 overflow-auto'),
|
||||||
option: ({ isFocused, isSelected }) =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
cn('mt-1 px-3 py-2 rounded cursor-pointer!', {
|
cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', {
|
||||||
'bg-indigo-600 text-white': isFocused,
|
'bg-indigo-600 text-white': isFocused,
|
||||||
'bg-blue-500!': isSelected,
|
'bg-blue-500!': isSelected,
|
||||||
'text-gray-700': !isFocused && !isSelected,
|
'text-gray-700': !isFocused && !isSelected,
|
||||||
@@ -211,8 +257,14 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
...components,
|
...components,
|
||||||
...(optionComponent ? { Option: optionComponent } : {}),
|
...(optionComponent ? { Option: optionComponent } : {}),
|
||||||
}}
|
}}
|
||||||
|
{...(startAdornment && {
|
||||||
|
shouldShowAdornment,
|
||||||
|
startAdornment,
|
||||||
|
})}
|
||||||
menuPortalTarget={
|
menuPortalTarget={
|
||||||
typeof document !== 'undefined' ? document.body : undefined
|
typeof document !== 'undefined'
|
||||||
|
? (menuPortalTarget ?? document.body)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
@@ -229,8 +281,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
|
|
||||||
const useSelect = <T,>(
|
const useSelect = <T,>(
|
||||||
basePath: string,
|
basePath: string,
|
||||||
valueKey: keyof T,
|
valueKey: keyof T | string,
|
||||||
labelKey: keyof T,
|
labelKey: keyof T | string,
|
||||||
searchKey: string = 'search',
|
searchKey: string = 'search',
|
||||||
params?: { [key: string]: string }
|
params?: { [key: string]: string }
|
||||||
) => {
|
) => {
|
||||||
@@ -241,7 +293,7 @@ const useSelect = <T,>(
|
|||||||
[searchKey]: inputValue ?? '',
|
[searchKey]: inputValue ?? '',
|
||||||
...params,
|
...params,
|
||||||
}).toString();
|
}).toString();
|
||||||
}, [inputValue, searchKey]);
|
}, [inputValue, searchKey, params]);
|
||||||
|
|
||||||
const optionsUrl = `${basePath}?${optionsUrlParams}`;
|
const optionsUrl = `${basePath}?${optionsUrlParams}`;
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const TextArea = ({
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
|
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface TextInputProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
startAdornment?: ReactNode;
|
startAdornment?: ReactNode;
|
||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
|
inputPrefix?: ReactNode;
|
||||||
|
inputSuffix?: ReactNode;
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
@@ -48,6 +50,8 @@ const TextInput = ({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
startAdornment,
|
startAdornment,
|
||||||
endAdornment,
|
endAdornment,
|
||||||
|
inputPrefix,
|
||||||
|
inputSuffix,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -85,9 +89,86 @@ const TextInput = ({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{inputPrefix || inputSuffix ? (
|
||||||
|
<div className='relative flex'>
|
||||||
|
{inputPrefix && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200',
|
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputPrefix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
|
||||||
|
{
|
||||||
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
'rounded-l-none!': inputPrefix,
|
||||||
|
'rounded-r-none!': inputSuffix,
|
||||||
|
'input-disabled': disabled,
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'bg-gray-50': disabled,
|
||||||
|
},
|
||||||
|
className?.inputWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'grow bg-transparent outline-none',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'text-gray-500': disabled,
|
||||||
|
},
|
||||||
|
className?.input
|
||||||
|
)}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(isLoading || endAdornment) && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
{isLoading && <span className='loading loading-spinner' />}
|
||||||
|
|
||||||
|
{endAdornment && endAdornment}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{inputSuffix && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputSuffix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -118,6 +199,7 @@ const TextInput = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
|
|||||||
@@ -49,14 +49,18 @@ const MenuItem = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li onClick={onClick}>
|
<li>
|
||||||
{href && (
|
{href && (
|
||||||
<Link href={href} className={menuItemBaseClassName}>
|
<Link href={href} className={menuItemBaseClassName}>
|
||||||
{menuItemContent}
|
{menuItemContent}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!href && <a className={menuItemBaseClassName}>{menuItemContent}</a>}
|
{!href && (
|
||||||
|
<button className={menuItemBaseClassName} onClick={onClick}>
|
||||||
|
{menuItemContent}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject } from 'react';
|
import { MouseEventHandler, RefObject, useState } from 'react';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import Button from '@/components/Button';
|
import Button, { ButtonProps } from '@/components/Button';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
|
||||||
|
|
||||||
interface ConfirmationModalProps {
|
export interface ConfirmationModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
type?: 'info' | 'success' | 'error';
|
type?: 'info' | 'success' | 'error';
|
||||||
text?: string;
|
text?: string;
|
||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
primaryButton?: {
|
primaryButton?: ButtonProps & {
|
||||||
text?: string;
|
text?: string;
|
||||||
color?: Color;
|
|
||||||
isLoading?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
};
|
||||||
secondaryButton?: {
|
secondaryButton?: ButtonProps & {
|
||||||
text?: string;
|
text?: string;
|
||||||
color?: Color;
|
|
||||||
isLoading?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
};
|
||||||
className?: {
|
className?: {
|
||||||
modal?: string;
|
modal?: string;
|
||||||
modalBox?: string;
|
modalBox?: string;
|
||||||
};
|
};
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmationModal = ({
|
const ConfirmationModal = ({
|
||||||
@@ -40,11 +34,24 @@ const ConfirmationModal = ({
|
|||||||
primaryButton,
|
primaryButton,
|
||||||
secondaryButton,
|
secondaryButton,
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
}: ConfirmationModalProps) => {
|
}: ConfirmationModalProps) => {
|
||||||
|
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
||||||
|
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const primaryButtonClickHandler: MouseEventHandler<
|
||||||
|
HTMLButtonElement
|
||||||
|
> = async (event) => {
|
||||||
|
setIsPrimaryButtonLoading(true);
|
||||||
|
|
||||||
|
await primaryButton?.onClick?.(event);
|
||||||
|
|
||||||
|
setIsPrimaryButtonLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
||||||
<div className='w-full flex flex-col gap-4'>
|
<div className='w-full flex flex-col gap-4'>
|
||||||
@@ -90,13 +97,20 @@ const ConfirmationModal = ({
|
|||||||
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{children && <div className='w-full'>{children}</div>}
|
||||||
|
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full flex flex-row gap-2'>
|
||||||
{secondaryButton && secondaryButton.text && (
|
{secondaryButton && secondaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
|
{...secondaryButton}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color={secondaryButton?.color ?? 'none'}
|
color={secondaryButton?.color}
|
||||||
isLoading={secondaryButton?.isLoading}
|
isLoading={secondaryButton?.isLoading}
|
||||||
disabled={secondaryButton?.isLoading}
|
disabled={
|
||||||
|
secondaryButton?.isLoading !== undefined
|
||||||
|
? secondaryButton?.isLoading
|
||||||
|
: isPrimaryButtonLoading
|
||||||
|
}
|
||||||
onClick={closeModalHandler}
|
onClick={closeModalHandler}
|
||||||
className='grow'
|
className='grow'
|
||||||
>
|
>
|
||||||
@@ -106,10 +120,19 @@ const ConfirmationModal = ({
|
|||||||
|
|
||||||
{primaryButton && primaryButton.text && (
|
{primaryButton && primaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
|
{...primaryButton}
|
||||||
color={primaryButton?.color ?? 'info'}
|
color={primaryButton?.color ?? 'info'}
|
||||||
onClick={primaryButton?.onClick}
|
onClick={primaryButtonClickHandler}
|
||||||
isLoading={primaryButton?.isLoading}
|
isLoading={
|
||||||
disabled={primaryButton?.isLoading}
|
primaryButton?.isLoading !== undefined
|
||||||
|
? primaryButton?.isLoading
|
||||||
|
: isPrimaryButtonLoading
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
primaryButton?.isLoading !== undefined
|
||||||
|
? primaryButton?.isLoading
|
||||||
|
: isPrimaryButtonLoading
|
||||||
|
}
|
||||||
className='grow'
|
className='grow'
|
||||||
>
|
>
|
||||||
{primaryButton?.text ?? 'Ya'}
|
{primaryButton?.text ?? 'Ya'}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useId, useState } from 'react';
|
||||||
|
|
||||||
|
import ConfirmationModal, {
|
||||||
|
ConfirmationModalProps,
|
||||||
|
} from '@/components/modal/ConfirmationModal';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
|
interface ConfirmationModalWithNotesProps
|
||||||
|
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
|
||||||
|
rows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
primaryButton?: {
|
||||||
|
text?: string;
|
||||||
|
color?: Color;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onClick?: (notes: string) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||||
|
ref,
|
||||||
|
type = 'info',
|
||||||
|
text,
|
||||||
|
closeOnBackdrop,
|
||||||
|
primaryButton,
|
||||||
|
secondaryButton,
|
||||||
|
className,
|
||||||
|
rows = 3,
|
||||||
|
placeholder = 'Catatan...',
|
||||||
|
}) => {
|
||||||
|
const randomId = useId();
|
||||||
|
const [notes, setNotes] = useState('');
|
||||||
|
|
||||||
|
const notesChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
||||||
|
setNotes(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
text={text}
|
||||||
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
|
primaryButton={{
|
||||||
|
...primaryButton,
|
||||||
|
onClick: () => {
|
||||||
|
primaryButton?.onClick?.(notes);
|
||||||
|
setNotes('');
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
secondaryButton={secondaryButton}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
name={randomId}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={notes}
|
||||||
|
onChange={notesChangeHandler}
|
||||||
|
rows={rows}
|
||||||
|
/>
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationModalWithNotes;
|
||||||
@@ -4,8 +4,16 @@ import StepItem from '@/components/steps/StepItem';
|
|||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { BaseApproval, BaseGroupedApproval } from '@/types/api/api-general';
|
import {
|
||||||
|
BaseApiResponse,
|
||||||
|
BaseApproval,
|
||||||
|
BaseGroupedApproval,
|
||||||
|
} from '@/types/api/api-general';
|
||||||
import { ApprovalLine } from '@/types/config/constant';
|
import { ApprovalLine } from '@/types/config/constant';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { httpClientFetcher } from '@/services/http/client';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export type ApprovalStepStatus = 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE';
|
export type ApprovalStepStatus = 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE';
|
||||||
|
|
||||||
@@ -120,7 +128,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
|
|
||||||
const currentStepNumber = approvalLineItem.step_number;
|
const currentStepNumber = approvalLineItem.step_number;
|
||||||
const lastStepNumber =
|
const lastStepNumber =
|
||||||
groupedApprovals[groupedApprovals.length - 1].step_number;
|
groupedApprovals[groupedApprovals.length - 1]?.step_number;
|
||||||
|
|
||||||
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
|
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -130,18 +138,27 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
|
|
||||||
if (!approvalGroup) {
|
if (!approvalGroup) {
|
||||||
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
||||||
|
const isPreviousApprovalRejected =
|
||||||
|
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
|
||||||
|
'REJECTED';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: approvalLineItem.step_name,
|
name: approvalLineItem.step_name,
|
||||||
status: isWaiting ? 'WAITING' : 'IDLE',
|
status: isPreviousApprovalRejected
|
||||||
|
? 'IDLE'
|
||||||
|
: isWaiting
|
||||||
|
? 'WAITING'
|
||||||
|
: 'IDLE',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let approvalStatus: ApprovalStepStatus;
|
let approvalStatus: ApprovalStepStatus = 'IDLE';
|
||||||
|
|
||||||
if (approvalGroup.step_number <= latestApproval.step_number) {
|
if (approvalGroup.step_number <= latestApproval.step_number) {
|
||||||
switch (approvalGroup.approvals[0].action) {
|
if (approvalGroup.approvals) {
|
||||||
|
switch (approvalGroup?.approvals[0]?.action) {
|
||||||
case 'CREATED':
|
case 'CREATED':
|
||||||
|
case 'UPDATED':
|
||||||
case 'APPROVED':
|
case 'APPROVED':
|
||||||
approvalStatus = 'APPROVED';
|
approvalStatus = 'APPROVED';
|
||||||
break;
|
break;
|
||||||
@@ -154,19 +171,20 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
approvalStatus = 'IDLE';
|
approvalStatus = 'IDLE';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (approvalGroup.step_number === latestApproval.step_number + 1) {
|
} else if (approvalGroup.step_number === latestApproval.step_number + 1) {
|
||||||
approvalStatus = 'WAITING';
|
approvalStatus = 'WAITING';
|
||||||
} else {
|
} else {
|
||||||
approvalStatus = 'IDLE';
|
approvalStatus = 'IDLE';
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals.map(
|
const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals
|
||||||
(approval) => ({
|
? approvalGroup.approvals.map((approval) => ({
|
||||||
action_by: approval.action_by.name,
|
action_by: approval.action_by.name,
|
||||||
date: approval.action_at,
|
date: approval.action_at,
|
||||||
notes: approval.notes,
|
notes: approval.notes,
|
||||||
})
|
}))
|
||||||
);
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: approvalGroup.step_name,
|
name: approvalGroup.step_name,
|
||||||
@@ -179,3 +197,178 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ApprovalSteps;
|
export default ApprovalSteps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengubah array BaseApproval (datar) menjadi BaseGroupedApproval (berkelompok).
|
||||||
|
*/
|
||||||
|
const groupApprovalsByStep = (
|
||||||
|
approvals: BaseApproval[]
|
||||||
|
): BaseGroupedApproval[] => {
|
||||||
|
const groups: Record<number, BaseGroupedApproval> = {};
|
||||||
|
for (const approval of approvals) {
|
||||||
|
if (!groups[approval.step_number]) {
|
||||||
|
groups[approval.step_number] = {
|
||||||
|
step_number: approval.step_number,
|
||||||
|
step_name: approval.step_name,
|
||||||
|
approvals: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
groups[approval.step_number].approvals.push(approval);
|
||||||
|
}
|
||||||
|
return Object.values(groups);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengubah array BaseGroupedApproval (berkelompok) kembali menjadi BaseApproval[] (datar).
|
||||||
|
*/
|
||||||
|
const flattenGroupedApprovals = (
|
||||||
|
groupedApprovals: BaseGroupedApproval[]
|
||||||
|
): BaseApproval[] => {
|
||||||
|
return groupedApprovals.flatMap((group) => group.approvals);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard untuk memeriksa apakah data adalah BaseGroupedApproval[].
|
||||||
|
*/
|
||||||
|
const isGroupedApprovalData = (
|
||||||
|
data: BaseApproval[] | BaseGroupedApproval[]
|
||||||
|
): data is BaseGroupedApproval[] => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const firstElement = data[0];
|
||||||
|
return (
|
||||||
|
typeof firstElement === 'object' &&
|
||||||
|
firstElement !== null &&
|
||||||
|
'approvals' in firstElement &&
|
||||||
|
Array.isArray(firstElement.approvals)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useApprovalSteps = ({
|
||||||
|
latestApproval,
|
||||||
|
approvalLines,
|
||||||
|
moduleName,
|
||||||
|
moduleId,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
latestApproval: BaseApproval | undefined;
|
||||||
|
approvalLines: ApprovalLine;
|
||||||
|
moduleName: string;
|
||||||
|
moduleId: string;
|
||||||
|
params?: {
|
||||||
|
page?: number;
|
||||||
|
limit: number;
|
||||||
|
search?: string;
|
||||||
|
group_step_number?: boolean;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
// Membuat URL Parameters
|
||||||
|
const paramString = new URLSearchParams({
|
||||||
|
page: params?.page?.toString() || '',
|
||||||
|
limit: params?.limit?.toString() || '',
|
||||||
|
search: params?.search || '',
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
// fetching data approvals
|
||||||
|
const SWR_KEY_APPROVALS =
|
||||||
|
moduleName && moduleId
|
||||||
|
? `/approvals?module_name=${moduleName}&module_id=${moduleId}${
|
||||||
|
params ? `&${paramString}` : ''
|
||||||
|
}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: approvalData,
|
||||||
|
isLoading: approvalIsLoading,
|
||||||
|
mutate: mutateApprovals,
|
||||||
|
} = useSWR(SWR_KEY_APPROVALS, async (url) => {
|
||||||
|
return await httpClientFetcher<
|
||||||
|
BaseApiResponse<BaseApproval[] | BaseGroupedApproval[]>
|
||||||
|
>(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fungsi Refresh
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
await mutateApprovals();
|
||||||
|
}, [mutateApprovals]);
|
||||||
|
|
||||||
|
const { groupedApprovals } = useMemo(() => {
|
||||||
|
const rawData = isResponseSuccess(approvalData)
|
||||||
|
? approvalData.data
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let processedGroupedApprovals: BaseGroupedApproval[] = [];
|
||||||
|
|
||||||
|
if (rawData) {
|
||||||
|
if (isGroupedApprovalData(rawData)) {
|
||||||
|
processedGroupedApprovals = rawData;
|
||||||
|
} else {
|
||||||
|
processedGroupedApprovals = groupApprovalsByStep(
|
||||||
|
rawData as BaseApproval[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupedApprovals: processedGroupedApprovals,
|
||||||
|
};
|
||||||
|
}, [approvalData]);
|
||||||
|
|
||||||
|
const isLoading = approvalIsLoading;
|
||||||
|
|
||||||
|
// Formatting Akhir
|
||||||
|
const approvals = useMemo(() => {
|
||||||
|
if (isLoading || !approvalLines.length || !latestApproval) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return formatGroupedApprovalsToApprovalSteps(
|
||||||
|
approvalLines,
|
||||||
|
groupedApprovals,
|
||||||
|
latestApproval
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Gagal memformat approval steps:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [isLoading, approvalLines, groupedApprovals, latestApproval]);
|
||||||
|
|
||||||
|
// Raw Data Approvals
|
||||||
|
const rawDataApprovals = useMemo(() => {
|
||||||
|
const rawData = isResponseSuccess(approvalData)
|
||||||
|
? approvalData.data
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!rawData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDataCurrentlyGrouped = isGroupedApprovalData(rawData);
|
||||||
|
const wantsGrouped = params?.group_step_number !== false;
|
||||||
|
|
||||||
|
if (wantsGrouped) {
|
||||||
|
if (isDataCurrentlyGrouped) {
|
||||||
|
return rawData as BaseGroupedApproval[];
|
||||||
|
} else {
|
||||||
|
return groupApprovalsByStep(rawData as BaseApproval[]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isDataCurrentlyGrouped) {
|
||||||
|
return flattenGroupedApprovals(rawData as BaseGroupedApproval[]);
|
||||||
|
} else {
|
||||||
|
return rawData as BaseApproval[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [approvalData, params?.group_step_number]);
|
||||||
|
|
||||||
|
// Return Hook
|
||||||
|
return {
|
||||||
|
approvals,
|
||||||
|
isLoading,
|
||||||
|
rawDataApprovals: rawDataApprovals,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useApprovalSteps };
|
||||||
|
|||||||
@@ -0,0 +1,507 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
|
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||||
|
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||||
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
|
|
||||||
|
import { Expense } from '@/types/api/expense';
|
||||||
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
|
import {
|
||||||
|
UploadRequestDocumentsFormSchema,
|
||||||
|
UploadRequestDocumentsFormValues,
|
||||||
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
|
|
||||||
|
interface ExpenseDetailProps {
|
||||||
|
initialValues?: Expense;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Modal hooks
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const approveModal = useModal();
|
||||||
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
// Modal loading state
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
|
||||||
|
const isLatestApprovalRejectedOrDone =
|
||||||
|
initialValues?.approval &&
|
||||||
|
(initialValues.approval.action === 'REJECTED' ||
|
||||||
|
initialValues.approval.step_number === 5);
|
||||||
|
|
||||||
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
request_documents: [],
|
||||||
|
},
|
||||||
|
validationSchema: UploadRequestDocumentsFormSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const addRequestDocumentsRes = await ExpenseApi.uploadRequestDocuments(
|
||||||
|
initialValues?.id as number,
|
||||||
|
values.request_documents
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(addRequestDocumentsRes)) {
|
||||||
|
toast.success(addRequestDocumentsRes.message);
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
toast.error(String(addRequestDocumentsRes?.message));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteExpenseClickHandler = () => {
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const approveClickHandler = () => {
|
||||||
|
approveModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectClickHandler = () => {
|
||||||
|
rejectModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal confirm click handler
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ExpenseApi.delete(initialValues?.id as number);
|
||||||
|
|
||||||
|
toast.success('Berhasil menghapus data biaya operasional!');
|
||||||
|
router.push('/expense');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Gagal menghapus data biaya operasional!');
|
||||||
|
} finally {
|
||||||
|
deleteModal.closeModal();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
const approveResponse = await ExpenseApi.approve(
|
||||||
|
initialValues?.id as number,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveResponse)) {
|
||||||
|
approveModal.closeModal();
|
||||||
|
|
||||||
|
toast.success('Berhasil approve pengajuan biaya operasional!');
|
||||||
|
router.push('/expense');
|
||||||
|
} else {
|
||||||
|
approveModal.closeModal();
|
||||||
|
|
||||||
|
toast.error('Gagal approve pengajuan biaya operasional!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
|
const rejectResponse = await ExpenseApi.reject(
|
||||||
|
initialValues?.id as number,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(rejectResponse)) {
|
||||||
|
rejectModal.closeModal();
|
||||||
|
|
||||||
|
toast.success('Berhasil reject pengajuan biaya operasional!');
|
||||||
|
router.push('/expense');
|
||||||
|
} else {
|
||||||
|
rejectModal.closeModal();
|
||||||
|
|
||||||
|
toast.error('Gagal reject pengajuan biaya operasional!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDocumentsChangeHandler = (val: File[]) => {
|
||||||
|
formik.setFieldTouched('request_documents', true);
|
||||||
|
formik.setFieldValue('request_documents', val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDocumentsDeleteHandler = (deletedFileIdx: number) => {
|
||||||
|
const newRequestDocuments = formik.values.request_documents;
|
||||||
|
|
||||||
|
newRequestDocuments?.splice(deletedFileIdx, 1);
|
||||||
|
|
||||||
|
formik.setFieldValue('request_documents', newRequestDocuments);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
|
<header className='flex flex-col gap-4'>
|
||||||
|
<Button
|
||||||
|
href='/expense'
|
||||||
|
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 Biaya Operasional
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
{!isLatestApprovalRejectedOrDone && (
|
||||||
|
<div className='w-full max-w-3xl mx-auto flex flex-row justify-end gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={approveClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='error'
|
||||||
|
onClick={rejectClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='warning'
|
||||||
|
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
||||||
|
className='px-4 ml-2'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={deleteExpenseClickHandler}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TODO: add and integrate ApprovalSteps component with API */}
|
||||||
|
|
||||||
|
<div className='overflow-x-auto w-full max-w-3xl mx-auto'>
|
||||||
|
<table className='table table-sm table-zebra'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Nomor PO</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{initialValues?.po_number ?? '-'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nomor Referensi</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{initialValues?.reference_number}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Lokasi</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{initialValues?.location.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Kandang</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
{initialValues?.kandangs
|
||||||
|
.map((item) => item.name)
|
||||||
|
.join(', ')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Vendor</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{initialValues?.vendor.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Tanggal Transaksi</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
{formatDate(
|
||||||
|
initialValues?.transaction_date,
|
||||||
|
'DD MMMM YYYY'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Tanggal Realisasi</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
{initialValues?.realization_date
|
||||||
|
? formatDate(
|
||||||
|
initialValues?.realization_date,
|
||||||
|
'DD MMMM YYYY'
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nama Pengaju</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{initialValues?.created_user.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nominal Biaya</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{formatCurrency(initialValues?.nominal ?? 0)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nominal Sudah Bayar</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{formatCurrency(initialValues?.paid ?? 0)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nominal Sisa Bayar</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>{formatCurrency(initialValues?.remaining_cost ?? 0)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Pencairan</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<RealizationStatusBadge
|
||||||
|
approval={initialValues?.approval}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Biaya</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<ExpenseStatusBadge approval={initialValues?.approval} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Dokumen Pengajuan</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
{initialValues?.request_documents.length === 0 && '-'}
|
||||||
|
|
||||||
|
{initialValues?.request_documents &&
|
||||||
|
initialValues?.request_documents.length > 0 && (
|
||||||
|
<ul className='list-disc'>
|
||||||
|
{initialValues?.request_documents.map(
|
||||||
|
(requestDocument, requestDocumentIdx) => (
|
||||||
|
<li key={requestDocumentIdx}>
|
||||||
|
<Link
|
||||||
|
href={requestDocument.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='text-blue-500 underline'
|
||||||
|
>
|
||||||
|
{requestDocument.name}{' '}
|
||||||
|
<Icon
|
||||||
|
icon='cuida:open-in-new-tab-outline'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
className='inline'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<DropFileInput
|
||||||
|
name='request_documents'
|
||||||
|
values={formik.values.request_documents}
|
||||||
|
onChange={requestDocumentsChangeHandler}
|
||||||
|
onDelete={requestDocumentsDeleteHandler}
|
||||||
|
accept={{
|
||||||
|
...ACCEPTED_FILE_TYPE.PDF,
|
||||||
|
...ACCEPTED_FILE_TYPE.IMAGE,
|
||||||
|
}}
|
||||||
|
maxFiles={10}
|
||||||
|
className={{
|
||||||
|
wrapper: 'mt-2',
|
||||||
|
inputWrapper: 'flex items-center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.values.request_documents &&
|
||||||
|
formik.values.request_documents.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={formik.submitForm}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
className='w-fit self-end'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||||
|
<h2 className='font-bold text-xl text-center'>
|
||||||
|
Rincian Pengajuan Biaya Operasional
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
|
{initialValues?.kandang_expenses.map(
|
||||||
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
|
kandangExpense.expenses.forEach(
|
||||||
|
(item) => (expenseGrandTotal += item.total_expense)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={kandangExpenseIdx}
|
||||||
|
className='overflow-x-auto w-full mx-auto'
|
||||||
|
>
|
||||||
|
<table className='table table-sm table-zebra'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
colSpan={5}
|
||||||
|
className='font-bold text-center text-base-content text-lg'
|
||||||
|
>
|
||||||
|
Biaya {kandangExpense.kandang.name}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nonstock</th>
|
||||||
|
<th>Total Kuantitas</th>
|
||||||
|
<th>Total Biaya</th>
|
||||||
|
<th>Catatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{kandangExpense.expenses.map(
|
||||||
|
(expenseItem, expenseIdx) => (
|
||||||
|
<tr key={expenseIdx}>
|
||||||
|
<td>{expenseItem.nonstock.name}</td>
|
||||||
|
<td>{expenseItem.total_quantity}</td>
|
||||||
|
<td>
|
||||||
|
{formatCurrency(expenseItem.total_expense)}
|
||||||
|
</td>
|
||||||
|
<td className='w-xs'>
|
||||||
|
{expenseItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className='border-y'>
|
||||||
|
<th colSpan={2} className='text-right'>
|
||||||
|
Total Biaya Keseluruhan:
|
||||||
|
</th>
|
||||||
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data transfer ke laying ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModalWithNotes
|
||||||
|
ref={approveModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
onClick: confirmationModalApproveClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModalWithNotes
|
||||||
|
ref={rejectModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isRejectLoading,
|
||||||
|
onClick: confirmationModalRejectClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseDetail;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import PillBadge from '@/components/PillBadge';
|
||||||
|
|
||||||
|
import { BaseApproval } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
interface ExpenseStatusBadgeProps {
|
||||||
|
approval?: BaseApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
|
||||||
|
const isLatestApprovalRejected = approval?.action === 'REJECTED';
|
||||||
|
|
||||||
|
const latestApprovalStepNumber = approval?.step_number;
|
||||||
|
|
||||||
|
let expenseStatusPillBadgeColor:
|
||||||
|
| 'yellow'
|
||||||
|
| 'green'
|
||||||
|
| 'gray'
|
||||||
|
| 'red'
|
||||||
|
| 'purple'
|
||||||
|
| 'blue' = 'gray';
|
||||||
|
|
||||||
|
switch (latestApprovalStepNumber) {
|
||||||
|
case 1:
|
||||||
|
expenseStatusPillBadgeColor = 'yellow';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
expenseStatusPillBadgeColor = 'purple';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
expenseStatusPillBadgeColor = 'blue';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
expenseStatusPillBadgeColor = 'red';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
expenseStatusPillBadgeColor = 'green';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLatestApprovalRejected) {
|
||||||
|
expenseStatusPillBadgeColor = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PillBadge
|
||||||
|
content={isLatestApprovalRejected ? 'Ditolak' : approval?.step_name}
|
||||||
|
color={expenseStatusPillBadgeColor}
|
||||||
|
className='text-xs'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseStatusBadge;
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
SortingState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
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 RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||||
|
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
|
||||||
|
import { Expense } from '@/types/api/expense';
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { cn, formatCurrency } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
approveClickHandler,
|
||||||
|
rejectClickHandler,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Expense, unknown>;
|
||||||
|
approveClickHandler: () => void;
|
||||||
|
rejectClickHandler: () => void;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
const showEditButton =
|
||||||
|
props.row.original.approval.action !== 'REJECTED' &&
|
||||||
|
props.row.original.approval.step_number !== 5 &&
|
||||||
|
props.row.original.approval.action !== 'APPROVED';
|
||||||
|
|
||||||
|
const showDeleteButton = showEditButton;
|
||||||
|
|
||||||
|
// TODO: apply RBAC
|
||||||
|
const showApproveButton = showEditButton;
|
||||||
|
const showRejectButton = showEditButton;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<Button
|
||||||
|
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showEditButton && (
|
||||||
|
<Button
|
||||||
|
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
{showApproveButton && (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='success'
|
||||||
|
onClick={approveClickHandler}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRejectButton && (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
onClick={rejectClickHandler}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDeleteButton && (
|
||||||
|
<Button
|
||||||
|
onClick={deleteClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</RowOptionsMenuWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpensesTable = () => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
transactionDate: '',
|
||||||
|
realizationDate: '',
|
||||||
|
locationId: '',
|
||||||
|
vendorId: '',
|
||||||
|
userId: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
transactionDate: 'transaction_date',
|
||||||
|
realizationDate: 'realization_date',
|
||||||
|
locationId: 'location_id',
|
||||||
|
vendorId: 'vendor_id',
|
||||||
|
userId: 'user_id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: expenses,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshExpenses,
|
||||||
|
} = useSWR(
|
||||||
|
`${ExpenseApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
ExpenseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const approveModal = useModal();
|
||||||
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
const [selectedExpense, setSelectedExpense] = useState<Expense | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
|
parseInt(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
const expensesColumns: ColumnDef<Expense>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<CheckboxInput
|
||||||
|
name='allRow'
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
indeterminate={table.getIsSomeRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isCheckboxDisabled =
|
||||||
|
!row.getCanSelect() || row.original.approval.action === 'REJECTED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CheckboxInput
|
||||||
|
name='row'
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={isCheckboxDisabled}
|
||||||
|
indeterminate={row.getIsSomeSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transaction_date',
|
||||||
|
header: 'Tanggal Pengajuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'realization_date',
|
||||||
|
header: 'Tanggal Realisasi',
|
||||||
|
cell: (props) => props.getValue() ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'location',
|
||||||
|
header: 'Lokasi',
|
||||||
|
cell: (props) => props.row.original.location.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.created_user.name ?? '-',
|
||||||
|
header: 'Nama Pengaju',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.vendor.name ?? '-',
|
||||||
|
header: 'Vendor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'nominal',
|
||||||
|
header: 'Nominal',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.nominal
|
||||||
|
? `Rp${formatCurrency(props.row.original.nominal)}`
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'paid',
|
||||||
|
header: 'Sudah Bayar',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.paid
|
||||||
|
? `Rp${formatCurrency(props.row.original.paid)}`
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'remaining_cost',
|
||||||
|
header: 'Sisa Bayar',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.remaining_cost
|
||||||
|
? `Rp${formatCurrency(props.row.original.remaining_cost)}`
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Status Pencairan',
|
||||||
|
cell: (props) => (
|
||||||
|
<RealizationStatusBadge approval={props.row.original.approval} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Status BOP',
|
||||||
|
cell: (props) => (
|
||||||
|
<ExpenseStatusBadge approval={props.row.original.approval} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 - 2;
|
||||||
|
|
||||||
|
const approveClickHandler = () => {
|
||||||
|
setSelectedExpense(props.row.original);
|
||||||
|
|
||||||
|
// Set row selection
|
||||||
|
setRowSelection({
|
||||||
|
[String(props.row.original.id)]: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
approveModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectClickHandler = () => {
|
||||||
|
setSelectedExpense(props.row.original);
|
||||||
|
|
||||||
|
// Set row selection
|
||||||
|
setRowSelection({
|
||||||
|
[String(props.row.original.id)]: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
rejectModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteClickHandler = () => {
|
||||||
|
setSelectedExpense(props.row.original);
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 2 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='dropdown'
|
||||||
|
props={props}
|
||||||
|
approveClickHandler={approveClickHandler}
|
||||||
|
rejectClickHandler={rejectClickHandler}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 2 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='dropdown'
|
||||||
|
props={props}
|
||||||
|
approveClickHandler={approveClickHandler}
|
||||||
|
rejectClickHandler={rejectClickHandler}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
||||||
|
row
|
||||||
|
) => {
|
||||||
|
return row.original.approval.action !== 'REJECTED';
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkApproveClickHandler = () => {
|
||||||
|
approveModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkRejectClickHandler = () => {
|
||||||
|
rejectModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
await ExpenseApi.delete(selectedExpense?.id as number);
|
||||||
|
refreshExpenses();
|
||||||
|
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Berhasil menghapus biaya operasional!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
const bulkApproveResponse = await ExpenseApi.bulkApprove(
|
||||||
|
selectedRowIds,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(bulkApproveResponse)) {
|
||||||
|
refreshExpenses();
|
||||||
|
approveModal.closeModal();
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
|
||||||
|
);
|
||||||
|
|
||||||
|
setRowSelection({});
|
||||||
|
} else {
|
||||||
|
approveModal.closeModal();
|
||||||
|
|
||||||
|
toast.error(
|
||||||
|
`Gagal approve ${selectedRowIds.length} data transfer ke laying!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalRejectClickHandler = async (notes: string) => {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
|
const bulkRejectResponse = await ExpenseApi.bulkReject(
|
||||||
|
selectedRowIds,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(bulkRejectResponse)) {
|
||||||
|
refreshExpenses();
|
||||||
|
rejectModal.closeModal();
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
|
||||||
|
);
|
||||||
|
setRowSelection({});
|
||||||
|
} else {
|
||||||
|
rejectModal.closeModal();
|
||||||
|
|
||||||
|
toast.error(
|
||||||
|
`Gagal reject ${selectedRowIds.length} data transfer ke laying!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
setInputValue: setVendorInputValue,
|
||||||
|
options: vendorOptions,
|
||||||
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const [selectedVendor, setSelectedVendor] = useState<OptionType | null>(null);
|
||||||
|
|
||||||
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedVendor(val as OptionType);
|
||||||
|
updateFilter('vendorId', val ? ((val as OptionType).value as string) : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactionDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
updateFilter('transactionDate', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const realizationDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
updateFilter('realizationDate', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
|
||||||
|
setPageSize(newVal.value as number);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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-between items-end sm:items-center gap-4'>
|
||||||
|
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||||
|
<Button
|
||||||
|
href='/expense/add'
|
||||||
|
variant='outline'
|
||||||
|
color='primary'
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<>
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={bulkApproveClickHandler}
|
||||||
|
disabled={selectedRowIds.length === 0}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='error'
|
||||||
|
onClick={bulkRejectClickHandler}
|
||||||
|
disabled={selectedRowIds.length === 0}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:close'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Biaya Operasional'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||||
|
<DateInput
|
||||||
|
required
|
||||||
|
label='Tanggal Transaksi'
|
||||||
|
name='transaction_date'
|
||||||
|
placeholder='Masukkan tanggal transaksi'
|
||||||
|
value={tableFilterState.transactionDate}
|
||||||
|
onChange={transactionDateChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
required
|
||||||
|
label='Tanggal Realisasi'
|
||||||
|
name='realization_date'
|
||||||
|
placeholder='Masukkan tanggal realisasi'
|
||||||
|
value={tableFilterState.realizationDate}
|
||||||
|
onChange={realizationDateChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
value={selectedLocation}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
options={vendorOptions}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
value={selectedVendor}
|
||||||
|
onChange={vendorChangeHandler}
|
||||||
|
onInputChange={setVendorInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Baris'
|
||||||
|
options={ROWS_OPTIONS}
|
||||||
|
value={{
|
||||||
|
label: String(tableFilterState.pageSize),
|
||||||
|
value: tableFilterState.pageSize,
|
||||||
|
}}
|
||||||
|
onChange={pageSizeChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 max-w-28 justify-self-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<Expense>
|
||||||
|
data={isResponseSuccess(expenses) ? expenses?.data : []}
|
||||||
|
columns={expensesColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20':
|
||||||
|
isResponseSuccess(expenses) && expenses?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data biaya operasional ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModalWithNotes
|
||||||
|
ref={approveModal.ref}
|
||||||
|
type='success'
|
||||||
|
text={`Apakah anda yakin ingin approve data biaya operasional ini (${selectedRowIds.length} data)?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
onClick: confirmationModalApproveClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModalWithNotes
|
||||||
|
ref={rejectModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin reject data biaya operasional ini (${selectedRowIds.length} data)?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isRejectLoading,
|
||||||
|
onClick: confirmationModalRejectClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesTable;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import PillBadge from '@/components/PillBadge';
|
||||||
|
|
||||||
|
import { BaseApproval } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
interface RealizationStatusBadgeProps {
|
||||||
|
approval?: BaseApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => {
|
||||||
|
const isLatestApprovalRejected = approval?.action === 'REJECTED';
|
||||||
|
|
||||||
|
const isExpenseRealized = approval?.step_number && approval.step_number >= 4;
|
||||||
|
|
||||||
|
const realizationStatus = isExpenseRealized
|
||||||
|
? 'Sudah Realisasi'
|
||||||
|
: 'Belum Realisasi';
|
||||||
|
|
||||||
|
let realizationStatusPillBadgeColor:
|
||||||
|
| 'yellow'
|
||||||
|
| 'green'
|
||||||
|
| 'gray'
|
||||||
|
| 'red'
|
||||||
|
| 'purple'
|
||||||
|
| 'blue' = isExpenseRealized ? 'green' : 'yellow';
|
||||||
|
|
||||||
|
if (isLatestApprovalRejected) {
|
||||||
|
realizationStatusPillBadgeColor = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PillBadge
|
||||||
|
content={isLatestApprovalRejected ? 'Ditolak' : realizationStatus}
|
||||||
|
color={realizationStatusPillBadgeColor}
|
||||||
|
className='text-xs'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RealizationStatusBadge;
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
|
import { cn, convertRowSelectionArrToObj } from '@/lib/helper';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
interface ExpenseKandangsTableProps {
|
||||||
|
locationId?: number;
|
||||||
|
type: 'add' | 'edit' | 'detail';
|
||||||
|
selectedKandangs: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
onChange: (kandangs: { id: number; name: string }[]) => void;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseKandangsTable = ({
|
||||||
|
type,
|
||||||
|
locationId,
|
||||||
|
selectedKandangs,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: ExpenseKandangsTableProps) => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
picSort: '',
|
||||||
|
locationId,
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
picSort: 'sort_pic',
|
||||||
|
locationId: 'location_id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: kandangs, isLoading } = useSWR(
|
||||||
|
locationId ? `${KandangApi.basePath}${getTableFilterQueryString()}` : null,
|
||||||
|
KandangApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(
|
||||||
|
isResponseSuccess(kandangs) ? kandangs.data.length > 0 : false
|
||||||
|
);
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
|
||||||
|
convertRowSelectionArrToObj(selectedKandangs.map((item) => item.id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const kandangsColumns: ColumnDef<Kandang>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<CheckboxInput
|
||||||
|
name='allRow'
|
||||||
|
checked={table.getIsAllPageRowsSelected()}
|
||||||
|
indeterminate={table.getIsSomePageRowsSelected()}
|
||||||
|
onChange={table.getToggleAllPageRowsSelectedHandler()}
|
||||||
|
disabled={type === 'detail'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<CheckboxInput
|
||||||
|
name='row'
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={!row.getCanSelect() || type === 'detail'}
|
||||||
|
indeterminate={row.getIsSomeSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Nama',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'pic',
|
||||||
|
header: 'PIC',
|
||||||
|
cell: (props) => props.row.original.pic.name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateSortingFilter = useCallback(
|
||||||
|
(
|
||||||
|
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>,
|
||||||
|
sortFilter: ColumnSort | undefined
|
||||||
|
) => {
|
||||||
|
if (!sortFilter) {
|
||||||
|
updateFilter(sortName, '');
|
||||||
|
} else {
|
||||||
|
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (locationId) updateFilter('locationId', locationId);
|
||||||
|
}, [locationId, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(isResponseSuccess(kandangs) ? kandangs.data.length > 0 : false);
|
||||||
|
}, [kandangs, isResponseSuccess]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(rowSelection).length !== 0 && isResponseSuccess(kandangs)) {
|
||||||
|
const formattedSelectedKandangs = Object.keys(rowSelection).map(
|
||||||
|
(item) => {
|
||||||
|
const selectedKandang = kandangs.data.find(
|
||||||
|
(kandang) => kandang.id === parseInt(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: parseInt(item),
|
||||||
|
name: selectedKandang?.name ?? 'Kandang tidak ditemukan!',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onChange(formattedSelectedKandangs);
|
||||||
|
} else {
|
||||||
|
onChange([]);
|
||||||
|
}
|
||||||
|
}, [rowSelection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedKandangs.length === 0 &&
|
||||||
|
Object.keys(rowSelection).length !== 0
|
||||||
|
) {
|
||||||
|
setRowSelection({});
|
||||||
|
}
|
||||||
|
}, [selectedKandangs]);
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic');
|
||||||
|
|
||||||
|
updateSortingFilter('nameSort', nameSortFilter);
|
||||||
|
updateSortingFilter('picSort', picSortFilter);
|
||||||
|
}, [sorting, updateSortingFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: className?.wrapper,
|
||||||
|
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'>Pilih Kandang</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!'
|
||||||
|
>
|
||||||
|
<Table<Kandang>
|
||||||
|
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
|
||||||
|
columns={kandangsColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20':
|
||||||
|
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 first:flex first:flex-row first:justify-start',
|
||||||
|
paginationClassName: cn({
|
||||||
|
hidden:
|
||||||
|
isResponseSuccess(kandangs) &&
|
||||||
|
kandangs?.meta?.total_pages === 1,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseKandangsTable;
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import { Expense } from '@/types/api/expense';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
|
type ExpenseFormSchemaType = {
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
transaction_date?: string;
|
||||||
|
kandangs?: { id: number; name: string }[];
|
||||||
|
vendor?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
existing_documents?: { name: string; url: string }[];
|
||||||
|
request_documents?: File[];
|
||||||
|
kandangExpenses: {
|
||||||
|
kandangId: number;
|
||||||
|
expenses: {
|
||||||
|
nonstock?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
totalQuantity?: number;
|
||||||
|
totalExpense?: number;
|
||||||
|
notes?: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||||
|
kandangs: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
id: Yup.number().required('Kandang wajib dipilih!'),
|
||||||
|
name: Yup.string().required('Kandang wajib dipilih!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Kandang wajib dipilih!')
|
||||||
|
.required('Kandang wajib dipilih!'),
|
||||||
|
|
||||||
|
vendor: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).required('Vendor wajib diisi!'),
|
||||||
|
|
||||||
|
existing_documents: Yup.array().of(
|
||||||
|
Yup.object({
|
||||||
|
name: Yup.string().required(),
|
||||||
|
url: Yup.string().required(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
request_documents: Yup.array().of(Yup.mixed<File>().required()).optional(),
|
||||||
|
|
||||||
|
kandangExpenses: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
kandangId: Yup.number().min(1, 'Wajib memilih kandang!').required(),
|
||||||
|
expenses: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
nonstock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).required('Nonstock wajib diisi!'),
|
||||||
|
totalQuantity: Yup.number().required(
|
||||||
|
'Total kuantitas wajib diisi!'
|
||||||
|
),
|
||||||
|
totalExpense: Yup.number().required('Total biaya wajib diisi!'),
|
||||||
|
notes: Yup.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Kandang harus memiliki setidaknya 1 biaya!')
|
||||||
|
.required('Biaya kandang wajib diisi!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Biaya kandang wajib diisi!')
|
||||||
|
.required('Biaya kandang wajib diisi!'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateExpenseRequestFormSchema = ExpenseRequestFormSchema;
|
||||||
|
|
||||||
|
export const UploadRequestDocumentsFormSchema = Yup.object({
|
||||||
|
request_documents: Yup.array().of(Yup.mixed<File>().required()).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExpenseRequestFormValues = Yup.InferType<
|
||||||
|
typeof ExpenseRequestFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UploadRequestDocumentsFormValues = Yup.InferType<
|
||||||
|
typeof UploadRequestDocumentsFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const getExpenseFormInitialValues = (
|
||||||
|
initialValues?: Expense
|
||||||
|
): ExpenseRequestFormValues => {
|
||||||
|
return {
|
||||||
|
location: initialValues?.location
|
||||||
|
? {
|
||||||
|
value: initialValues.location.id,
|
||||||
|
label: initialValues.location.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
transaction_date: initialValues?.transaction_date
|
||||||
|
? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
|
||||||
|
: undefined,
|
||||||
|
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||||
|
id: kandang.id,
|
||||||
|
name: kandang.name,
|
||||||
|
})),
|
||||||
|
vendor: initialValues?.vendor
|
||||||
|
? {
|
||||||
|
value: initialValues.vendor.id,
|
||||||
|
label: initialValues.vendor.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
existing_documents: initialValues?.request_documents,
|
||||||
|
request_documents: [],
|
||||||
|
kandangExpenses: initialValues?.kandang_expenses
|
||||||
|
? initialValues.kandang_expenses.map((kandangExpense) => ({
|
||||||
|
kandangId: kandangExpense.kandang.id,
|
||||||
|
expenses: kandangExpense.expenses.map((expenseItem) => ({
|
||||||
|
nonstock: {
|
||||||
|
value: expenseItem.nonstock.id,
|
||||||
|
label: expenseItem.nonstock.name,
|
||||||
|
},
|
||||||
|
totalQuantity: expenseItem.total_quantity,
|
||||||
|
totalExpense: expenseItem.total_expense,
|
||||||
|
notes: expenseItem.notes,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
|
||||||
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
|
import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExpenseRequestFormSchema,
|
||||||
|
ExpenseRequestFormValues,
|
||||||
|
getExpenseFormInitialValues,
|
||||||
|
UpdateExpenseRequestFormSchema,
|
||||||
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
import {
|
||||||
|
Expense,
|
||||||
|
CreateExpensePayload,
|
||||||
|
UpdateExpensePayload,
|
||||||
|
} from '@/types/api/expense';
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { cn, sleep } from '@/lib/helper';
|
||||||
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
|
interface ExpenseFormProps {
|
||||||
|
type?: 'add' | 'edit' | 'detail';
|
||||||
|
initialValues?: Expense;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: integrate this with real API
|
||||||
|
const ExpenseRequestForm = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: ExpenseFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Modal hooks
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const approveModal = useModal();
|
||||||
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
||||||
|
|
||||||
|
const createExpenseHandler = useCallback(
|
||||||
|
async (payload: CreateExpensePayload) => {
|
||||||
|
const createExpenseRes = await ExpenseApi.create(
|
||||||
|
ExpenseApi.convertPayloadToFormData(payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(createExpenseRes)) {
|
||||||
|
setExpenseFormErrorMessage(createExpenseRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(createExpenseRes?.message as string);
|
||||||
|
router.push('/expense');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateExpenseHandler = useCallback(
|
||||||
|
async (expenseId: number, payload: UpdateExpensePayload) => {
|
||||||
|
const updateExpenseRes = await ExpenseApi.update(
|
||||||
|
expenseId,
|
||||||
|
ExpenseApi.convertPayloadToFormData(payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateExpenseRes?.status === 'error') {
|
||||||
|
setExpenseFormErrorMessage(updateExpenseRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(updateExpenseRes?.message as string);
|
||||||
|
router.refresh();
|
||||||
|
router.push('/expense');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<ExpenseRequestFormValues>({
|
||||||
|
initialValues: getExpenseFormInitialValues(initialValues),
|
||||||
|
validationSchema:
|
||||||
|
type === 'edit'
|
||||||
|
? UpdateExpenseRequestFormSchema
|
||||||
|
: ExpenseRequestFormSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
setExpenseFormErrorMessage('');
|
||||||
|
|
||||||
|
const expensePayload: CreateExpensePayload = {
|
||||||
|
locationId: values.location?.value as number,
|
||||||
|
kandangIds: values.kandangs
|
||||||
|
? values.kandangs.map((item) => item.id)
|
||||||
|
: [],
|
||||||
|
transaction_date: values.transaction_date as string,
|
||||||
|
vendorId: values.vendor?.value as number,
|
||||||
|
request_documents: values.request_documents as File[],
|
||||||
|
kandang_expenses: values.kandangExpenses.map((kandangExpense) => ({
|
||||||
|
kandangId: kandangExpense.kandangId,
|
||||||
|
expenses: kandangExpense.expenses.map((expenseItem) => ({
|
||||||
|
nonstockId: expenseItem.nonstock?.value as number,
|
||||||
|
total_quantity: expenseItem.totalQuantity as number,
|
||||||
|
total_expense: expenseItem.totalExpense as number,
|
||||||
|
notes: expenseItem.notes,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createExpenseHandler(expensePayload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
await updateExpenseHandler(
|
||||||
|
initialValues?.id as number,
|
||||||
|
expensePayload
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setVendorInputValue,
|
||||||
|
options: vendorOptions,
|
||||||
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('location', true);
|
||||||
|
formik.setFieldValue('location', val);
|
||||||
|
|
||||||
|
formik.setFieldValue('kandangs', []);
|
||||||
|
formik.setFieldValue('kandangExpenses', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
|
||||||
|
formik.setFieldTouched('kandangs', true);
|
||||||
|
formik.setFieldValue('kandangs', kandangs);
|
||||||
|
|
||||||
|
const newKandangExpenses = [...(formik.values.kandangExpenses ?? [])];
|
||||||
|
|
||||||
|
// add new kandangExpenses
|
||||||
|
kandangs.forEach((kandangItem) => {
|
||||||
|
const isKandangExistInKandangExpense = newKandangExpenses.find(
|
||||||
|
(kandangExpenseItem) => kandangExpenseItem.kandangId === kandangItem.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isKandangExistInKandangExpense) return;
|
||||||
|
|
||||||
|
newKandangExpenses.push({
|
||||||
|
kandangId: kandangItem.id,
|
||||||
|
expenses: [
|
||||||
|
{
|
||||||
|
nonstock: undefined,
|
||||||
|
totalExpense: undefined,
|
||||||
|
totalQuantity: undefined,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// prune kandangExpenses
|
||||||
|
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
|
||||||
|
const deletedKandangExpensesIdx: number[] = [];
|
||||||
|
|
||||||
|
newKandangExpenses.forEach((kandangExpense, idx) => {
|
||||||
|
const isKandangExpenseValid = kandangIds.has(kandangExpense.kandangId);
|
||||||
|
|
||||||
|
if (!isKandangExpenseValid) {
|
||||||
|
deletedKandangExpensesIdx.push(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deletedKandangExpensesIdx.forEach((deletedKandangExpenseIdx) => {
|
||||||
|
newKandangExpenses.splice(deletedKandangExpenseIdx, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
formik.setFieldValue('kandangExpenses', newKandangExpenses);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('vendor', true);
|
||||||
|
formik.setFieldValue('vendor', val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDocumentsChangeHandler = (val: File[]) => {
|
||||||
|
formik.setFieldTouched('request_documents', true);
|
||||||
|
formik.setFieldValue('request_documents', val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteExpenseClickHandler = () => {
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalRejectClickHandler = async () => {
|
||||||
|
await sleep(750);
|
||||||
|
|
||||||
|
rejectModal.closeModal();
|
||||||
|
toast.success('Berhasil melakukan reject biaya operasional!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalApproveClickHandler = async () => {
|
||||||
|
await sleep(750);
|
||||||
|
|
||||||
|
approveModal.closeModal();
|
||||||
|
toast.success('Berhasil melakukan approve biaya operasional!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
await ExpenseApi.delete(initialValues?.id as number);
|
||||||
|
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Expense!');
|
||||||
|
router.push('/expense');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formikSetValues(getExpenseFormInitialValues(initialValues));
|
||||||
|
}, [formikSetValues, getExpenseFormInitialValues, initialValues]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-5xl'>
|
||||||
|
<header className='flex flex-col gap-4'>
|
||||||
|
<Button
|
||||||
|
href='/expense'
|
||||||
|
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'>
|
||||||
|
{type === 'add' && 'Tambah Biaya Operasional'}
|
||||||
|
{type === 'edit' && 'Edit Biaya Operasional'}
|
||||||
|
{type === 'detail' && 'Detail Biaya Operasional'}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formik.handleReset}
|
||||||
|
className='w-full mt-8 flex flex-col gap-6'
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-12 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
required
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
className={{ wrapper: 'col-span-12 sm:col-span-6' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='transaction_date'
|
||||||
|
label='Tanggal Transaksi'
|
||||||
|
required
|
||||||
|
value={formik.values.transaction_date}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExpenseKandangsTable
|
||||||
|
type={type}
|
||||||
|
locationId={formik.values.location?.value}
|
||||||
|
selectedKandangs={formik.values.kandangs ?? []}
|
||||||
|
onChange={kandangsChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full col-span-12',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
required
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
value={formik.values.vendor}
|
||||||
|
onChange={vendorChangeHandler}
|
||||||
|
options={vendorOptions}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
onInputChange={setVendorInputValue}
|
||||||
|
className={{ wrapper: 'col-span-12' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DropFileInput
|
||||||
|
label='Dokumen Pengajuan'
|
||||||
|
name='request_documents'
|
||||||
|
values={formik.values.request_documents}
|
||||||
|
onChange={requestDocumentsChangeHandler}
|
||||||
|
accept={{
|
||||||
|
...ACCEPTED_FILE_TYPE.PDF,
|
||||||
|
...ACCEPTED_FILE_TYPE.IMAGE,
|
||||||
|
}}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12',
|
||||||
|
inputWrapper: 'h-12 flex items-center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.values.existing_documents &&
|
||||||
|
formik.values.existing_documents.length > 0 && (
|
||||||
|
<div className='w-full col-span-12'>
|
||||||
|
<ul className='pl-4 list-disc'>
|
||||||
|
{formik.values.existing_documents.map(
|
||||||
|
(existingDocument, existingDocumentIdx) => (
|
||||||
|
<li key={existingDocumentIdx}>
|
||||||
|
<Link
|
||||||
|
href={existingDocument.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='text-blue-500 underline'
|
||||||
|
>
|
||||||
|
{existingDocument.name}{' '}
|
||||||
|
<Icon
|
||||||
|
icon='cuida:open-in-new-tab-outline'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
className='inline'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ExpenseRequestKandangDetailExpense
|
||||||
|
formik={formik}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
{type !== 'add' && (
|
||||||
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={deleteExpenseClickHandler}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{type !== 'edit' && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='warning'
|
||||||
|
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-row justify-end gap-2', {
|
||||||
|
'w-full': type === 'add',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button type='reset' color='warning' className='px-4'>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
color='primary'
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expenseFormErrorMessage && (
|
||||||
|
<div role='alert' className='alert alert-error'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{expenseFormErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{type !== 'add' && (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data Expense ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'detail' && (
|
||||||
|
<>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={approveModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
onClick: confirmationModalApproveClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={rejectModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
onClick: confirmationModalRejectClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseRequestForm;
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FormikContextType } from 'formik';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
|
import { ExpenseRequestFormValues } from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
|
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
||||||
|
|
||||||
|
interface ExpenseRequestKandangDetailExpenseProps {
|
||||||
|
type?: 'add' | 'edit' | 'detail';
|
||||||
|
formik: FormikContextType<ExpenseRequestFormValues>;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseRequestKandangDetailExpense: React.FC<
|
||||||
|
ExpenseRequestKandangDetailExpenseProps
|
||||||
|
> = ({ type, formik, className }) => {
|
||||||
|
const {
|
||||||
|
setInputValue: setNonstockInputValue,
|
||||||
|
options: nonstockOptions,
|
||||||
|
isLoadingOptions: isLoadingNonstockOptions,
|
||||||
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const nonstockChangeHandler = (
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number,
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`kandangExpenses[${kandangExpenseIdx}].expenses[${expenseIdx}].nonstock`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`kandangExpenses[${kandangExpenseIdx}].expenses[${expenseIdx}].nonstock`,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addExpenseItemHandler = (kandangExpenseIdx: number) => {
|
||||||
|
const newExpensesValue = [
|
||||||
|
...formik.values.kandangExpenses[kandangExpenseIdx].expenses,
|
||||||
|
{
|
||||||
|
nonstock: undefined,
|
||||||
|
totalExpense: undefined,
|
||||||
|
totalQuantity: undefined,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
formik.setFieldValue(
|
||||||
|
`kandangExpenses[${kandangExpenseIdx}].expenses`,
|
||||||
|
newExpensesValue
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteExpenseItemHandler = (
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number
|
||||||
|
) => {
|
||||||
|
const path = `kandangExpenses[${kandangExpenseIdx}].expenses`;
|
||||||
|
|
||||||
|
// trims values, errors, and touched at expenseIdx
|
||||||
|
removeArrayItemAndSync(formik, path, expenseIdx);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpenseRepeaterInputError = (
|
||||||
|
column: 'nonstock' | 'totalQuantity' | 'totalExpense' | 'notes',
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
formik.touched.kandangExpenses?.[kandangExpenseIdx]?.expenses?.[
|
||||||
|
expenseIdx
|
||||||
|
]?.[column] &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors.kandangExpenses?.[kandangExpenseIdx] instanceof Object &&
|
||||||
|
formik.errors.kandangExpenses?.[kandangExpenseIdx].expenses?.[
|
||||||
|
expenseIdx
|
||||||
|
] instanceof Object &&
|
||||||
|
formik.errors.kandangExpenses?.[kandangExpenseIdx].expenses?.[
|
||||||
|
expenseIdx
|
||||||
|
]?.[column]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: cn('w-full', className?.wrapper),
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mb-4 text-center'>
|
||||||
|
<h4 className='font-bold text-xl'>
|
||||||
|
Rincian Pengajuan Biaya Operasional
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-full flex flex-col gap-6'>
|
||||||
|
{formik.values.kandangExpenses.length === 0 && (
|
||||||
|
<div>
|
||||||
|
<p className='text-sm text-gray-400 text-center'>
|
||||||
|
Pilih kandang terlebih dahulu!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.kandangExpenses.map(
|
||||||
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
const kandangName = formik.values.kandangs?.find(
|
||||||
|
(kandang) => kandang.id === kandangExpense.kandangId
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
kandangName?.name && (
|
||||||
|
<div
|
||||||
|
key={`kandangExpense-${kandangExpenseIdx}`}
|
||||||
|
className='w-full flex flex-col gap-4'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h5 className='mb-2 text-lg font-bold text-center'>
|
||||||
|
Biaya {kandangName?.name}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nonstock</th>
|
||||||
|
<th>Total Kuantitas</th>
|
||||||
|
<th>Total Biaya</th>
|
||||||
|
<th>Catatan</th>
|
||||||
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{kandangExpense.expenses.map(
|
||||||
|
(expenseItem, expenseIdx) => (
|
||||||
|
<tr key={`expense-${expenseIdx}`}>
|
||||||
|
<td className='p-2'>
|
||||||
|
<SelectInput
|
||||||
|
placeholder='Pilih Nonstock'
|
||||||
|
value={expenseItem.nonstock}
|
||||||
|
onChange={(val) => {
|
||||||
|
nonstockChangeHandler(
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={nonstockOptions}
|
||||||
|
isLoading={isLoadingNonstockOptions}
|
||||||
|
onInputChange={setNonstockInputValue}
|
||||||
|
className={{ wrapper: 'min-w-48' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='p-2'>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
name={`kandangExpenses[${kandangExpenseIdx}].expenses[${expenseIdx}].totalQuantity`}
|
||||||
|
placeholder='Masukkan Total Kuantitas'
|
||||||
|
value={
|
||||||
|
formik.values.kandangExpenses[
|
||||||
|
kandangExpenseIdx
|
||||||
|
].expenses[expenseIdx].totalQuantity ?? ''
|
||||||
|
}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'totalQuantity',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
className={{ wrapper: 'min-w-24' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='p-2'>
|
||||||
|
<NumberInput
|
||||||
|
name={`kandangExpenses[${kandangExpenseIdx}].expenses[${expenseIdx}].totalExpense`}
|
||||||
|
placeholder='Masukkan Total Biaya'
|
||||||
|
value={
|
||||||
|
formik.values.kandangExpenses[
|
||||||
|
kandangExpenseIdx
|
||||||
|
].expenses[expenseIdx].totalExpense ?? ''
|
||||||
|
}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'totalExpense',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
inputPrefix={
|
||||||
|
<span className='text-gray-600 font-medium'>
|
||||||
|
Rp
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
className={{ wrapper: 'min-w-24' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='p-2'>
|
||||||
|
<TextInput
|
||||||
|
name={`kandangExpenses[${kandangExpenseIdx}].expenses[${expenseIdx}].notes`}
|
||||||
|
placeholder='Tuliskan catatan'
|
||||||
|
value={
|
||||||
|
formik.values.kandangExpenses[
|
||||||
|
kandangExpenseIdx
|
||||||
|
].expenses[expenseIdx].notes ?? ''
|
||||||
|
}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'notes',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
className={{ wrapper: 'min-w-24' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() =>
|
||||||
|
deleteExpenseItemHandler(
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={() => addExpenseItemHandler(kandangExpenseIdx)}
|
||||||
|
className='w-fit mx-auto'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseRequestKandangDetailExpense;
|
||||||
@@ -10,11 +10,7 @@ import { inventoryAdjustmentApi } from '@/services/api/inventory';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import {
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
ColumnDef,
|
|
||||||
ColumnSort,
|
|
||||||
SortingState,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -44,10 +40,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const {
|
const { data: inventoryAdjustments, isLoading } = useSWR(
|
||||||
data: inventoryAdjustments,
|
|
||||||
isLoading,
|
|
||||||
} = useSWR(
|
|
||||||
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||||
inventoryAdjustmentApi.getAllFetcher
|
inventoryAdjustmentApi.getAllFetcher
|
||||||
);
|
);
|
||||||
@@ -187,8 +180,13 @@ const InventoryAdjustmentTable = () => {
|
|||||||
<div className='w-full p-0 sm:p-4'>
|
<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-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
<Button href='/inventory/adjustment/add' color='primary'>
|
<Button
|
||||||
|
href='/inventory/adjustment/add'
|
||||||
|
variant='outline'
|
||||||
|
color='primary'
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
@@ -211,7 +209,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
value: tableFilterState.pageSize,
|
value: tableFilterState.pageSize,
|
||||||
}}
|
}}
|
||||||
onChange={pageSizeChangeHandler}
|
onChange={pageSizeChangeHandler}
|
||||||
className={{ wrapper: 'max-w-28' }}
|
className={{ wrapper: 'min-w-28' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,24 +4,21 @@ export const InventoryAdjustmentFormSchema = Yup.object({
|
|||||||
product_category: Yup.object({
|
product_category: Yup.object({
|
||||||
value: Yup.number().required('ID Kategori Produk wajib diisi!'),
|
value: Yup.number().required('ID Kategori Produk wajib diisi!'),
|
||||||
label: Yup.string().required('Nama Kategori Produk wajib diisi!'),
|
label: Yup.string().required('Nama Kategori Produk wajib diisi!'),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable(),
|
|
||||||
|
|
||||||
product_category_id: Yup.number().nullable(),
|
product_category_id: Yup.number().nullable(),
|
||||||
|
|
||||||
product: Yup.object({
|
product: Yup.object({
|
||||||
value: Yup.number().required('ID Produk wajib diisi!'),
|
value: Yup.number().required('ID Produk wajib diisi!'),
|
||||||
label: Yup.string().required('Nama Produk wajib diisi!'),
|
label: Yup.string().required('Nama Produk wajib diisi!'),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable(),
|
|
||||||
|
|
||||||
product_id: Yup.number().nullable(),
|
product_id: Yup.number().nullable(),
|
||||||
|
|
||||||
warehouse: Yup.object({
|
warehouse: Yup.object({
|
||||||
value: Yup.number().required('ID Gudang wajib diisi!'),
|
value: Yup.number().required('ID Gudang wajib diisi!'),
|
||||||
label: Yup.string().required('Nama Gudang wajib diisi!'),
|
label: Yup.string().required('Nama Gudang wajib diisi!'),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable(),
|
|
||||||
|
|
||||||
warehouse_id: Yup.number().nullable(),
|
warehouse_id: Yup.number().nullable(),
|
||||||
|
|
||||||
|
|||||||
@@ -51,9 +51,8 @@ const InventoryAdjustmentForm = ({
|
|||||||
// Submit Handler
|
// Submit Handler
|
||||||
const createInventoryAdjustmentHandler = useCallback(
|
const createInventoryAdjustmentHandler = useCallback(
|
||||||
async (payload: CreateInventoryAdjustmentPayload) => {
|
async (payload: CreateInventoryAdjustmentPayload) => {
|
||||||
const createInventoryAdjustmentRes = await inventoryAdjustmentApi.create(
|
const createInventoryAdjustmentRes =
|
||||||
payload
|
await inventoryAdjustmentApi.create(payload);
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseError(createInventoryAdjustmentRes)) {
|
if (isResponseError(createInventoryAdjustmentRes)) {
|
||||||
setInventoryAdjustmentFormErrorMessage(
|
setInventoryAdjustmentFormErrorMessage(
|
||||||
@@ -68,7 +67,9 @@ const InventoryAdjustmentForm = ({
|
|||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(() => {
|
const formikInitialValues = useMemo<
|
||||||
|
Partial<InventoryAdjustmentFormValues>
|
||||||
|
>(() => {
|
||||||
return {
|
return {
|
||||||
product_category_id: initialValues?.product_category?.id ?? 0,
|
product_category_id: initialValues?.product_category?.id ?? 0,
|
||||||
product_id: initialValues?.product?.id ?? 0,
|
product_id: initialValues?.product?.id ?? 0,
|
||||||
@@ -185,7 +186,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
warehouseChangeHandler(null);
|
warehouseChangeHandler(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const { setValues: formikSetValues } = formik;
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
// Effect
|
// Effect
|
||||||
@@ -225,7 +225,13 @@ const InventoryAdjustmentForm = ({
|
|||||||
const type = initialValues.transaction_type.toLowerCase();
|
const type = initialValues.transaction_type.toLowerCase();
|
||||||
setQuantityLabel(type === 'increase' ? 'Tambah Stok' : 'Kurangi Stok');
|
setQuantityLabel(type === 'increase' ? 'Tambah Stok' : 'Kurangi Stok');
|
||||||
}
|
}
|
||||||
}, [formik, initialValues, setQuantityLabel, setDisabledProduct, setSelectedProductCategories]);
|
}, [
|
||||||
|
formik,
|
||||||
|
initialValues,
|
||||||
|
setQuantityLabel,
|
||||||
|
setDisabledProduct,
|
||||||
|
setSelectedProductCategories,
|
||||||
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
@@ -364,7 +370,11 @@ const InventoryAdjustmentForm = ({
|
|||||||
errorMessage={formik.errors.transaction_type as string}
|
errorMessage={formik.errors.transaction_type as string}
|
||||||
variant='radio-primary'
|
variant='radio-primary'
|
||||||
required
|
required
|
||||||
bottomLabel={formik.values.transaction_type == undefined ? 'Pilih salah satu tipe transaksi' : undefined}
|
bottomLabel={
|
||||||
|
formik.values.transaction_type == undefined
|
||||||
|
? 'Pilih salah satu tipe transaksi'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
disabled={type === 'detail'}
|
disabled={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -395,8 +405,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Text Area Input Reason */}
|
{/* Text Area Input Reason */}
|
||||||
<TextArea
|
<TextArea
|
||||||
required
|
required
|
||||||
@@ -413,14 +421,23 @@ const InventoryAdjustmentForm = ({
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<div className='flex flex-row justify-end gap-2'>
|
<div className='flex flex-row justify-end gap-2'>
|
||||||
<Button type='button' color='warning' className='px-4' onClick={resetHandler}>
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='warning'
|
||||||
|
className='px-4'
|
||||||
|
onClick={resetHandler}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color='primary'
|
color='primary'
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
disabled={!formik.isValid || formik.isSubmitting || formik.values.product == undefined}
|
disabled={
|
||||||
|
!formik.isValid ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
formik.values.product == undefined
|
||||||
|
}
|
||||||
className='px-4'
|
className='px-4'
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
|
|||||||
@@ -1,24 +1,46 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { SortingState } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { useModal } from '@/components/Modal';
|
import { Icon } from '@iconify/react';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import { Movement } from '@/types/api/inventory/movement';
|
import { Movement } from '@/types/api/inventory/movement';
|
||||||
import { MovementApi } from '@/services/api/inventory';
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
import Button from '@/components/Button';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import { TableRowOptions } from '@/components/table/TableRowOptions';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Movement, unknown>;
|
||||||
|
}) => (
|
||||||
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<Button
|
||||||
|
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RowOptionsMenuWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
const {
|
const {
|
||||||
@@ -28,30 +50,47 @@ const MovementTable = () => {
|
|||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: { search: '' },
|
initial: {
|
||||||
paramMap: { page: 'page', pageSize: 'limit' },
|
search: '',
|
||||||
|
product: '',
|
||||||
|
warehouse: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
product: 'product_id',
|
||||||
|
warehouse: 'warehouse_id',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [selectedMovement, setSelectedMovement] = useState<
|
|
||||||
Movement | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const deleteModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: movements,
|
setInputValue: setProductInputValue,
|
||||||
isLoading,
|
options: productOptions,
|
||||||
mutate: refreshMovements,
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
} = useSWR(
|
} = useSelect<Product>('/products', 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setWarehouseInputValue,
|
||||||
|
options: warehouseOptions,
|
||||||
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
|
} = useSelect<Warehouse>('/warehouses', 'id', 'name');
|
||||||
|
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: movements, isLoading } = useSWR(
|
||||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||||
MovementApi.getAllFetcher
|
MovementApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
setPage(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -60,41 +99,17 @@ const MovementTable = () => {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
setIsDeleteLoading(true);
|
setSelectedProduct(val as OptionType);
|
||||||
try {
|
updateFilter('product', val ? ((val as OptionType).value as string) : '');
|
||||||
await MovementApi.delete(selectedMovement?.id as number);
|
|
||||||
refreshMovements();
|
|
||||||
deleteModal.closeModal();
|
|
||||||
} finally {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
<div className='flex flex-col gap-4'>
|
setSelectedWarehouse(val as OptionType);
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
|
||||||
<TableToolbar
|
};
|
||||||
addButton={{
|
|
||||||
href: '/inventory/movement/add',
|
|
||||||
label: 'Tambah Movement',
|
|
||||||
}}
|
|
||||||
search={{
|
|
||||||
value: tableFilterState.search,
|
|
||||||
onChange: searchChangeHandler,
|
|
||||||
placeholder: 'Cari Movement',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableRowSizeSelector
|
|
||||||
value={tableFilterState.pageSize}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table<Movement>
|
const movementColumns: ColumnDef<Movement>[] = [
|
||||||
data={isResponseSuccess(movements) ? movements?.data : []}
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
header: '#',
|
header: '#',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
@@ -118,9 +133,7 @@ const MovementTable = () => {
|
|||||||
accessorKey: 'transfer_date',
|
accessorKey: 'transfer_date',
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
new Date(props.row.original.transfer_date).toLocaleDateString(
|
new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
|
||||||
'id-ID'
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row) => {
|
accessorFn: (row) => {
|
||||||
@@ -135,52 +148,104 @@ const MovementTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const currentPageSize =
|
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||||
props.table.getPaginationRowModel().rows.length;
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
const currentPageRows =
|
|
||||||
props.table.getPaginationRowModel().flatRows;
|
|
||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
const deleteClickHandler = () => {
|
|
||||||
setSelectedMovement(props.row.original);
|
|
||||||
deleteModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentPageSize > 2 && (
|
{currentPageSize > 2 && (
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
<TableRowOptions
|
<RowOptionsMenu type='dropdown' props={props} />
|
||||||
type='dropdown'
|
|
||||||
recordId={props.row.original.id}
|
|
||||||
basePath='/inventory/movement'
|
|
||||||
queryParam='movementId'
|
|
||||||
showEdit={false}
|
|
||||||
showDelete={false}
|
|
||||||
/>
|
|
||||||
</RowDropdownOptions>
|
</RowDropdownOptions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
{currentPageSize <= 2 && (
|
||||||
<RowCollapseOptions>
|
<RowCollapseOptions>
|
||||||
<TableRowOptions
|
<RowOptionsMenu type='collapse' props={props} />
|
||||||
type='collapse'
|
|
||||||
recordId={props.row.original.id}
|
|
||||||
basePath='/inventory/movement'
|
|
||||||
queryParam='movementId'
|
|
||||||
showEdit={false}
|
|
||||||
showDelete={false}
|
|
||||||
/>
|
|
||||||
</RowCollapseOptions>
|
</RowCollapseOptions>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
|
<div className='w-full flex flex-row gap-2'>
|
||||||
|
<Button
|
||||||
|
href='/inventory/movement/add'
|
||||||
|
variant='outline'
|
||||||
|
color='primary'
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Movement'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 justify-end gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Produk'
|
||||||
|
options={productOptions}
|
||||||
|
isLoading={isLoadingProductOptions}
|
||||||
|
value={selectedProduct}
|
||||||
|
onChange={productChangeHandler}
|
||||||
|
onInputChange={setProductInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
isLoading={isLoadingWarehouseOptions}
|
||||||
|
value={selectedWarehouse}
|
||||||
|
onChange={warehouseChangeHandler}
|
||||||
|
onInputChange={setWarehouseInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Baris'
|
||||||
|
options={ROWS_OPTIONS}
|
||||||
|
value={{
|
||||||
|
label: String(tableFilterState.pageSize),
|
||||||
|
value: tableFilterState.pageSize,
|
||||||
|
}}
|
||||||
|
onChange={pageSizeChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper:
|
||||||
|
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<Movement>
|
||||||
|
data={isResponseSuccess(movements) ? movements?.data : []}
|
||||||
|
columns={movementColumns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
|
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
|
||||||
totalItems={
|
totalItems={
|
||||||
@@ -205,22 +270,8 @@ const MovementTable = () => {
|
|||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={deleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
onClick: confirmationModalDeleteClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,82 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { Movement } from '@/types/api/inventory/movement';
|
import { Movement } from '@/types/api/inventory/movement';
|
||||||
|
|
||||||
export type ProductSchema = {
|
type MovementFormSchemaType = {
|
||||||
product: {
|
transfer_reason: string;
|
||||||
|
transfer_date: string;
|
||||||
|
source_warehouse?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
} | null;
|
||||||
|
source_warehouse_id: number;
|
||||||
|
destination_warehouse?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
} | null;
|
||||||
|
destination_warehouse_id: number;
|
||||||
|
products: {
|
||||||
|
product?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
product_qty: number;
|
product_qty: number | string;
|
||||||
};
|
}[];
|
||||||
|
deliveries: {
|
||||||
export type DeliverySchema = {
|
delivery_cost?: number | string;
|
||||||
delivery_cost?: number | undefined;
|
delivery_cost_per_item?: number | string;
|
||||||
delivery_cost_per_item?: number | undefined;
|
|
||||||
document?: File | string | null;
|
document?: File | string | null;
|
||||||
document_path?: string | null;
|
document_path?: string | null;
|
||||||
driver_name: string;
|
driver_name: string;
|
||||||
vehicle_plate: string;
|
vehicle_plate: string;
|
||||||
supplier: {
|
supplier?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
products: {
|
products: {
|
||||||
product: {
|
product?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
product_qty: number;
|
product_qty: number | string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductSchema = {
|
||||||
|
product?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
product_id: number;
|
||||||
|
product_qty: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeliverySchema = {
|
||||||
|
delivery_cost?: number | string;
|
||||||
|
delivery_cost_per_item?: number | string;
|
||||||
|
document?: File | string | null;
|
||||||
|
document_path?: string | null;
|
||||||
|
driver_name: string;
|
||||||
|
vehicle_plate: string;
|
||||||
|
supplier?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
supplier_id: number;
|
||||||
|
products: {
|
||||||
|
product?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
product_id: number;
|
||||||
|
product_qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +150,8 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
.required('Produk wajib diisi!'),
|
.required('Produk wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MovementFormSchema = Yup.object({
|
export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
|
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
|
||||||
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
|
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
|
||||||
source_warehouse: Yup.object({
|
source_warehouse: Yup.object({
|
||||||
@@ -133,8 +182,6 @@ export const MovementFormSchema = Yup.object({
|
|||||||
.required('Pengiriman wajib diisi!'),
|
.required('Pengiriman wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMovementFormSchema = MovementFormSchema;
|
|
||||||
|
|
||||||
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
||||||
|
|
||||||
export const getMovementFormInitialValues = (
|
export const getMovementFormInitialValues = (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user