mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1621f2ab7d | |||
| 5540787154 | |||
| 1b499bc967 | |||
| 44a5c51023 | |||
| aa13e989c1 | |||
| ebe7c367e7 | |||
| 2f085c287f | |||
| 058f9f403d | |||
| 8b8b7be4b7 | |||
| efcecf4f66 | |||
| a6c63a7dcb | |||
| 0263db9fae | |||
| cc08e3af15 | |||
| 0929461ec5 | |||
| ace6633f79 | |||
| f1a952ca6b | |||
| ed34a99117 | |||
| 4beaba1f15 | |||
| 8ea029efdd | |||
| 02e4dba288 | |||
| c42fdbf33d | |||
| 2cfa8c046b | |||
| 30d5516161 | |||
| f83abc91da | |||
| 918c51e83b | |||
| f1a4d9b648 | |||
| 29e33560f8 | |||
| fb9e863862 | |||
| 1b3e5f94f1 | |||
| e1856926ea | |||
| 2b096099d3 | |||
| ea25417e8d | |||
| deabb1c3ee | |||
| 121c44070c | |||
| 0dbad23cd5 | |||
| b9a17f472b | |||
| c07b245eeb | |||
| d7e32f8f5b | |||
| 698fe2e851 | |||
| cdf0442a2b | |||
| 422c7c9fb0 | |||
| 3042b54577 | |||
| e5a686e5ee | |||
| 37d5a6b675 | |||
| 2ff32094ce | |||
| 7207f1ba75 | |||
| 41d2e8737b | |||
| b2016314f5 | |||
| 7366d6490c | |||
| e5e9b517fd | |||
| b6629b0bbb | |||
| bac6766fa2 | |||
| 53e018aece | |||
| ca58e19a48 | |||
| 0971e6ddeb | |||
| 25fbf95062 | |||
| b2f6c6c485 | |||
| cc86151631 | |||
| 755f3fa0bb | |||
| ce1114d724 | |||
| 128b765045 | |||
| 92c07e7841 | |||
| 1aba297920 | |||
| 2aef6522bb | |||
| 3bab96c325 | |||
| 847772616e | |||
| 344140e973 | |||
| 3ce1299091 | |||
| aea35d4b9f | |||
| 5b134148a5 | |||
| 32f4cf411f | |||
| 04d01970aa | |||
| 84cbbaf238 | |||
| 9176373072 | |||
| 5c50e4a0c1 | |||
| 7e64ec0f79 | |||
| e2be39af18 | |||
| 9322d6298c | |||
| e9cd84e89e | |||
| 89cfd31155 | |||
| ec5962bccc | |||
| 0eb4fa99a7 | |||
| 2ef8b2dc9f | |||
| aed1a1ed01 | |||
| 2c9c2660c0 | |||
| b840f42ae0 | |||
| 6bc86af32f | |||
| 1603ae62e0 | |||
| 8fd442621a | |||
| 35471fc597 | |||
| bd4242c4fd | |||
| 56bde974ad | |||
| 38258e4311 | |||
| 149e525ff4 | |||
| 8fb761f02c | |||
| 3bc5a5b75e | |||
| 79112e0da8 | |||
| bf9eb91ea2 | |||
| e8c8ffadfe | |||
| 2ae1c5b382 | |||
| 961f81411b | |||
| de439275e0 |
+30
-2
@@ -15,7 +15,7 @@ default:
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
stage: build
|
stage: build
|
||||||
image: node:20-alpine
|
image: public.ecr.aws/docker/library/node:20-alpine
|
||||||
cache:
|
cache:
|
||||||
key: npm-cache
|
key: npm-cache
|
||||||
paths:
|
paths:
|
||||||
@@ -56,7 +56,7 @@ default:
|
|||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image:
|
image:
|
||||||
name: amazon/aws-cli:latest
|
name: public.ecr.aws/aws-cli/aws-cli:latest
|
||||||
entrypoint: ['/bin/sh', '-c']
|
entrypoint: ['/bin/sh', '-c']
|
||||||
script:
|
script:
|
||||||
- set -e
|
- set -e
|
||||||
@@ -183,3 +183,31 @@ deploy:staging:
|
|||||||
environment:
|
environment:
|
||||||
name: staging
|
name: staging
|
||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ====== STAGING (Branch production) ======
|
||||||
|
# ==========================================================
|
||||||
|
build:production:
|
||||||
|
<<: *build_template
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
variables:
|
||||||
|
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
|
||||||
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||||
|
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||||
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
|
||||||
|
deploy:production:
|
||||||
|
<<: *deploy_template
|
||||||
|
needs: ['build:production']
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
variables:
|
||||||
|
S3_BUCKET: 'production-lti-erp.mbugroup.id'
|
||||||
|
AWS_REGION: 'ap-southeast-3'
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
url: https://lti-erp.mbugroup.id
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM public.ecr.aws/docker/library/node:20-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache git bash build-base curl
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
||||||
|
|
||||||
|
const MasterKandangPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterKandangContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterKandangPage;
|
||||||
@@ -66,7 +66,7 @@ const ExpenseRealizationForm = ({
|
|||||||
toast.success(createExpenseRes?.message as string);
|
toast.success(createExpenseRes?.message as string);
|
||||||
router.push('/expense');
|
router.push('/expense');
|
||||||
},
|
},
|
||||||
[router, initialValues?.id]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateExpenseHandler = useCallback(
|
const updateExpenseHandler = useCallback(
|
||||||
|
|||||||
@@ -178,14 +178,12 @@ const ExpenseRequestForm = ({
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
loadMore: loadMoreLocationOptions,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: supplierOptions,
|
options: supplierOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
loadMore: loadMoreVendorOptions,
|
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -416,7 +414,6 @@ const ExpenseRequestForm = ({
|
|||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
onMenuScrollToBottom={loadMoreLocationOptions}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -460,7 +457,6 @@ const ExpenseRequestForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.supplier_id as string}
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
onMenuScrollToBottom={loadMoreVendorOptions}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.document'>
|
<RequirePermission permissions='lti.expense.document'>
|
||||||
|
|||||||
@@ -1,154 +1,212 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import {
|
||||||
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
|
Document,
|
||||||
|
Image,
|
||||||
|
Link,
|
||||||
|
Page,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
|
||||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
|
||||||
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
|
||||||
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
|
||||||
|
|
||||||
interface ExpensePDFProps {
|
interface ExpensePDFProps {
|
||||||
expense?: Expense;
|
expense?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const ExpensePDFStyle = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
fontSize: 10,
|
paddingTop: 24,
|
||||||
fontFamily: 'Helvetica',
|
paddingBottom: 64,
|
||||||
padding: 20,
|
paddingHorizontal: 32,
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
},
|
},
|
||||||
titleSection: {
|
|
||||||
marginBottom: 10,
|
companyInfoHeader: {
|
||||||
},
|
width: '100%',
|
||||||
parameterContainer: {
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
infoTableSection: {
|
companyLogo: {
|
||||||
marginBottom: 12,
|
width: 64,
|
||||||
|
height: 'auto',
|
||||||
},
|
},
|
||||||
infoTableTitle: {
|
companyInfoHeaderDate: {
|
||||||
fontSize: 10,
|
paddingTop: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
fontSize: 12,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginBottom: 6,
|
marginBottom: 4,
|
||||||
color: '#333',
|
|
||||||
},
|
},
|
||||||
tableSection: {
|
companyAddress: {
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
tableTitle: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 6,
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
color: '#666',
|
maxWidth: 400,
|
||||||
fontStyle: 'italic',
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: '150%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontFamily: 'Times-Roman',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
|
||||||
|
position: 'absolute',
|
||||||
|
fontSize: 10,
|
||||||
|
bottom: 30,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
|
||||||
|
// wrapper
|
||||||
|
generalInfoTable: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000000',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
|
||||||
|
generalInfoTableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
},
|
||||||
|
|
||||||
|
// columns
|
||||||
|
generalInfoTableColLabel: {
|
||||||
|
width: '30%',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
generalInfoTableColSeparator: {
|
||||||
|
width: '3%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
|
generalInfoTableColValue: {
|
||||||
|
width: '67%',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
generalInfoTableLabelText: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
generalInfoTableValueText: {},
|
||||||
|
|
||||||
|
// expense detail table
|
||||||
|
expenseDetailContainer: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
expenseDetailTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '150%',
|
||||||
|
fontFamily: 'Times-Roman',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
kandangExpenseContainer: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
kandangExpenseTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '150%',
|
||||||
|
fontFamily: 'Times-Roman',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
kandangExpenseTable: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000000',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
kandangExpenseTableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
},
|
||||||
|
kandangExpenseTableColLabel: {
|
||||||
|
width: '20%',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
kandangExpenseTableColLabelBorderRight: {
|
||||||
|
borderRight: '1px solid #000000',
|
||||||
|
},
|
||||||
|
kandangExpenseTableColNonstock: {
|
||||||
|
width: '20%',
|
||||||
|
},
|
||||||
|
kandangExpenseTableColNote: {
|
||||||
|
width: '40%',
|
||||||
|
},
|
||||||
|
kandangExpenseHeaderLabelText: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
kandangExpenseLabelText: {
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
kandangExpenseTableFooterColTotalExpenseCaption: {
|
||||||
|
width: '40%',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
kandangExpenseTableFooterColTotalExpenseValue: {
|
||||||
|
width: '60%',
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// utils
|
||||||
|
doubleDivider: {
|
||||||
|
width: '100%',
|
||||||
|
height: 6,
|
||||||
|
borderTop: '2px solid black',
|
||||||
|
borderBottom: '2px solid black',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type ExpenseKandang = Expense['kandangs'][number];
|
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||||
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
|
|
||||||
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
|
|
||||||
|
|
||||||
const valueText = (v: unknown) => {
|
|
||||||
if (v === null || v === undefined) return '-';
|
|
||||||
if (typeof v === 'number') return formatNumber(v);
|
|
||||||
return String(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
header: 'No',
|
|
||||||
flex: 0.5,
|
|
||||||
align: 'center',
|
|
||||||
cell: ({ index }) => index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nonstock',
|
|
||||||
header: 'Nonstock',
|
|
||||||
flex: 1.5,
|
|
||||||
cell: ({ row }) => row.nonstock.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'qty',
|
|
||||||
header: 'Kuantitas',
|
|
||||||
flex: 1,
|
|
||||||
align: 'right',
|
|
||||||
cell: ({ row }) => valueText(row.qty),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'price',
|
|
||||||
header: 'Harga Satuan',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
cell: ({ row }) => formatCurrency(row.price),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'notes',
|
|
||||||
header: 'Catatan',
|
|
||||||
flex: 1.5,
|
|
||||||
cell: ({ row }) => row.notes || '-',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getRealisasiColumns = (): PdfColumn<RealisasiItem>[] => [
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
header: 'No',
|
|
||||||
flex: 0.5,
|
|
||||||
align: 'center',
|
|
||||||
cell: ({ index }) => index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nonstock',
|
|
||||||
header: 'Nonstock',
|
|
||||||
flex: 1.5,
|
|
||||||
cell: ({ row }) => row.nonstock.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'qty',
|
|
||||||
header: 'Kuantitas',
|
|
||||||
flex: 1,
|
|
||||||
align: 'right',
|
|
||||||
cell: ({ row }) => valueText(row.qty),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'price',
|
|
||||||
header: 'Harga Satuan',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
cell: ({ row }) => formatCurrency(row.price),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'notes',
|
|
||||||
header: 'Catatan',
|
|
||||||
flex: 1.5,
|
|
||||||
cell: ({ row }) => row.notes || '-',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getInfoTableRows = (expense?: Expense) => {
|
|
||||||
const isLatestApprovalRejected =
|
const isLatestApprovalRejected =
|
||||||
expense?.latest_approval?.action === 'REJECTED';
|
expense?.latest_approval?.action === 'REJECTED';
|
||||||
const isExpenseRealized =
|
const isExpenseRealized =
|
||||||
expense?.latest_approval?.step_number &&
|
expense?.latest_approval?.step_number &&
|
||||||
expense?.latest_approval.step_number >= 5;
|
expense?.latest_approval.step_number >= 5;
|
||||||
|
|
||||||
const realizationStatus = isExpenseRealized
|
const realizationStatus = isExpenseRealized
|
||||||
? 'Sudah Realisasi'
|
? 'Sudah Realisasi'
|
||||||
: 'Belum Realisasi';
|
: 'Belum Realisasi';
|
||||||
|
|
||||||
return [
|
const rows = [
|
||||||
{ label: 'Nomor PO', value: expense?.po_number || '-' },
|
{ label: 'Nomor PO', value: expense?.po_number },
|
||||||
{ label: 'Nomor Referensi', value: expense?.reference_number || '-' },
|
{ label: 'Nomor Referensi', value: expense?.reference_number },
|
||||||
{
|
{
|
||||||
label: 'Kategori',
|
label: 'Kategori',
|
||||||
value:
|
value:
|
||||||
@@ -156,9 +214,9 @@ const getInfoTableRows = (expense?: Expense) => {
|
|||||||
? 'Biaya Operasional'
|
? 'Biaya Operasional'
|
||||||
: expense?.category === 'NON-BOP'
|
: expense?.category === 'NON-BOP'
|
||||||
? 'Non Biaya Operasional'
|
? 'Non Biaya Operasional'
|
||||||
: '-',
|
: '',
|
||||||
},
|
},
|
||||||
{ label: 'Lokasi', value: expense?.location?.name || '-' },
|
{ label: 'Lokasi', value: expense?.location.name },
|
||||||
{
|
{
|
||||||
label: 'Kandang',
|
label: 'Kandang',
|
||||||
value:
|
value:
|
||||||
@@ -169,7 +227,7 @@ const getInfoTableRows = (expense?: Expense) => {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Vendor', value: expense?.supplier?.name || '-' },
|
{ label: 'Vendor', value: expense?.supplier.name },
|
||||||
{
|
{
|
||||||
label: 'Tanggal Transaksi',
|
label: 'Tanggal Transaksi',
|
||||||
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
|
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
|
||||||
@@ -180,12 +238,12 @@ const getInfoTableRows = (expense?: Expense) => {
|
|||||||
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
|
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Nama Pengaju', value: expense?.created_user?.name || '-' },
|
{ label: 'Nama Pengaju', value: expense?.created_user.name },
|
||||||
{
|
{
|
||||||
label: 'Nominal Biaya',
|
label: 'Nominal Biaya',
|
||||||
value: formatCurrency(
|
value: formatCurrency(
|
||||||
expense?.latest_approval?.step_number === 5 ||
|
expense?.latest_approval.step_number === 5 ||
|
||||||
expense?.latest_approval?.step_number === 6
|
expense?.latest_approval.step_number === 6
|
||||||
? (expense?.total_realisasi ?? 0)
|
? (expense?.total_realisasi ?? 0)
|
||||||
: (expense?.total_pengajuan ?? 0)
|
: (expense?.total_pengajuan ?? 0)
|
||||||
),
|
),
|
||||||
@@ -205,136 +263,401 @@ const getInfoTableRows = (expense?: Expense) => {
|
|||||||
label: 'Status Biaya',
|
label: 'Status Biaya',
|
||||||
value: isLatestApprovalRejected
|
value: isLatestApprovalRejected
|
||||||
? 'Ditolak'
|
? 'Ditolak'
|
||||||
: expense?.latest_approval?.step_name || '-',
|
: expense?.latest_approval?.step_name,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
interface InfoRow {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
|
|
||||||
{
|
|
||||||
key: 'label',
|
|
||||||
header: 'Field',
|
|
||||||
flex: 1,
|
|
||||||
cell: ({ row }) => row.label,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'value',
|
|
||||||
header: 'Value',
|
|
||||||
flex: 2,
|
|
||||||
cell: ({ row }) => row.value,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|
||||||
const kandangs = expense?.kandangs || [];
|
|
||||||
const infoRows = getInfoTableRows(expense);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page style={styles.page} size='A4'>
|
<Page style={ExpensePDFStyle.page}>
|
||||||
{/* Title Section */}
|
<View>
|
||||||
<View style={styles.titleSection}>
|
<View style={ExpensePDFStyle.companyInfoHeader}>
|
||||||
<PdfTypography size='h1' variant='primary'>
|
<Image
|
||||||
|
style={ExpensePDFStyle.companyLogo}
|
||||||
|
src='/assets/img/lti-logo.png'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={ExpensePDFStyle.companyInfoHeaderDate}>
|
||||||
|
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text style={ExpensePDFStyle.companyName}>
|
||||||
|
PT LUMBUNG TELUR INDONESIA
|
||||||
|
</Text>
|
||||||
|
<Text style={ExpensePDFStyle.companyAddress}>
|
||||||
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={ExpensePDFStyle.doubleDivider} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={ExpensePDFStyle.title}>
|
||||||
Laporan{' '}
|
Laporan{' '}
|
||||||
{expense?.category === 'BOP'
|
{expense?.category === 'BOP'
|
||||||
? 'Biaya Operasional'
|
? 'Biaya Operasional'
|
||||||
: 'Non-Biaya Operasional'}
|
: 'Non-Biaya Operasional'}{' '}
|
||||||
</PdfTypography>
|
{expense?.po_number}
|
||||||
<PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
|
</Text>
|
||||||
<View style={styles.parameterContainer}>
|
|
||||||
<PdfParamBadge>
|
{/* General info table */}
|
||||||
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
<View style={ExpensePDFStyle.generalInfoTable}>
|
||||||
</PdfParamBadge>
|
{rows.map((row) => (
|
||||||
<PdfParamBadge>
|
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
|
||||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
<View style={ExpensePDFStyle.generalInfoTableColLabel}>
|
||||||
</PdfParamBadge>
|
<Text style={ExpensePDFStyle.generalInfoTableLabelText}>
|
||||||
|
{row.label}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
|
||||||
|
<Text>:</Text>
|
||||||
|
</View>
|
||||||
|
<View style={ExpensePDFStyle.generalInfoTableColValue}>
|
||||||
|
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
|
||||||
|
{row.value}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Info Table Section */}
|
{/* Detail expense request */}
|
||||||
<View style={styles.infoTableSection}>
|
<View
|
||||||
<Text style={styles.infoTableTitle}>Informasi Biaya</Text>
|
minPresenceAhead={80}
|
||||||
<PdfTable columns={getInfoTableColumns()} data={infoRows} />
|
style={ExpensePDFStyle.expenseDetailContainer}
|
||||||
</View>
|
>
|
||||||
|
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
||||||
|
Rincian Pengajuan Biaya Operasional
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Rincian Pengajuan Section */}
|
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||||
<View style={styles.tableSection}>
|
let expenseRequestTotal = 0;
|
||||||
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
|
|
||||||
{kandangs.length === 0 ? (
|
kandangExpense.pengajuans?.forEach(
|
||||||
<Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
|
(item) => (expenseRequestTotal += item.qty * item.price)
|
||||||
) : (
|
);
|
||||||
kandangs.map((kandang, idx) => {
|
|
||||||
const pengajuans = kandang.pengajuans || [];
|
|
||||||
const kandangName =
|
|
||||||
kandang.kandang_id && kandang.name
|
|
||||||
? kandang.name
|
|
||||||
: expense?.location?.name || 'Umum';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={idx} style={{ marginBottom: 12 }}>
|
<View
|
||||||
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
key={kandangExpenseIdx}
|
||||||
{idx + 1}) {kandangName}
|
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||||
</PdfTypography>
|
>
|
||||||
{pengajuans.length > 0 ? (
|
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||||
<PdfTable
|
{kandangExpense.kandang_id && kandangExpense.name
|
||||||
columns={getPengajuanColumns()}
|
? `Biaya ${kandangExpense.name}`
|
||||||
data={pengajuans}
|
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||||
showFooter={true}
|
</Text>
|
||||||
footerLabel='Total'
|
|
||||||
/>
|
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||||
) : (
|
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||||
<Text style={styles.emptyText}>
|
<View
|
||||||
Tidak ada item pengajuan untuk kandang ini.
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Nonstock
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Kuantitas
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Harga Satuan
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Catatan
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Rincian Realisasi Section */}
|
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
|
||||||
<View style={styles.tableSection}>
|
<View
|
||||||
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
|
key={pengajuanIdx}
|
||||||
{kandangs.length === 0 ? (
|
style={ExpensePDFStyle.kandangExpenseTableRow}
|
||||||
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
|
>
|
||||||
) : (
|
<View
|
||||||
kandangs.map((kandang, idx) => {
|
style={[
|
||||||
const realisasi = kandang.realisasi || [];
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
const kandangName =
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
kandang.kandang_id && kandang.name
|
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||||
? kandang.name
|
]}
|
||||||
: expense?.location?.name || 'Umum';
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{pengajuan.nonstock.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{formatNumber(pengajuan.qty)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{formatCurrency(pengajuan.price)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{pengajuan.notes}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Total Biaya Keseluruhan
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
{formatCurrency(expenseRequestTotal)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Detail expense realization */}
|
||||||
|
<View
|
||||||
|
minPresenceAhead={80}
|
||||||
|
style={ExpensePDFStyle.expenseDetailContainer}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
||||||
|
Rincian Realisasi Biaya Operasional
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseRealizationTotal = 0;
|
||||||
|
|
||||||
|
kandangExpense.realisasi?.forEach(
|
||||||
|
(item) => (expenseRealizationTotal += item.qty * item.price)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={idx} style={{ marginBottom: 12 }}>
|
<View
|
||||||
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
key={kandangExpenseIdx}
|
||||||
{idx + 1}) {kandangName}
|
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||||
</PdfTypography>
|
>
|
||||||
{realisasi.length > 0 ? (
|
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||||
<PdfTable
|
{kandangExpense.kandang_id && kandangExpense.name
|
||||||
columns={getRealisasiColumns()}
|
? `Biaya ${kandangExpense.name}`
|
||||||
data={realisasi}
|
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||||
showFooter={true}
|
</Text>
|
||||||
footerLabel='Total'
|
|
||||||
/>
|
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||||
) : (
|
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||||
<Text style={styles.emptyText}>
|
<View
|
||||||
Tidak ada item realisasi untuk kandang ini.
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Nonstock
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Kuantitas
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Harga Satuan
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Catatan
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PdfPageNumber />
|
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
|
||||||
|
<View
|
||||||
|
key={realisasiIdx}
|
||||||
|
style={ExpensePDFStyle.kandangExpenseTableRow}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{realisasi.nonstock.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{formatNumber(realisasi.qty)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{formatCurrency(realisasi.price)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
|
{realisasi.notes}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
||||||
|
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
Total Biaya Keseluruhan
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||||
|
>
|
||||||
|
{formatCurrency(expenseRealizationTotal)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={ExpensePDFStyle.footer} fixed>
|
||||||
|
<Link
|
||||||
|
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
|
||||||
|
>
|
||||||
|
{expense?.po_number}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
`${pageNumber} / ${totalPages}`
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[tableFilterState.pageSize, tableFilterState.page]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSortingFilter = useCallback(
|
const updateSortingFilter = useCallback(
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ const InventoryAdjustmentForm = ({
|
|||||||
isLoadingOptions: isLoadingProductOptions,
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
rawData: products,
|
rawData: products,
|
||||||
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search');
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||||
|
include_all: 'true',
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setDepletionProductInputValue,
|
setInputValue: setDepletionProductInputValue,
|
||||||
|
|||||||
@@ -323,8 +323,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setFieldValue, setFieldTouched, setFieldError } = formik;
|
|
||||||
|
|
||||||
const prevSourceWarehouseIdRef = useRef<number | null>(
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
formik.values.source_warehouse_id
|
formik.values.source_warehouse_id
|
||||||
);
|
);
|
||||||
@@ -338,14 +336,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
prevSourceWarehouseId !== null
|
prevSourceWarehouseId !== null
|
||||||
) {
|
) {
|
||||||
setFieldValue('products', [
|
formik.setFieldValue('products', [
|
||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
product_qty: '',
|
product_qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setFieldTouched('products', false);
|
formik.setFieldTouched('products', false);
|
||||||
|
|
||||||
const updatedDeliveries = formik.values.deliveries.map(
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
(delivery: DeliverySchema) => ({
|
(delivery: DeliverySchema) => ({
|
||||||
@@ -359,17 +357,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
setFieldTouched('deliveries', false);
|
formik.setFieldTouched('deliveries', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
}, [
|
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||||
formik.values.source_warehouse_id,
|
|
||||||
formik.values.deliveries,
|
|
||||||
setFieldValue,
|
|
||||||
setFieldTouched,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const {
|
const {
|
||||||
@@ -462,9 +455,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
const handleTransferDateChange = useCallback(
|
const handleTransferDateChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFieldValue('transfer_date', e.target.value);
|
formik.setFieldValue('transfer_date', e.target.value);
|
||||||
},
|
},
|
||||||
[setFieldValue]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSourceWarehouseChange = useCallback(
|
const handleSourceWarehouseChange = useCallback(
|
||||||
@@ -484,16 +477,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFieldTouched('source_warehouse', true);
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
setFieldValue('source_warehouse', val);
|
formik.setFieldValue('source_warehouse', val);
|
||||||
setFieldTouched('source_warehouse_id', true);
|
formik.setFieldTouched('source_warehouse_id', true);
|
||||||
setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
formik.values.destination_warehouse_id,
|
formik.values.destination_warehouse_id,
|
||||||
formik.values.destination_warehouse,
|
formik.values.destination_warehouse,
|
||||||
setFieldTouched,
|
|
||||||
setFieldValue,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -514,17 +505,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFieldTouched('destination_warehouse', true);
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
setFieldValue('destination_warehouse', val);
|
formik.setFieldValue('destination_warehouse', val);
|
||||||
setFieldTouched('destination_warehouse_id', true);
|
formik.setFieldTouched('destination_warehouse_id', true);
|
||||||
setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
|
formik.setFieldValue(
|
||||||
|
'destination_warehouse_id',
|
||||||
|
newDestinationWarehouseId
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||||
formik.values.source_warehouse_id,
|
|
||||||
formik.values.source_warehouse,
|
|
||||||
setFieldTouched,
|
|
||||||
setFieldValue,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProduct = useCallback(() => {
|
const addProduct = useCallback(() => {
|
||||||
@@ -536,15 +525,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
product_qty: '',
|
product_qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
setFieldValue('products', newProducts);
|
formik.setFieldValue('products', newProducts);
|
||||||
}, [formik.values.products, setFieldValue]);
|
}, [formik.values.products]);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const removeProduct = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
const updatedProducts = formik.values.products?.filter(
|
const updatedProducts = formik.values.products?.filter(
|
||||||
(_, idx) => idx !== i
|
(_, idx) => idx !== i
|
||||||
);
|
);
|
||||||
setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
|
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
|
|
||||||
@@ -553,12 +542,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[formik.values.products, productQtyErrorShown, setSelectedProducts]
|
||||||
formik.values.products,
|
|
||||||
productQtyErrorShown,
|
|
||||||
setSelectedProducts,
|
|
||||||
setFieldValue,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
@@ -566,32 +550,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.products?.filter(
|
formik.values.products?.filter(
|
||||||
(_, idx) => !selectedProducts.includes(idx)
|
(_, idx) => !selectedProducts.includes(idx)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
|
|
||||||
if (productQtyErrorShown) {
|
if (productQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]);
|
||||||
selectedProducts,
|
|
||||||
setSelectedProducts,
|
|
||||||
productQtyErrorShown,
|
|
||||||
setFieldValue,
|
|
||||||
formik.values.products,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleProductChange = useCallback(
|
const handleProductChange = useCallback(
|
||||||
(idx: number, val: OptionType | OptionType[] | null) => {
|
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||||
setFieldTouched(`products.${idx}.product`, true);
|
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||||
setFieldValue(`products.${idx}.product`, val);
|
formik.setFieldValue(`products.${idx}.product`, val);
|
||||||
setFieldTouched(`products.${idx}.product_id`, true);
|
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||||
setFieldValue(
|
formik.setFieldValue(
|
||||||
`products.${idx}.product_id`,
|
`products.${idx}.product_id`,
|
||||||
(val as ProductWarehouseOptionType)?.value
|
(val as ProductWarehouseOptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setFieldTouched, setFieldValue]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleProductSelectAllChange = useCallback(
|
const handleProductSelectAllChange = useCallback(
|
||||||
@@ -618,7 +596,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addDelivery = useCallback(() => {
|
const addDelivery = useCallback(() => {
|
||||||
setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
delivery_cost: '',
|
delivery_cost: '',
|
||||||
@@ -637,14 +615,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, [formik.values.deliveries, setFieldValue]);
|
}, [formik.values.deliveries]);
|
||||||
|
|
||||||
const removeDelivery = useCallback(
|
const removeDelivery = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
const updatedDeliveries = formik.values.deliveries?.filter(
|
const updatedDeliveries = formik.values.deliveries?.filter(
|
||||||
(_, idx) => idx !== i
|
(_, idx) => idx !== i
|
||||||
);
|
);
|
||||||
setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
|
|
||||||
@@ -653,12 +631,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries]
|
||||||
formik.values.deliveries,
|
|
||||||
deliveryQtyErrorShown,
|
|
||||||
setSelectedDeliveries,
|
|
||||||
setFieldValue,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
@@ -666,7 +639,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.deliveries?.filter(
|
formik.values.deliveries?.filter(
|
||||||
(_, idx) => !selectedDeliveries.includes(idx)
|
(_, idx) => !selectedDeliveries.includes(idx)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
|
|
||||||
if (deliveryQtyErrorShown) {
|
if (deliveryQtyErrorShown) {
|
||||||
@@ -674,11 +647,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
formik,
|
||||||
selectedDeliveries,
|
selectedDeliveries,
|
||||||
setSelectedDeliveries,
|
setSelectedDeliveries,
|
||||||
deliveryQtyErrorShown,
|
deliveryQtyErrorShown,
|
||||||
setFieldValue,
|
|
||||||
formik.values.deliveries,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDeliverySelectAllChange = useCallback(
|
const handleDeliverySelectAllChange = useCallback(
|
||||||
@@ -708,28 +680,34 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
const handleDeliveryProductChange = useCallback(
|
const handleDeliveryProductChange = useCallback(
|
||||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
|
formik.setFieldTouched(
|
||||||
setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
`deliveries.${deliveryIdx}.products.0.product`,
|
||||||
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
|
true
|
||||||
setFieldValue(
|
);
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setFieldTouched, setFieldValue]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliverySupplierChange = useCallback(
|
const handleDeliverySupplierChange = useCallback(
|
||||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||||
setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||||
setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||||
setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${deliveryIdx}.supplier_id`,
|
`deliveries.${deliveryIdx}.supplier_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setFieldTouched, setFieldValue]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryDocumentChange = useCallback(
|
const handleDeliveryDocumentChange = useCallback(
|
||||||
@@ -741,15 +719,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setFieldValue]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChange = useCallback(
|
const handleDeliveryCostChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
@@ -759,18 +737,21 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const perItem = value / productQty;
|
const perItem = value / productQty;
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
formik.setFieldValue(
|
||||||
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
|
perItem
|
||||||
|
);
|
||||||
} else if (value === 0) {
|
} else if (value === 0) {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.deliveries, setFieldValue]
|
[formik.values.deliveries]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
@@ -780,13 +761,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const totalCost = value * productQty;
|
const totalCost = value * productQty;
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
} else if (value === 0) {
|
} else if (value === 0) {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.deliveries, setFieldValue]
|
[formik.values.deliveries]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -1063,7 +1044,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
|
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
|
||||||
})
|
})
|
||||||
) ?? []),
|
) ?? []),
|
||||||
[formik.values.deliveries, validateDeliveryQty, type]
|
[
|
||||||
|
formik.values.deliveries,
|
||||||
|
formik.values.products,
|
||||||
|
validateDeliveryQty,
|
||||||
|
type,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasInvalidQty = useMemo(
|
const hasInvalidQty = useMemo(
|
||||||
@@ -1080,27 +1066,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.products, getProductQtyError, type]);
|
}, [formik.values.products, getProductQtyError, type]);
|
||||||
|
|
||||||
const deliveryCostDepString = useMemo(
|
|
||||||
() =>
|
|
||||||
formik.values.deliveries
|
|
||||||
?.map((d, idx) => ({
|
|
||||||
idx,
|
|
||||||
productQty: d.products.reduce(
|
|
||||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
|
||||||
deliveryCostPerItem:
|
|
||||||
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
|
||||||
}))
|
|
||||||
.map(
|
|
||||||
(item) =>
|
|
||||||
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
|
||||||
)
|
|
||||||
.join('|'),
|
|
||||||
[formik.values.deliveries]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== EFFECTS =====
|
// ===== EFFECTS =====
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.values.deliveries?.forEach((delivery, idx) => {
|
formik.values.deliveries?.forEach((delivery, idx) => {
|
||||||
@@ -1117,16 +1082,36 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (deliveryCost > 0 && productQty > 0) {
|
if (deliveryCost > 0 && productQty > 0) {
|
||||||
const perItem = deliveryCost / productQty;
|
const perItem = deliveryCost / productQty;
|
||||||
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
formik.setFieldValue(
|
||||||
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
|
perItem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
||||||
const totalCost = deliveryCostPerItem * productQty;
|
const totalCost = deliveryCostPerItem * productQty;
|
||||||
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
||||||
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
|
}, [
|
||||||
|
formik.values.deliveries
|
||||||
|
?.map((d, idx) => ({
|
||||||
|
idx,
|
||||||
|
productQty: d.products.reduce(
|
||||||
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
||||||
|
deliveryCostPerItem:
|
||||||
|
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
||||||
|
)
|
||||||
|
.join('|'),
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -1136,7 +1121,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
!isInitialized
|
!isInitialized
|
||||||
) {
|
) {
|
||||||
if (formik.values.products.length === 0) {
|
if (formik.values.products.length === 0) {
|
||||||
setFieldValue('products', [
|
formik.setFieldValue('products', [
|
||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
@@ -1145,7 +1130,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (formik.values.deliveries.length === 0) {
|
if (formik.values.deliveries.length === 0) {
|
||||||
setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
{
|
{
|
||||||
delivery_cost: undefined,
|
delivery_cost: undefined,
|
||||||
delivery_cost_per_item: undefined,
|
delivery_cost_per_item: undefined,
|
||||||
@@ -1167,14 +1152,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [
|
}, [formik.values.source_warehouse_id, isInitialized, type]);
|
||||||
formik.values.source_warehouse_id,
|
|
||||||
isInitialized,
|
|
||||||
type,
|
|
||||||
setFieldValue,
|
|
||||||
formik.values.products.length,
|
|
||||||
formik.values.deliveries.length,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -1183,7 +1161,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.source_warehouse_id ===
|
formik.values.source_warehouse_id ===
|
||||||
formik.values.destination_warehouse_id
|
formik.values.destination_warehouse_id
|
||||||
) {
|
) {
|
||||||
setFieldError(
|
formik.setFieldError(
|
||||||
'destination_warehouse_id',
|
'destination_warehouse_id',
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||||
);
|
);
|
||||||
@@ -1192,14 +1170,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.errors.destination_warehouse_id ===
|
formik.errors.destination_warehouse_id ===
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||||
) {
|
) {
|
||||||
setFieldError('destination_warehouse_id', undefined);
|
formik.setFieldError('destination_warehouse_id', undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
formik.values.source_warehouse_id,
|
formik.values.source_warehouse_id,
|
||||||
formik.values.destination_warehouse_id,
|
formik.values.destination_warehouse_id,
|
||||||
formik.errors.destination_warehouse_id,
|
formik.errors.destination_warehouse_id,
|
||||||
setFieldError,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1235,37 +1212,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [formik.values.products, formik.values.deliveries, setFieldValue]);
|
}, [formik.values.products]);
|
||||||
|
|
||||||
const productQtyDepString = useMemo(
|
|
||||||
() => formik.values.products?.map((p) => p.product_qty).join(','),
|
|
||||||
[formik.values.products]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (productQtyErrorShown) {
|
if (productQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [productQtyErrorShown]);
|
}, [formik.values.products?.map((p) => p.product_qty).join(',')]);
|
||||||
|
|
||||||
const deliveryProductQtyDepString = useMemo(
|
|
||||||
() =>
|
|
||||||
formik.values.deliveries
|
|
||||||
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
|
||||||
.join('|'),
|
|
||||||
[formik.values.deliveries]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deliveryQtyErrorShown) {
|
if (deliveryQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
|
}, [
|
||||||
|
formik.values.deliveries
|
||||||
|
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
||||||
|
.join('|'),
|
||||||
|
formik.values.products?.map((p) => p.product_qty).join(','),
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
|
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
|
||||||
|
|||||||
@@ -536,13 +536,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
formModal.closeModal();
|
formModal.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasLoadedInitialValues = useRef(false);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getFilledInitialValues = async () => {
|
const getFilledInitialValues = async () => {
|
||||||
if (marketingId && isResponseSuccess(marketing)) {
|
if (marketingId && isResponseSuccess(marketing)) {
|
||||||
if (hasLoadedInitialValues.current) return;
|
|
||||||
hasLoadedInitialValues.current = true;
|
|
||||||
|
|
||||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||||
marketing.data
|
marketing.data
|
||||||
);
|
);
|
||||||
@@ -586,15 +582,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
}, [step]);
|
}, [step]);
|
||||||
|
|
||||||
const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
|
// sync delivery order values to formik
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
|
||||||
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
|
|
||||||
JSON.stringify(deliveryOrderValues)
|
|
||||||
) {
|
|
||||||
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
|
|
||||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||||
}
|
|
||||||
}, [deliveryOrderValues]);
|
}, [deliveryOrderValues]);
|
||||||
|
|
||||||
const grandTotal = useMemo(() => {
|
const grandTotal = useMemo(() => {
|
||||||
|
|||||||
@@ -226,6 +226,11 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const productsClickHandler = (item: Marketing) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
productsModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
selectedItem?.id as number
|
selectedItem?.id as number
|
||||||
@@ -445,11 +450,6 @@ const MarketingTable = () => {
|
|||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const productsClickHandler = (item: Marketing) => {
|
|
||||||
setSelectedItem(item);
|
|
||||||
productsModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
return (
|
return (
|
||||||
@@ -504,7 +504,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [deleteModal, deliveryModal, setSelectedItem, productsModal]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -458,13 +458,9 @@ const SalesOrderFormModal = ({
|
|||||||
);
|
);
|
||||||
}, [memoSalesOrder]);
|
}, [memoSalesOrder]);
|
||||||
|
|
||||||
const hasLoadedInitialValues = useRef(false);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getFilledInitialValues = async () => {
|
const getFilledInitialValues = async () => {
|
||||||
if (marketingId && isResponseSuccess(marketing)) {
|
if (marketingId && isResponseSuccess(marketing)) {
|
||||||
if (hasLoadedInitialValues.current) return;
|
|
||||||
hasLoadedInitialValues.current = true;
|
|
||||||
|
|
||||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||||
marketing.data
|
marketing.data
|
||||||
);
|
);
|
||||||
|
|||||||
+8
-19
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DeliveryOrderProductFormValues,
|
DeliveryOrderProductFormValues,
|
||||||
DeliveryOrderProductSchema,
|
DeliveryOrderProductSchema,
|
||||||
@@ -224,8 +224,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetForm } = formik;
|
|
||||||
|
|
||||||
const hasWeekField = useMemo(() => {
|
const hasWeekField = useMemo(() => {
|
||||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||||
if (marketingType === 'ayam_pullet') {
|
if (marketingType === 'ayam_pullet') {
|
||||||
@@ -245,9 +243,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
return false;
|
return false;
|
||||||
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
||||||
|
|
||||||
const handleResetForm = useCallback(() => {
|
const handleResetForm = () => {
|
||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
resetForm({
|
formik.resetForm({
|
||||||
values: {
|
values: {
|
||||||
delivery_date: '',
|
delivery_date: '',
|
||||||
vehicle_number: '',
|
vehicle_number: '',
|
||||||
@@ -271,10 +269,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// setSelectedProduct(null);
|
// setSelectedProduct(null);
|
||||||
}, [resetForm]);
|
};
|
||||||
|
|
||||||
const handleBlurField = useCallback(
|
const handleBlurField = (field: string) => {
|
||||||
(field: string) => {
|
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
|
|
||||||
handleMarketingCalculation(field, {
|
handleMarketingCalculation(field, {
|
||||||
@@ -282,9 +279,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
setFieldValue: formik.setFieldValue,
|
setFieldValue: formik.setFieldValue,
|
||||||
hasSisaBerat,
|
hasSisaBerat,
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[formik.values, formik.setFieldValue, hasSisaBerat]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||||
const handleFieldChange = (
|
const handleFieldChange = (
|
||||||
@@ -329,12 +324,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
const { setValues: setFormikValues } = formik;
|
const { setValues: setFormikValues } = formik;
|
||||||
|
|
||||||
const processedInitialValuesRef = useRef<number | null>(null);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (processedInitialValuesRef.current === initialValues.id) return;
|
|
||||||
processedInitialValuesRef.current = initialValues.id as number;
|
|
||||||
|
|
||||||
if (!Boolean(initialValues.qty)) {
|
if (!Boolean(initialValues.qty)) {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
@@ -347,7 +338,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [handleResetForm, initialValues, setFormikValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
@@ -365,10 +356,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formik.values.week) {
|
|
||||||
handleBlurField('week');
|
handleBlurField('week');
|
||||||
}
|
}, [formik.values.week]);
|
||||||
}, [formik.values.week, handleBlurField]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
SalesOrderProductFormValues,
|
SalesOrderProductFormValues,
|
||||||
SalesOrderProductSchema,
|
SalesOrderProductSchema,
|
||||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||||
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
import { RefObject, useEffect, useMemo, useState } from 'react';
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
@@ -240,8 +240,7 @@ const SalesOrderProductForm = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlurField = useCallback(
|
const handleBlurField = (field: string) => {
|
||||||
(field: string) => {
|
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
|
|
||||||
handleMarketingCalculation(field, {
|
handleMarketingCalculation(field, {
|
||||||
@@ -249,9 +248,7 @@ const SalesOrderProductForm = ({
|
|||||||
setFieldValue: formik.setFieldValue,
|
setFieldValue: formik.setFieldValue,
|
||||||
hasSisaBerat,
|
hasSisaBerat,
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[formik.values, formik.setFieldValue, hasSisaBerat]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||||
const handleFieldChange = (
|
const handleFieldChange = (
|
||||||
@@ -310,10 +307,8 @@ const SalesOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formik.values.week) {
|
|
||||||
handleBlurField('week');
|
handleBlurField('week');
|
||||||
}
|
}, [formik.values.week]);
|
||||||
}, [formik.values.week, handleBlurField]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
interface DeliveryOrderExportProps {
|
interface DeliveryOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -19,6 +20,9 @@ const DeliveryOrderExport = ({
|
|||||||
}: DeliveryOrderExportProps) => {
|
}: DeliveryOrderExportProps) => {
|
||||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||||
const salesData = data;
|
const salesData = data;
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const action = searchParams.get('action');
|
||||||
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
@@ -49,6 +53,7 @@ const DeliveryOrderExport = ({
|
|||||||
toast.error('Failed to generate PDF. Please try again.');
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
|
window.location.href = `/marketing?action=${action}&id=${id}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +92,7 @@ const PDFDocument = ({
|
|||||||
return (
|
return (
|
||||||
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
||||||
);
|
);
|
||||||
}, [deliveryOrder.deliveries]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
interface SalesOrderExportProps {
|
interface SalesOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -15,6 +16,9 @@ interface SalesOrderExportProps {
|
|||||||
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||||
const salesData = data;
|
const salesData = data;
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const action = searchParams.get('action');
|
||||||
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
@@ -43,6 +47,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
toast.error('Failed to generate PDF. Please try again.');
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
|
window.location.href = `/marketing?action=${action}&id=${id}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -314,6 +314,10 @@ const KandangsTable = () => {
|
|||||||
accessorFn: (row) => row.pic?.name ?? '-',
|
accessorFn: (row) => row.pic?.name ?? '-',
|
||||||
header: 'PIC',
|
header: 'PIC',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.kandang_group?.name ?? '-',
|
||||||
|
header: 'Kandang Group',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Kandang, unknown>) => {
|
cell: (props: CellContext<Kandang, unknown>) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
type KandangFormSchemaType = {
|
type KandangFormSchemaType = {
|
||||||
@@ -19,6 +20,7 @@ type KandangFormSchemaType = {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
| null;
|
| null;
|
||||||
|
group?: OptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||||
@@ -42,6 +44,11 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
|||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
|
|
||||||
|
group: Yup.object({
|
||||||
|
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
|
||||||
|
label: Yup.string().required('Kandang Grup wajib diisi!'),
|
||||||
|
}).required('Kandang Grup wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { getIn, useFormik } from 'formik';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -34,6 +34,8 @@ import NumberInput from '@/components/input/NumberInput';
|
|||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { User } from '@/types/api/api-general';
|
import { User } from '@/types/api/api-general';
|
||||||
|
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
interface KandangFormProps {
|
interface KandangFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -96,6 +98,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
label: initialValues.pic.name,
|
label: initialValues.pic.name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
group: initialValues?.kandang_group
|
||||||
|
? {
|
||||||
|
value: initialValues.kandang_group.id,
|
||||||
|
label: initialValues.kandang_group.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
@@ -111,6 +119,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
location_id: values.locationId!,
|
location_id: values.locationId!,
|
||||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||||
pic_id: values.picId!,
|
pic_id: values.picId!,
|
||||||
|
group_id: values.group?.value as number,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -162,6 +171,23 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
formik.setFieldValue('picId', (val as OptionType)?.value);
|
formik.setFieldValue('picId', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kandang Group
|
||||||
|
const {
|
||||||
|
setInputValue: setKandangGroupSelectInputValue,
|
||||||
|
options: kandangGroupOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangGroupOptions,
|
||||||
|
loadMore: loadMoreKandangGroups,
|
||||||
|
} = useSelect<DailyChecklistKandang>(
|
||||||
|
DailyChecklistKandangApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
|
||||||
|
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('group', true);
|
||||||
|
formik.setFieldValue('group', val);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteKandangClickHandler = () => {
|
const deleteKandangClickHandler = () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
@@ -269,6 +295,24 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Kandang Group'
|
||||||
|
value={formik.values.group ?? undefined}
|
||||||
|
onChange={kandangGroupChangeHandler}
|
||||||
|
options={kandangGroupOptions}
|
||||||
|
onInputChange={setKandangGroupSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangGroups}
|
||||||
|
isLoading={isLoadingKandangGroupOptions}
|
||||||
|
isError={formik.touched.group && Boolean(formik.errors.group)}
|
||||||
|
errorMessage={
|
||||||
|
getIn(formik.errors.group, 'value') ??
|
||||||
|
(formik.errors.group as string)
|
||||||
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ const ChickinFormKandang = ({
|
|||||||
|
|
||||||
const afterSubmitFormChickin = () => {
|
const afterSubmitFormChickin = () => {
|
||||||
setOpenChickin(true);
|
setOpenChickin(true);
|
||||||
if (afterSubmit) {
|
afterSubmit && afterSubmit();
|
||||||
afterSubmit();
|
|
||||||
}
|
|
||||||
refreshApprovals();
|
refreshApprovals();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ChickinLogsView = ({
|
|||||||
rawDataApprovals: BaseApproval[];
|
rawDataApprovals: BaseApproval[];
|
||||||
}) => {
|
}) => {
|
||||||
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
||||||
const { openChickinApproveModal } = useChickinStore();
|
const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
|
||||||
|
|
||||||
const handleClickApprove = () => {
|
const handleClickApprove = () => {
|
||||||
openChickinApproveModal(initialValues, async (notes?: string) => {
|
openChickinApproveModal(initialValues, async (notes?: string) => {
|
||||||
@@ -40,8 +40,21 @@ const ChickinLogsView = ({
|
|||||||
toast.error(approveChickinRes?.message as string);
|
toast.error(approveChickinRes?.message as string);
|
||||||
setChickinErrorMessage(approveChickinRes?.message as string);
|
setChickinErrorMessage(approveChickinRes?.message as string);
|
||||||
}
|
}
|
||||||
if (afterSubmit) {
|
afterSubmit && afterSubmit();
|
||||||
afterSubmit();
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteChickin = (chickinId: number) => {
|
||||||
|
openChickinDeleteModal(chickinId, async () => {
|
||||||
|
const deleteRes = await ChickinApi.delete(chickinId);
|
||||||
|
|
||||||
|
if (isResponseSuccess(deleteRes)) {
|
||||||
|
toast.success(deleteRes?.message || 'Chickin berhasil dihapus');
|
||||||
|
afterSubmit && afterSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResponseError(deleteRes)) {
|
||||||
|
toast.error(deleteRes?.message || 'Gagal menghapus chickin');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -88,6 +101,7 @@ const ChickinLogsView = ({
|
|||||||
<div className='text-lg font-semibold'>
|
<div className='text-lg font-semibold'>
|
||||||
Chick In #{index + 1} - {latestApproval?.step_number}
|
Chick In #{index + 1} - {latestApproval?.step_number}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex flex-row gap-2 items-center'>
|
||||||
<PillBadge
|
<PillBadge
|
||||||
content={
|
content={
|
||||||
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
|
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
|
||||||
@@ -96,6 +110,21 @@ const ChickinLogsView = ({
|
|||||||
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
|
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isApproved && (
|
||||||
|
<Button
|
||||||
|
color='error'
|
||||||
|
className='w-fit text-sm text-base-100 rounded-lg shadow-sm btn-xs!'
|
||||||
|
onClick={() => handleDeleteChickin(chickin.id)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:trash-solid'
|
||||||
|
width={10}
|
||||||
|
height={10}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tanggal Chick In */}
|
{/* Tanggal Chick In */}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const confirmModal = useModal();
|
const confirmModal = useModal();
|
||||||
const successModal = useModal();
|
const successModal = useModal();
|
||||||
const chickinApproveModal = useModal();
|
const chickinApproveModal = useModal();
|
||||||
|
const chickinDeleteModal = useModal();
|
||||||
const closingModal = useModal();
|
const closingModal = useModal();
|
||||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||||
'APPROVED'
|
'APPROVED'
|
||||||
@@ -214,6 +215,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
chickinApproveCallback,
|
chickinApproveCallback,
|
||||||
closeChickinApproveModal,
|
closeChickinApproveModal,
|
||||||
setChickinApproveLoading,
|
setChickinApproveLoading,
|
||||||
|
isChickinDeleteModalOpen,
|
||||||
|
isChickinDeleteLoading,
|
||||||
|
chickinDeleteCallback,
|
||||||
|
closeChickinDeleteModal,
|
||||||
|
setChickinDeleteLoading,
|
||||||
} = useChickinStore();
|
} = useChickinStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -478,6 +484,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
}
|
}
|
||||||
}, [isChickinApproveModalOpen, chickinApproveModal]);
|
}, [isChickinApproveModalOpen, chickinApproveModal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isChickinDeleteModalOpen) {
|
||||||
|
chickinDeleteModal.openModal();
|
||||||
|
} else {
|
||||||
|
chickinDeleteModal.closeModal();
|
||||||
|
}
|
||||||
|
}, [isChickinDeleteModalOpen, chickinDeleteModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isClosingModalOpen) {
|
if (isClosingModalOpen) {
|
||||||
closingModal.openModal();
|
closingModal.openModal();
|
||||||
@@ -1208,6 +1222,38 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Chickin Delete Modal */}
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={chickinDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text='Apakah anda yakin ingin menghapus data chick in ini?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
onClick: () => {
|
||||||
|
closeChickinDeleteModal();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className={{
|
||||||
|
modal: 'z-9999',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isChickinDeleteLoading,
|
||||||
|
onClick: async () => {
|
||||||
|
if (chickinDeleteCallback) {
|
||||||
|
setChickinDeleteLoading(true);
|
||||||
|
try {
|
||||||
|
await chickinDeleteCallback();
|
||||||
|
} finally {
|
||||||
|
setChickinDeleteLoading(false);
|
||||||
|
closeChickinDeleteModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
|||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -104,20 +106,76 @@ const RowOptionsMenu = ({
|
|||||||
return recording.approval?.action === 'REJECTED';
|
return recording.approval?.action === 'REJECTED';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRecordingEditable = (recording: Recording) => {
|
||||||
|
const isGrowingCategory =
|
||||||
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
|
if (isGrowingLockedByLaying) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
const restriction = getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restriction.isLocked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||||
|
const isGrowingCategory =
|
||||||
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
|
if (isGrowingLockedByLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
return getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const isApproved = isRecordingApproved(props.row.original);
|
const isApproved = isRecordingApproved(props.row.original);
|
||||||
const isRejected = isRecordingRejected(props.row.original);
|
const isRejected = isRecordingRejected(props.row.original);
|
||||||
|
const isEditable = isRecordingEditable(props.row.original);
|
||||||
|
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
|
<Tooltip
|
||||||
|
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
popoverTarget={popoverId}
|
popoverTarget={popoverId}
|
||||||
anchorName={popoverAnchorName}
|
anchorName={popoverAnchorName}
|
||||||
|
className={restrictionInfo.isLocked ? 'text-error' : ''}
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
id={popoverId}
|
id={popoverId}
|
||||||
@@ -138,6 +196,7 @@ const RowOptionsMenu = ({
|
|||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
{isEditable && (
|
||||||
<RequirePermission permissions='lti.production.recording.update'>
|
<RequirePermission permissions='lti.production.recording.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
|
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
|
||||||
@@ -150,7 +209,8 @@ const RowOptionsMenu = ({
|
|||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
{!isApproved && !isRejected && (
|
)}
|
||||||
|
{!restrictionInfo.isLocked && !isApproved && !isRejected && (
|
||||||
<RequirePermission permissions='lti.production.recording.approve'>
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -166,7 +226,7 @@ const RowOptionsMenu = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{!isApproved && !isRejected && (
|
{!restrictionInfo.isLocked && !isApproved && !isRejected && (
|
||||||
<RequirePermission permissions='lti.production.recording.approve'>
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -182,6 +242,7 @@ const RowOptionsMenu = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
{isEditable && (
|
||||||
<RequirePermission permissions='lti.production.recording.delete'>
|
<RequirePermission permissions='lti.production.recording.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -196,6 +257,7 @@ const RowOptionsMenu = ({
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -545,12 +607,17 @@ const RecordingTable = () => {
|
|||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await RecordingApi.delete(selectedRecording?.id as number);
|
const response = await RecordingApi.delete(selectedRecording?.id as number);
|
||||||
refreshRecordings();
|
|
||||||
|
|
||||||
singleDeleteModal.closeModal();
|
singleDeleteModal.closeModal();
|
||||||
toast.success('Successfully delete Recording!');
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Recording!');
|
||||||
|
refreshRecordings();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Recording');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveHandler = async (notes: string) => {
|
const approveHandler = async (notes: string) => {
|
||||||
@@ -746,11 +813,21 @@ const RecordingTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
|
const isTransition = props.row.original.is_transition;
|
||||||
const category =
|
const category =
|
||||||
props.row.original.project_flock?.project_flock_category;
|
props.row.original.project_flock?.project_flock_category ||
|
||||||
if (!category) return '-';
|
'GROWING';
|
||||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
return (
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<StatusBadge color={color} text={formatTitleCase(category)} />
|
||||||
|
{isTransition && (
|
||||||
|
<span className='text-xs text-warning font-medium'>
|
||||||
|
(Transisi)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import {
|
|||||||
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
||||||
|
|
||||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber, cn } from '@/lib/helper';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import ApprovalSteps, {
|
import ApprovalSteps, {
|
||||||
useApprovalSteps,
|
useApprovalSteps,
|
||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
LAYING_RECORDING_APPROVAL_LINE,
|
LAYING_RECORDING_APPROVAL_LINE,
|
||||||
} from '@/config/approval-line';
|
} from '@/config/approval-line';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
import { getRecordingRestriction } from '../recording-utils';
|
||||||
|
|
||||||
interface RecordingFormProps {
|
interface RecordingFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -242,6 +243,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
|
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const calculateWeek = useCallback(
|
||||||
|
(day: number): number => {
|
||||||
|
if (
|
||||||
|
productionStandards?.details &&
|
||||||
|
productionStandards.details.length > 0
|
||||||
|
) {
|
||||||
|
const firstWeek = productionStandards.details[0].week;
|
||||||
|
|
||||||
|
const weekOffset = Math.ceil(day / 7) - 1;
|
||||||
|
return firstWeek + weekOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(day / 7);
|
||||||
|
},
|
||||||
|
[productionStandards]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkProductionStandardModalOpen = () => {
|
const checkProductionStandardModalOpen = () => {
|
||||||
const isOpen = productionStandardModal.ref.current?.open || false;
|
const isOpen = productionStandardModal.ref.current?.open || false;
|
||||||
@@ -370,11 +388,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (!initialValues?.id) return;
|
if (!initialValues?.id) return;
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
await RecordingApi.delete(initialValues.id);
|
const response = await RecordingApi.delete(initialValues.id);
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Successfully delete Recording!');
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Recording!');
|
||||||
router.push('/production/recording');
|
router.push('/production/recording');
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Recording');
|
||||||
|
}
|
||||||
}, [deleteModal, initialValues?.id, router]);
|
}, [deleteModal, initialValues?.id, router]);
|
||||||
|
|
||||||
// ===== API DATA FETCHING =====
|
// ===== API DATA FETCHING =====
|
||||||
@@ -396,13 +420,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!selectedProjectFlock || !selectedKandang) return null;
|
if (!selectedProjectFlock || !selectedKandang || !selectedRecordDate)
|
||||||
|
return null;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: selectedProjectFlock.value.toString(),
|
project_flock_id: selectedProjectFlock.value.toString(),
|
||||||
kandang_id: selectedKandang.value.toString(),
|
kandang_id: selectedKandang.value.toString(),
|
||||||
|
record_date: selectedRecordDate,
|
||||||
});
|
});
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [selectedProjectFlock, selectedKandang]);
|
}, [selectedProjectFlock, selectedKandang, selectedRecordDate]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -434,13 +460,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
() => ProductionStandardApi.getSingle(productionStandardId!)
|
() => ProductionStandardApi.getSingle(productionStandardId!)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: productionStandardForAdd } = useSWR(
|
||||||
|
type === 'add' && productionStandardId
|
||||||
|
? `production-standard-add-${productionStandardId}`
|
||||||
|
: null,
|
||||||
|
() => ProductionStandardApi.getSingle(productionStandardId!)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (productionStandard?.status === 'success') {
|
if (productionStandard?.status === 'success') {
|
||||||
setProductionStandards(
|
setProductionStandards(
|
||||||
productionStandard.data as ProductionStandard | null
|
productionStandard.data as ProductionStandard | null
|
||||||
);
|
);
|
||||||
|
} else if (productionStandardForAdd?.status === 'success') {
|
||||||
|
setProductionStandards(
|
||||||
|
productionStandardForAdd.data as ProductionStandard | null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [productionStandard]);
|
}, [productionStandard, productionStandardForAdd]);
|
||||||
|
|
||||||
const projectFlockKandangDetailUrl = useMemo(() => {
|
const projectFlockKandangDetailUrl = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
@@ -466,6 +503,74 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
? projectFlockKandangDetailData.data
|
? projectFlockKandangDetailData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// ===== TRANSITION RESTRICTION LOGIC =====
|
||||||
|
const isTransitionPeriod = useMemo(() => {
|
||||||
|
return (
|
||||||
|
initialValues?.is_transition ??
|
||||||
|
projectFlockKandangLookup?.is_transition ??
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}, [initialValues, projectFlockKandangLookup]);
|
||||||
|
|
||||||
|
const recordingRestriction = useMemo(() => {
|
||||||
|
// Determine isLaying primarily from transition flags.
|
||||||
|
let isLaying: boolean;
|
||||||
|
if (initialValues?.is_laying !== undefined) {
|
||||||
|
isLaying = initialValues.is_laying;
|
||||||
|
} else if (projectFlockKandangLookup?.is_laying !== undefined) {
|
||||||
|
isLaying = projectFlockKandangLookup.is_laying;
|
||||||
|
} else {
|
||||||
|
isLaying =
|
||||||
|
projectFlockKandangDetail?.project_flock?.category === 'LAYING' ||
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTransition =
|
||||||
|
initialValues?.is_transition ??
|
||||||
|
projectFlockKandangLookup?.is_transition ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
|
||||||
|
|
||||||
|
return getRecordingRestriction(
|
||||||
|
isLaying,
|
||||||
|
isTransition,
|
||||||
|
type === 'edit' ? currentIsLaying : undefined
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
initialValues,
|
||||||
|
projectFlockKandangLookup,
|
||||||
|
projectFlockKandangDetail,
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||||
|
if (!recording) return true;
|
||||||
|
|
||||||
|
const isGrowingCategory =
|
||||||
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
|
if (isGrowingLockedByLaying) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
const restriction = getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restriction.isLocked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: stockProductOptions,
|
options: stockProductOptions,
|
||||||
rawData: stockProducts,
|
rawData: stockProducts,
|
||||||
@@ -571,15 +676,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return approvedProjectFlockKandangsData.data;
|
return approvedProjectFlockKandangsData.data;
|
||||||
}, [approvedProjectFlockKandangsData]);
|
}, [approvedProjectFlockKandangsData]);
|
||||||
|
|
||||||
const isLayingCategory =
|
const isLayingCategory = useMemo(() => {
|
||||||
initialValues?.project_flock?.project_flock_category === 'LAYING' ||
|
// Priority 1: initialValues (for edit/detail mode)
|
||||||
projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
|
if (initialValues?.is_laying !== undefined) {
|
||||||
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
|
return initialValues.is_laying;
|
||||||
|
}
|
||||||
|
|
||||||
const isGrowingCategory =
|
// Priority 2: projectFlockKandangLookup flag (for add mode)
|
||||||
initialValues?.project_flock?.project_flock_category === 'GROWING' ||
|
if (projectFlockKandangLookup?.is_laying !== undefined) {
|
||||||
projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
|
return projectFlockKandangLookup.is_laying;
|
||||||
projectFlockKandangDetail?.project_flock?.category === 'GROWING';
|
}
|
||||||
|
|
||||||
|
// Priority 3: projectFlockKandangDetail (fallback for edit/detail mode)
|
||||||
|
return (
|
||||||
|
projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
initialValues?.is_laying,
|
||||||
|
projectFlockKandangLookup,
|
||||||
|
projectFlockKandangDetail,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isGrowingCategory = !isLayingCategory;
|
||||||
|
|
||||||
const recordingApprovalLines = useMemo(() => {
|
const recordingApprovalLines = useMemo(() => {
|
||||||
if (isLayingCategory) {
|
if (isLayingCategory) {
|
||||||
@@ -1289,6 +1407,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setSelectedLocation(location);
|
setSelectedLocation(location);
|
||||||
setSelectedProjectFlock(null);
|
setSelectedProjectFlock(null);
|
||||||
setSelectedKandang(null);
|
setSelectedKandang(null);
|
||||||
|
setProductionStandards(null);
|
||||||
|
setNextDayRecording(null);
|
||||||
if (duplicateErrorShown) {
|
if (duplicateErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
@@ -1313,6 +1433,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
setSelectedProjectFlock(projectFlock);
|
setSelectedProjectFlock(projectFlock);
|
||||||
setSelectedKandang(null);
|
setSelectedKandang(null);
|
||||||
|
setProductionStandards(null);
|
||||||
|
setNextDayRecording(null);
|
||||||
if (duplicateErrorShown) {
|
if (duplicateErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
@@ -1333,6 +1455,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldValue('kandang_id', kandangId);
|
formik.setFieldValue('kandang_id', kandangId);
|
||||||
|
|
||||||
setSelectedKandang(kandang);
|
setSelectedKandang(kandang);
|
||||||
|
setProductionStandards(null);
|
||||||
|
setNextDayRecording(null);
|
||||||
if (duplicateErrorShown) {
|
if (duplicateErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
@@ -1869,10 +1993,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<p className='font-semibold'>
|
<p className='font-semibold'>
|
||||||
{type === 'add'
|
{type === 'add'
|
||||||
? nextDayRecording
|
? nextDayRecording
|
||||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${calculateWeek(nextDayRecording.next_day)})`
|
||||||
: '-'
|
: '-'
|
||||||
: initialValues?.day
|
: initialValues?.day
|
||||||
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
|
? `Hari ke-${initialValues.day} (Minggu ke-${calculateWeek(initialValues.day)})`
|
||||||
: '-'}
|
: '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1946,18 +2070,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<span className='text-sm text-gray-600'>Kategori</span>
|
<span className='text-sm text-gray-600'>Kategori</span>
|
||||||
<p className='font-semibold'>
|
<p className='font-semibold'>
|
||||||
<Badge
|
{(() => {
|
||||||
variant='soft'
|
const category =
|
||||||
color={
|
initialValues.project_flock?.project_flock_category ||
|
||||||
initialValues.project_flock
|
'GROWING';
|
||||||
?.project_flock_category === 'LAYING'
|
const color =
|
||||||
? 'info'
|
category === 'LAYING' ? 'info' : 'warning';
|
||||||
: 'warning'
|
return (
|
||||||
}
|
<Badge variant='soft' color={color} size='sm'>
|
||||||
size='sm'
|
{category}
|
||||||
>
|
|
||||||
{initialValues.project_flock?.project_flock_category}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -2093,9 +2217,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
{type === 'detail' && initialValues && (
|
{type === 'detail' && initialValues && (
|
||||||
<div
|
<div
|
||||||
className={`grid gap-6 mb-6 grid-cols-1 ${
|
className={`grid gap-6 mb-6 grid-cols-1 ${
|
||||||
initialValues.project_flock?.project_flock_category === 'LAYING'
|
initialValues.is_laying ? 'xl:grid-cols-3' : 'xl:grid-cols-2'
|
||||||
? 'xl:grid-cols-3'
|
|
||||||
: 'xl:grid-cols-2'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* FCR Section */}
|
{/* FCR Section */}
|
||||||
@@ -2186,8 +2308,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
{/* Egg Production Section - Only for LAYING category */}
|
{/* Egg Production Section - Only for LAYING category */}
|
||||||
{type === 'detail' &&
|
{type === 'detail' &&
|
||||||
initialValues &&
|
initialValues &&
|
||||||
initialValues.project_flock?.project_flock_category ===
|
initialValues.is_laying && (
|
||||||
'LAYING' && (
|
|
||||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||||
<div className='px-4 py-3 border-b border-gray-200'>
|
<div className='px-4 py-3 border-b border-gray-200'>
|
||||||
<span className='card-title font-bold text-xl'>
|
<span className='card-title font-bold text-xl'>
|
||||||
@@ -2314,6 +2435,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setSelectedStocks([]);
|
setSelectedStocks([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={!recordingRestriction.canEditStock}
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -2363,6 +2485,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={!recordingRestriction.canEditStock}
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -2415,7 +2538,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
isSearchable
|
isSearchable
|
||||||
isDisabled={
|
isDisabled={
|
||||||
type === 'detail' ||
|
type === 'detail' ||
|
||||||
!formik.values.project_flock_kandang_id
|
!formik.values.project_flock_kandang_id ||
|
||||||
|
!recordingRestriction.canEditStock
|
||||||
}
|
}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
@@ -2462,7 +2586,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
disabled={type === 'detail'}
|
disabled={
|
||||||
|
type === 'detail' ||
|
||||||
|
!recordingRestriction.canEditStock
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{getStockUsageAdornment(idx)}
|
{getStockUsageAdornment(idx)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2474,6 +2601,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
onClick={() => removeStock(idx)}
|
onClick={() => removeStock(idx)}
|
||||||
|
disabled={!recordingRestriction.canEditStock}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon='mdi:trash-can'
|
icon='mdi:trash-can'
|
||||||
@@ -2491,7 +2619,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
{selectedStocks.length > 0 && (
|
{selectedStocks.length > 0 &&
|
||||||
|
recordingRestriction.canEditStock && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -2503,26 +2632,68 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
Hapus Terpilih ({selectedStocks.length})
|
Hapus Terpilih ({selectedStocks.length})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
!recordingRestriction.canEditStock
|
||||||
|
? 'Stock tidak dapat ditambahkan pada masa transisi Laying'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='success'
|
color='success'
|
||||||
onClick={addStock}
|
onClick={addStock}
|
||||||
className='w-fit'
|
className='w-fit'
|
||||||
|
disabled={!recordingRestriction.canEditStock}
|
||||||
>
|
>
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah Stok
|
Tambah Stok
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Transition Warning Banner -- MOVED UP -- */}
|
||||||
|
{isTransitionPeriod && (
|
||||||
|
<div className='alert alert-warning mb-4'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:warning-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{isLayingCategory
|
||||||
|
? 'Masa Transisi Laying: Hanya Deplesi yang dapat diisi. Stock (Pakan/OVK) tidak dapat diinput.'
|
||||||
|
: 'Masa Transisi Growing: Hanya Stock (Pakan/OVK) yang dapat diisi. Deplesi tidak dapat diinput.'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Locked Recording Warning */}
|
||||||
|
{recordingRestriction.isLocked && (
|
||||||
|
<div className='alert alert-error mb-4'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:lock-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{recordingRestriction.lockReason}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Depletions Table */}
|
{/* Depletions Table */}
|
||||||
{((type as 'add' | 'edit' | 'detail') !== 'detail' ||
|
{((type as 'add' | 'edit' | 'detail') !== 'detail' ||
|
||||||
(formik.values.depletions?.length ?? 0) > 0) && (
|
(formik.values.depletions?.length ?? 0) > 0) && (
|
||||||
<Card
|
<Card
|
||||||
title='Deplesi'
|
title='Deplesi'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full mb-4 shadow',
|
wrapper: cn('w-full mb-4 shadow', {
|
||||||
|
'opacity-60':
|
||||||
|
!recordingRestriction.canEditDepletion &&
|
||||||
|
(type as 'add' | 'edit' | 'detail') !== 'detail',
|
||||||
|
}),
|
||||||
title: 'mb-4',
|
title: 'mb-4',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2552,6 +2723,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setSelectedDepletions([]);
|
setSelectedDepletions([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={!recordingRestriction.canEditDepletion}
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -2588,6 +2760,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={!recordingRestriction.canEditDepletion}
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -2630,7 +2803,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
idx
|
idx
|
||||||
).errorMessage
|
).errorMessage
|
||||||
}
|
}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={
|
||||||
|
type === 'detail' ||
|
||||||
|
!recordingRestriction.canEditDepletion
|
||||||
|
}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-48',
|
wrapper: 'w-full min-w-48',
|
||||||
}}
|
}}
|
||||||
@@ -2669,7 +2845,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
disabled={type === 'detail'}
|
disabled={
|
||||||
|
type === 'detail' ||
|
||||||
|
!recordingRestriction.canEditDepletion
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||||
@@ -2679,6 +2858,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
onClick={() => removeDepletion(idx)}
|
onClick={() => removeDepletion(idx)}
|
||||||
|
disabled={
|
||||||
|
!recordingRestriction.canEditDepletion
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon='mdi:trash-can'
|
icon='mdi:trash-can'
|
||||||
@@ -2696,7 +2878,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||||
{selectedDepletions.length > 0 && (
|
{selectedDepletions.length > 0 &&
|
||||||
|
recordingRestriction.canEditDepletion && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -2708,15 +2891,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
Hapus Terpilih ({selectedDepletions.length})
|
Hapus Terpilih ({selectedDepletions.length})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
!recordingRestriction.canEditDepletion
|
||||||
|
? 'Deplesi tidak dapat ditambahkan pada masa transisi Growing'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='success'
|
color='success'
|
||||||
onClick={addDepletion}
|
onClick={addDepletion}
|
||||||
className='w-fit'
|
className='w-fit'
|
||||||
|
disabled={!recordingRestriction.canEditDepletion}
|
||||||
>
|
>
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah Depletion
|
Tambah Depletion
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -2990,7 +3183,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<div className='flex flex-col sm:flex-row sm:justify-between gap-2'>
|
<div className='flex flex-col sm:flex-row sm:justify-between gap-2'>
|
||||||
{/* Left side - Detail & Edit actions */}
|
{/* Left side - Detail & Edit actions */}
|
||||||
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
|
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
|
||||||
{type === 'detail' && deleteRecordingClickHandler && (
|
{type === 'detail' &&
|
||||||
|
deleteRecordingClickHandler &&
|
||||||
|
isRecordingEditable(initialValues) && (
|
||||||
<RequirePermission permissions='lti.production.recording.delete'>
|
<RequirePermission permissions='lti.production.recording.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -3008,7 +3203,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{type === 'detail' && initialValues && (
|
{type === 'detail' &&
|
||||||
|
initialValues &&
|
||||||
|
isRecordingEditable(initialValues) && (
|
||||||
<RequirePermission permissions='lti.production.recording.update'>
|
<RequirePermission permissions='lti.production.recording.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export type RecordingRestriction = {
|
||||||
|
canEditStock: boolean;
|
||||||
|
canEditDepletion: boolean;
|
||||||
|
canEditEgg: boolean;
|
||||||
|
isLocked: boolean;
|
||||||
|
lockReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecordingRestriction = (
|
||||||
|
isLaying: boolean,
|
||||||
|
isTransition: boolean,
|
||||||
|
currentIsLaying?: boolean
|
||||||
|
): RecordingRestriction => {
|
||||||
|
if (currentIsLaying && !isLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTransition && !isLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: true,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason: 'Kondisi transisi tidak valid',
|
||||||
|
};
|
||||||
|
};
|
||||||
+13
-3
@@ -50,12 +50,18 @@ const TransferToLayingConfirmationModalTable = ({
|
|||||||
transferToLayingForm?: TransferToLayingFormValues;
|
transferToLayingForm?: TransferToLayingFormValues;
|
||||||
transferToLayingId?: number;
|
transferToLayingId?: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isValidId =
|
||||||
|
transferToLayingId !== undefined &&
|
||||||
|
transferToLayingId !== null &&
|
||||||
|
!isNaN(transferToLayingId) &&
|
||||||
|
transferToLayingId > 0;
|
||||||
|
|
||||||
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
||||||
useSWR(
|
useSWR(
|
||||||
transferToLayingId
|
isValidId
|
||||||
? ['detail-transfer-to-laying', String(transferToLayingId)]
|
? ['detail-transfer-to-laying', String(transferToLayingId)]
|
||||||
: undefined,
|
: undefined,
|
||||||
([id]) => TransferToLayingApi.getSingle(Number(id))
|
([, id]) => TransferToLayingApi.getSingle(Number(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
|
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
|
||||||
@@ -273,7 +279,11 @@ const TransferToLayingConfirmationModal = ({
|
|||||||
|
|
||||||
{transferToLayingIds &&
|
{transferToLayingIds &&
|
||||||
!transferToLayingForm &&
|
!transferToLayingForm &&
|
||||||
transferToLayingIds.map((transferToLayingId, idx) => (
|
transferToLayingIds
|
||||||
|
.filter(
|
||||||
|
(id) => id !== undefined && id !== null && !isNaN(id) && id > 0
|
||||||
|
)
|
||||||
|
.map((transferToLayingId, idx) => (
|
||||||
<TransferToLayingConfirmationModalTable
|
<TransferToLayingConfirmationModalTable
|
||||||
key={idx}
|
key={idx}
|
||||||
transferToLayingId={transferToLayingId}
|
transferToLayingId={transferToLayingId}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
if (modalAction === 'detail') {
|
if (modalAction === 'detail') {
|
||||||
detailModal.openModal();
|
detailModal.openModal();
|
||||||
}
|
}
|
||||||
}, [modalAction, detailModal]);
|
}, [modalAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ const TransferToLayingFormModal = () => {
|
|||||||
ProjectFlock | undefined
|
ProjectFlock | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
|
const [maxSourceQuantity, setMaxSourceQuantity] = useState<number>(0);
|
||||||
|
|
||||||
const selectedFlockDestinationRawData = isResponseSuccess(
|
const selectedFlockDestinationRawData = isResponseSuccess(
|
||||||
flockDestinationRawData
|
flockDestinationRawData
|
||||||
)
|
)
|
||||||
@@ -353,19 +355,14 @@ const TransferToLayingFormModal = () => {
|
|||||||
return { available: countAvailable, unavailable: countUnavailable };
|
return { available: countAvailable, unavailable: countUnavailable };
|
||||||
}, [mappedFlockDestinationKandangsMaxTargetQty]);
|
}, [mappedFlockDestinationKandangsMaxTargetQty]);
|
||||||
|
|
||||||
const totalEnteredChickenForTransfer =
|
|
||||||
formik.values.flockSourceKandangs.reduce(
|
|
||||||
(acc, item) => acc + Number(item.quantity),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
|
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
|
||||||
(acc, item) => acc + Number(item.quantity),
|
(acc, item) => acc + Number(item.quantity),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
totalEnteredChickenForTransfer - totalTransferedChicken;
|
maxSourceQuantity - totalTransferedChicken;
|
||||||
|
|
||||||
const isNextButtonDisabled = useMemo(() => {
|
const isNextButtonDisabled = useMemo(() => {
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
@@ -397,6 +394,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
formik.setFieldValue('maxTotalQuantity', '');
|
formik.setFieldValue('maxTotalQuantity', '');
|
||||||
formik.setFieldValue('reason', '');
|
formik.setFieldValue('reason', '');
|
||||||
formik.setFieldTouched('reason', false);
|
formik.setFieldTouched('reason', false);
|
||||||
|
setMaxSourceQuantity(0);
|
||||||
|
|
||||||
setStep(2);
|
setStep(2);
|
||||||
};
|
};
|
||||||
@@ -404,6 +402,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('flockSource', val);
|
formik.setFieldValue('flockSource', val);
|
||||||
formik.setFieldValue('flockSourceKandangs', []);
|
formik.setFieldValue('flockSourceKandangs', []);
|
||||||
|
setMaxSourceQuantity(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flockDestinationChangeHandler = (
|
const flockDestinationChangeHandler = (
|
||||||
@@ -469,6 +468,26 @@ const TransferToLayingFormModal = () => {
|
|||||||
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
|
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
|
||||||
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
|
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
|
||||||
|
|
||||||
|
// Auto-fill source kandang quantity from total destination quantity
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik.values.flockSourceKandangs.length > 0) {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'flockSourceKandangs.0.quantity',
|
||||||
|
totalTransferedChicken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [totalTransferedChicken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
formik.values.flockSourceKandangs.length > 0 &&
|
||||||
|
formik.values.flockSourceKandangs[0].maxQuantity &&
|
||||||
|
maxSourceQuantity === 0
|
||||||
|
) {
|
||||||
|
setMaxSourceQuantity(formik.values.flockSourceKandangs[0].maxQuantity);
|
||||||
|
}
|
||||||
|
}, [formik.values.flockSourceKandangs, maxSourceQuantity]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -583,14 +602,9 @@ const TransferToLayingFormModal = () => {
|
|||||||
k.kandang.value === item.project_flock_kandang_id
|
k.kandang.value === item.project_flock_kandang_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const flockSourceKandangCheckboxChangeHandler: FormEventHandler<
|
const flockSourceKandangRadioChangeHandler = () => {
|
||||||
HTMLInputElement
|
if (isAvailable) {
|
||||||
> = (e) => {
|
|
||||||
const checked = (e.target as HTMLInputElement)
|
|
||||||
.checked;
|
|
||||||
if (checked) {
|
|
||||||
formik.setFieldValue('flockSourceKandangs', [
|
formik.setFieldValue('flockSourceKandangs', [
|
||||||
...formik.values.flockSourceKandangs,
|
|
||||||
{
|
{
|
||||||
kandang: {
|
kandang: {
|
||||||
value: item.project_flock_kandang_id,
|
value: item.project_flock_kandang_id,
|
||||||
@@ -600,15 +614,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
maxQuantity: item.available_qty,
|
maxQuantity: item.available_qty,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
setMaxSourceQuantity(item.available_qty);
|
||||||
formik.setFieldValue(
|
|
||||||
'flockSourceKandangs',
|
|
||||||
formik.values.flockSourceKandangs.filter(
|
|
||||||
(k) =>
|
|
||||||
k.kandang.value !==
|
|
||||||
item.project_flock_kandang_id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -618,28 +624,22 @@ const TransferToLayingFormModal = () => {
|
|||||||
className='w-full p-3 flex flex-row items-center justify-between'
|
className='w-full p-3 flex flex-row items-center justify-between'
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-3'>
|
<div className='flex flex-row items-center gap-3'>
|
||||||
<CheckboxInput
|
<input
|
||||||
name={`flockSourceKandang.${itemIdx}.value`}
|
type='radio'
|
||||||
|
name='flockSourceKandang'
|
||||||
value={item.project_flock_kandang_id}
|
value={item.project_flock_kandang_id}
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onChange={
|
onChange={flockSourceKandangRadioChangeHandler}
|
||||||
flockSourceKandangCheckboxChangeHandler
|
|
||||||
}
|
|
||||||
size='md'
|
|
||||||
disabled={!isAvailable}
|
disabled={!isAvailable}
|
||||||
classNames={{
|
className={cn('radio radio-md radio-primary', {
|
||||||
checkbox: cn({
|
'opacity-50 cursor-not-allowed': !isAvailable,
|
||||||
'bg-base-200 border border-base-content/10 opacity-100':
|
})}
|
||||||
!isAvailable,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor={`flockSourceKandang.${itemIdx}.value`}
|
|
||||||
className={cn('text-sm text-base-content/50', {
|
className={cn('text-sm text-base-content/50', {
|
||||||
'cursor-pointer': isAvailable,
|
'cursor-pointer': isAvailable,
|
||||||
'cursor-not-allowed': !isAvailable,
|
'cursor-not-allowed opacity-50': !isAvailable,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.kandang_name}{' '}
|
{item.kandang_name}{' '}
|
||||||
@@ -858,7 +858,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
|
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
|
||||||
name={`flockSourceKandangs.${index}.quantity`}
|
name={`flockSourceKandangs.${index}.quantity`}
|
||||||
placeholder='Masukkan Kuantitas'
|
placeholder='Masukkan Kuantitas pada Kandang Tujuan'
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
isError={isInvalid}
|
isError={isInvalid}
|
||||||
@@ -875,6 +875,8 @@ const TransferToLayingFormModal = () => {
|
|||||||
<div className='w-px bg-base-content/10' />
|
<div className='w-px bg-base-content/10' />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
className={{
|
className={{
|
||||||
inputPrefix:
|
inputPrefix:
|
||||||
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
||||||
@@ -1000,7 +1002,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
isError={totalAvailableChickenForTransfer < 0}
|
isError={totalAvailableChickenForTransfer < 0}
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ const RowOptionsMenu = ({
|
|||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const showEditButton =
|
const showEditButton = props.row.original.approval.action !== 'APPROVED';
|
||||||
props.row.original.approval.action !== 'APPROVED' &&
|
|
||||||
props.row.original.approval.action !== 'REJECTED';
|
|
||||||
|
|
||||||
const showDeleteButton = showEditButton;
|
const showDeleteButton =
|
||||||
|
props.row.original.approval.action === 'APPROVED' ||
|
||||||
|
props.row.original.approval.step_name.toLowerCase() === 'pengajuan';
|
||||||
|
|
||||||
const popoverId = `transferToLaying#${props.row.original.id}`;
|
const popoverId = `transferToLaying#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
||||||
@@ -463,7 +463,7 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('filter_by', '');
|
updateFilter('filter_by', '');
|
||||||
updateFilter('sort_by', '');
|
updateFilter('sort_by', '');
|
||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
|||||||
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
|
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewUniformityDetails = () => {
|
||||||
|
if (!uniformity_details || uniformity_details.length === 0) {
|
||||||
|
setShouldFetchDetails(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpandedDrawerContent(
|
||||||
|
<UniformityDetailsPreview
|
||||||
|
info_umum={initialValues.info_umum}
|
||||||
|
uniformity_details={uniformity_details}
|
||||||
|
uniformityId={initialValues.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setExpandedDrawerOpen(true);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
shouldFetchDetails &&
|
shouldFetchDetails &&
|
||||||
@@ -183,25 +202,6 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'document-name') {
|
if (id === 'document-name') {
|
||||||
const handleViewUniformityDetails = () => {
|
|
||||||
if (!uniformity_details || uniformity_details.length === 0) {
|
|
||||||
setShouldFetchDetails(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExpandedDrawerContent(
|
|
||||||
<UniformityDetailsPreview
|
|
||||||
info_umum={initialValues.info_umum}
|
|
||||||
uniformity_details={uniformity_details}
|
|
||||||
uniformityId={initialValues.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setExpandedDrawerOpen(true);
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span>{valueMap[id]}</span>
|
<span>{valueMap[id]}</span>
|
||||||
@@ -231,14 +231,7 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[initialValues, handleViewUniformityDetails, isLoading]
|
||||||
initialValues,
|
|
||||||
isLoading,
|
|
||||||
uniformity_details,
|
|
||||||
setShouldFetchDetails,
|
|
||||||
setExpandedDrawerContent,
|
|
||||||
setExpandedDrawerOpen,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const samplingTableData: DetailOptionType[] = useMemo(() => {
|
const samplingTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
'lti.daily_checklist.master_data.employee',
|
'lti.daily_checklist.master_data.employee',
|
||||||
'lti.daily_checklist.master_data.activity',
|
'lti.daily_checklist.master_data.activity',
|
||||||
'lti.daily_checklist.master_data.configuration',
|
'lti.daily_checklist.master_data.configuration',
|
||||||
|
'lti.daily_checklist.master_data.kandang',
|
||||||
],
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/daily-checklist/master-data/activity',
|
link: '/daily-checklist/master-data/activity',
|
||||||
permission: ['lti.daily_checklist.master_data.activity'],
|
permission: ['lti.daily_checklist.master_data.activity'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Kandang',
|
||||||
|
link: '/daily-checklist/master-data/kandang',
|
||||||
|
permission: ['lti.daily_checklist.master_data.kandang'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Konfigurasi',
|
text: 'Konfigurasi',
|
||||||
link: '/daily-checklist/master-data/configuration',
|
link: '/daily-checklist/master-data/configuration',
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/daily-checklist/master-data/configuration/': [
|
'/daily-checklist/master-data/configuration/': [
|
||||||
'lti.daily_checklist.master_data.configuration',
|
'lti.daily_checklist.master_data.configuration',
|
||||||
],
|
],
|
||||||
|
'/daily-checklist/master-data/kandang/': [
|
||||||
|
'lti.daily_checklist.master_data.kandang',
|
||||||
|
],
|
||||||
|
|
||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +29,8 @@ interface MultiSelectProps {
|
|||||||
selected: string[];
|
selected: string[];
|
||||||
onChange: (selected: string[]) => void;
|
onChange: (selected: string[]) => void;
|
||||||
onSearchChange?: (value: string) => void;
|
onSearchChange?: (value: string) => void;
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
isLoadingMore?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -39,6 +41,8 @@ export function MultiSelect({
|
|||||||
selected,
|
selected,
|
||||||
onChange,
|
onChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
|
onLoadMore,
|
||||||
|
isLoadingMore,
|
||||||
placeholder = 'Select items...',
|
placeholder = 'Select items...',
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -115,7 +119,18 @@ export function MultiSelect({
|
|||||||
onValueChange={onSearchChange}
|
onValueChange={onSearchChange}
|
||||||
/>
|
/>
|
||||||
<CommandEmpty>No item found.</CommandEmpty>
|
<CommandEmpty>No item found.</CommandEmpty>
|
||||||
<CommandList className='max-h-[300px] overflow-y-auto'>
|
<CommandList
|
||||||
|
className='max-h-[300px] overflow-y-auto'
|
||||||
|
onScroll={(e) => {
|
||||||
|
const target = e.currentTarget;
|
||||||
|
if (
|
||||||
|
target.scrollHeight - target.scrollTop <=
|
||||||
|
target.clientHeight + 1
|
||||||
|
) {
|
||||||
|
onLoadMore?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CommandGroup className='overflow-visible'>
|
<CommandGroup className='overflow-visible'>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -134,6 +149,11 @@ export function MultiSelect({
|
|||||||
{option.label}
|
{option.label}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className='py-4 flex justify-center w-full'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ function SelectContent({
|
|||||||
children,
|
children,
|
||||||
position = 'popper',
|
position = 'popper',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
||||||
|
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||||
|
}) {
|
||||||
|
const { onScroll, ...restProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
@@ -67,7 +71,7 @@ function SelectContent({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
@@ -76,6 +80,7 @@ function SelectContent({
|
|||||||
position === 'popper' &&
|
position === 'popper' &&
|
||||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
||||||
)}
|
)}
|
||||||
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
|
import {
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
|
Save,
|
||||||
|
Send,
|
||||||
|
Info,
|
||||||
|
FilePlus,
|
||||||
|
ListChecks,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Label } from '@/figma-make/components/base/label';
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
@@ -26,7 +35,6 @@ import {
|
|||||||
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -43,6 +51,7 @@ import DropFileInput from '@/components/input/DropFileInput';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
// Static categories
|
// Static categories
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@@ -86,16 +95,11 @@ export function DailyChecklistContent() {
|
|||||||
searchParams.get('category') || ''
|
searchParams.get('category') || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'name',
|
loadMore: loadMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: phases } = useSWR<
|
const { data: phases } = useSWR<
|
||||||
BaseApiResponse<Phase[] | undefined>,
|
BaseApiResponse<Phase[] | undefined>,
|
||||||
@@ -168,6 +172,16 @@ export function DailyChecklistContent() {
|
|||||||
const [documents, setDocuments] = useState<File[]>([]);
|
const [documents, setDocuments] = useState<File[]>([]);
|
||||||
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Sync state to URL query params
|
// Sync state to URL query params
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
@@ -301,93 +315,8 @@ export function DailyChecklistContent() {
|
|||||||
checkAndLoadChecklist();
|
checkAndLoadChecklist();
|
||||||
}, [date, kandangId, selectedCategory]);
|
}, [date, kandangId, selectedCategory]);
|
||||||
|
|
||||||
// Load employees when kandang changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (kandangId) {
|
|
||||||
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
setAssignments({});
|
|
||||||
} else {
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
setAssignments({});
|
|
||||||
}
|
|
||||||
}, [kandangId]);
|
|
||||||
|
|
||||||
// Load activities and tasks when phases change
|
// Load activities and tasks when phases change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAssignments = async (taskIds: string[]) => {
|
|
||||||
if (taskIds.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingDailyChecklist =
|
|
||||||
await DailyChecklistApi.getOneDailyChecklist(
|
|
||||||
String(dailyChecklistId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseError(existingDailyChecklist)) {
|
|
||||||
console.error(
|
|
||||||
'Error loading assignments:',
|
|
||||||
existingDailyChecklist.message
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set existing document
|
|
||||||
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
|
|
||||||
|
|
||||||
// Build assignments map
|
|
||||||
const assignmentMap: {
|
|
||||||
[taskId: string]: {
|
|
||||||
[employeeId: string]: { checked: boolean; note: string };
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
(existingDailyChecklist?.data.tasks || []).forEach(
|
|
||||||
(dailyChecklistTask) => {
|
|
||||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
|
||||||
assignmentMap[dailyChecklistTask.id] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
dailyChecklistTask.assignments.forEach((assignment) => {
|
|
||||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
|
||||||
assignmentMap[dailyChecklistTask.id] = {};
|
|
||||||
}
|
|
||||||
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
|
|
||||||
checked: assignment.checked,
|
|
||||||
note: assignment.note || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setAssignments(assignmentMap);
|
|
||||||
|
|
||||||
// Load employees from assignments
|
|
||||||
const employeeIds = Array.from(
|
|
||||||
new Set(
|
|
||||||
(existingDailyChecklist?.data.assigned_employees || []).map(
|
|
||||||
(a) => a.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (employeeIds.length > 0) {
|
|
||||||
const existingDailyChecklist =
|
|
||||||
await DailyChecklistApi.getOneDailyChecklist(
|
|
||||||
String(dailyChecklistId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseSuccess(existingDailyChecklist)) {
|
|
||||||
setSelectedEmployees(
|
|
||||||
existingDailyChecklist.data.assigned_employees
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading assignments:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadActivitiesAndTasks = async () => {
|
const loadActivitiesAndTasks = async () => {
|
||||||
if (!dailyChecklistId || selectedPhaseIds.length === 0) {
|
if (!dailyChecklistId || selectedPhaseIds.length === 0) {
|
||||||
setActivitiesByPhase({});
|
setActivitiesByPhase({});
|
||||||
@@ -462,6 +391,87 @@ export function DailyChecklistContent() {
|
|||||||
loadActivitiesAndTasks();
|
loadActivitiesAndTasks();
|
||||||
}, [dailyChecklistId, selectedPhaseIds]);
|
}, [dailyChecklistId, selectedPhaseIds]);
|
||||||
|
|
||||||
|
// Load employees when kandang changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (kandangId) {
|
||||||
|
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
||||||
|
setSelectedEmployees([]);
|
||||||
|
setAssignments({});
|
||||||
|
} else {
|
||||||
|
setSelectedEmployees([]);
|
||||||
|
setAssignments({});
|
||||||
|
}
|
||||||
|
}, [kandangId]);
|
||||||
|
|
||||||
|
const loadAssignments = async (taskIds: string[]) => {
|
||||||
|
if (taskIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingDailyChecklist =
|
||||||
|
await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
|
||||||
|
|
||||||
|
if (isResponseError(existingDailyChecklist)) {
|
||||||
|
console.error(
|
||||||
|
'Error loading assignments:',
|
||||||
|
existingDailyChecklist.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set existing document
|
||||||
|
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
|
||||||
|
|
||||||
|
// Build assignments map
|
||||||
|
const assignmentMap: {
|
||||||
|
[taskId: string]: {
|
||||||
|
[employeeId: string]: { checked: boolean; note: string };
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
(existingDailyChecklist?.data.tasks || []).forEach(
|
||||||
|
(dailyChecklistTask) => {
|
||||||
|
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||||
|
assignmentMap[dailyChecklistTask.id] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyChecklistTask.assignments.forEach((assignment) => {
|
||||||
|
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||||
|
assignmentMap[dailyChecklistTask.id] = {};
|
||||||
|
}
|
||||||
|
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
|
||||||
|
checked: assignment.checked,
|
||||||
|
note: assignment.note || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setAssignments(assignmentMap);
|
||||||
|
|
||||||
|
// Load employees from assignments
|
||||||
|
const employeeIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
(existingDailyChecklist?.data.assigned_employees || []).map(
|
||||||
|
(a) => a.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (employeeIds.length > 0) {
|
||||||
|
const existingDailyChecklist =
|
||||||
|
await DailyChecklistApi.getOneDailyChecklist(
|
||||||
|
String(dailyChecklistId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(existingDailyChecklist)) {
|
||||||
|
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading assignments:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Phase selection modal
|
// Phase selection modal
|
||||||
const handleAddPhase = () => {
|
const handleAddPhase = () => {
|
||||||
if (!selectedCategory) {
|
if (!selectedCategory) {
|
||||||
@@ -998,7 +1008,7 @@ export function DailyChecklistContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Pilih kandang' />
|
<SelectValue placeholder='Pilih kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={kandang.value}
|
key={kandang.value}
|
||||||
@@ -1007,6 +1017,12 @@ export function DailyChecklistContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/figma-make/components/base/select';
|
} from '@/figma-make/components/base/select';
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
import { Users, AlertCircle, Info } from 'lucide-react';
|
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react';
|
||||||
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
@@ -36,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
const KANDANG_COLORS = [
|
const KANDANG_COLORS = [
|
||||||
'#0069e0', // Blue (primary)
|
'#0069e0', // Blue (primary)
|
||||||
@@ -77,16 +77,20 @@ export function Dashboard() {
|
|||||||
httpClientFetcher
|
httpClientFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
loadMore: loadMoreKandang,
|
||||||
'name',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
limit: '100',
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const kandangColorMap: { [key: string]: string } = {};
|
const kandangColorMap: { [key: string]: string } = {};
|
||||||
(kandangOptions || []).forEach((k, index) => {
|
(kandangOptions || []).forEach((k, index) => {
|
||||||
@@ -164,7 +168,7 @@ export function Dashboard() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua Kandang' />
|
<SelectValue placeholder='Semua Kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -174,6 +178,11 @@ export function Dashboard() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+30
-13
@@ -1,7 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
|
import {
|
||||||
|
Eye,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Search,
|
||||||
|
Trash2,
|
||||||
|
Edit,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
@@ -34,9 +42,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -93,21 +101,25 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'name',
|
loadMore: loadMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const checklistList = isResponseSuccess(checklistListRes)
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
? checklistListRes.data || []
|
? checklistListRes.data || []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
@@ -490,7 +502,7 @@ export function ListDailyChecklistContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua Kandang' />
|
<SelectValue placeholder='Semua Kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -500,6 +512,11 @@ export function ListDailyChecklistContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
@@ -137,7 +137,15 @@ export function DetailDailyChecklistContent() {
|
|||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
const fetchChecklistDetail = useCallback(async () => {
|
useEffect(() => {
|
||||||
|
if (checklistId) {
|
||||||
|
fetchChecklistDetail();
|
||||||
|
} else {
|
||||||
|
router.push('/404');
|
||||||
|
}
|
||||||
|
}, [checklistId]);
|
||||||
|
|
||||||
|
const fetchChecklistDetail = async () => {
|
||||||
if (!checklistId) {
|
if (!checklistId) {
|
||||||
console.warn('checklistId missing');
|
console.warn('checklistId missing');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -312,15 +320,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [checklistId, router]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (checklistId) {
|
|
||||||
fetchChecklistDetail();
|
|
||||||
} else {
|
|
||||||
router.push('/404');
|
|
||||||
}
|
|
||||||
}, [checklistId, fetchChecklistDetail, router]);
|
|
||||||
|
|
||||||
const groupDetailData = (rows: ChecklistDetailRow[]) => {
|
const groupDetailData = (rows: ChecklistDetailRow[]) => {
|
||||||
// Group by phase_id
|
// Group by phase_id
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
import {
|
||||||
|
Plus,
|
||||||
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Label } from '@/figma-make/components/base/label';
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
@@ -49,8 +56,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
export function MasterEmployeeContent() {
|
export function MasterEmployeeContent() {
|
||||||
const {
|
const {
|
||||||
@@ -85,16 +92,20 @@ export function MasterEmployeeContent() {
|
|||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
loadMore: loadMoreKandang,
|
||||||
'name',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
{
|
|
||||||
page: '1',
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
limit: '100',
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
@@ -351,7 +362,7 @@ export function MasterEmployeeContent() {
|
|||||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
<SelectValue placeholder='Semua Kandang' />
|
<SelectValue placeholder='Semua Kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
<SelectItem value='all'>Semua Kandang</SelectItem>
|
<SelectItem value='all'>Semua Kandang</SelectItem>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -361,6 +372,11 @@ export function MasterEmployeeContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -471,6 +487,12 @@ export function MasterEmployeeContent() {
|
|||||||
kandang_ids: selected.map((id) => Number(id)),
|
kandang_ids: selected.map((id) => Number(id)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
onLoadMore={() => {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoadingMore={isLoadingMoreKandang}
|
||||||
placeholder='Pilih kandang'
|
placeholder='Pilih kandang'
|
||||||
className='mt-1.5'
|
className='mt-1.5'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
|
import { Input } from '@/figma-make/components/base/input';
|
||||||
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
|
import { MultiSelect } from '@/figma-make/components/base/multi-select';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/figma-make/components/base/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/figma-make/components/base/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/figma-make/components/base/alert-dialog';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/figma-make/components/base/dropdown-menu';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
|
export function MasterKandangContent() {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
location_id: '',
|
||||||
|
status: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
search: 'search',
|
||||||
|
location_id: 'location_id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: dailyChecklistKandangs,
|
||||||
|
isLoading: isLoadingDailyChecklistKandangs,
|
||||||
|
mutate: refreshDailyChecklistKandangs,
|
||||||
|
} = useSWR(
|
||||||
|
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
DailyChecklistKandangApi.getAllFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { options: locationOptions } = useSelect(
|
||||||
|
LocationApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options: picOptions } = useSelect(
|
||||||
|
UserApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingMore: isLoadingKandangOptionsMore,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
|
||||||
|
const [kandangForm, setKandangForm] = useState({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: [] as number[],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Nama',
|
||||||
|
accessorKey: 'name',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'location',
|
||||||
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => row.original.location.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pic',
|
||||||
|
header: 'PIC',
|
||||||
|
accessorKey: 'pic',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => row.original.pic.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recording_kandangs',
|
||||||
|
header: 'Kandang Recording',
|
||||||
|
accessorKey: 'recording_kandangs',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.recording_kandangs?.length > 0
|
||||||
|
? row.original.recording_kandangs.map((item) => item.name).join(', ')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'action',
|
||||||
|
header: 'Aksi',
|
||||||
|
accessorKey: 'action',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
className='h-8 w-8 p-0 hover:bg-gray-100'
|
||||||
|
>
|
||||||
|
<MoreVertical className='h-4 w-4 text-gray-600' />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||||
|
<Pencil className='mr-2 h-4 w-4' />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteClick(row.original.id)}
|
||||||
|
className='text-red-600'
|
||||||
|
>
|
||||||
|
<Trash2 className='mr-2 h-4 w-4' />
|
||||||
|
Hapus
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setModalMode('create');
|
||||||
|
setKandangForm({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: []
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
|
||||||
|
setModalMode('edit');
|
||||||
|
setKandangForm({
|
||||||
|
id: dailyChecklistKandang.id,
|
||||||
|
name: dailyChecklistKandang.name,
|
||||||
|
location_id: dailyChecklistKandang.location.id,
|
||||||
|
pic_id: dailyChecklistKandang.pic.id,
|
||||||
|
// recording_kandangs:
|
||||||
|
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!kandangForm.name.trim()) {
|
||||||
|
toast.error('Nama harus diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kandangForm.location_id) {
|
||||||
|
toast.error('Lokasi wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!kandangForm.recording_kandangs.length) {
|
||||||
|
// toast.error('Kandang recording wajib diisi');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (modalMode === 'create') {
|
||||||
|
const createDailyChecklistKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.create({
|
||||||
|
name: kandangForm.name.trim(),
|
||||||
|
location_id: kandangForm.location_id,
|
||||||
|
pic_id: kandangForm.pic_id,
|
||||||
|
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(createDailyChecklistKandangResponse)) {
|
||||||
|
console.error(
|
||||||
|
'Error creating kandang:',
|
||||||
|
createDailyChecklistKandangResponse.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menambahkan kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil ditambahkan');
|
||||||
|
} else {
|
||||||
|
const updateDailyChecklistKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.update(kandangForm.id, {
|
||||||
|
name: kandangForm.name.trim(),
|
||||||
|
location_id: kandangForm.location_id,
|
||||||
|
pic_id: kandangForm.pic_id,
|
||||||
|
// recording_kandang_ids: kandangForm.recording_kandangs,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(updateDailyChecklistKandangResponse)) {
|
||||||
|
console.error(
|
||||||
|
'Error updating kandang:',
|
||||||
|
updateDailyChecklistKandangResponse.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menambahkan Kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil diubah');
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
setKandangForm({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
location_id: 0,
|
||||||
|
pic_id: 0,
|
||||||
|
// recording_kandangs: [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving kandang:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menyimpan kandang');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (kandangId: number) => {
|
||||||
|
setKandangToDelete(kandangId);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (!kandangToDelete) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteKandangResponse =
|
||||||
|
await DailyChecklistKandangApi.delete(kandangToDelete);
|
||||||
|
|
||||||
|
if (isResponseError(deleteKandangResponse)) {
|
||||||
|
console.error('Error deleting kandang:', deleteKandangResponse.message);
|
||||||
|
toast.error('Gagal menghapus kandang');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDailyChecklistKandangs();
|
||||||
|
toast.success('Kandang berhasil dihapus');
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setKandangToDelete(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting kandang:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menghapus kandang');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Master Kandang
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-12 text-center text-gray-500'>
|
||||||
|
Memuat data...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
{/* Page Title */}
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Master Kandang
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data • <span className='text-[#0069e0]'>Kandang</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Card */}
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-0'>
|
||||||
|
{/* Single Toolbar Row */}
|
||||||
|
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
|
||||||
|
{/* LEFT: Search + Filters */}
|
||||||
|
<div className='flex items-center gap-3 flex-wrap'>
|
||||||
|
<div className='relative'>
|
||||||
|
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
|
||||||
|
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari kandang...'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={(e) => updateFilter('search', e.target.value)}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
||||||
|
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||||
|
input: 'text-sm',
|
||||||
|
}}
|
||||||
|
startAdornment={
|
||||||
|
<Search className='text-gray-400 w-4 h-4' />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tableFilterState.location_id}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateFilter('location_id', value === 'all' ? '' : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
|
<SelectValue placeholder='Semua Lokasi' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='all'>Semua Lokasi</SelectItem>
|
||||||
|
{locationOptions.map((kandang) => (
|
||||||
|
<SelectItem
|
||||||
|
key={kandang.value}
|
||||||
|
value={String(kandang.value)}
|
||||||
|
>
|
||||||
|
{kandang.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT: Export + Add */}
|
||||||
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
|
<Button
|
||||||
|
onClick={handleAdd}
|
||||||
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||||
|
>
|
||||||
|
<Plus className='w-4 h-4 mr-2' />
|
||||||
|
Tambah Kandang
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Table<DailyChecklistKandang>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={dailyChecklistKandangColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(dailyChecklistKandangs)
|
||||||
|
? dailyChecklistKandangs?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingDailyChecklistKandangs}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(dailyChecklistKandangs) &&
|
||||||
|
dailyChecklistKandangs?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
|
||||||
|
headerRowClassName: 'bg-gray-50/50',
|
||||||
|
headerColumnClassName:
|
||||||
|
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
|
||||||
|
paginationClassName: 'px-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add/Edit Modal */}
|
||||||
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{modalMode === 'create'
|
||||||
|
? 'Masukkan detail Kandang baru'
|
||||||
|
: 'Ubah detail Kandang'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className='space-y-4 py-4'>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='nama-kandang'>
|
||||||
|
Nama Kandang <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id='nama-kandang'
|
||||||
|
value={kandangForm.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setKandangForm({ ...kandangForm, name: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder='Masukkan nama Kandang'
|
||||||
|
className='mt-1.5'
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='category'>
|
||||||
|
Lokasi <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
kandangForm.location_id ? String(kandangForm.location_id) : ''
|
||||||
|
}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setKandangForm({ ...kandangForm, location_id: Number(value) })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id='category' className='mt-1.5'>
|
||||||
|
<SelectValue placeholder='Pilih lokasi' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{locationOptions.map((cat) => (
|
||||||
|
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||||
|
{cat.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='pic'>
|
||||||
|
PIC <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setKandangForm({ ...kandangForm, pic_id: Number(value) })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id='pic' className='mt-1.5'>
|
||||||
|
<SelectValue placeholder='Pilih PIC' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{picOptions.map((cat) => (
|
||||||
|
<SelectItem key={cat.value} value={String(cat.value)}>
|
||||||
|
{cat.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||||
|
>
|
||||||
|
{loading ? 'Menyimpan...' : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||||
|
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Data Kandang akan dihapus secara permanen.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
{loading ? 'Menghapus...' : 'Hapus'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/figma-make/components/base/select';
|
} from '@/figma-make/components/base/select';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
|
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
@@ -26,7 +26,8 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
||||||
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
|
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Download } from 'lucide-react';
|
import { Download, Loader2 } from 'lucide-react';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
const MONTH_OPTIONS = [
|
const MONTH_OPTIONS = [
|
||||||
{ value: '1', label: 'Januari' },
|
{ value: '1', label: 'Januari' },
|
||||||
@@ -129,18 +130,23 @@ export function DailyChecklistReportsContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: kandangOptions } = useSelect(
|
const {
|
||||||
KandangApi.basePath,
|
options: kandangOptions,
|
||||||
'id',
|
loadMore: loadMoreKandang,
|
||||||
'name',
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
'search',
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
{
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
area_id: tableFilterState.area_id,
|
area_id: tableFilterState.area_id,
|
||||||
location_id: tableFilterState.location_id,
|
location_id: tableFilterState.location_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreKandang) {
|
||||||
|
loadMoreKandang();
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { options: phaseOptions } = useSelect(
|
const { options: phaseOptions } = useSelect(
|
||||||
PhaseApi.basePath,
|
PhaseApi.basePath,
|
||||||
@@ -435,7 +441,7 @@ export function DailyChecklistReportsContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua Kandang' />
|
<SelectValue placeholder='Semua Kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangOptions.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -445,6 +451,11 @@ export function DailyChecklistReportsContent() {
|
|||||||
{kandang.label}
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreKandang && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -305,17 +305,3 @@ export function transformConstants(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function omit<T extends Record<string, unknown>, K extends keyof T>(
|
|
||||||
obj: T,
|
|
||||||
keys: K | K[]
|
|
||||||
): Omit<T, K> {
|
|
||||||
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
||||||
const result = { ...obj };
|
|
||||||
|
|
||||||
keysArray.forEach((key) => {
|
|
||||||
delete result[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import {
|
||||||
|
DailyChecklistKandang,
|
||||||
|
CreateDailyChecklistKandangPayload,
|
||||||
|
UpdateDailyChecklistKandangPayload,
|
||||||
|
} from '@/types/api/daily-checklist/kandang';
|
||||||
|
|
||||||
|
export class DailyChecklistKandangApiService extends BaseApiService<
|
||||||
|
DailyChecklistKandang,
|
||||||
|
CreateDailyChecklistKandangPayload,
|
||||||
|
UpdateDailyChecklistKandangPayload
|
||||||
|
> {
|
||||||
|
constructor(basePath: string = '/master-data/kandang-groups') {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DailyChecklistKandangApi = new DailyChecklistKandangApiService(
|
||||||
|
'/master-data/kandang-groups'
|
||||||
|
);
|
||||||
@@ -4,13 +4,16 @@ import { create } from 'zustand';
|
|||||||
import { devtools } from 'zustand/middleware';
|
import { devtools } from 'zustand/middleware';
|
||||||
import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
|
import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
|
||||||
import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
|
import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
|
||||||
|
import { createChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
|
||||||
|
import { ChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
|
||||||
|
|
||||||
export type ChickinStore = ChickinApprovalSlice;
|
export type ChickinStore = ChickinApprovalSlice & ChickinDeleteSlice;
|
||||||
|
|
||||||
export const useChickinStore = create<ChickinStore>()(
|
export const useChickinStore = create<ChickinStore>()(
|
||||||
devtools(
|
devtools(
|
||||||
(...args) => ({
|
(...args) => ({
|
||||||
...createChickinApprovalSlice(...args),
|
...createChickinApprovalSlice(...args),
|
||||||
|
...createChickinDeleteSlice(...args),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'ChickinStore',
|
name: 'ChickinStore',
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
|
||||||
|
export type ChickinDeleteSlice = {
|
||||||
|
// State
|
||||||
|
isChickinDeleteModalOpen: boolean;
|
||||||
|
selectedChickinIdForDelete: number | null;
|
||||||
|
isChickinDeleteLoading: boolean;
|
||||||
|
chickinDeleteCallback: (() => Promise<void>) | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
openChickinDeleteModal: (
|
||||||
|
chickinId: number,
|
||||||
|
callback: () => Promise<void>
|
||||||
|
) => void;
|
||||||
|
closeChickinDeleteModal: () => void;
|
||||||
|
setChickinDeleteLoading: (loading: boolean) => void;
|
||||||
|
resetChickinDelete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createChickinDeleteSlice: StateCreator<
|
||||||
|
ChickinDeleteSlice,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
ChickinDeleteSlice
|
||||||
|
> = (set) => ({
|
||||||
|
// Initial state
|
||||||
|
isChickinDeleteModalOpen: false,
|
||||||
|
selectedChickinIdForDelete: null,
|
||||||
|
isChickinDeleteLoading: false,
|
||||||
|
chickinDeleteCallback: null,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
openChickinDeleteModal: (chickinId, callback) =>
|
||||||
|
set({
|
||||||
|
isChickinDeleteModalOpen: true,
|
||||||
|
selectedChickinIdForDelete: chickinId,
|
||||||
|
chickinDeleteCallback: callback,
|
||||||
|
}),
|
||||||
|
|
||||||
|
closeChickinDeleteModal: () =>
|
||||||
|
set({
|
||||||
|
isChickinDeleteModalOpen: false,
|
||||||
|
selectedChickinIdForDelete: null,
|
||||||
|
chickinDeleteCallback: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
setChickinDeleteLoading: (loading) =>
|
||||||
|
set({ isChickinDeleteLoading: loading }),
|
||||||
|
|
||||||
|
resetChickinDelete: () =>
|
||||||
|
set({
|
||||||
|
isChickinDeleteModalOpen: false,
|
||||||
|
selectedChickinIdForDelete: null,
|
||||||
|
isChickinDeleteLoading: false,
|
||||||
|
chickinDeleteCallback: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
|
import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import { omit } from '@/lib/helper';
|
|
||||||
import { StateCreator } from 'zustand';
|
import { StateCreator } from 'zustand';
|
||||||
|
|
||||||
export const createTabActionsSlice: StateCreator<
|
export const createTabActionsSlice: StateCreator<
|
||||||
@@ -21,9 +20,10 @@ export const createTabActionsSlice: StateCreator<
|
|||||||
})),
|
})),
|
||||||
|
|
||||||
clearTabActions: (tabId) =>
|
clearTabActions: (tabId) =>
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
tabActions: omit(state.tabActions, tabId),
|
const { [tabId]: _, ...rest } = state.tabActions;
|
||||||
})),
|
return { tabActions: rest };
|
||||||
|
}),
|
||||||
|
|
||||||
clearAllTabActions: () => set({ tabActions: {} }),
|
clearAllTabActions: () => set({ tabActions: {} }),
|
||||||
});
|
});
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { BaseLocation } from '@/types/api/master-data/location';
|
||||||
|
import { BaseUser } from '@/types/api/user';
|
||||||
|
|
||||||
|
export type BaseDailyChecklistKandang = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
location: BaseLocation;
|
||||||
|
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
|
||||||
|
pic: BaseUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
|
||||||
|
|
||||||
|
export type CreateDailyChecklistKandangPayload = {
|
||||||
|
name: string;
|
||||||
|
location_id: number;
|
||||||
|
pic_id: number;
|
||||||
|
// recording_kandang_ids: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDailyChecklistKandangPayload =
|
||||||
|
CreateDailyChecklistKandangPayload;
|
||||||
+3
@@ -1,6 +1,7 @@
|
|||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { BaseLocation } from '@/types/api/master-data/location';
|
import { BaseLocation } from '@/types/api/master-data/location';
|
||||||
import { BaseUser } from '@/types/api/user';
|
import { BaseUser } from '@/types/api/user';
|
||||||
|
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
|
||||||
export type BaseKandang = {
|
export type BaseKandang = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -10,6 +11,7 @@ export type BaseKandang = {
|
|||||||
capacity: number;
|
capacity: number;
|
||||||
pic: BaseUser;
|
pic: BaseUser;
|
||||||
project_flock_kandang_id?: number;
|
project_flock_kandang_id?: number;
|
||||||
|
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Kandang = BaseMetadata & BaseKandang;
|
export type Kandang = BaseMetadata & BaseKandang;
|
||||||
@@ -19,6 +21,7 @@ export type CreateKandangPayload = {
|
|||||||
location_id: number;
|
location_id: number;
|
||||||
capacity: number;
|
capacity: number;
|
||||||
pic_id: number;
|
pic_id: number;
|
||||||
|
group_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateKandangPayload = CreateKandangPayload;
|
export type UpdateKandangPayload = CreateKandangPayload;
|
||||||
|
|||||||
+2
@@ -74,6 +74,8 @@ export type ProjectFlockKandangLookup = {
|
|||||||
available_quantity?: number;
|
available_quantity?: number;
|
||||||
population: number;
|
population: number;
|
||||||
chick_in_date: string;
|
chick_in_date: string;
|
||||||
|
is_transition: boolean;
|
||||||
|
is_laying: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectFlockAvailableQuantity = {
|
export type ProjectFlockAvailableQuantity = {
|
||||||
|
|||||||
+2
@@ -49,6 +49,8 @@ export type BaseRecording = {
|
|||||||
project_flock: ProjectFlock;
|
project_flock: ProjectFlock;
|
||||||
record_datetime: string;
|
record_datetime: string;
|
||||||
day: number;
|
day: number;
|
||||||
|
is_transition: boolean;
|
||||||
|
is_laying: boolean;
|
||||||
} & ProductionMetrics;
|
} & ProductionMetrics;
|
||||||
|
|
||||||
export type RecordingDepletion = {
|
export type RecordingDepletion = {
|
||||||
|
|||||||
Reference in New Issue
Block a user