Compare commits

...

64 Commits

Author SHA1 Message Date
kris ebc960abb5 Update .gitlab-ci.yml file 2025-11-11 04:32:51 +00:00
kris af4926b1d7 Update .gitlab-ci.yml file 2025-11-11 04:26:32 +00:00
kris 3d134d7b8e Update .gitlab-ci.yml file 2025-11-11 04:20:32 +00:00
kris ea5ab83795 Update .gitlab-ci.yml file 2025-11-11 04:08:07 +00:00
kris 132ce52f23 Update .gitlab-ci.yml file 2025-11-11 03:59:20 +00:00
kris b1482fb586 Update .gitlab-ci.yml file 2025-11-10 09:48:36 +00:00
Mitra Berlian Unggas 56b75af69f Update .gitlab-ci.yml file 2025-11-10 09:31:50 +00:00
kris e05db3c0c4 Update .gitlab-ci.yml file 2025-11-10 09:05:07 +00:00
kris 695b7d64ec Update .gitlab-ci.yml file 2025-11-10 08:57:15 +00:00
kris f2c581fcc2 Update .gitlab-ci.yml file 2025-11-10 08:52:09 +00:00
kris f761a12137 Update .gitlab-ci.yml file 2025-11-10 08:44:55 +00:00
kris fef1b59138 Update .gitlab-ci.yml file 2025-11-10 08:34:58 +00:00
kris 472ff1d3da Update .gitlab-ci.yml file 2025-11-10 08:26:05 +00:00
kris 90de8f4e4d Update .gitlab-ci.yml file 2025-11-10 08:22:49 +00:00
kris 8912a82dba Update .gitlab-ci.yml file 2025-11-10 08:06:34 +00:00
kris 3ae5a0f9b7 Update .gitlab-ci.yml file 2025-11-10 08:05:40 +00:00
kris 2aaaf9a442 Update .gitlab-ci.yml file 2025-11-10 08:04:37 +00:00
kris caf406a383 Update .gitlab-ci.yml file 2025-11-10 06:59:47 +00:00
kris 10ed17b0ed Update .gitlab-ci.yml file 2025-11-10 06:44:05 +00:00
kris fd47a3b407 Update .gitlab-ci.yml file 2025-11-10 06:36:25 +00:00
kris 5cab1a072d Update .gitlab-ci.yml file 2025-11-10 06:32:29 +00:00
kris 288c675de7 Update .gitlab-ci.yml file 2025-11-10 06:24:44 +00:00
kris d8f16558a3 Update .gitlab-ci.yml file 2025-11-10 06:20:57 +00:00
kris 13d57c206b Update .gitlab-ci.yml file 2025-11-09 10:21:06 +00:00
kris 773aa2dbb1 Update .gitlab-ci.yml file 2025-11-09 10:10:19 +00:00
kris f14adc46d3 Update .gitlab-ci.yml file 2025-11-09 09:50:29 +00:00
kris e7592eb221 Update .gitlab-ci.yml file 2025-11-09 09:48:13 +00:00
kris 32f202d814 Update .gitlab-ci.yml file 2025-11-09 09:23:32 +00:00
kris 942b19375e Merge branch 'chore/build-cicd' into 'development'
update Dockerfile

See merge request mbugroup/lti-web-client!47
2025-11-09 09:09:14 +00:00
GitLab Deploy Bot b62427c5f4 update Dockerfile 2025-11-09 16:08:22 +07:00
kris f126e976fd Update .gitlab-ci.yml file 2025-11-09 08:34:51 +00:00
kris 0a2373572f Merge branch 'chore/build-cicd' into 'development'
edit Dockerfile

See merge request mbugroup/lti-web-client!46
2025-11-09 08:22:15 +00:00
GitLab Deploy Bot 73d2de6dfb edit Dockerfile 2025-11-09 15:21:15 +07:00
kris 49e648689a Merge branch 'chore/build-cicd' into 'development'
edit Dockerfile

See merge request mbugroup/lti-web-client!45
2025-11-09 08:16:08 +00:00
GitLab Deploy Bot d3cc38aed5 edit Dockerfile 2025-11-09 15:15:26 +07:00
kris a9620246c0 Update .gitlab-ci.yml file 2025-11-09 08:05:11 +00:00
kris 2d649eb0ff Merge branch 'chore/build-cicd' into 'development'
edit .gitlab-ci

See merge request mbugroup/lti-web-client!44
2025-11-09 08:02:31 +00:00
GitLab Deploy Bot 66b6579f27 edit .gitlab-ci 2025-11-09 15:01:10 +07:00
kris 4f9695aabe Merge branch 'chore/build-cicd' into 'development'
edit .gitlab-ci

See merge request mbugroup/lti-web-client!43
2025-11-09 07:54:31 +00:00
GitLab Deploy Bot 29ff1bb50a edit .gitlab-ci 2025-11-09 14:53:49 +07:00
kris fefb665485 Merge branch 'chore/build-cicd' into 'development'
build docker via gitlab

See merge request mbugroup/lti-web-client!42
2025-11-09 07:49:21 +00:00
GitLab Deploy Bot 52e8fb4a3b build with tag docker 2025-11-09 14:44:58 +07:00
GitLab Deploy Bot 8a11c176aa build docker via gitlab 2025-11-09 14:21:58 +07:00
Adnan Zahir d679c9f278 Merge branch 'fix/ISSUE-236/table-dropdown-issue' into 'development'
Fix: LTI Issue #236

