Compare commits

..

56 Commits

Author SHA1 Message Date
rstubryan c832c4adeb fix(resolve): resolve merge issue 2025-10-29 15:56:57 +07:00
rstubryan eda3f0f1be chore(FE-Storyless): update Prettier to version 3.6.2 and remove .prettierrc from .gitignore 2025-10-29 15:52:34 +07:00
rstubryan c7b04c5bc6 feat(FE-137): integrate flock periods data fetching in RecordingForm for accurate recording validation 2025-10-28 10:58:37 +07:00
rstubryan c37950a230 refactor(FE-137): optimize recordedProjectFlockIds calculation to filter today's recordings 2025-10-28 10:44:08 +07:00
rstubryan 7da95b80b0 refactor(FE-Storyless): conditionally handle onChange prop in SelectInput for better flexibility 2025-10-28 08:58:01 +07:00
rstubryan c74ed18a16 refactor(FE-137): enable clearable option for select inputs in RecordingForm 2025-10-27 14:15:48 +07:00
rstubryan 15e6372c30 feat(FE-137): add product flag badges to RecordingForm for enhanced visibility 2025-10-27 13:03:37 +07:00
rstubryan 6dd3593f70 feat(FE-137): integrate Badge component to display project flock period in RecordingForm 2025-10-27 12:57:00 +07:00
rstubryan 5d376f8783 refactor(FE-137): remove unnecessary padding from SelectInput for improved layout 2025-10-27 12:56:38 +07:00
rstubryan 304d14a6fe refactor(FE-137): remove 'Periode' column from RecordingTable for cleaner display 2025-10-27 11:57:46 +07:00
rstubryan 0b0ecd3bc4 refactor(FE-137): replace stock availability text with Badge component in MovementForm 2025-10-27 11:25:15 +07:00
rstubryan 58369b8ffa refactor(FE-137): simplify stock display in MovementForm and RecordingForm, enhance input handling in SelectInput 2025-10-27 11:05:06 +07:00
rstubryan 943c0e05b9 refactor(FE-137): conditionally render location SelectInput in RecordingForm based on type 2025-10-27 06:50:48 +07:00
rstubryan 9143248e1d refactor(FE-137): remove redundant status column from RecordingTable 2025-10-27 06:28:51 +07:00
rstubryan 4b9d0d2064 refactor(FE-137): enhance RecordingForm validation to prevent duplicate project flock entries 2025-10-27 06:18:27 +07:00
rstubryan c8f596ad2a refactor(FE-137): update RecordingForm to improve project flock handling and label formatting 2025-10-27 05:54:14 +07:00
rstubryan 135fc2d5d3 feat(FE-114): update MovementForm and RecordingForm to use inputPrefix and inputSuffix for improved input handling 2025-10-25 14:24:51 +07:00
rstubryan 189c152745 feat(FE-114): add inputPrefix and inputSuffix props for enhanced input customization 2025-10-25 14:24:23 +07:00
rstubryan a0556ea1f4 refactor(FE-114): add currency prefix and unit suffix to delivery cost and body weight inputs 2025-10-25 13:53:53 +07:00
rstubryan 81ce36e326 refactor(FE-137): remove ID column from RecordingTable for cleaner presentation 2025-10-25 13:41:18 +07:00
rstubryan d7ce8c667a refactor(FE-114): simplify input handling in MovementForm and RecordingForm by removing unnecessary value normalization 2025-10-25 11:26:38 +07:00
rstubryan 6290199074 feat(FE-Storyless): integrate NumberInput and PatternInput components with react-number-format for enhanced input handling 2025-10-25 10:49:07 +07:00
rstubryan 896a0c6de2 refactor(FE-64): integrate product and supplier selection with API data fetching in MovementForm 2025-10-24 21:10:03 +07:00
rstubryan 9c5dc0dbb5 refactor(FE-137): integrate approve and reject functionality in RecordingForm with loading states and modal confirmations 2025-10-24 20:44:15 +07:00
rstubryan 81003eac63 feat(FE-137): enhance stock product selection in RecordingForm with initial values support 2025-10-24 20:37:11 +07:00
rstubryan e322e0d078 feat(FE-137): update RECORDING_FLAG_OPTIONS values for consistency in constant.ts 2025-10-24 20:29:33 +07:00
rstubryan 17e6eef0c5 feat(FE-137): add approve and reject functionality in RecordingForm with confirmation modals 2025-10-24 18:02:41 +07:00
rstubryan 6114d706ad feat(FE-137): disable input field in RecordingForm when type is 'detail' 2025-10-24 14:13:21 +07:00
rstubryan d14fa2ed2b feat(FE-137): integrate advanced filtering options in RecordingTable with dropdowns for area, location, and kandang 2025-10-24 13:53:20 +07:00
rstubryan 537fc617ff feat(FE-137): implement bulk approval and rejection functionality in RecordingTable with user feedback 2025-10-24 13:40:27 +07:00
rstubryan 7a6a35568f feat(FE-137): enhance RecordingTable to support recording deletion with user feedback and refresh functionality 2025-10-24 13:32:46 +07:00
rstubryan d2c485fdf0 feat(FE-114,137): implement stock validation in RecordingForm to manage usage limits and enhance user feedback 2025-10-24 12:45:07 +07:00
rstubryan 0c49978033 feat(FE-114,137): enhance RecordingForm to handle stock usage and depletion total changes with improved input handling 2025-10-24 12:26:33 +07:00
rstubryan 00de4782e7 feat(FE-137): simplify RecordingTable by removing unused columns and enhancing data clarity 2025-10-24 12:14:47 +07:00
rstubryan c546bd6b3c feat(FE-137): refactor RecordingTable to remove unused types and streamline data fetching 2025-10-24 11:37:25 +07:00
rstubryan 258324f092 feat(US-137): update RecordingTable to enhance data display and add new columns for project details 2025-10-24 11:36:14 +07:00
rstubryan 12a69b7c6c feat(FE-137): integrate SWR for fetching recordings and update table to display API data 2025-10-24 11:35:11 +07:00
rstubryan b148a09e84 feat(US-137): update API endpoints and default values in RecordingForm for production environment 2025-10-24 11:27:32 +07:00
rstubryan adc995dbe7 feat(US-114): enhance auto-calculation logic in RecordingForm to handle manual edits 2025-10-24 11:00:14 +07:00
rstubryan 9cbc703a63 feat(FE-114): integrate row selection functionality in RecordingTable and Table components 2025-10-24 10:18:56 +07:00
rstubryan 41e6848d75 refactor(FE-114): remove optional product_warehouse_id validation from RecordingForm schema 2025-10-24 10:08:38 +07:00
rstubryan ca5b236565 refactor(FE-114): enforce required usage amount in RecordingForm validation 2025-10-24 10:00:35 +07:00
rstubryan 714072aea1 fix(merge): resolve merge conflict 2025-10-24 09:57:38 +07:00
rstubryan a9f0696b38 refactor(FE-114): auto-populate notes with product name and enhance tooltip visibility in RecordingForm 2025-10-24 09:50:12 +07:00
rstubryan c30fcd81b2 refactor(FE-114): simplify CreateRecordingPayload structure and update validation in RecordingForm 2025-10-24 08:53:41 +07:00
rstubryan 7f5ae94706 feat(FE-114): integrate product stock fetching and selection in RecordingForm 2025-10-23 22:59:41 +07:00
rstubryan 6060ec0f7e feat(FE-114): prevent auto-calculation override during manual average weight editing in RecordingForm 2025-10-23 22:02:12 +07:00
rstubryan ef249fee12 feat(FE-114): add average weight calculation and input handling in RecordingForm 2025-10-23 21:54:06 +07:00
rstubryan 71df86c8df feat(FE-114): integrate location and project flock selection in RecordingForm 2025-10-23 21:34:40 +07:00
rstubryan d61c0ab844 feat(FE-114): integrate date time handling in RecordingForm for on-time status 2025-10-23 20:59:20 +07:00
rstubryan b653cc1dab refactor(FE-114): replace button elements with Button component for consistency and improved styling 2025-10-23 20:44:59 +07:00
rstubryan 392e211181 refactor(FE-Storyless): replace img with Image component for optimized loading 2025-10-23 19:54:17 +07:00
rstubryan cebe738beb refactor(FE-114): enhance type safety and improve checkbox input handling 2025-10-23 19:52:38 +07:00
rstubryan 6e5875a7b7 refactor(FE-Storyless): add flock_id, area_id, fcr_id, location_id, and kandang_ids to project-flock type definition 2025-10-23 19:52:21 +07:00
rstubryan db8cb56984 fix(merge): resolve conflict on merge 2025-10-23 18:24:02 +07:00
rstubryan 22f1a32e1b feat(FE-137): integrate API for daily recording with enhanced data structure and validation 2025-10-23 11:59:22 +07:00
109 changed files with 3383 additions and 3312 deletions
-3
View File
@@ -40,8 +40,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# prettier
.prettierrc
# idea
.idea
+69 -142
View File
@@ -1,149 +1,76 @@
stages:
- build
- deploy
stages: [notify]
# ====== TEMPLATE: BUILD STATIC NEXT.JS ======
.build_template: &build_template
stage: build
image: node:20-alpine
cache:
key: npm-cache
paths:
- node_modules/
variables:
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
.deploy_template: &deploy_template
stage: deploy
image:
name: amazon/aws-cli:latest
entrypoint: ["/bin/sh", "-c"]
script:
- set -e
- aws --version
- echo "Cleaning up newline characters in AWS credentials..."
- export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
- echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
- aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION"
- aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
# CloudFront invalidation
- |
STATUS="success"
if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
echo "Invalidating CloudFront cache..."
if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then
echo "CloudFront invalidation failed."
STATUS="failed"
fi
else
echo "No CloudFront distribution specified — skipping invalidation"
fi
# Notifikasi Discord
- |
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
# 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
# --- Notify when MR is opened/updated ---
notify_discord_mr:
stage: notify
image: alpine:3.20
rules:
- if: '$CI_COMMIT_BRANCH == "devops-s3"'
environment:
name: devops-s3
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
NEXT_PUBLIC_API_BASE_URL: "https://dev-api-lti.mbugroup.id"
NEXT_PUBLIC_SSO_LOGIN_URL: "https://dev-api-sso.mbugroup.id"
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}"
deploy:dev:
<<: *deploy_template
needs: ["build:dev"]
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"
# --- Notify when MR is merged ---
notify_discord_merge:
stage: notify
image: alpine:3.20
rules:
- if: '$CI_COMMIT_BRANCH == "devops-s3"'
# Only run for merge request pipelines that are in merged state
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
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
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}"
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
@@ -1,25 +0,0 @@
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
@@ -1,39 +0,0 @@
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",
],
},
];
+1 -1
View File
@@ -39,7 +39,7 @@
"eslint": "^9",
"eslint-config-next": "15.5.3",
"husky": "^9.1.7",
"prettier": "^3.6.2",
"prettier": "3.6.2",
"tailwindcss": "^4",
"typescript": "^5"
}
+2 -3
View File
@@ -7,8 +7,7 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"prepare": "husky",
"format": "prettier --write ."
"prepare": "husky"
},
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
@@ -42,7 +41,7 @@
"eslint": "^9",
"eslint-config-next": "15.5.3",
"husky": "^9.1.7",
"prettier": "^3.6.2",
"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;
+4 -2
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,6 +37,8 @@
--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;
+7 -8
View File
@@ -7,12 +7,11 @@ 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 }
@@ -25,20 +24,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;
+15 -17
View File
@@ -1,47 +1,45 @@
'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;
+16 -19
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,36 +14,33 @@ 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,44 +9,39 @@ 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();
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;
}
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
<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>
);
}
}
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;
export default ProductCategoryEdit;
@@ -29,24 +29,16 @@ 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,8 +13,9 @@ 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) {
@@ -41,4 +42,4 @@ const ProductEdit = () => {
);
};
export default ProductEdit;
export default ProductEdit;
+4 -3
View File
@@ -13,8 +13,9 @@ 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) {
@@ -41,4 +42,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,8 +43,9 @@ 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'
);
@@ -263,8 +264,7 @@ 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,47 +1,46 @@
'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,11 +42,7 @@ 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;
+18 -15
View File
@@ -1,11 +1,14 @@
'use client';
import { HTMLAttributes, ReactNode } from 'react';
import {
HTMLAttributes,
ReactNode,
} from 'react';
import { cn } from '@/lib/helper';
import Image from 'next/image';
export interface CardProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
title?: string;
subtitle?: string;
image?: string;
@@ -42,17 +45,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(
@@ -82,9 +85,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);
@@ -106,7 +109,7 @@ const Card = ({
return (
<div className={getCardClasses()} {...props}>
<figure>
<img
<Image
src={image}
alt={imageAlt || title || 'Card image'}
className={getImageClasses()}
@@ -127,7 +130,7 @@ const Card = ({
<div className={getCardClasses()} {...props}>
{image && (
<figure>
<img
<Image
src={image}
alt={imageAlt || title || 'Card image'}
className={getImageClasses()}
+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}
/>
+65 -15
View File
@@ -9,6 +9,11 @@ interface FormActionsProps<T> {
editUrl?: string;
onDelete?: () => void;
disableSubmit?: boolean;
onApprove?: () => void;
onReject?: () => void;
isApproveLoading?: boolean;
isRejectLoading?: boolean;
showApproveReject?: boolean;
}
export const FormActions = <T,>({
@@ -17,25 +22,32 @@ export const FormActions = <T,>({
editUrl,
onDelete,
disableSubmit = false,
onApprove,
onReject,
isApproveLoading = false,
isRejectLoading = false,
showApproveReject = false,
}: FormActionsProps<T>) => {
return (
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && onDelete && (
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={onDelete}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{onDelete && (
<Button
type='button'
color='error'
onClick={onDelete}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
)}
{type !== 'edit' && editUrl && (
<Button
type='button'
@@ -52,6 +64,44 @@ export const FormActions = <T,>({
Edit
</Button>
)}
{type === 'detail' && showApproveReject && (onApprove || onReject) && (
<>
{onApprove && (
<Button
type='button'
color='success'
onClick={onApprove}
className='px-4'
isLoading={isApproveLoading}
>
<Icon
icon='material-symbols:check-circle-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Approve
</Button>
)}
{onReject && (
<Button
type='button'
color='error'
onClick={onReject}
className='px-4'
isLoading={isRejectLoading}
>
<Icon
icon='material-symbols:cancel-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Reject
</Button>
)}
</>
)}
</div>
)}
{type !== 'detail' && (
+9 -2
View File
@@ -1,6 +1,10 @@
'use client';
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
import {
ChangeEventHandler,
FocusEventHandler,
ReactNode,
} from 'react';
import { cn } from '@/lib/helper';
@@ -105,7 +109,10 @@ 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}
/>
+4 -1
View File
@@ -69,7 +69,10 @@ 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}
/>
+2 -2
View File
@@ -49,8 +49,8 @@ const NumberInput = ({
onValueChange={valueChangeHandler}
decimalScale={decimalScale}
allowNegative={allowNegative}
startAdornment={inputPrefix}
endAdornment={inputSuffix}
inputPrefix={inputPrefix}
inputSuffix={inputSuffix}
{...restProps}
/>
);
+60
View File
@@ -0,0 +1,60 @@
'use client';
import { ChangeEvent } from 'react';
import { PatternFormat, OnValueChange } from 'react-number-format';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
interface PatternInputProps extends Omit<TextInputProps, 'type'> {
type?: 'password' | 'tel' | 'text' | undefined;
/** Format pattern, e.g. "##/##/####", "(###) ###-####", "####-####-####" */
format: string;
/** Mask character for empty slots, e.g. "_" */
mask?: string;
/** Allow showing mask even when value is empty */
allowEmptyFormatting?: boolean;
patternChar?: string;
}
const PatternInput = ({
type = 'text',
format,
mask = '_',
allowEmptyFormatting = false,
patternChar = '#',
onChange,
...restProps
}: PatternInputProps) => {
const valueChangeHandler: OnValueChange = (
patternFormatValues,
sourceInfo
) => {
const newChangeEvent = sourceInfo.event as
| ChangeEvent<HTMLInputElement>
| undefined;
if (newChangeEvent) {
newChangeEvent.target.value = patternFormatValues.value;
onChange?.(newChangeEvent);
}
};
return (
<PatternFormat
type={type}
format={format}
mask={mask}
allowEmptyFormatting={allowEmptyFormatting}
patternChar={patternChar}
customInput={TextInput}
onValueChange={valueChangeHandler}
{...restProps}
/>
);
};
export default PatternInput;
+25 -1
View File
@@ -53,6 +53,7 @@ interface SelectInputBaseProps<T = OptionType> {
openMenu?: boolean;
delay?: number;
onInputChange?: (search: string) => void;
startAdornment?: ReactNode;
}
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
@@ -87,6 +88,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
delay = 300,
createables = false,
onInputChange,
startAdornment,
} = props;
const [internalInputValue, setInternalInputValue] = useState('');
@@ -149,7 +151,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
<SelectComponent<T, boolean, GroupBase<T>>
instanceId='select'
value={value ?? (isMulti ? [] : null)}
onChange={handleChange}
onChange={onChange ? handleChange : undefined}
options={options}
menuIsOpen={openMenu}
inputValue={internalInputValue}
@@ -210,6 +212,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
components={{
...components,
...(optionComponent ? { Option: optionComponent } : {}),
...(startAdornment ? {
Control: ({ children, innerRef, innerProps, menuIsOpen, isFocused, isDisabled }) => (
<div
ref={innerRef}
{...innerProps}
className={cn(
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer! flex items-center',
{
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'border-gray-300': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
}
)}
>
<div className='flex-1 px-4! gap-1 flex items-center'>
{startAdornment}
{children}
</div>
</div>
),
} : {}),
}}
menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined
+111 -29
View File
@@ -31,6 +31,8 @@ export interface TextInputProps {
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
inputPrefix?: ReactNode;
inputSuffix?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}
@@ -48,6 +50,8 @@ const TextInput = ({
errorMessage,
startAdornment,
endAdornment,
inputPrefix,
inputSuffix,
disabled = false,
required = false,
onChange,
@@ -85,39 +89,117 @@ const TextInput = ({
</label>
)}
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
{inputPrefix || inputSuffix ? (
<div className='relative flex'>
{inputPrefix && (
<div
className={cn(
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
{
'bg-gray-100 border-gray-300': !disabled,
'bg-gray-50 border-gray-200': disabled,
}
)}
>
{inputPrefix}
</div>
)}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
<div
className={cn(
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
{
'border-error': isError,
'border-success!': isValid,
'rounded-l-none!': inputPrefix,
'rounded-r-none!': inputSuffix,
'input-disabled': disabled,
'cursor-not-allowed': disabled,
'bg-gray-50': disabled,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn(
'grow bg-transparent outline-none',
{
'cursor-not-allowed': disabled,
'text-gray-500': disabled,
},
className?.input
)}
readOnly={readOnly}
/>
{endAdornment && endAdornment}
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
)}
</div>
{inputSuffix && (
<div
className={cn(
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
{
'bg-gray-100 border-gray-300': !disabled,
'bg-gray-50 border-gray-200': disabled,
}
)}
>
{inputSuffix}
</div>
)}
</div>
) : (
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
)}
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
+2 -6
View File
@@ -49,18 +49,14 @@ const MenuItem = ({
);
return (
<li>
<li onClick={onClick}>
{href && (
<Link href={href} className={menuItemBaseClassName}>
{menuItemContent}
</Link>
)}
{!href && (
<button className={menuItemBaseClassName} onClick={onClick}>
{menuItemContent}
</button>
)}
{!href && <a className={menuItemBaseClassName}>{menuItemContent}</a>}
</li>
);
};
+30 -147
View File
@@ -3,24 +3,11 @@ import Steps from '@/components/steps/Steps';
import StepItem from '@/components/steps/StepItem';
import Tooltip from '@/components/Tooltip';
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;
};
import { formatDate } from '@/lib/helper';
import { ApprovalsLine } from '@/types/api/api-general';
interface ApprovalStepsProps {
approvals: {
name?: string;
status: ApprovalStepStatus;
logs?: ApprovalStepLog[];
}[];
approvals: ApprovalsLine;
}
const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
@@ -28,77 +15,45 @@ 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'
: approval.status === 'WAITING'
? 'warning'
: undefined;
: approval.status === 'rejected'
? 'error'
: undefined;
const stepItemIcon =
approval.status === 'APPROVED'
approval.status === 'approved'
? 'material-symbols:check-rounded'
: 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';
: approval.status === 'rejected'
? 'material-symbols:close-rounded'
: 'bxs:hourglass';
return (
<StepItem
key={idx}
color={stepItemColor}
icon={
<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.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>
)
}
>
{approval.name}
{approval.role}
</StepItem>
);
})}
@@ -106,76 +61,4 @@ 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,7 +10,11 @@ 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';
@@ -40,7 +44,10 @@ const InventoryAdjustmentTable = () => {
});
// Fetch Data
const { data: inventoryAdjustments, isLoading } = useSWR(
const {
data: inventoryAdjustments,
isLoading,
} = useSWR(
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
inventoryAdjustmentApi.getAllFetcher
);
@@ -106,8 +113,8 @@ const InventoryAdjustmentTable = () => {
type === 'INCREASE'
? 'Peningkatan'
: type === 'DECREASE'
? 'Penurunan'
: '-';
? 'Penurunan'
: '-';
return (
<div
@@ -180,13 +187,8 @@ 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='w-full flex flex-row'>
<Button
href='/inventory/adjustment/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/inventory/adjustment/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
@@ -209,7 +211,7 @@ const InventoryAdjustmentTable = () => {
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'min-w-28' }}
className={{ wrapper: 'max-w-28' }}
/>
</div>
</div>
@@ -4,21 +4,24 @@ 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,8 +51,9 @@ 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(
@@ -67,9 +68,7 @@ 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,
@@ -186,6 +185,7 @@ const InventoryAdjustmentForm = ({
warehouseChangeHandler(null);
};
const { setValues: formikSetValues } = formik;
// Effect
@@ -225,13 +225,7 @@ 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]);
@@ -370,19 +364,15 @@ 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'
@@ -405,6 +395,8 @@ const InventoryAdjustmentForm = ({
readOnly={type === 'detail'}
/>
{/* Text Area Input Reason */}
<TextArea
required
@@ -421,23 +413,14 @@ 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',
label: 'Tambah Movement',
}}
search={{
value: tableFilterState.search,
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,6 @@ 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';
@@ -33,7 +32,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
variant='ghost'
@@ -68,7 +76,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -142,7 +150,7 @@ const AreasTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -191,15 +199,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/area/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/area/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Area
</Button>
</div>
@@ -14,7 +14,6 @@ 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';
@@ -33,7 +32,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
variant='ghost'
@@ -58,7 +66,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -68,7 +76,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -155,7 +163,7 @@ const BanksTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -204,15 +212,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/bank/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/bank/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Bank
</Button>
</div>
@@ -8,7 +8,6 @@ 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';
@@ -16,7 +15,10 @@ 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';
@@ -31,7 +33,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
variant='ghost'
@@ -42,10 +53,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
@@ -54,7 +65,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -64,7 +75,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -163,7 +174,7 @@ const CustomersTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -199,15 +210,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/customer/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/customer/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Customer
</Button>
</div>
@@ -279,4 +285,4 @@ const CustomersTable = () => {
);
};
export default CustomersTable;
export default CustomersTable;
@@ -11,11 +11,7 @@ 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';
@@ -154,8 +150,7 @@ 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,7 +14,6 @@ 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';
@@ -33,7 +32,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
variant='ghost'
@@ -58,7 +66,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -68,7 +76,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -142,7 +150,7 @@ const FcrsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -191,15 +199,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/fcr/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/fcr/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah FCR
</Button>
</div>
@@ -12,7 +12,6 @@ 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';
@@ -31,7 +30,16 @@ const RowsOptions = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
variant='ghost'
@@ -46,7 +54,7 @@ const RowsOptions = ({
/>
Edit
</Button>
<Button
<Button
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
variant='ghost'
color='primary'
@@ -64,7 +72,7 @@ const RowsOptions = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -74,7 +82,7 @@ const RowsOptions = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -195,15 +203,9 @@ 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='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
<div className='flex flex-row'>
<Button href='/master-data/flock/add' color='primary'>
Tambah Flock
</Button>
</div>
@@ -273,4 +275,4 @@ const FlockTable = () => {
);
};
export default FlockTable;
export default FlockTable;
@@ -3,7 +3,10 @@ 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,15 +1,11 @@
'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';
@@ -52,8 +48,7 @@ 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,7 +19,6 @@ 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';
@@ -38,7 +37,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
variant='ghost'
@@ -63,7 +71,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -73,7 +81,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -165,7 +173,7 @@ const KandangsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -230,15 +238,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/kandang/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/kandang/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Kandang
</Button>
</div>
@@ -19,7 +19,6 @@ 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';
@@ -38,7 +37,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
variant='ghost'
@@ -63,7 +71,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -73,7 +81,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -164,7 +172,7 @@ const LocationsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -229,15 +237,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/location/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/location/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Location
</Button>
</div>
@@ -19,7 +19,6 @@ 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';
@@ -38,7 +37,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
variant='ghost'
@@ -63,7 +71,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -73,7 +81,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -176,7 +184,7 @@ const NonstocksTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -241,15 +249,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/nonstock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/nonstock/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Nonstock
</Button>
</div>
@@ -14,7 +14,6 @@ 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';
@@ -33,7 +32,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
variant='ghost'
@@ -56,7 +64,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='mdi:delete-outline'
@@ -66,7 +74,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -146,7 +154,7 @@ const ProductCategoryTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -192,15 +200,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/product-category/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/product-category/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Product Category
</Button>
</div>
<DebouncedTextInput
@@ -1,14 +1,10 @@
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,10 +30,7 @@ interface ProductCategoryFormProps {
initialValues?: ProductCategory;
}
const ProductCategoryForm = ({
type = 'add',
initialValues,
}: ProductCategoryFormProps) => {
const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFormProps) => {
const router = useRouter();
const deleteModal = useModal();
@@ -80,10 +77,7 @@ const ProductCategoryForm = ({
const formik = useFormik<ProductCategoryFormValues>({
initialValues: formikInitialValues,
validationSchema:
type === 'edit'
? UpdateProductCategoryFormSchema
: ProductCategoryFormSchema,
validationSchema: type === 'edit' ? UpdateProductCategoryFormSchema : ProductCategoryFormSchema,
onSubmit: async (values) => {
setFormErrorMessage('');
@@ -97,10 +91,7 @@ const ProductCategoryForm = ({
await createProductCategoryHandler(payload);
break;
case 'edit':
await updateProductCategoryHandler(
initialValues?.id as number,
payload
);
await updateProductCategoryHandler(initialValues?.id as number, payload);
break;
}
},
@@ -272,4 +263,4 @@ const ProductCategoryForm = ({
);
};
export default ProductCategoryForm;
export default ProductCategoryForm;
@@ -19,7 +19,6 @@ 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';
@@ -37,7 +36,16 @@ const RowOptionsMenu = ({
props: CellContext<Product, unknown>;
deleteClickHandler: () => void;
}) => (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
variant='ghost'
@@ -60,7 +68,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -70,7 +78,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
const ProductsTable = () => {
@@ -209,7 +217,7 @@ const ProductsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -272,15 +280,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/product/add'
variant='outline'
className='w-full sm:w-fit'
color='primary'
>
<div className='flex flex-row'>
<Button href='/master-data/product/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Produk
</Button>
</div>
<DebouncedTextInput
@@ -5,50 +5,49 @@ 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,12 +24,7 @@ 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';
@@ -72,37 +67,30 @@ 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 = {
@@ -115,12 +103,8 @@ 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':
@@ -138,10 +122,7 @@ 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 }))
: [];
@@ -155,10 +136,7 @@ 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 }))
: [];
@@ -172,22 +150,16 @@ 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 = () => {
@@ -288,10 +260,7 @@ 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
@@ -305,10 +274,7 @@ 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'}
/>
@@ -321,10 +287,7 @@ 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'}
/>
@@ -350,10 +313,7 @@ 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'}
/>
@@ -361,17 +321,12 @@ 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
@@ -380,15 +335,10 @@ 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)}
@@ -485,4 +435,4 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
);
};
export default ProductForm;
export default ProductForm;
@@ -8,7 +8,6 @@ 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';
@@ -31,7 +30,16 @@ const RowOptions = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
variant='ghost'
@@ -64,7 +72,7 @@ const RowOptions = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -74,7 +82,7 @@ const RowOptions = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -218,15 +226,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/supplier/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/supplier/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Supplier
</Button>
</div>
@@ -1,44 +1,41 @@
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,9 +41,7 @@ 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;
@@ -169,7 +167,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,
@@ -179,13 +177,11 @@ 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
@@ -309,9 +305,7 @@ 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,7 +14,6 @@ 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';
@@ -33,7 +32,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
variant='ghost'
@@ -58,7 +66,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -68,7 +76,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -142,7 +150,7 @@ const UomsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -191,15 +199,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/uom/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/uom/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah UOM
</Button>
</div>
@@ -19,7 +19,6 @@ 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';
@@ -38,7 +37,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
variant='ghost'
@@ -73,7 +81,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -198,7 +206,7 @@ const WarehousesTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -269,15 +277,10 @@ 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='w-full flex flex-row'>
<Button
href='/master-data/warehouse/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href='/master-data/warehouse/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
Tambah Warehouse
</Button>
</div>
@@ -8,7 +8,6 @@ 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';
@@ -88,9 +87,7 @@ 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
@@ -133,20 +130,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,
@@ -169,7 +166,7 @@ const ChickinTable = () => {
deleteModal.openModal();
};
const editClickHandler = () => {
const editClickHandler = () => {
setSelectedChickin(props.row.original);
chickinModal.openModal();
};
@@ -243,9 +240,7 @@ 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'
@@ -260,14 +255,10 @@ const ChickinTable = () => {
/>
</Button>
</div>
<ChickinForm
initialValues={selectedChickin}
formType='edit'
afterSubmit={() => {
refreshChickins();
chickinModal.closeModal();
}}
/>
<ChickinForm initialValues={selectedChickin} formType='edit' afterSubmit={() => {
refreshChickins()
chickinModal.closeModal()
}}/>
</Modal>
</>
);
@@ -285,7 +276,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<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='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -318,7 +318,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -3,11 +3,9 @@ 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,7 +9,6 @@ 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';
@@ -38,7 +37,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
@@ -74,7 +82,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -84,7 +92,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -251,7 +259,6 @@ 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'
>
@@ -364,10 +371,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()) &&
@@ -501,7 +508,7 @@ const ProjectFlockTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
@@ -24,8 +24,7 @@ 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,8 +79,9 @@ 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'
);
@@ -142,11 +143,10 @@ 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,7 +207,10 @@ 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,13 +38,10 @@ 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;
@@ -1,6 +1,7 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { SortingState } from '@tanstack/react-table';
import { cn } from '@/lib/helper';
@@ -8,114 +9,24 @@ import { useModal } from '@/components/Modal';
import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput';
import SelectInput from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar';
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';
const dummyRecordings: Recording[] = [
{
id: 1,
flock: {
id: 1,
name: 'Flock Recording 1',
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
recording_date: '2024-01-01',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
coop: {
id: 1,
name: 'Coop 1',
status: 'ACTIVE',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
},
pic: {
id: 1,
id_user: 1,
email: 'pic@example.com',
name: 'PIC User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
feed_data: [
{
feed_name: 'Feed 1',
feed_qty: 100,
feed_stock: 500,
},
],
body_weight: [
{
chicken_weight: 2.5,
chicken_count: 1000,
average_chicken_weight: 2.5,
},
],
vaccination: [
{
vaccine_name: 'Vaccine 1',
total_stock: 200,
used_stock: 150,
},
],
mortality: [
{
condition: 'NORMAL',
count: 5,
},
],
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
];
import { type BaseApiResponse } from '@/types/api/api-general';
import { RecordingApi } from '@/services/api/production';
import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data';
import { KandangApi } from '@/services/api/master-data';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -127,7 +38,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`recording/detail/?recordingId=${props.row.original.id}`}
variant='ghost'
@@ -150,7 +70,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='mdi:delete-outline'
@@ -160,17 +80,39 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
const RecordingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
areaFilter: '',
locationFilter: '',
kandangFilter: '',
periodFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
areaFilter: 'area_id',
locationFilter: 'location_id',
kandangFilter: 'kandang_id',
periodFilter: 'period',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedRecordings, setSelectedRecordings] = useState<number[]>([]);
const [, setSelectedRecording] = useState<Recording | undefined>(undefined);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false);
@@ -179,12 +121,81 @@ const RecordingTable = () => {
const bulkApproveModal = useModal();
const bulkRejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(null);
const {
data: recordings,
isLoading,
mutate: refreshRecordings,
} = useSWR(
`${RecordingApi.basePath}${getTableFilterQueryString()}`,
RecordingApi.getAllFetcher
);
// Fetch data for dropdowns
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
search: areaSelectInputValue,
limit: '100',
}).toString()}`;
const {
data: areas,
isLoading: isLoadingAreas,
} = useSWR(areaUrl, AreaApi.getAllFetcher);
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
search: locationSelectInputValue,
area_id: selectedArea != null ? selectedArea.value.toString() : '',
limit: '100',
}).toString()}`;
const {
data: locations,
isLoading: isLoadingLocations,
} = useSWR(locationUrl, LocationApi.getAllFetcher);
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: kandangSelectInputValue,
location_id:
selectedLocation != null ? selectedLocation.value.toString() : '',
limit: '100',
}).toString()}`;
const {
data: kandangs,
isLoading: isLoadingKandang,
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
// Data to Options Mapping
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const optionsLocation = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
const optionsKandang = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
updateFilter('search', e.target.value);
setPage(1);
},
[]
[updateFilter, setPage]
);
const pageSizeChangeHandler = useCallback(
@@ -193,52 +204,80 @@ const RecordingTable = () => {
setPageSize(newVal.value as number);
setPage(1);
},
[]
[setPageSize, setPage]
);
const paginatedData = useMemo(() => {
const filteredData = dummyRecordings.filter(
(recording) =>
recording.flock.name.toLowerCase().includes(search.toLowerCase()) ||
recording.location.name.toLowerCase().includes(search.toLowerCase()) ||
recording.coop.name.toLowerCase().includes(search.toLowerCase())
);
const start = (page - 1) * pageSize;
return filteredData.slice(start, start + pageSize);
}, [page, pageSize, search]);
if (!recordings || recordings.status !== 'success') return [];
return recordings.data;
}, [recordings]);
const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item));
const bulkApproveHandler = async () => {
setIsBulkApproveLoading(true);
console.log(
'Approved recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => {
setIsBulkApproveLoading(false);
setSelectedRecordings([]);
const approveResponse = await RecordingApi.customRequest<
BaseApiResponse<Recording[]>
>('approvals', {
method: 'POST',
payload: {
action: 'APPROVED',
approvable_ids: selectedRowIds,
notes: 'Bulk Approved',
},
});
if (isResponseSuccess(approveResponse)) {
await refreshRecordings();
setRowSelection({});
bulkApproveModal.closeModal();
}, 1000);
toast.success(`Successfully approved ${selectedRowIds.length} recordings!`);
}
if (isResponseError(approveResponse)) {
toast.error(approveResponse?.message as string);
bulkApproveModal.closeModal();
}
setIsBulkApproveLoading(false);
};
const bulkRejectHandler = async () => {
setIsBulkRejectLoading(true);
console.log(
'Rejected recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => {
setIsBulkRejectLoading(false);
setSelectedRecordings([]);
const rejectResponse = await RecordingApi.customRequest<
BaseApiResponse<Recording[]>
>('approvals', {
method: 'POST',
payload: {
action: 'REJECTED',
approvable_ids: selectedRowIds,
notes: 'Bulk Rejected',
},
});
if (isResponseSuccess(rejectResponse)) {
refreshRecordings();
setRowSelection({});
bulkRejectModal.closeModal();
}, 1000);
toast.success(`Successfully rejected ${selectedRowIds.length} recordings!`);
}
if (isResponseError(rejectResponse)) {
toast.error(rejectResponse?.message as string);
bulkRejectModal.closeModal();
}
setIsBulkRejectLoading(false);
};
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
setTimeout(() => {
setIsDeleteLoading(false);
singleDeleteModal.closeModal();
}, 1000);
await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
};
return (
@@ -247,24 +286,192 @@ const RecordingTable = () => {
<TableToolbar
addButton={{
href: 'recording/add',
label: 'Tambah',
label: 'Tambah Recording',
}}
search={{
value: search,
value: tableFilterState.search,
onChange: searchChangeHandler,
placeholder: 'Cari Recording',
}}
/>
<TableRowSizeSelector
value={pageSize}
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
{/* Filter Dropdowns - Desktop */}
<div className='hidden sm:grid sm:grid-cols-4 gap-4 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
{/* Filter Dropdowns - Mobile */}
<div className='sm:hidden flex flex-col gap-3 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
</div>
{/* Bulk action buttons */}
<div className={'flex justify-end items-center'}>
{selectedRecordings.length > 0 && (
{selectedRowIds.length > 0 && (
<div className='flex gap-2 mb-4'>
<Button
type='button'
@@ -277,7 +484,7 @@ const RecordingTable = () => {
width={20}
height={20}
/>
Approve ({selectedRecordings.length})
Approve ({selectedRowIds.length})
</Button>
<Button
type='button'
@@ -290,7 +497,7 @@ const RecordingTable = () => {
width={20}
height={20}
/>
Reject ({selectedRecordings.length})
Reject ({selectedRowIds.length})
</Button>
</div>
)}
@@ -298,7 +505,7 @@ const RecordingTable = () => {
<ConfirmationModal
ref={bulkApproveModal.ref}
type='success'
text={`Apakah anda yakin ingin menyetujui ${selectedRecordings.length} data Recording yang dipilih?`}
text={`Apakah anda yakin ingin menyetujui ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -313,7 +520,7 @@ const RecordingTable = () => {
<ConfirmationModal
ref={bulkRejectModal.ref}
type='error'
text={`Apakah anda yakin ingin menolak ${selectedRecordings.length} data Recording yang dipilih?`}
text={`Apakah anda yakin ingin menolak ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -331,75 +538,83 @@ const RecordingTable = () => {
columns={[
{
id: 'select',
accessorKey: 'id',
header: ({ table }) => (
<input
type='checkbox'
className='checkbox'
checked={
table.getRowModel().rows.length > 0 &&
table
.getRowModel()
.rows.every((row) => selectedRecordings.includes(row.index))
}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings(
table.getRowModel().rows.map((row) => row.index)
);
} else {
setSelectedRecordings([]);
}
}}
/>
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }) => (
<input
type='checkbox'
className='checkbox'
checked={selectedRecordings.includes(row.index)}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings([...selectedRecordings, row.index]);
} else {
setSelectedRecordings(
selectedRecordings.filter((i) => i !== row.index)
);
}
}}
/>
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
},
{
header: '#',
cell: (props) => pageSize * (page - 1) + props.row.index + 1,
cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1,
},
{
accessorKey: 'flock.name',
header: 'Flock',
header: 'Nama Project',
cell: (props) => `Project ${props.row.original.project_flock_kandang_id}`,
},
{
accessorKey: 'recording_date',
header: 'Tanggal Recording',
header: 'Umur (hari)',
cell: (props) => props.row.original.day,
},
{
accessorKey: 'record_date',
header: 'Waktu Recording',
cell: (props) =>
new Date(props.row.original.recording_date).toLocaleDateString(),
new Date(props.row.original.record_date).toLocaleDateString(),
},
{
accessorKey: 'location.name',
header: 'Lokasi',
header: 'Populasi Awal',
cell: (props) => props.row.original.total_chick?.toLocaleString() || '-',
},
{
accessorKey: 'coop.name',
header: 'Kandang',
header: 'BW',
cell: (props) => props.row.original.avg_daily_gain?.toFixed(2) || '-',
},
{
accessorKey: 'mortality',
header: 'Total Mortality',
header: 'Pakan',
cell: (props) => props.row.original.cum_intake?.toLocaleString() || '-',
},
{
header: 'FCR',
cell: (props) => props.row.original.fcr_value?.toFixed(2) || '-',
},
{
accessorKey: 'total_depletion',
header: 'Total Deplesi',
cell: (props) => props.row.original.total_depletion,
},
{
header: 'Deplesi (%)',
cell: (props) => props.row.original.daily_depletion_rate?.toFixed(2) || '-',
},
{
header: 'Populasi Akhir',
cell: (props) => (props.row.original.total_chick - props.row.original.total_depletion)?.toLocaleString() || '-',
},
{
header: 'Ketepatan Waktu',
cell: (props) => props.row.original.ontime ? 'Tepat Waktu' : 'Terlambat',
},
{
header: 'Tanggal Submit',
cell: (props) =>
props.row.original.mortality.reduce(
(acc, curr) => acc + curr.count,
0
),
new Date(props.row.original.created_at).toLocaleString(),
},
{
header: 'Aksi',
@@ -444,13 +659,15 @@ const RecordingTable = () => {
},
},
]}
pageSize={pageSize}
page={page}
totalItems={dummyRecordings.length}
pageSize={tableFilterState.pageSize}
page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page}
totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0}
onPageChange={setPage}
isLoading={false}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'mb-20': paginatedData.length === 0,
@@ -469,7 +686,7 @@ const RecordingTable = () => {
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Recording ini?`}
text={`Apakah anda yakin ingin menghapus data Recording ini (ID: ${selectedRecording?.id})?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -1,212 +1,222 @@
import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { Recording } from '@/types/api/production/recording';
import {
Recording,
CreateRecordingPayload,
} from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({
flock: Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
flock_id: Yup.number()
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Flock wajib diisi!')
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-flock',
'Flock wajib diisi!',
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Flock wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
location_id: Yup.number()
.default(0)
.typeError('Lokasi wajib diisi!')
.required('Project Flock Kandang wajib diisi!')
.test(
'is-valid-location',
'Lokasi wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Lokasi wajib diisi!'),
coop: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
coop_id: Yup.number()
.default(0)
.typeError('Kandang wajib diisi!')
.test(
'is-valid-coop',
'Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Kandang wajib diisi!'),
recording_date: Yup.date()
.required('Tanggal recording wajib diisi')
.typeError('Format tanggal tidak valid'),
feed_data: Yup.array()
'not-already-recorded',
'Project Flock ini sudah direcord hari ini!',
function(value) {
const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set<number>;
const formType = this.options.context?.type as 'add' | 'edit' | 'detail';
if (formType !== 'add') return true;
if (value && recordedProjectFlockIds?.has(value)) {
return false;
}
return true;
}
),
body_weights: Yup.array()
.of(
Yup.object({
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.mixed<number | ''>().notRequired(),
feed_stock: Yup.number()
.required('Jumlah pakan yang digunakan wajib diisi!')
.min(1, 'Jumlah pakan minimal 1!')
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
.test(
'is-not-exceed-qty',
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { feed_qty } = this.parent;
if (value === undefined) return true;
if (
feed_qty === undefined ||
feed_qty === '' ||
typeof feed_qty !== 'number'
)
return true;
return value <= feed_qty;
}
),
})
)
.min(1, 'Minimal harus ada 1 data pakan!')
.required('Data pakan wajib diisi!'),
body_weight: Yup.array()
.of(
Yup.object({
chicken_weight: Yup.number()
weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
chicken_count: Yup.number()
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!'),
average_chicken_weight: Yup.number()
.required('Rata-rata berat ayam wajib diisi!')
.min(1, 'Rata-rata berat ayam minimal 1 gram!')
.typeError('Rata-rata berat ayam harus berupa angka!'),
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
vaccination: Yup.array()
stocks: Yup.array()
.of(
Yup.object({
vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
total_stock: Yup.mixed<number | ''>().notRequired(),
used_stock: Yup.number()
.required('Jumlah vaksin yang digunakan wajib diisi!')
.min(1, 'Jumlah vaksin minimal 1!')
.typeError('Jumlah vaksin yang digunakan harus berupa angka!')
.test(
'is-not-exceed-total',
'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { total_stock } = this.parent;
if (value === undefined) return true;
if (
total_stock === undefined ||
total_stock === '' ||
typeof total_stock !== 'number'
)
return true;
return value <= total_stock;
}
),
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
usage_amount: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
})
)
.min(1, 'Minimal harus ada 1 data vaksinasi!')
.required('Data vaksinasi wajib diisi!'),
mortality: Yup.array()
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(
Yup.object({
condition: Yup.mixed<string>()
total: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf(
RECORDING_FLAG_OPTIONS.map((opt) => opt.value),
'Kondisi tidak valid!'
RECORDING_FLAG_OPTIONS.map((option) => option.value),
'Kondisi depletions tidak valid!'
)
.required('Kondisi wajib diisi!'),
count: Yup.number()
.required('Jumlah mortalitas wajib diisi!')
.min(1, 'Jumlah mortalitas minimal 1 ekor!')
.typeError('Jumlah mortalitas harus berupa angka!'),
.typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 data mortalitas!')
.required('Data mortalitas wajib diisi!'),
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export const UpdateRecordingFormSchema = RecordingFormSchema;
export const UpdateRecordingFormSchema = Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
body_weights: Yup.array()
.of(
Yup.object({
weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!')
.default(1),
average_weight: Yup.number()
.optional()
.min(0, 'Rata-rata berat tidak boleh negatif!')
.typeError('Rata-rata berat harus berupa angka!')
.default(0),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(
Yup.object({
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
usage_amount: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(0, 'Jumlah penggunaan tidak boleh negatif!')
.typeError('Jumlah penggunaan harus berupa angka!'),
notes: Yup.string().optional(),
})
)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(
Yup.object({
total: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
notes: Yup.string()
.required('Kondisi depletions wajib diisi!')
.oneOf(
RECORDING_FLAG_OPTIONS.map((option) => option.value),
'Kondisi depletions tidak valid!'
)
.typeError('Kondisi depletions harus berupa teks!')
.min(1, 'Kondisi depletions wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateRecordingPayload['body_weights'];
stocks?: CreateRecordingPayload['stocks'];
depletions?: CreateRecordingPayload['depletions'];
};
export const getRecordingFormInitialValues = (
initialValues?: Recording
initialValues?: RecordingFormData
): RecordingFormValues => ({
flock: initialValues?.flock
project_flock_kandang: initialValues?.project_flock_kandang_id
? {
value: initialValues.flock.id,
label: initialValues.flock.name,
value: initialValues.project_flock_kandang_id,
label: `Project Flock #${initialValues.project_flock_kandang_id}`,
}
: null,
flock_id: initialValues?.flock?.id ?? 0,
location: initialValues?.location
? {
value: initialValues.location.id,
label: initialValues.location.name,
}
: null,
location_id: initialValues?.location?.id ?? 0,
coop: initialValues?.coop
? {
value: initialValues.coop.id,
label: initialValues.coop.name,
}
: null,
coop_id: initialValues?.coop?.id ?? 0,
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{
feed_id: '',
feed_qty: '',
feed_stock: 0,
},
],
body_weight: initialValues?.body_weight ?? [
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateRecordingPayload['body_weights']>[0]) => ({
weight: bw.weight,
qty: bw.qty,
average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0,
})
) ?? [
{
chicken_weight: 0,
chicken_count: 0,
average_chicken_weight: 0,
weight: 0,
qty: 0,
average_weight: 0,
},
],
vaccination: initialValues?.vaccination
? initialValues.vaccination.map((vaccine) => ({
vaccine_id: vaccine.vaccine_name,
total_stock: vaccine.total_stock,
used_stock: vaccine.used_stock,
}))
: [
{
vaccine_id: '',
total_stock: '',
used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
stocks: initialValues?.stocks?.map(
(stock: NonNullable<CreateRecordingPayload['stocks']>[0]) => ({
product_warehouse_id: stock.product_warehouse_id,
usage_amount: stock.usage_amount,
notes: stock.notes,
})
) ?? [
{
condition: '',
count: 0,
product_warehouse_id: 0,
usage_amount: 0,
notes: '',
},
],
depletions: initialValues?.depletions?.map(
(depletion: NonNullable<CreateRecordingPayload['depletions']>[0]) => ({
product_warehouse_id: depletion.product_warehouse_id,
total: depletion.total,
notes: depletion.notes,
})
) ?? [
{
product_warehouse_id: 0,
total: 0,
notes: '',
},
],
});
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
return;
}
toast.success(res?.message as string);
router.push('/flock/recording');
router.push('/production/recording');
},
[router]
);
@@ -38,7 +38,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
}
toast.success(res?.message as string);
router.refresh();
router.push('/flock/recording');
router.push('/production/recording');
},
[router]
);
@@ -55,7 +55,7 @@ export const useRecordingFormHandlers = (initialValuesId?: number) => {
deleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
router.push('/flock/recording');
router.push('/production/recording');
}, [deleteModal, initialValuesId, router]);
return {
@@ -19,7 +19,6 @@ 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';
@@ -44,7 +43,16 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
@@ -89,7 +97,7 @@ const RowOptionsMenu = ({
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
@@ -99,7 +107,7 @@ const RowOptionsMenu = ({
/>
Delete
</Button>
</RowOptionsMenuWrapper>
</div>
);
};
@@ -283,7 +291,7 @@ const TransferToLayingsTable = () => {
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
type='dropdown'
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
@@ -320,8 +328,9 @@ const TransferToLayingsTable = () => {
const confirmationModalApproveClickHandler = async () => {
setIsApproveLoading(true);
const bulkApproveResponse =
await TransferToLayingApi.bulkApprove(selectedRowIds);
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
selectedRowIds
);
if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings();
@@ -349,8 +358,9 @@ const TransferToLayingsTable = () => {
const confirmationModalRejectClickHandler = async () => {
setIsRejectLoading(true);
const bulkRejectResponse =
await TransferToLayingApi.bulkReject(selectedRowIds);
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
selectedRowIds
);
if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings();
@@ -427,12 +437,11 @@ 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
Tambah Transfer ke Laying
</Button>
{selectedRowIds.length > 0 && (
@@ -475,9 +484,7 @@ const TransferToLayingsTable = () => {
placeholder='Cari TransferToLaying'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{
wrapper: 'sm:max-w-3xs',
}}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
@@ -490,9 +497,7 @@ 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
+1 -8
View File
@@ -24,14 +24,7 @@ const StepItem = ({ children, icon, className, color }: StepItemProps) => {
return (
<li className={cn(stepItemBaseClassName, className)}>
<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>
<span className='step-icon'>{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 min-w-36'
className='w-fit'
titleClassName='p-0! justify-self-end'
>
{children}
@@ -1,29 +0,0 @@
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;
+13 -4
View File
@@ -1,6 +1,6 @@
import { Icon } from '@iconify/react';
import Button from '../Button';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { cn } from '@/lib/helper';
interface TableRowOptionsProps {
type?: 'dropdown' | 'collapse';
@@ -21,7 +21,16 @@ export const TableRowOptions = ({
showEdit = true,
showDelete = true,
}: TableRowOptionsProps) => (
<RowOptionsMenuWrapper type={type}>
<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'
)}
>
<Button
href={`${basePath}/detail/?${queryParam}=${recordId}`}
variant='ghost'
@@ -47,7 +56,7 @@ export const TableRowOptions = ({
onClick={onDelete}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon
icon='mdi:delete-outline'
@@ -58,5 +67,5 @@ export const TableRowOptions = ({
Delete
</Button>
)}
</RowOptionsMenuWrapper>
</div>
);
+2 -7
View File
@@ -18,13 +18,8 @@ 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='w-full flex flex-row'>
<Button
href={addButton.href}
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<div className='flex flex-row'>
<Button href={addButton.href} color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
{addButton.label}
</Button>
-12
View File
@@ -1,12 +0,0 @@
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 -3
View File
@@ -221,7 +221,7 @@ export const SUPPLIER_FLAG_OPTIONS = [
];
export const RECORDING_FLAG_OPTIONS = [
{ label: 'Ayam Afkir', value: 'Ayam Afkir' },
{ label: 'Ayam Culling', value: 'Ayam Culling' },
{ label: 'Ayam Mati', value: 'Ayam Mati' },
{ label: 'Ayam Afkir', value: 'Afkir' },
{ label: 'Ayam Culling', value: 'Culling' },
{ label: 'Ayam Mati', value: 'Mati' },
];
+4 -3
View File
@@ -19,7 +19,7 @@ export const cn = (...inputs: ClassValue[]) => {
export const formatNumber = (
value: number | bigint | Intl.StringNumericLiteral,
locale = 'id-ID',
locale = 'en-US',
minimumFractionDigits = 0,
maximumFractionDigits = 2
) => {
@@ -29,10 +29,11 @@ export const formatNumber = (
}).format(value);
};
export const formatCurrency = (
value: number | bigint | Intl.StringNumericLiteral,
currency = 'IDR',
locale = 'id-ID',
currency = 'USD',
locale = 'en-US',
minimumFractionDigits = 0,
maximumFractionDigits = 2
) => {
+4 -7
View File
@@ -105,13 +105,10 @@ 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
@@ -24,7 +24,7 @@ export const RecordingApi = new BaseApiService<
Recording,
CreateRecordingPayload,
UpdateRecordingPayload
>('/flock/recordings');
>('/production/recordings');
export const ChickinApi = new BaseApiService<
Chickin,
CreateChickinPayload,

Some files were not shown because too many files have changed in this diff Show More