See merge request mbugroup/lti-web-client!41
2025-11-03 09:08:21 +07:00
ValdiANS 0ae4fe0831 chore: format code using prettier 2025-11-01 15:58:47 +07:00
ValdiANS f01dae5f97 chore: add format script 2025-11-01 15:58:03 +07:00
ValdiANS 42b4206e66 chore: install prettier 2025-11-01 15:53:37 +07:00
ValdiANS 46572fd992 chore: update add button styling 2025-11-01 15:36:21 +07:00
ValdiANS b2540f1d43 chore: use RowOptionsMenuWrapper 2025-11-01 15:36:11 +07:00
ValdiANS ad10ffbba3 chore: set min width for RowCollapseOptions 2025-11-01 15:35:49 +07:00
ValdiANS 8a3c7d35ec chore: update add button styling and copywriting 2025-11-01 15:35:34 +07:00
ValdiANS d853b43e17 fix: use RowOptionsMenuWrapper component for RowOptionsMenu 2025-11-01 15:31:11 +07:00
ValdiANS e6187555ce chore: create RowOptionsMenuWrapper component 2025-11-01 15:26:25 +07:00
ValdiANS bba8fb15e5 chore: change a element to button 2025-11-01 15:24:52 +07:00
Adnan Zahir e708911429 Merge branch 'fix/FE/US-82/approval-workflow-steps-component' into 'development'
[FIX/FE][US#82] Rework Approval Steps component

See merge request mbugroup/lti-web-client!40
2025-10-30 11:04:04 +07:00
ValdiANS 79cfcad026 chore(FE-91): set formatCurrency default currency to indonesian currency 2025-10-30 11:03:22 +07:00
ValdiANS 37afcc76c3 Merge branch 'development' into fix/FE/US-82/approval-workflow-steps-component 2025-10-30 10:50:57 +07:00
ValdiANS f7eb89c113 feat(FE-91): create constant type file 2025-10-30 10:49:50 +07:00
ValdiANS c9c343b840 chore(FE-91): create BaseGroupedApproval, Approvals, and GroupedApprovals api types 2025-10-30 10:49:36 +07:00
ValdiANS 5c3b1c489f chore(FE-91): set color for step-warning 2025-10-30 10:48:37 +07:00
ValdiANS dd3a0079db chore(FE-91): set formatNumber locale to id-ID as default 2025-10-30 10:48:18 +07:00
ValdiANS bce58c585d feat(FE-91): create approval-line config file 2025-10-30 10:47:51 +07:00
ValdiANS b720c1411b chore(FE-91): make warning step icon glow 2025-10-30 10:47:29 +07:00
ValdiANS 82c1645d92 chore(FE-91): rework ApprovalSteps and create helper function for formatting approval workflow 2025-10-30 10:45:41 +07:00
99 changed files with 1998 additions and 1613 deletions
+142 -69
View File
@@ -1,76 +1,149 @@
stages: [notify]
stages:
- build
- deploy
# --- Notify when MR is opened/updated ---
notify_discord_mr:
stage: notify
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# ====== TEMPLATE: BUILD STATIC NEXT.JS ======
.build_template: &build_template
stage: build
image: node:20-alpine
cache:
key: npm-cache
paths:
- node_modules/
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
NPM_CONFIG_PRODUCTION: "false"
NODE_ENV: ""
script:
- echo "Installing dependencies..."
- 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
jq -n \
--arg repo "$CI_PROJECT_PATH" \
--arg mr "#${CI_MERGE_REQUEST_IID}" \
--arg url "$MR_URL" \
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
--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 Opened/Updated",
description: ($mr + " in " + $repo),
url: $url,
color: 3447003,
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"
.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"
# --- Notify when MR is merged ---
notify_discord_merge:
stage: notify
image: alpine:3.20
# 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}"
# Tentukan nama environment
if [ "$CI_COMMIT_BRANCH" = "devops-s3" ]; 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 \
--arg title "$TITLE" \
--arg desc "$DESC" \
--arg color "$COLOR" \
--arg repo "$CI_PROJECT_PATH" \
--arg actor "$GITLAB_USER_LOGIN" \
--arg commit "$CI_COMMIT_SHA" \
--arg run_url "$RUN_URL" \
'{
username: "CI Bot - LTI WEB",
embeds: [{
title: $title,
description: $desc,
color: ($color|tonumber),
fields: [
{name: "Repository", value: $repo, inline: true},
{name: "Actor", value: $actor, inline: true},
{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 @payload.json "$DISCORD_WEBHOOK_URL"
# ====== DEVELOPMENT (Branch devops-s3) ======
build:dev:
<<: *build_template
rules:
# Only run for merge request pipelines that are in merged state
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
- if: '$CI_COMMIT_BRANCH == "devops-s3"'
environment:
name: devops-s3
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id"
NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id"
deploy:dev:
<<: *deploy_template
needs: ["build:dev"]
rules:
- if: '$CI_COMMIT_BRANCH == "devops-s3"'
variables:
S3_BUCKET: "dev-lti-erp.mbugroup.id"
AWS_REGION: "ap-southeast-3"
CLOUDFRONT_DISTRIBUTION_ID: "E1Z8XTA8XF1GIV"
environment:
name: devops-s3
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"
+25
View File
@@ -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"]
+39
View File
@@ -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
View File
@@ -1,6 +1,6 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -10,14 +10,14 @@ const compat = new FlatCompat({
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
],
},
];
+17
View File
@@ -39,6 +39,7 @@
"eslint": "^9",
"eslint-config-next": "15.5.3",
"husky": "^9.1.7",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -5669,6 +5670,22 @@
"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": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+3 -1
View File
@@ -7,7 +7,8 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"prepare": "husky"
"prepare": "husky",
"format": "prettier --write ."
},
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
@@ -41,6 +42,7 @@
"eslint": "^9",
"eslint-config-next": "15.5.3",
"husky": "^9.1.7",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5"
}
+1 -1
View File
@@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: ['@tailwindcss/postcss'],
};
export default config;
+2 -4
View File
@@ -3,10 +3,10 @@
@import '../styles/daisyui.css';
@plugin "daisyui/theme" {
name: "lti";
name: 'lti';
default: false;
prefersdark: false;
color-scheme: "light";
color-scheme: 'light';
--color-base-100: oklch(98% 0.001 106.423);
--color-base-200: oklch(97% 0.001 106.424);
--color-base-300: oklch(92% 0.003 48.717);
@@ -37,8 +37,6 @@
--noise: 0;
}
:root {
--color-primary: #1f74bf;
}
+5 -5
View File
@@ -1,11 +1,11 @@
import InventoryAdjustmentForm from "@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm";
import InventoryAdjustmentForm from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm';
const CreateInventoryAdjustment = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<InventoryAdjustmentForm/>
<section className='w-full p-4 flex flex-row justify-center'>
<InventoryAdjustmentForm />
</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 = ({
children
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>
}
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
export default Layout;
+8 -7
View File
@@ -7,11 +7,12 @@ import type { InventoryAdjustment } from '@/types/api/inventory/adjustment';
const DetailInventoryAdjustment = () => {
const router = useRouter();
const [inventoryAdjustment, setInventoryAdjustment] = useState<InventoryAdjustment | null>(null);
const [inventoryAdjustment, setInventoryAdjustment] =
useState<InventoryAdjustment | null>(null);
// Ambil data dari router state
useEffect(() => {
console.log("Router State");
console.log('Router State');
console.log(window.history.state);
const state = window.history.state?.usr as
| { inventoryAdjustment?: InventoryAdjustment }
@@ -24,20 +25,20 @@ const DetailInventoryAdjustment = () => {
}, [router]);
const finalData = inventoryAdjustment;
console.log("Final Data");
console.log('Final Data');
console.log(finalData);
if (!finalData) {
return (
<div className="w-full flex flex-row justify-center items-center p-4">
<span className="loading loading-spinner loading-xl" />
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
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} />
</section>
);
+5 -5
View File
@@ -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 = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<CustomerForm/>
<section className='w-full p-4 flex flex-row justify-center'>
<CustomerForm />
</section>
);
}
};
export default AddCustomer;
export default AddCustomer;
+17 -15
View File
@@ -1,45 +1,47 @@
'use client'
'use client';
import { useRouter, useSearchParams } from "next/navigation";
import useSWR from "swr";
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { CustomerApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
const CustomerDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const costumerId = searchParams.get("customerId");
const costumerId = searchParams.get('customerId');
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
costumerId,
(id: number) => CustomerApi.getSingle(id)
);
if(!costumerId){
if (!costumerId) {
router.back();
return (
<div className="w-full flex flex-row justify-center items-center p-4">
<span className="loading loading-spinner loading-xl" />
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if(!isLoadingCostumer && (!costumer || isResponseError(costumer))){
router.replace("/404");
if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) {
router.replace('/404');
return;
}
return (
<div className="w-full p-4 flex flex-row justify-center">
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingCostumer && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingCostumer && isResponseSuccess(costumer) && (
<CustomerForm formType="detail" initialValues={costumer.data} />
<CustomerForm formType='detail' initialValues={costumer.data} />
)}
</div>
)
);
};
export default CustomerDetail;
+4 -4
View File
@@ -1,11 +1,11 @@
import CustomersTable from "@/components/pages/master-data/customer/CustomersTable";
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
const Customer = () => {
return (
<section className="w-full p-4">
<section className='w-full p-4'>
<CustomersTable />
</section>
)
);
};
export default Customer;
export default Customer;
+3 -3
View File
@@ -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 = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<section className='w-full p-4 flex flex-row justify-center'>
<FlockForm />
</section>
);
}
};
export default AddFlock;
@@ -1,10 +1,10 @@
'use client'
'use client';
import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
import { FlockApi } from "@/services/api/master-data";
import { useRouter, useSearchParams } from "next/navigation";
import useSWR from "swr";
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FlockApi } from '@/services/api/master-data';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const FlockEdit = () => {
const router = useRouter();
@@ -44,6 +44,6 @@ const FlockEdit = () => {
)}
</div>
);
}
};
export default FlockEdit;
export default FlockEdit;
+6 -6
View File
@@ -1,11 +1,11 @@
import SuspenseHelper from "@/components/helper/SuspenseHelper"
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>
}
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
export default Layout;
+19 -16
View File
@@ -1,10 +1,10 @@
'use client'
'use client';
import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
import { FlockApi } from "@/services/api/master-data";
import { useRouter, useSearchParams } from "next/navigation";
import useSWR from "swr";
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FlockApi } from '@/services/api/master-data';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const FlockDetail = () => {
const router = useRouter();
@@ -14,33 +14,36 @@ const FlockDetail = () => {
const flockId = searchParams.get('flockId');
// 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();
return (
<div className="w-full flex flex-row justify-center items-center p-4">
<span className="loading loading-spinner loading-xl" />
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if(!isLoadingFlock && (!flock || isResponseError(flock))){
if (!isLoadingFlock && (!flock || isResponseError(flock))) {
router.replace('/404');
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 && (
<span className="loading loading-spinner loading-xl" />
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingFlock && isResponseSuccess(flock) && (
<FlockForm formType="detail" initialValues={flock.data} />
<FlockForm formType='detail' initialValues={flock.data} />
)}
</div>
);
}
};
export default FlockDetail;
export default FlockDetail;
+5 -5
View File
@@ -1,11 +1,11 @@
import FlockTable from "@/components/pages/master-data/flock/FlocksTable";
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
const Flock = () => {
return (
<section className="w-full p-4">
<FlockTable/>
<section className='w-full p-4'>
<FlockTable />
</section>
);
}
);
};
export default Flock;
@@ -1,11 +1,11 @@
import ProductCategoryForm from "@/components/pages/master-data/product-category/form/ProductCategoryForm";
import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm';
const AddProductCategory = () => {
return (
<div className="w-full p-4 flex flex-row justify-center">
<div className='w-full p-4 flex flex-row justify-center'>
<ProductCategoryForm />
</div>
);
};
export default AddProductCategory;
export default AddProductCategory;
@@ -9,39 +9,44 @@ import { ProductCategoryApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const ProductCategoryEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const router = useRouter();
const searchParams = useSearchParams();
const productCategoryId = searchParams.get('productCategoryId');
const productCategoryId = searchParams.get('productCategoryId');
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
productCategoryId,
(id: number) => ProductCategoryApi.getSingle(id)
);
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
productCategoryId,
(id: number) => ProductCategoryApi.getSingle(id)
);
if (!productCategoryId) {
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 (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
router.replace('/404');
return;
}
if (!productCategoryId) {
router.back();
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
)}
</div>
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
}
export default ProductCategoryEdit;
if (
!isLoadingProductCategory &&
(!productCategory || isResponseError(productCategory))
) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingProductCategory && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
)}
</div>
);
};
export default ProductCategoryEdit;
@@ -29,16 +29,24 @@ const ProductCategoryDetail = () => {
);
}
if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
if (
!isLoadingProductCategory &&
(!productCategory || isResponseError(productCategory))
) {
router.replace('/404');
return;
}
return (
<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) && (
<ProductCategoryForm type='detail' initialValues={productCategory.data} />
<ProductCategoryForm
type='detail'
initialValues={productCategory.data}
/>
)}
</div>
);
@@ -1,11 +1,11 @@
import ProductCategoryTable from "@/components/pages/master-data/product-category/ProductCategoryTable";
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
const ProductCategory = () => {
return (
<section className="w-full p-4">
<section className='w-full p-4'>
<ProductCategoryTable />
</section>
);
};
export default ProductCategory;
export default ProductCategory;
+2 -2
View File
@@ -2,10 +2,10 @@ import ProductForm from '@/components/pages/master-data/product/form/ProductForm
const AddProduct = () => {
return (
<div className="w-full p-4 flex flex-row justify-center">
<div className='w-full p-4 flex flex-row justify-center'>
<ProductForm />
</div>
);
};
export default AddProduct;
export default AddProduct;
@@ -13,9 +13,8 @@ const ProductEdit = () => {
const productId = searchParams.get('productId');
const { data: product, isLoading } = useSWR(
productId,
(id: number) => ProductApi.getSingle(id)
const { data: product, isLoading } = useSWR(productId, (id: number) =>
ProductApi.getSingle(id)
);
if (!productId) {
@@ -42,4 +41,4 @@ const ProductEdit = () => {
);
};
export default ProductEdit;
export default ProductEdit;
+3 -4
View File
@@ -13,9 +13,8 @@ const ProductDetail = () => {
const productId = searchParams.get('productId');
const { data: product, isLoading } = useSWR(
productId,
(id: number) => ProductApi.getSingle(id)
const { data: product, isLoading } = useSWR(productId, (id: number) =>
ProductApi.getSingle(id)
);
if (!productId) {
@@ -42,4 +41,4 @@ const ProductDetail = () => {
);
};
export default ProductDetail;
export default ProductDetail;
+4 -4
View File
@@ -1,11 +1,11 @@
import ProductsTable from "@/components/pages/master-data/product/ProductTable";
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
const Product = () => {
return (
<section className="w-full p-4">
<ProductsTable />
<section className='w-full p-4'>
<ProductsTable />
</section>
);
};
export default Product;
export default Product;
+1 -1
View File
@@ -8,4 +8,4 @@ const AddSupplier = () => {
);
};
export default AddSupplier;
export default AddSupplier;
+1 -1
View File
@@ -46,4 +46,4 @@ const SupplierDetail = () => {
);
};
export default SupplierDetail;
export default SupplierDetail;
+1 -1
View File
@@ -1,4 +1,4 @@
import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable";
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
const Supplier = () => {
return (
+6 -6
View File
@@ -1,11 +1,11 @@
import SuspenseHelper from "@/components/helper/SuspenseHelper"
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>
}
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
export default Layout;
+6 -6
View File
@@ -1,11 +1,11 @@
import SuspenseHelper from "@/components/helper/SuspenseHelper"
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>
}
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
export default Layout;
+5 -5
View File
@@ -20,7 +20,7 @@ import useSWR from 'swr';
/**
* TODO: Refactor code - pindahin detail ke reuseable component
* setelah implement approval and reject
* setelah implement approval and reject
*/
const DetailChickin = () => {
@@ -43,9 +43,8 @@ const DetailChickin = () => {
// chickin.data?.approval.step_number == 1 ? false : true
true
);
const [isRejectedDisabled, setIsRejectedDisabled] = useState(
!isApprovedDisabled
);
const [isRejectedDisabled, setIsRejectedDisabled] =
useState(!isApprovedDisabled);
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
);
@@ -264,7 +263,8 @@ const DetailChickin = () => {
<Icon icon='mdi:times' width={24} height={24} />
Delete
</Button>
<Button color='warning'
<Button
color='warning'
onClick={() => {
chickinModal.openModal();
}}
+5 -5
View File
@@ -1,10 +1,10 @@
import ChickinTable from "@/components/pages/production/chickin/ChickinTable";
import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
const Chickin = () => {
return (
<section className="w-full p-4">
<ChickinTable/>
<section className='w-full p-4'>
<ChickinTable />
</section>
);
}
export default Chickin;
};
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 = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<ProjectFlockForm formType="add"/>
<section className='w-full p-4 flex flex-row justify-center'>
<ProjectFlockForm formType='add' />
</section>
);
}
};
export default AddProjectFlock;
export default AddProjectFlock;
@@ -1,46 +1,47 @@
'use client'
'use client';
import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
import { ProjectFlockApi } from "@/services/api/production";
import { useRouter, useSearchParams } from "next/navigation";
import useSWR from "swr";
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ProjectFlockApi } from '@/services/api/production';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const ProjectFlockEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const projectFlockId = searchParams.get("projectFlockId");
const projectFlockId = searchParams.get('projectFlockId');
const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
projectFlockId,
(id: number) => ProjectFlockApi.getSingle(id)
);
if(!projectFlockId){
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 className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if(!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))){
router.replace("/404");
if (!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))) {
router.replace('/404');
return;
}
return (
<div className="w-full p-4 flex flex-row justify-center">
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingCostumer && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingCostumer && isResponseSuccess(projectFlock) && (
<ProjectFlockForm formType="edit" initialValues={projectFlock.data} />
<ProjectFlockForm formType='edit' initialValues={projectFlock.data} />
)}
</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 = ({
children
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>
}
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
export default Layout;
@@ -42,7 +42,11 @@ const ProjectFlockDetail = () => {
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
<ProjectFlockForm formType='detail' initialValues={projectFlock.data} refreshProjectFlocks={refreshProjectFlock} />
<ProjectFlockForm
formType='detail'
initialValues={projectFlock.data}
refreshProjectFlocks={refreshProjectFlock}
/>
)}
</div>
);
+4 -4
View File
@@ -1,11 +1,11 @@
import ProjectFlockTable from "@/components/pages/production/project-flock/ProjectFlockTable";
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
const ProjectFlock = () => {
return (
<section className="w-full p-4">
<ProjectFlockTable/>
<section className='w-full p-4'>
<ProjectFlockTable />
</section>
);
}
};
export default ProjectFlock;
+13 -15
View File
@@ -1,13 +1,11 @@
'use client';
import {
HTMLAttributes,
ReactNode,
} from 'react';
import { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/helper';
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
export interface CardProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
title?: string;
subtitle?: string;
image?: string;
@@ -44,17 +42,17 @@ const Card = ({
const baseClasses = 'card bg-base-100';
const variantClasses = {
'default': '',
'compact': 'card-compact',
'bordered': 'border border-base-300',
'shadow': 'shadow-xl',
default: '',
compact: 'card-compact',
bordered: 'border border-base-300',
shadow: 'shadow-xl',
'image-full': 'card-side card-compact shadow-xl',
};
const sizeClasses = {
'sm': 'w-64',
'md': 'w-96',
'lg': 'w-[28rem]',
sm: 'w-64',
md: 'w-96',
lg: 'w-[28rem]',
};
return cn(
@@ -84,9 +82,9 @@ const Card = ({
const getTitleClasses = () => {
const sizeClasses = {
'sm': 'text-lg',
'md': 'text-xl',
'lg': 'text-2xl',
sm: 'text-lg',
md: 'text-xl',
lg: 'text-2xl',
};
return cn('card-title font-bold', sizeClasses[size], className?.title);
+10 -10
View File
@@ -185,17 +185,17 @@ const Pagination = ({
currentPage <= 2
? currentPage + 2
: currentPage === totalPages - 2
? 3
: currentPage >= totalPages - 1
? 4
: 1
? 3
: currentPage >= totalPages - 1
? 4
: 1
}
endPage={
currentPage <= 2 || currentPage >= totalPages - 1
? totalPages - 3
: currentPage === totalPages - 2
? totalPages - 4
: 2
? totalPages - 4
: 2
}
onPageItemClick={pageChangeHandler}
/>
@@ -242,15 +242,15 @@ const Pagination = ({
currentPage <= 3
? currentPage + 2
: currentPage >= 4
? currentPage + 2
: 1
? currentPage + 2
: 1
}
endPage={
currentPage <= 3
? totalPages - 2
: currentPage >= 4
? totalPages - 1
: 0
? totalPages - 1
: 0
}
onPageItemClick={pageChangeHandler}
/>
+2 -9
View File
@@ -1,10 +1,6 @@
'use client';
import {
ChangeEventHandler,
FocusEventHandler,
ReactNode,
} from 'react';
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
import { cn } from '@/lib/helper';
@@ -109,10 +105,7 @@ const DateInput = ({
min={min}
max={max}
disabled={disabled}
className={cn(
'grow bg-transparent cursor-pointer',
className?.input
)}
className={cn('grow bg-transparent cursor-pointer', className?.input)}
readOnly={readOnly}
/>
+1 -4
View File
@@ -69,10 +69,7 @@ const FileInput = ({
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn(
'grow file-input w-full h-12 rounded',
className?.input
)}
className={cn('grow file-input w-full h-12 rounded', className?.input)}
readOnly={readOnly}
/>
+8 -8
View File
@@ -1,10 +1,10 @@
"use client";
'use client';
import { ChangeEvent, ReactNode } from "react";
import { NumericFormat, OnValueChange } from "react-number-format";
import TextInput, { TextInputProps } from "@/components/input/TextInput";
import { ChangeEvent, ReactNode } from 'react';
import { NumericFormat, OnValueChange } from 'react-number-format';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
interface NumberInputProps extends Omit<TextInputProps, "type"> {
interface NumberInputProps extends Omit<TextInputProps, 'type'> {
thousandSeparator?: string;
decimalSeparator?: string;
decimalScale?: number;
@@ -17,8 +17,8 @@ interface NumberInputProps extends Omit<TextInputProps, "type"> {
}
const NumberInput = ({
thousandSeparator = ",",
decimalSeparator = ".",
thousandSeparator = ',',
decimalSeparator = '.',
decimalScale = 5,
allowNegative = true,
onChange,
@@ -28,7 +28,7 @@ const NumberInput = ({
}: NumberInputProps) => {
const valueChangeHandler: OnValueChange = (
numberFormatValues,
sourceInfo,
sourceInfo
) => {
const newChangeEvent = sourceInfo.event as
| ChangeEvent<HTMLInputElement>
+6 -2
View File
@@ -49,14 +49,18 @@ const MenuItem = ({
);
return (
<li onClick={onClick}>
<li>
{href && (
<Link href={href} className={menuItemBaseClassName}>
{menuItemContent}
</Link>
)}
{!href && <a className={menuItemBaseClassName}>{menuItemContent}</a>}
{!href && (
<button className={menuItemBaseClassName} onClick={onClick}>
{menuItemContent}
</button>
)}
</li>
);
};
+147 -30
View File
@@ -3,11 +3,24 @@ import Steps from '@/components/steps/Steps';
import StepItem from '@/components/steps/StepItem';
import Tooltip from '@/components/Tooltip';
import { formatDate } from '@/lib/helper';
import { ApprovalsLine } from '@/types/api/api-general';
import { cn, formatDate } from '@/lib/helper';
import { BaseApproval, BaseGroupedApproval } from '@/types/api/api-general';
import { ApprovalLine } from '@/types/config/constant';
export type ApprovalStepStatus = 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE';
export type ApprovalStepLog = {
action_by?: string;
date?: string;
notes?: string | null;
};
interface ApprovalStepsProps {
approvals: ApprovalsLine;
approvals: {
name?: string;
status: ApprovalStepStatus;
logs?: ApprovalStepLog[];
}[];
}
const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
@@ -15,45 +28,77 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
<Steps direction='vertical' className='w-full md:steps-horizontal'>
{approvals.map((approval, idx) => {
const stepItemColor =
approval.status === 'approved'
approval.status === 'APPROVED'
? 'success'
: approval.status === 'rejected'
? 'error'
: undefined;
: approval.status === 'REJECTED'
? 'error'
: approval.status === 'WAITING'
? 'warning'
: undefined;
const stepItemIcon =
approval.status === 'approved'
approval.status === 'APPROVED'
? 'material-symbols:check-rounded'
: approval.status === 'rejected'
? 'material-symbols:close-rounded'
: 'bxs:hourglass';
: approval.status === 'REJECTED'
? 'material-symbols:close-rounded'
: approval.status === 'WAITING'
? 'pajamas:dash-circle'
: approval.logs && approval.logs.length > 0
? 'material-symbols:info-outline-rounded'
: 'bxs:hourglass';
return (
<StepItem
key={idx}
color={stepItemColor}
icon={
approval.status !== 'waiting' && (
<Tooltip
color={stepItemColor}
position='right'
className={{
wrapper: 'md:tooltip-bottom',
}}
content={
<div className='flex flex-col text-base'>
<span>{formatDate(approval.date, 'YYYY-MM-DD')}</span>
<span>Oleh: {approval.action_by}</span>
<span>Catatan: {approval.notes}</span>
</div>
}
>
<Icon icon={stepItemIcon} width={24} height={24} />
</Tooltip>
)
<Tooltip
color={stepItemColor}
position='right'
className={{
wrapper: 'md:tooltip-bottom',
}}
content={
<>
{approval.logs && approval.logs.length > 0 && (
<div className='flex flex-col gap-2'>
{approval.logs?.map((approvalLog, logIdx) => (
<div
key={logIdx}
className='flex flex-col text-base text-start'
>
{approvalLog.date && (
<span>
{formatDate(
approvalLog.date,
'YYYY-MM-DD, HH:mm:ss'
)}
</span>
)}
<span>Oleh: {approvalLog.action_by ?? '-'}</span>
<span>Catatan: {approvalLog.notes ?? '-'}</span>
</div>
))}
</div>
)}
</>
}
>
<Icon
icon={stepItemIcon}
width={24}
height={24}
className={cn({
invisible:
approval.status === 'IDLE' &&
(!approval.logs ||
(approval.logs && approval.logs.length === 0)),
})}
/>
</Tooltip>
}
>
{approval.role}
{approval.name}
</StepItem>
);
})}
@@ -61,4 +106,76 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
);
};
export const formatGroupedApprovalsToApprovalSteps = (
approvalLine: ApprovalLine,
groupedApprovals: BaseGroupedApproval[],
latestApproval: BaseApproval
): ApprovalStepsProps['approvals'] => {
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
approvalLine.map((approvalLineItem) => {
const approvalGroup = groupedApprovals.find(
(approvalGroupItem) =>
approvalGroupItem.step_number === approvalLineItem.step_number
);
const currentStepNumber = approvalLineItem.step_number;
const lastStepNumber =
groupedApprovals[groupedApprovals.length - 1].step_number;
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
throw new Error(
`Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
);
}
if (!approvalGroup) {
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
return {
name: approvalLineItem.step_name,
status: isWaiting ? 'WAITING' : 'IDLE',
};
}
let approvalStatus: ApprovalStepStatus;
if (approvalGroup.step_number <= latestApproval.step_number) {
switch (approvalGroup.approvals[0].action) {
case 'CREATED':
case 'APPROVED':
approvalStatus = 'APPROVED';
break;
case 'REJECTED':
approvalStatus = 'REJECTED';
break;
default:
approvalStatus = 'IDLE';
break;
}
} else if (approvalGroup.step_number === latestApproval.step_number + 1) {
approvalStatus = 'WAITING';
} else {
approvalStatus = 'IDLE';
}
const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals.map(
(approval) => ({
action_by: approval.action_by.name,
date: approval.action_at,
notes: approval.notes,
})
);
return {
name: approvalGroup.step_name,
status: approvalStatus,
logs: approvalLogs,
};
});
return formattedApprovalSteps;
};
export default ApprovalSteps;
@@ -10,11 +10,7 @@ import { inventoryAdjustmentApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Icon } from '@iconify/react';
import {
ColumnDef,
ColumnSort,
SortingState,
} from '@tanstack/react-table';
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
import { useCallback, useEffect, useState } from 'react';
import useSWR from 'swr';
@@ -44,10 +40,7 @@ const InventoryAdjustmentTable = () => {
});
// Fetch Data
const {
data: inventoryAdjustments,
isLoading,
} = useSWR(
const { data: inventoryAdjustments, isLoading } = useSWR(
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
inventoryAdjustmentApi.getAllFetcher
);
@@ -113,8 +106,8 @@ const InventoryAdjustmentTable = () => {
type === 'INCREASE'
? 'Peningkatan'
: type === 'DECREASE'
? 'Penurunan'
: '-';
? 'Penurunan'
: '-';
return (
<div
@@ -187,8 +180,13 @@ const InventoryAdjustmentTable = () => {
<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='flex flex-row'>
<Button href='/inventory/adjustment/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/inventory/adjustment/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
@@ -211,7 +209,7 @@ const InventoryAdjustmentTable = () => {
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
className={{ wrapper: 'min-w-28' }}
/>
</div>
</div>
@@ -4,24 +4,21 @@ export const InventoryAdjustmentFormSchema = Yup.object({
product_category: Yup.object({
value: Yup.number().required('ID Kategori Produk wajib diisi!'),
label: Yup.string().required('Nama Kategori Produk wajib diisi!'),
})
.nullable(),
}).nullable(),
product_category_id: Yup.number().nullable(),
product: Yup.object({
value: Yup.number().required('ID Produk wajib diisi!'),
label: Yup.string().required('Nama Produk wajib diisi!'),
})
.nullable(),
}).nullable(),
product_id: Yup.number().nullable(),
warehouse: Yup.object({
value: Yup.number().required('ID Gudang wajib diisi!'),
label: Yup.string().required('Nama Gudang wajib diisi!'),
})
.nullable(),
}).nullable(),
warehouse_id: Yup.number().nullable(),
@@ -51,9 +51,8 @@ const InventoryAdjustmentForm = ({
// Submit Handler
const createInventoryAdjustmentHandler = useCallback(
async (payload: CreateInventoryAdjustmentPayload) => {
const createInventoryAdjustmentRes = await inventoryAdjustmentApi.create(
payload
);
const createInventoryAdjustmentRes =
await inventoryAdjustmentApi.create(payload);
if (isResponseError(createInventoryAdjustmentRes)) {
setInventoryAdjustmentFormErrorMessage(
@@ -68,7 +67,9 @@ const InventoryAdjustmentForm = ({
[router]
);
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(() => {
const formikInitialValues = useMemo<
Partial<InventoryAdjustmentFormValues>
>(() => {
return {
product_category_id: initialValues?.product_category?.id ?? 0,
product_id: initialValues?.product?.id ?? 0,
@@ -185,7 +186,6 @@ const InventoryAdjustmentForm = ({
warehouseChangeHandler(null);
};
const { setValues: formikSetValues } = formik;
// Effect
@@ -225,7 +225,13 @@ const InventoryAdjustmentForm = ({
const type = initialValues.transaction_type.toLowerCase();
setQuantityLabel(type === 'increase' ? 'Tambah Stok' : 'Kurangi Stok');
}
}, [formik, initialValues, setQuantityLabel, setDisabledProduct, setSelectedProductCategories]);
}, [
formik,
initialValues,
setQuantityLabel,
setDisabledProduct,
setSelectedProductCategories,
]);
useEffect(() => {
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
}, [formikSetValues, formikInitialValues]);
@@ -364,15 +370,19 @@ const InventoryAdjustmentForm = ({
errorMessage={formik.errors.transaction_type as string}
variant='radio-primary'
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'}
/>
{/* Number Input Stock */}
<TextInput
className={{
wrapper: `${formik.values.transaction_type != undefined ? '' : 'hidden'}`,
}}
className={{
wrapper: `${formik.values.transaction_type != undefined ? '' : 'hidden'}`,
}}
required
label={quantityLabel}
name='quantity'
@@ -395,8 +405,6 @@ const InventoryAdjustmentForm = ({
readOnly={type === 'detail'}
/>
{/* Text Area Input Reason */}
<TextArea
required
@@ -413,14 +421,23 @@ const InventoryAdjustmentForm = ({
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'detail' && (
<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
</Button>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting || formik.values.product == undefined}
disabled={
!formik.isValid ||
formik.isSubmitting ||
formik.values.product == undefined
}
className='px-4'
>
Submit
@@ -77,7 +77,7 @@ const MovementTable = () => {
<TableToolbar
addButton={{
href: '/inventory/movement/add',
label: 'Tambah Movement',
label: 'Tambah',
}}
search={{
value: tableFilterState.search,
@@ -14,6 +14,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Area } from '@/types/api/master-data/area';
import { AreaApi } from '@/services/api/master-data';
@@ -32,16 +33,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
variant='ghost'
@@ -76,7 +68,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -150,7 +142,7 @@ const AreasTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -199,10 +191,15 @@ const AreasTable = () => {
<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='flex flex-row'>
<Button href='/master-data/area/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/area/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Area
Tambah
</Button>
</div>
@@ -14,6 +14,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Bank } from '@/types/api/master-data/bank';
import { BankApi } from '@/services/api/master-data';
@@ -32,16 +33,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
variant='ghost'
@@ -66,7 +58,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -76,7 +68,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -163,7 +155,7 @@ const BanksTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -212,10 +204,15 @@ const BanksTable = () => {
<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='flex flex-row'>
<Button href='/master-data/bank/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/bank/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Bank
Tambah
</Button>
</div>
@@ -8,6 +8,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -15,10 +16,7 @@ import { CustomerApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Customer } from '@/types/api/master-data/customer';
import { Icon } from '@iconify/react';
import {
CellContext,
ColumnDef,
} from '@tanstack/react-table';
import { CellContext, ColumnDef } from '@tanstack/react-table';
import { useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
@@ -33,16 +31,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
variant='ghost'
@@ -53,10 +42,10 @@ const RowOptionsMenu = ({
Detail
</Button>
<Button
className='justify-start text-sm'
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
@@ -65,7 +54,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -75,7 +64,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -174,7 +163,7 @@ const CustomersTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -210,10 +199,15 @@ const CustomersTable = () => {
<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='flex flex-row'>
<Button href='/master-data/customer/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/customer/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Customer
Tambah
</Button>
</div>
@@ -285,4 +279,4 @@ const CustomersTable = () => {
);
};
export default CustomersTable;
export default CustomersTable;
@@ -11,7 +11,11 @@ import {
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { CustomerFormSchema, CustomerFormValues, UpdateCustomerFormSchema } from '@/components/pages/master-data/customer/form/CustomerForm.schema';
import {
CustomerFormSchema,
CustomerFormValues,
UpdateCustomerFormSchema,
} from '@/components/pages/master-data/customer/form/CustomerForm.schema';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
@@ -150,7 +154,8 @@ const CustomerForm = ({
const formik = useFormik<CustomerFormValues>({
initialValues: formikInitialValues,
enableReinitialize: true,
validationSchema: formType === 'edit' ? UpdateCustomerFormSchema : CustomerFormSchema,
validationSchema:
formType === 'edit' ? UpdateCustomerFormSchema : CustomerFormSchema,
onSubmit: async (values) => {
// reset error message
setCustomerFormErrorMessage('');
@@ -14,6 +14,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Fcr } from '@/types/api/master-data/fcr';
import { FcrApi } from '@/services/api/master-data';
@@ -32,16 +33,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
variant='ghost'
@@ -66,7 +58,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -76,7 +68,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -150,7 +142,7 @@ const FcrsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -199,10 +191,15 @@ const FcrsTable = () => {
<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='flex flex-row'>
<Button href='/master-data/fcr/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/fcr/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah FCR
Tambah
</Button>
</div>
@@ -12,6 +12,7 @@ import { FlockApi } from '@/services/api/master-data';
import { useModal } from '@/components/Modal';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import toast from 'react-hot-toast';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
@@ -30,16 +31,7 @@ const RowsOptions = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
variant='ghost'
@@ -54,7 +46,7 @@ const RowsOptions = ({
/>
Edit
</Button>
<Button
<Button
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
variant='ghost'
color='primary'
@@ -72,7 +64,7 @@ const RowsOptions = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -82,7 +74,7 @@ const RowsOptions = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -203,9 +195,15 @@ const FlockTable = () => {
<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='flex flex-row'>
<Button href='/master-data/flock/add' color='primary'>
Tambah Flock
<div className='w-full flex flex-row'>
<Button
href='/master-data/flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</div>
@@ -275,4 +273,4 @@ const FlockTable = () => {
);
};
export default FlockTable;
export default FlockTable;
@@ -3,10 +3,7 @@ import * as Yup from 'yup';
export const FlockFormSchema = Yup.object({
name: Yup.string()
.required('Nama wajib diisi!')
.matches(
/^[\p{L}\p{N}\s]+$/u,
'Nama tidak boleh mengandung simbol'
),
.matches(/^[\p{L}\p{N}\s]+$/u, 'Nama tidak boleh mengandung simbol'),
});
export const UpdateFlockFormSchema = FlockFormSchema;
@@ -1,11 +1,15 @@
'use client'
'use client';
import { useModal } from '@/components/Modal';
import { FlockApi } from '@/services/api/master-data';
import { Flock } from '@/types/api/master-data/flock';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { FlockFormSchema, FlockFormValues, UpdateFlockFormSchema } from '@/components/pages/master-data/flock/form/FlockForm.schema';
import {
FlockFormSchema,
FlockFormValues,
UpdateFlockFormSchema,
} from '@/components/pages/master-data/flock/form/FlockForm.schema';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
@@ -48,7 +52,8 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
const formik = useFormik<FlockFormValues>({
initialValues: formikInitialValue,
enableReinitialize: true,
validationSchema: formType === 'edit' ? UpdateFlockFormSchema : FlockFormSchema,
validationSchema:
formType === 'edit' ? UpdateFlockFormSchema : FlockFormSchema,
onSubmit: async (values) => {
// reset error message
setFlockFormErrorMessage('');
@@ -19,6 +19,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi } from '@/services/api/master-data';
@@ -37,16 +38,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
variant='ghost'
@@ -71,7 +63,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -81,7 +73,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -173,7 +165,7 @@ const KandangsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -238,10 +230,15 @@ const KandangsTable = () => {
<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='flex flex-row'>
<Button href='/master-data/kandang/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/kandang/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Kandang
Tambah
</Button>
</div>
@@ -19,6 +19,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Location } from '@/types/api/master-data/location';
import { LocationApi } from '@/services/api/master-data';
@@ -37,16 +38,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
variant='ghost'
@@ -71,7 +63,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -81,7 +73,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -172,7 +164,7 @@ const LocationsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -237,10 +229,15 @@ const LocationsTable = () => {
<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='flex flex-row'>
<Button href='/master-data/location/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/location/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Location
Tambah
</Button>
</div>
@@ -19,6 +19,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { NonstockApi } from '@/services/api/master-data';
@@ -37,16 +38,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
variant='ghost'
@@ -71,7 +63,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -81,7 +73,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -184,7 +176,7 @@ const NonstocksTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -249,10 +241,15 @@ const NonstocksTable = () => {
<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='flex flex-row'>
<Button href='/master-data/nonstock/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/nonstock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Nonstock
Tambah
</Button>
</div>
@@ -14,6 +14,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
@@ -32,16 +33,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
variant='ghost'
@@ -64,7 +56,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
@@ -74,7 +66,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -154,7 +146,7 @@ const ProductCategoryTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -200,10 +192,15 @@ const ProductCategoryTable = () => {
<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='flex flex-row'>
<Button href='/master-data/product-category/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/product-category/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Product Category
Tambah
</Button>
</div>
<DebouncedTextInput
@@ -1,10 +1,14 @@
import * as Yup from 'yup';
export const ProductCategoryFormSchema = Yup.object({
code: Yup.string().required('Kode wajib diisi!').max(3, 'Kode kategori produk melebihi 3 karakter!'),
code: Yup.string()
.required('Kode wajib diisi!')
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
name: Yup.string().required('Nama wajib diisi!'),
});
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
export type ProductCategoryFormValues = Yup.InferType<typeof ProductCategoryFormSchema>;
export type ProductCategoryFormValues = Yup.InferType<
typeof ProductCategoryFormSchema
>;
@@ -30,7 +30,10 @@ interface ProductCategoryFormProps {
initialValues?: ProductCategory;
}
const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFormProps) => {
const ProductCategoryForm = ({
type = 'add',
initialValues,
}: ProductCategoryFormProps) => {
const router = useRouter();
const deleteModal = useModal();
@@ -77,7 +80,10 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
const formik = useFormik<ProductCategoryFormValues>({
initialValues: formikInitialValues,
validationSchema: type === 'edit' ? UpdateProductCategoryFormSchema : ProductCategoryFormSchema,
validationSchema:
type === 'edit'
? UpdateProductCategoryFormSchema
: ProductCategoryFormSchema,
onSubmit: async (values) => {
setFormErrorMessage('');
@@ -91,7 +97,10 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
await createProductCategoryHandler(payload);
break;
case 'edit':
await updateProductCategoryHandler(initialValues?.id as number, payload);
await updateProductCategoryHandler(
initialValues?.id as number,
payload
);
break;
}
},
@@ -263,4 +272,4 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
);
};
export default ProductCategoryForm;
export default ProductCategoryForm;
@@ -19,6 +19,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Product } from '@/types/api/master-data/product';
import { ProductApi } from '@/services/api/master-data';
@@ -36,16 +37,7 @@ const RowOptionsMenu = ({
props: CellContext<Product, unknown>;
deleteClickHandler: () => void;
}) => (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
variant='ghost'
@@ -68,7 +60,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -78,7 +70,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
const ProductsTable = () => {
@@ -217,7 +209,7 @@ const ProductsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -280,10 +272,15 @@ const ProductsTable = () => {
<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='flex flex-row'>
<Button href='/master-data/product/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/product/add'
variant='outline'
className='w-full sm:w-fit'
color='primary'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Produk
Tambah
</Button>
</div>
<DebouncedTextInput
@@ -5,49 +5,50 @@ export const ProductFormSchema = Yup.object({
brand: Yup.string().required('Merek wajib diisi!'),
sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
uom_id: Yup.number().required('Satuan wajib diisi!').typeError('Satuan wajib diisi!'),
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
uom_id: Yup.number()
.required('Satuan wajib diisi!')
.typeError('Satuan wajib diisi!'),
product_category: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
product_category_id: Yup.number()
.required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'),
.required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'),
product_price: Yup.number()
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
supplier_ids: Yup.array()
.of(Yup.number().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
.of(Yup.number().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
flags: Yup.array()
.of(Yup.string())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
.of(Yup.string())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
});
export const UpdateProductFormSchema = ProductFormSchema;
export type ProductFormValues = Yup.InferType<typeof ProductFormSchema>;
@@ -24,7 +24,12 @@ import {
CreateProductPayload,
UpdateProductPayload,
} from '@/types/api/master-data/product';
import { UomApi, ProductCategoryApi, SupplierApi, ProductApi } from '@/services/api/master-data';
import {
UomApi,
ProductCategoryApi,
SupplierApi,
ProductApi,
} from '@/services/api/master-data';
import { cn } from '@/lib/helper';
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
@@ -67,30 +72,37 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
[router]
);
const formikInitialValues = useMemo<ProductFormValues>(() => ({
name: initialValues?.name ?? '',
brand: initialValues?.brand ?? '',
sku: initialValues?.sku ?? '',
uom: initialValues?.uom
? { value: initialValues.uom.id, label: initialValues.uom.name }
: null,
uom_id: initialValues?.uom?.id ?? 0,
product_category: initialValues?.product_category
? { value: initialValues.product_category.id, label: initialValues.product_category.name }
: null,
product_category_id: initialValues?.product_category?.id ?? 0,
product_price: initialValues?.product_price ?? 0,
selling_price: initialValues?.selling_price ?? 0,
tax: initialValues?.tax ?? 0,
expiry_period: initialValues?.expiry_period ?? 0,
supplier: null, // not used for payload, just for UI
supplier_ids: initialValues?.suppliers?.map(s => s.id) ?? [],
flags: initialValues?.flags ?? [],
}), [initialValues]);
const formikInitialValues = useMemo<ProductFormValues>(
() => ({
name: initialValues?.name ?? '',
brand: initialValues?.brand ?? '',
sku: initialValues?.sku ?? '',
uom: initialValues?.uom
? { value: initialValues.uom.id, label: initialValues.uom.name }
: null,
uom_id: initialValues?.uom?.id ?? 0,
product_category: initialValues?.product_category
? {
value: initialValues.product_category.id,
label: initialValues.product_category.name,
}
: null,
product_category_id: initialValues?.product_category?.id ?? 0,
product_price: initialValues?.product_price ?? 0,
selling_price: initialValues?.selling_price ?? 0,
tax: initialValues?.tax ?? 0,
expiry_period: initialValues?.expiry_period ?? 0,
supplier: null, // not used for payload, just for UI
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
flags: initialValues?.flags ?? [],
}),
[initialValues]
);
const formik = useFormik<ProductFormValues>({
initialValues: formikInitialValues,
validationSchema: type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
validationSchema:
type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
onSubmit: async (values) => {
setProductFormErrorMessage('');
const payload: CreateProductPayload = {
@@ -103,8 +115,12 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
selling_price: values.selling_price,
tax: values.tax,
expiry_period: values.expiry_period,
supplier_ids: (values.supplier_ids ?? []).filter((id): id is number => typeof id === 'number'),
flags: (values.flags ?? []).filter((f): f is string => typeof f === 'string'),
supplier_ids: (values.supplier_ids ?? []).filter(
(id): id is number => typeof id === 'number'
),
flags: (values.flags ?? []).filter(
(f): f is string => typeof f === 'string'
),
};
switch (type) {
case 'add':
@@ -122,7 +138,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
// UOM
const [uomSelectInputValue, setUomSelectInputValue] = useState('');
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`;
const { data: uoms, isLoading: isLoadingUoms } = useSWR(uomsUrl, UomApi.getAllFetcher);
const { data: uoms, isLoading: isLoadingUoms } = useSWR(
uomsUrl,
UomApi.getAllFetcher
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name }))
: [];
@@ -136,7 +155,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
// Product Category
const [categorySelectInputValue, setCategorySelectInputValue] = useState('');
const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`;
const { data: categories, isLoading: isLoadingCategories } = useSWR(categoriesUrl, ProductCategoryApi.getAllFetcher);
const { data: categories, isLoading: isLoadingCategories } = useSWR(
categoriesUrl,
ProductCategoryApi.getAllFetcher
);
const categoryOptions = isResponseSuccess(categories)
? categories?.data.map((cat) => ({ value: cat.id, label: cat.name }))
: [];
@@ -150,16 +172,22 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
// Supplier (multi select)
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(suppliersUrl, SupplierApi.getAllFetcher);
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
suppliersUrl,
SupplierApi.getAllFetcher
);
const supplierOptions = isResponseSuccess(suppliers)
? suppliers?.data
.filter((sup) => sup.category === 'SAPRONAK')
.map((sup) => ({ value: sup.id, label: sup.name }))
: [];
? suppliers?.data
.filter((sup) => sup.category === 'SAPRONAK')
.map((sup) => ({ value: sup.id, label: sup.name }))
: [];
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
formik.setFieldTouched('supplier_ids', true);
formik.setFieldValue('supplier_ids', arr.map((v) => (v as OptionType).value));
formik.setFieldValue(
'supplier_ids',
arr.map((v) => (v as OptionType).value)
);
};
const deleteProductClickHandler = () => {
@@ -260,7 +288,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
options={categoryOptions}
onInputChange={setCategorySelectInputValue}
isLoading={isLoadingCategories}
isError={formik.touched.product_category_id && Boolean(formik.errors.product_category_id)}
isError={
formik.touched.product_category_id &&
Boolean(formik.errors.product_category_id)
}
errorMessage={formik.errors.product_category_id as string}
isDisabled={type === 'detail'}
isClearable
@@ -274,7 +305,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
value={formik.values.product_price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.product_price && Boolean(formik.errors.product_price)}
isError={
formik.touched.product_price &&
Boolean(formik.errors.product_price)
}
errorMessage={formik.errors.product_price as string}
readOnly={type === 'detail'}
/>
@@ -287,7 +321,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
value={formik.values.selling_price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.selling_price && Boolean(formik.errors.selling_price)}
isError={
formik.touched.selling_price &&
Boolean(formik.errors.selling_price)
}
errorMessage={formik.errors.selling_price as string}
readOnly={type === 'detail'}
/>
@@ -313,7 +350,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
value={formik.values.expiry_period}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.expiry_period && Boolean(formik.errors.expiry_period)}
isError={
formik.touched.expiry_period &&
Boolean(formik.errors.expiry_period)
}
errorMessage={formik.errors.expiry_period as string}
readOnly={type === 'detail'}
/>
@@ -321,12 +361,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required
label='Supplier'
isMulti
value={supplierOptions.filter(opt => formik.values.supplier_ids.includes(opt.value))}
value={supplierOptions.filter((opt) =>
formik.values.supplier_ids.includes(opt.value)
)}
onChange={supplierChangeHandler}
options={supplierOptions}
onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers}
isError={formik.touched.supplier_ids && Boolean(formik.errors.supplier_ids)}
isError={
formik.touched.supplier_ids &&
Boolean(formik.errors.supplier_ids)
}
errorMessage={formik.errors.supplier_ids as string}
isDisabled={type === 'detail'}
isClearable
@@ -335,10 +380,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required
label='Flags'
isMulti
value={PRODUCT_FLAG_OPTIONS.filter(opt => formik.values.flags.includes(opt.value))}
onChange={val => {
value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
formik.values.flags.includes(opt.value)
)}
onChange={(val) => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
formik.setFieldValue('flags', arr.map((v) => (v as OptionType).value));
formik.setFieldValue(
'flags',
arr.map((v) => (v as OptionType).value)
);
}}
options={PRODUCT_FLAG_OPTIONS}
isError={formik.touched.flags && Boolean(formik.errors.flags)}
@@ -435,4 +485,4 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
);
};
export default ProductForm;
export default ProductForm;
@@ -8,6 +8,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -30,16 +31,7 @@ const RowOptions = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
variant='ghost'
@@ -72,7 +64,7 @@ const RowOptions = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -82,7 +74,7 @@ const RowOptions = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -226,10 +218,15 @@ const SuppliersTable = () => {
<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='flex flex-row'>
<Button href='/master-data/supplier/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/supplier/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Supplier
Tambah
</Button>
</div>
@@ -1,41 +1,44 @@
import * as Yup from 'yup';
export const SupplierFormSchema = Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
alias: Yup.string()
.matches(/^[A-Za-z0-9]+$/, 'Alias hanya boleh berisi huruf dan angka tanpa spasi atau simbol!')
name: Yup.string().required('Nama wajib diisi!'),
alias: Yup.string()
.matches(
/^[A-Za-z0-9]+$/,
'Alias hanya boleh berisi huruf dan angka tanpa spasi atau simbol!'
)
.max(5, 'Alias maksimal 5 karakter!')
.required('Alias wajib diisi!'),
pic: Yup.string().required('PIC wajib diisi!'),
type: Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
})
.required('Tipe wajib diisi!'),
category: Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
})
.required('Tipe wajib diisi!'),
hatchery: Yup.string().required('Hatchery wajib diisi!'),
phone: Yup.string()
.matches(/^[0-9]+$/, 'Nomor telepon hanya boleh berisi angka!')
.min(10, 'Nomor telepon minimal 10 digit!')
.max(12, 'Nomor telepon maksimal 12 digit!')
.required('Nomor telepon wajib diisi!'),
email: Yup.string()
.email('Format email tidak valid!')
.required('Email wajib diisi!'),
address: Yup.string().required('Alamat wajib diisi!'),
npwp: Yup.string()
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
.required('Nomor NPWP wajib diisi!'),
account_number: Yup.string()
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
.required('Nomor rekening wajib diisi!'),
due_date: Yup.number().min(1, 'Tanggal jatuh tempo wajib diisi!').required('Tanggal jatuh tempo wajib diisi!'),
pic: Yup.string().required('PIC wajib diisi!'),
type: Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
}).required('Tipe wajib diisi!'),
category: Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
}).required('Tipe wajib diisi!'),
hatchery: Yup.string().required('Hatchery wajib diisi!'),
phone: Yup.string()
.matches(/^[0-9]+$/, 'Nomor telepon hanya boleh berisi angka!')
.min(10, 'Nomor telepon minimal 10 digit!')
.max(12, 'Nomor telepon maksimal 12 digit!')
.required('Nomor telepon wajib diisi!'),
email: Yup.string()
.email('Format email tidak valid!')
.required('Email wajib diisi!'),
address: Yup.string().required('Alamat wajib diisi!'),
npwp: Yup.string()
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
.required('Nomor NPWP wajib diisi!'),
account_number: Yup.string()
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
.required('Nomor rekening wajib diisi!'),
due_date: Yup.number()
.min(1, 'Tanggal jatuh tempo wajib diisi!')
.required('Tanggal jatuh tempo wajib diisi!'),
});
export const UpdateSupplierFormSchema = SupplierFormSchema;
export type SupplierFormValues = Yup.InferType<typeof SupplierFormSchema>;
export type SupplierFormValues = Yup.InferType<typeof SupplierFormSchema>;
@@ -41,7 +41,9 @@ const SupplierForm = ({
// Setup State
const [supplierFormErrorMessage, setSupplierFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [hatcheryOptionsValues, setHatcheryOptionValues] = useState<OptionType[]>([]);
const [hatcheryOptionsValues, setHatcheryOptionValues] = useState<
OptionType[]
>([]);
// -- Options data mapping
const typeOptions = TYPE_OPTIONS;
@@ -167,7 +169,7 @@ const SupplierForm = ({
// Initialize Formik
useEffect(() => {
formikSetValues(formikInitialValues);
if(formType != 'add'){
if (formType != 'add') {
const hatcheryArrays = formikInitialValues.hatchery.split(',');
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
value: item,
@@ -177,11 +179,13 @@ const SupplierForm = ({
}
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
useEffect(() => {
const commaSeparatedValues = hatcheryOptionsValues.map((item) => item.value).join(',');
const commaSeparatedValues = hatcheryOptionsValues
.map((item) => item.value)
.join(',');
formikSetValues({
...formik.values,
hatchery: commaSeparatedValues,
})
});
}, [hatcheryOptionsValues, formikSetValues]);
// Option Handler
@@ -305,7 +309,9 @@ const SupplierForm = ({
console.log(val); // pastikan val = array of { value, label }
setHatcheryOptionValues(val as OptionType[]);
}}
isError={formik.touched.hatchery && Boolean(formik.errors.hatchery)}
isError={
formik.touched.hatchery && Boolean(formik.errors.hatchery)
}
errorMessage={formik.errors.hatchery as string}
isDisabled={formType === 'detail'}
isClearable
@@ -14,6 +14,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Uom } from '@/types/api/master-data/uom';
import { UomApi } from '@/services/api/master-data';
@@ -32,16 +33,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
variant='ghost'
@@ -66,7 +58,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -76,7 +68,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -150,7 +142,7 @@ const UomsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -199,10 +191,15 @@ const UomsTable = () => {
<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='flex flex-row'>
<Button href='/master-data/uom/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/uom/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah UOM
Tambah
</Button>
</div>
@@ -19,6 +19,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { WarehouseApi } from '@/services/api/master-data';
@@ -37,16 +38,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
variant='ghost'
@@ -81,7 +73,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -206,7 +198,7 @@ const WarehousesTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -277,10 +269,15 @@ const WarehousesTable = () => {
<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='flex flex-row'>
<Button href='/master-data/warehouse/add' color='primary'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/warehouse/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Warehouse
Tambah
</Button>
</div>
@@ -8,6 +8,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -87,7 +88,9 @@ const ChickinTable = () => {
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<Button
href='/production/chickin/add?projectFlockId=1'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='uil:plus' width={24} height={24} />
Tambah
@@ -130,20 +133,20 @@ const ChickinTable = () => {
} else {
return '-';
}
}
},
},
{
accessorFn: (row) => row.chick_in_date,
header: 'Tanggal Chickin',
cell: (props) => {
if (props.row.original.chick_in_date) {
return new Date(props.row.original.chick_in_date).toLocaleDateString(
'id-ID'
);
return new Date(
props.row.original.chick_in_date
).toLocaleDateString('id-ID');
} else {
return '-';
}
}
},
},
{
accessorFn: (row) => row.note,
@@ -166,7 +169,7 @@ const ChickinTable = () => {
deleteModal.openModal();
};
const editClickHandler = () => {
const editClickHandler = () => {
setSelectedChickin(props.row.original);
chickinModal.openModal();
};
@@ -240,7 +243,9 @@ const ChickinTable = () => {
<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 - { selectedChickin?.project_flock_kandang && selectedChickin?.project_flock_kandang.kandang?.name}
Chickin Kandang -{' '}
{selectedChickin?.project_flock_kandang &&
selectedChickin?.project_flock_kandang.kandang?.name}
</h1>
<Button
color='error'
@@ -255,10 +260,14 @@ const ChickinTable = () => {
/>
</Button>
</div>
<ChickinForm initialValues={selectedChickin} formType='edit' afterSubmit={() => {
refreshChickins()
chickinModal.closeModal()
}}/>
<ChickinForm
initialValues={selectedChickin}
formType='edit'
afterSubmit={() => {
refreshChickins();
chickinModal.closeModal();
}}
/>
</Modal>
</>
);
@@ -276,16 +285,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/production/chickin/detail?chickinId=${props.row.original.id}`}
variant='ghost'
@@ -308,7 +308,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -318,7 +318,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -3,9 +3,11 @@ import * as Yup from 'yup';
export const ChickinFormSchema = Yup.object({
chick_in_date: Yup.string().required('Tanggal masuk wajib diisi!'),
note: Yup.string().required('Catatan wajib diisi!'),
quantity: Yup.number().min(1, 'Jumlah wajib diisi!').required('Jumlah wajib diisi!'),
})
quantity: Yup.number()
.min(1, 'Jumlah wajib diisi!')
.required('Jumlah wajib diisi!'),
});
export type ChickinFormValues = Yup.InferType<typeof ChickinFormSchema>;
export const UpdateChickinFormSchema = ChickinFormSchema;
export const UpdateChickinFormSchema = ChickinFormSchema;
@@ -9,6 +9,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -37,16 +38,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
@@ -82,7 +74,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -92,7 +84,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -259,6 +251,7 @@ const ProjectFlockTable = () => {
<div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button
href='/production/project-flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
@@ -371,10 +364,10 @@ const ProjectFlockTable = () => {
const selectableRows = allRows.filter(
(row) => row.original?.approval?.step_number == 1
);
const allSelected = selectableRows.every((row) =>
row.getIsSelected()
) && selectableRows.length != 0;
const allSelected =
selectableRows.every((row) => row.getIsSelected()) &&
selectableRows.length != 0;
const someSelected =
selectableRows.some((row) => row.getIsSelected()) &&
@@ -508,7 +501,7 @@ const ProjectFlockTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -24,7 +24,8 @@ export const ProjectFlockFormSchema = Yup.object({
value: Yup.string().required('Nilai Kategori wajib diisi!'),
label: Yup.string().required('Label Kategori wajib diisi!'),
}).nullable(),
category: Yup.string().oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
category: Yup.string()
.oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
.required('Kategori wajib diisi!'),
// FCR
@@ -79,9 +79,8 @@ const ProjectFlockForm = ({
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
initialValues?.approval.step_name == 'Pengajuan' ? false : true
);
const [isRejectedDisabled, setIsRejectedDisabled] = useState(
!isApprovedDisabled
);
const [isRejectedDisabled, setIsRejectedDisabled] =
useState(!isApprovedDisabled);
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
);
@@ -143,10 +142,11 @@ const ProjectFlockForm = ({
search: '',
location_id: selectedLocation == '' ? '0' : selectedLocation,
}).toString()}`;
const { data: kandang, isLoading: isLoadingKandang, mutate: refreshKandang} = useSWR(
kandangUrl,
KandangApi.getAllFetcher
);
const {
data: kandang,
isLoading: isLoadingKandang,
mutate: refreshKandang,
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
const getPeriodFlocksUrl = `flocks/${selectedFlock}/periods`;
@@ -207,10 +207,7 @@ const ProjectFlockForm = ({
setOpenSelectKandangs(true);
const newRowSelection = Object.fromEntries(
initialValues.kandangs.map((k: Kandang) => [
k.id.toString(),
true,
])
initialValues.kandangs.map((k: Kandang) => [k.id.toString(), true])
);
setRowSelection(newRowSelection);
}
@@ -38,10 +38,13 @@ const ProjectFlockKandangTable = ({
const allSelected =
selectableRows.every((row) => row.getIsSelected()) &&
selectableRows.length != 0 && formType != 'detail';
selectableRows.length != 0 &&
formType != 'detail';
const someSelected =
selectableRows.some((row) => row.getIsSelected()) && !allSelected && formType != 'detail';
selectableRows.some((row) => row.getIsSelected()) &&
!allSelected &&
formType != 'detail';
const toggleSelectableRows = () => {
const shouldSelect = !allSelected;
@@ -14,6 +14,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import Table from '@/components/Table';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { type CellContext } from '@tanstack/react-table';
import { type Recording } from '@/types/api/production/recording';
@@ -126,16 +127,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`recording/detail/?recordingId=${props.row.original.id}`}
variant='ghost'
@@ -158,7 +150,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
@@ -168,7 +160,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -255,7 +247,7 @@ const RecordingTable = () => {
<TableToolbar
addButton={{
href: 'recording/add',
label: 'Tambah Recording',
label: 'Tambah',
}}
search={{
value: search,
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
@@ -43,16 +44,7 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
@@ -97,7 +89,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -107,7 +99,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
@@ -291,7 +283,7 @@ const TransferToLayingsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
type='collapse'
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
@@ -328,9 +320,8 @@ const TransferToLayingsTable = () => {
const confirmationModalApproveClickHandler = async () => {
setIsApproveLoading(true);
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
selectedRowIds
);
const bulkApproveResponse =
await TransferToLayingApi.bulkApprove(selectedRowIds);
if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings();
@@ -358,9 +349,8 @@ const TransferToLayingsTable = () => {
const confirmationModalRejectClickHandler = async () => {
setIsRejectLoading(true);
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
selectedRowIds
);
const bulkRejectResponse =
await TransferToLayingApi.bulkReject(selectedRowIds);
if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings();
@@ -437,11 +427,12 @@ const TransferToLayingsTable = () => {
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button
href='/production/transfer-to-laying/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Transfer ke Laying
Tambah
</Button>
{selectedRowIds.length > 0 && (
@@ -484,7 +475,9 @@ const TransferToLayingsTable = () => {
placeholder='Cari TransferToLaying'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
className={{
wrapper: 'sm:max-w-3xs',
}}
/>
</div>
@@ -497,7 +490,9 @@ const TransferToLayingsTable = () => {
placeholder='Masukkan tanggal transfer'
value={tableFilterState.transferDate}
onChange={transferDateChangeHandler}
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
className={{
wrapper: 'col-span-12 sm:col-span-3',
}}
/>
<SelectInput
+8 -1
View File
@@ -24,7 +24,14 @@ const StepItem = ({ children, icon, className, color }: StepItemProps) => {
return (
<li className={cn(stepItemBaseClassName, className)}>
<span className='step-icon'>{icon}</span>
<span
className={cn('step-icon', {
'transition-shadow shadow-[0_0_10px_2px_var(--color-warning)] hover:shadow-[0_0_15px_5px_var(--color-warning)]':
color === 'warning',
})}
>
{icon}
</span>
<div>{children}</div>
</li>
+1 -1
View File
@@ -16,7 +16,7 @@ const RowCollapseOptions = ({ children }: RowCollapseOptionsProps) => {
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</Button>
}
className='w-fit'
className='w-fit min-w-36'
titleClassName='p-0! justify-self-end'
>
{children}
@@ -0,0 +1,29 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
interface RowOptionsMenuWrapperProps {
children?: ReactNode;
type: 'dropdown' | 'collapse';
}
const RowOptionsMenuWrapper = ({
children,
type,
}: RowOptionsMenuWrapperProps) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content mr-2': type === 'dropdown',
'w-fit ml-auto mt-2': type === 'collapse',
},
'p-2.5 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<div className='flex flex-col gap-1'>{children}</div>
</div>
);
};
export default RowOptionsMenuWrapper;
+4 -13
View File
@@ -1,6 +1,6 @@
import { Icon } from '@iconify/react';
import Button from '../Button';
import { cn } from '@/lib/helper';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
interface TableRowOptionsProps {
type?: 'dropdown' | 'collapse';
@@ -21,16 +21,7 @@ export const TableRowOptions = ({
showEdit = true,
showDelete = true,
}: TableRowOptionsProps) => (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<RowOptionsMenuWrapper type={type}>
<Button
href={`${basePath}/detail/?${queryParam}=${recordId}`}
variant='ghost'
@@ -56,7 +47,7 @@ export const TableRowOptions = ({
onClick={onDelete}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
@@ -67,5 +58,5 @@ export const TableRowOptions = ({
Delete
</Button>
)}
</div>
</RowOptionsMenuWrapper>
);
+7 -2
View File
@@ -18,8 +18,13 @@ export const TableToolbar = ({ addButton, search }: TableToolbarProps) => {
return (
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
{addButton && (
<div className='flex flex-row'>
<Button href={addButton.href} color='primary'>
<div className='w-full flex flex-row'>
<Button
href={addButton.href}
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
{addButton.label}
</Button>
+12
View File
@@ -0,0 +1,12 @@
import { ApprovalLine } from '@/types/config/constant';
export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Aktif',
},
] as const;
+3 -4
View File
@@ -19,7 +19,7 @@ export const cn = (...inputs: ClassValue[]) => {
export const formatNumber = (
value: number | bigint | Intl.StringNumericLiteral,
locale = 'en-US',
locale = 'id-ID',
minimumFractionDigits = 0,
maximumFractionDigits = 2
) => {
@@ -29,11 +29,10 @@ export const formatNumber = (
}).format(value);
};
export const formatCurrency = (
value: number | bigint | Intl.StringNumericLiteral,
currency = 'USD',
locale = 'en-US',
currency = 'IDR',
locale = 'id-ID',
minimumFractionDigits = 0,
maximumFractionDigits = 2
) => {
+7 -4
View File
@@ -105,10 +105,13 @@ export class BaseApiService<T, CreatePayloadGeneric, UpdatePayloadGeneric> {
const url = options?.params
? `${urlBase}?${new URLSearchParams(
Object.entries(options.params).reduce((acc, [key, value]) => {
if (value !== undefined) acc[key] = String(value);
return acc;
}, {} as Record<string, string>)
Object.entries(options.params).reduce(
(acc, [key, value]) => {
if (value !== undefined) acc[key] = String(value);
return acc;
},
{} as Record<string, string>
)
)}`
: urlBase;
+1 -1
View File
@@ -140,4 +140,4 @@ export const FlockApi = new BaseApiService<
Flock,
CreateFlockPayload,
UpdateFlockPayload
>('/master-data/flocks');
>('/master-data/flocks');
+1 -1
View File
@@ -29,4 +29,4 @@ export const ChickinApi = new BaseApiService<
Chickin,
CreateChickinPayload,
UpdateChickinPayload
>('/production/chickins');
>('/production/chickins');
+5
View File
@@ -9,6 +9,11 @@
--step-fg: var(--color-error-content);
}
.step.step-warning::before {
--step-bg: var(--color-warning);
--step-fg: var(--color-warning-content);
}
.table :where(th, td) {
vertical-align: top;
}
+10 -10
View File
@@ -97,21 +97,21 @@ export type flags =
| 'FINISHER'
| 'OVK';
export type ApprovalsLine = {
action_by?: string;
date?: string;
notes?: string;
role?: string;
status: 'approved' | 'rejected' | 'waiting';
}[];
export type BaseApproval = {
step_number: number;
step_name: string;
action: string;
notes: string | null;
notes?: string | null;
action_by: CreatedUser;
action_at: string;
};
export type ApproveAction = 'APPROVED' | 'REJECTED';
export type BaseGroupedApproval = {
step_number: number;
step_name: string;
approvals: BaseApproval[];
};
export type Approvals = BaseApiResponse<BaseApproval>;
export type GroupedApprovals = BaseApiResponse<BaseGroupedApproval[]>;
+20 -20
View File
@@ -1,27 +1,27 @@
import { BaseMetadata, CreatedUser } from "@/types/api/api-general";
import { BaseMetadata, CreatedUser } from '@/types/api/api-general';
export type BaseCustomer = {
id: number;
name: string;
pic_id: number;
pic: CreatedUser;
type: string;
address: string;
phone: string;
email: string;
account_number: string;
}
id: number;
name: string;
pic_id: number;
pic: CreatedUser;
type: string;
address: string;
phone: string;
email: string;
account_number: string;
};
export type Customer = BaseMetadata & BaseCustomer;
export type CreateCustomerPayload = {
name: string;
pic_id: number;
type: string;
address: string;
phone: string;
email: string;
account_number: string;
}
name: string;
pic_id: number;
type: string;
address: string;
phone: string;
email: string;
account_number: string;
};
export type UpdateCustomerPayload = CreateCustomerPayload;
export type UpdateCustomerPayload = CreateCustomerPayload;
+1 -1
View File
@@ -34,4 +34,4 @@ export type CreateProductPayload = {
flags: string[];
};
export type UpdateProductPayload = CreateProductPayload;
export type UpdateProductPayload = CreateProductPayload;
+30 -30
View File
@@ -1,38 +1,38 @@
import { BaseMetadata } from '@/types/api/api-general';
export type BaseSupplier = {
id: number;
name: string;
alias: string;
pic: string;
type: string;
category: string;
hatchery: string;
phone: string;
email: string;
address: string;
npwp: string;
account_number: string;
due_date: number;
balance?: number;
}
id: number;
name: string;
alias: string;
pic: string;
type: string;
category: string;
hatchery: string;
phone: string;
email: string;
address: string;
npwp: string;
account_number: string;
due_date: number;
balance?: number;
};
export type Supplier = BaseMetadata & BaseSupplier;
export type CreateSupplierPayload = {
name: string;
alias: string;
pic: string;
type: string;
category: string;
hatchery: string;
phone: string;
email: string;
address: string;
npwp: string;
account_number: string;
due_date: number;
balance?: number;
}
name: string;
alias: string;
pic: string;
type: string;
category: string;
hatchery: string;
phone: string;
email: string;
address: string;
npwp: string;
account_number: string;
due_date: number;
balance?: number;
};
export type UpdateSupplierPayload = CreateSupplierPayload;
export type UpdateSupplierPayload = CreateSupplierPayload;
+5 -5
View File
@@ -1,5 +1,5 @@
import { BaseApproval, BaseMetadata } from "@/types/api/api-general";
import { ProjectFlockKandang } from "@/types/api/production/project-flock-kandang";
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
export type BaseChickin = {
id?: number;
@@ -8,7 +8,7 @@ export type BaseChickin = {
note?: string;
project_flock_kandang?: ProjectFlockKandang;
approval: BaseApproval;
}
};
export type Chickin = BaseMetadata & BaseChickin;
@@ -17,7 +17,7 @@ export type CreateChickinPayload = {
chick_in_date: string;
note: string;
quantity?: number;
}
};
export type UpdateChickinPayload = CreateChickinPayload & {
id: number;
@@ -26,4 +26,4 @@ export type UpdateChickinPayload = CreateChickinPayload & {
export type ChickinApprovalPayload = {
action: 'APPROVED' | 'REJECTED';
approvable_ids: number[];
};
};
+4 -4
View File
@@ -1,5 +1,5 @@
import { Kandang } from "@/type/master-data/kandang";
import { ProjectFlock } from "@/types/api/production/project-flock";
import { Kandang } from '@/type/master-data/kandang';
import { ProjectFlock } from '@/types/api/production/project-flock';
export type BaseProjectFlockKandang = {
id: number;
@@ -8,11 +8,11 @@ export type BaseProjectFlockKandang = {
kandang: Kandang;
project_flock: ProjectFlock;
available_quantity?: number;
}
};
export type ProjectFlockKandang = BaseProjectFlockKandang;
export type LookupProjectFlockKandangPayload = {
project_flock_id: number;
kandang_id: number;
}
};
+4
View File
@@ -0,0 +1,4 @@
export type ApprovalLine = {
step_number: number;
step_name: string;
}[];