mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
Merge branch 'staging' into 'production'
refactor(FE): Add tab state management and skeleton for See merge request mbugroup/lti-web-client!335
This commit is contained in:
@@ -3,11 +3,10 @@
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
||||
import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs';
|
||||
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { FlockApi } from '@/services/api/master-data';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||
|
||||
@@ -34,33 +33,6 @@ const ClosingDetailPage = () => {
|
||||
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
||||
);
|
||||
|
||||
const { data: salesData, isLoading: isLoadingSales } = useSWR(
|
||||
kandangId
|
||||
? `sales-${closingId}-${kandangId}`
|
||||
: closingId
|
||||
? `sales-${closingId}`
|
||||
: null,
|
||||
() =>
|
||||
kandangId
|
||||
? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId))
|
||||
: ClosingApi.getPenjualan(Number(closingId))
|
||||
);
|
||||
|
||||
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
||||
kandangId
|
||||
? `hpp-ekspedisi-${closingId}-${kandangId}`
|
||||
: closingId
|
||||
? `hpp-ekspedisi-${closingId}`
|
||||
: null,
|
||||
() =>
|
||||
kandangId
|
||||
? ClosingApi.getHppEkspedisiByKandang(
|
||||
Number(closingId),
|
||||
Number(kandangId)
|
||||
)
|
||||
: ClosingApi.getHppEkspedisi(Number(closingId))
|
||||
);
|
||||
|
||||
if (!closingId) {
|
||||
router.back();
|
||||
|
||||
@@ -76,12 +48,7 @@ const ClosingDetailPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLoading =
|
||||
isLoadingClosing ||
|
||||
isLoadingSales ||
|
||||
isLoadingHppEkspedisi ||
|
||||
isLoadingProject ||
|
||||
isLoadingKandang;
|
||||
const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang;
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
@@ -91,12 +58,6 @@ const ClosingDetailPage = () => {
|
||||
<ClosingDetail
|
||||
id={Number(closingId)}
|
||||
initialValue={closing.data}
|
||||
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
||||
hppExpeditionData={
|
||||
isResponseSuccess(hppEkspedisiData)
|
||||
? hppEkspedisiData.data
|
||||
: undefined
|
||||
}
|
||||
projectData={
|
||||
isResponseSuccess(projectData) ? projectData.data : undefined
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
||||
|
||||
const Closing = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<section className='w-full p-3'>
|
||||
<ClosingsTable />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import useSWR from 'swr';
|
||||
import { FinanceApi } from '@/services/api/finance';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||
|
||||
const EditFinanceTransactionPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail';
|
||||
import useSWR from 'swr';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { FinanceApi } from '@/services/api/finance';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
const FinanceDetailPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
const AddFcr = () => {
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
<FcrForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFcr;
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||
|
||||
const FcrEdit = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const fcrId = searchParams.get('fcrId');
|
||||
|
||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||
fcrId,
|
||||
(id: number) =>
|
||||
FcrApi.getSingle(id) as Promise<
|
||||
BaseApiResponse<FcrWithStandards> | undefined
|
||||
>
|
||||
);
|
||||
|
||||
if (!fcrId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||
<FcrForm type='edit' initialValues={fcr.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrEdit;
|
||||
@@ -1,11 +0,0 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
const FcrDetail = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const fcrId = searchParams.get('fcrId');
|
||||
|
||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||
fcrId,
|
||||
(id: number) =>
|
||||
FcrApi.getSingle(id) as Promise<
|
||||
BaseApiResponse<FcrWithStandards> | undefined
|
||||
>
|
||||
);
|
||||
|
||||
if (!fcrId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||
<FcrForm type='detail' initialValues={fcr.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrDetail;
|
||||
@@ -1,11 +0,0 @@
|
||||
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
|
||||
|
||||
const Fcr = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<FcrsTable />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fcr;
|
||||
+1
-2
@@ -3,10 +3,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
import { redirectToSSO } from '@/lib/auth-helper';
|
||||
|
||||
export default function Home() {
|
||||
const { user, isLoadingUser } = useAuth();
|
||||
const { isLoadingUser } = useAuth();
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||
import React, { useImperativeHandle } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import React from 'react';
|
||||
// import React, { useImperativeHandle } from 'react';
|
||||
|
||||
const AddProjectFlock = () => {
|
||||
// useImperativeHandle(ref, () => ({
|
||||
|
||||
@@ -12,11 +12,10 @@ const ProjectFlockEdit = () => {
|
||||
|
||||
const projectFlockId = searchParams.get('projectFlockId');
|
||||
|
||||
const {
|
||||
data: projectFlock,
|
||||
isLoading: isLoadingProjectFlock,
|
||||
mutate: refreshProjectFlocks,
|
||||
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||
projectFlockId,
|
||||
(id: number) => ProjectFlockApi.getSingle(id)
|
||||
);
|
||||
|
||||
if (!projectFlockId) {
|
||||
router.back();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
@@ -13,11 +12,10 @@ const ProjectFlockDetailPage = () => {
|
||||
|
||||
const projectFlockId = searchParams.get('projectFlockId');
|
||||
|
||||
const {
|
||||
data: projectFlock,
|
||||
isLoading: isLoadingProjectFlock,
|
||||
mutate: refreshProjectFlock,
|
||||
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||
projectFlockId,
|
||||
(id: number) => ProjectFlockApi.getSingle(id)
|
||||
);
|
||||
|
||||
if (!projectFlockId) {
|
||||
router.back();
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
|
||||
import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs';
|
||||
|
||||
const ReportExpense = () => {
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<ReportExpenseTable />
|
||||
</div>
|
||||
);
|
||||
return <ReportExpenseTabs />;
|
||||
};
|
||||
|
||||
export default ReportExpense;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||
import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs';
|
||||
|
||||
const MarketingReportPage = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<MarketingReportContent />
|
||||
</section>
|
||||
);
|
||||
return <MarketingReportContent />;
|
||||
};
|
||||
|
||||
export default MarketingReportPage;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
|
||||
import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs';
|
||||
|
||||
const ProductionResultReportPage = () => {
|
||||
return (
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<ProductionResultContent />
|
||||
<section className='w-full max-w-full'>
|
||||
<ProductionResultTabs />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import Image from 'next/image';
|
||||
@@ -13,7 +12,6 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
||||
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||
import { isPathActive } from '@/lib/helper';
|
||||
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({
|
||||
}));
|
||||
|
||||
const emptyContentDefaultValue = (
|
||||
<div className='w-full p-5 text-center'>
|
||||
<span className='text-lg opacity-50'>
|
||||
<div className='w-full text-center py-4'>
|
||||
<span className='text-sm opacity-50'>
|
||||
Tidak ada data yang dapat ditampilkan...
|
||||
</span>
|
||||
</div>
|
||||
@@ -452,6 +452,20 @@ const Table = <TData extends object>({
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
||||
!isLoading && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={
|
||||
table.getAllLeafColumns().length + (withCheckbox ? 1 : 0)
|
||||
}
|
||||
className='p-0'
|
||||
>
|
||||
{emptyContent}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
<tfoot
|
||||
className={cn(
|
||||
@@ -489,10 +503,6 @@ const Table = <TData extends object>({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
||||
!isLoading &&
|
||||
emptyContent}
|
||||
|
||||
{data.length > 0 &&
|
||||
table.getRowModel().rows.length > 0 &&
|
||||
!isLoading &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
||||
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface TabItem {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfParamBadgeProps = {
|
||||
children: React.ReactNode;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
||||
return (
|
||||
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
||||
<Text>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfStatusBadgeProps = {
|
||||
children: React.ReactNode;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statusBadge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 12,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
statusBadgeText: {
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
color: '#333333',
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
|
||||
const styleRecord = style as Record<string, unknown>;
|
||||
const color = styleRecord?.color as string | undefined;
|
||||
|
||||
const viewStyle = Object.entries(styleRecord || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key !== 'color') {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfPageNumberProps = {
|
||||
style?: Style;
|
||||
/**
|
||||
* Format template for page number.
|
||||
* Use {pageNumber} and {totalPages} as placeholders.
|
||||
* Default: "{pageNumber} / {totalPages}"
|
||||
*/
|
||||
format?: string;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
position: 'absolute',
|
||||
fontSize: 8,
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfPageNumber = ({
|
||||
style,
|
||||
format = '{pageNumber} / {totalPages}',
|
||||
}: PdfPageNumberProps) => {
|
||||
return (
|
||||
<View style={style || styles.footer} fixed>
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
format
|
||||
.replace('{pageNumber}', String(pageNumber))
|
||||
.replace('{totalPages}', String(totalPages))
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { View, StyleSheet } from '@react-pdf/renderer';
|
||||
import { PdfThead, PdfColumn } from './PdfThead';
|
||||
import { PdfTbody, PdfTbodyCell } from './PdfTbody';
|
||||
import { PdfTfoot, PdfTfootCell } from './PdfTfoot';
|
||||
import type { PdfColumn } from './types';
|
||||
import { PdfThead } from './PdfThead';
|
||||
import { PdfTbody } from './PdfTbody';
|
||||
import { PdfTfoot } from './PdfTfoot';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
table: {
|
||||
@@ -13,10 +14,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTableProps {
|
||||
columns: PdfColumn[];
|
||||
data: PdfTbodyCell[][];
|
||||
footer?: PdfTfootCell[];
|
||||
interface PdfTableProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
showFooter?: boolean;
|
||||
footerLabel?: string;
|
||||
firstRow?: {
|
||||
valueKey: string;
|
||||
@@ -26,20 +27,26 @@ interface PdfTableProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const PdfTable = ({
|
||||
export const PdfTable = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
footer,
|
||||
showFooter = false,
|
||||
footerLabel = 'Total',
|
||||
firstRow,
|
||||
}: PdfTableProps) => {
|
||||
}: PdfTableProps<TData>) => {
|
||||
// Check if any column has footer defined
|
||||
const hasFooter =
|
||||
showFooter || columns.some((col) => col.footer !== undefined);
|
||||
|
||||
return (
|
||||
<View style={styles.table}>
|
||||
<PdfThead columns={columns} />
|
||||
<PdfTbody columns={columns} rows={data} firstRow={firstRow} />
|
||||
{footer && footer.length > 0 && (
|
||||
<PdfTfoot columns={columns} cells={footer} label={footerLabel} />
|
||||
<PdfThead columns={columns} data={data} />
|
||||
<PdfTbody columns={columns} data={data} firstRow={firstRow} />
|
||||
{hasFooter && data.length > 0 && (
|
||||
<PdfTfoot columns={columns} data={data} label={footerLabel} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export interface PdfTbodyCell {
|
||||
key: string;
|
||||
value: string | number | React.ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
color?: string;
|
||||
formatAs?: 'text' | 'date' | 'currency' | 'number';
|
||||
formatDate?: string;
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -71,21 +57,22 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTbodyProps {
|
||||
columns: PdfColumn[];
|
||||
rows: PdfTbodyCell[][];
|
||||
interface PdfTbodyProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
firstRow?: {
|
||||
valueKey: string;
|
||||
value: number;
|
||||
align?: 'right';
|
||||
color?: string;
|
||||
};
|
||||
formatDate?: (date: string, format: string) => string;
|
||||
formatNumber?: (num: number) => string;
|
||||
formatCurrency?: (num: number) => string;
|
||||
}
|
||||
|
||||
export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
export const PdfTbody = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
firstRow,
|
||||
}: PdfTbodyProps<TData>) => {
|
||||
return (
|
||||
<>
|
||||
{/* First Row */}
|
||||
@@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
||||
{columns.map((column, index) => {
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
const isfirstRowColumn = column.key === firstRow.valueKey;
|
||||
const align = column.align || 'center';
|
||||
const isFirstRowColumn = column.key === firstRow.valueKey;
|
||||
const align = column.align || 'left';
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [styles.tableCellNo, { flex: column.flex }]
|
||||
: isfirstRowColumn
|
||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||
: isFirstRowColumn
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
color: firstRow.color || 'black',
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellLast,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
]
|
||||
: [styles.tableCell, { flex: column.flex }];
|
||||
: [styles.tableCell, { flex: column.flex || 1 }];
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text>
|
||||
<Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
)}
|
||||
|
||||
{/* Data Rows */}
|
||||
{rows.map((row, rowIndex) => {
|
||||
const isLastRow = rowIndex === rows.length - 1;
|
||||
{data.map((row, rowIndex) => {
|
||||
const isLastRow = rowIndex === data.length - 1;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -156,19 +143,27 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
]}
|
||||
>
|
||||
{columns.map((column, colIndex) => {
|
||||
const cell = row.find((c) => c.key === column.key);
|
||||
const isLastColumn = colIndex === columns.length - 1;
|
||||
const align = cell?.align || column.align || 'center';
|
||||
const align = column.align || 'left';
|
||||
|
||||
// Get cell content from column.cell function or fallback to row value
|
||||
let cellContent: ReactNode;
|
||||
if (column.cell) {
|
||||
cellContent = column.cell({ row, index: rowIndex });
|
||||
} else {
|
||||
cellContent =
|
||||
((row as Record<string, unknown>)[column.key] as ReactNode) ??
|
||||
'-';
|
||||
}
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [styles.tableCellNo, { flex: column.flex }]
|
||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||
: align === 'right'
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: isLastColumn
|
||||
? [
|
||||
styles.tableCellLast,
|
||||
{ flex: column.flex, borderRightWidth: 0 },
|
||||
{ flex: column.flex || 1, borderRightWidth: 0 },
|
||||
]
|
||||
: [
|
||||
styles.tableCell,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
{cell?.value !== undefined &&
|
||||
cell?.value !== null &&
|
||||
cell?.value !== '' ? (
|
||||
typeof cell.value === 'object' ? (
|
||||
cell.value
|
||||
) : (
|
||||
<Text>{String(cell.value)}</Text>
|
||||
)
|
||||
{typeof cellContent === 'string' ||
|
||||
typeof cellContent === 'number' ? (
|
||||
<Text>{String(cellContent)}</Text>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
cellContent
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export interface PdfTfootCell {
|
||||
key: string;
|
||||
value: string | number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
flex?: number;
|
||||
color?: string;
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -69,63 +56,86 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTfootProps {
|
||||
columns: PdfColumn[];
|
||||
cells: PdfTfootCell[];
|
||||
interface PdfTfootProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const PdfTfoot = ({
|
||||
export const PdfTfoot = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
cells,
|
||||
data,
|
||||
label = 'Total',
|
||||
}: PdfTfootProps) => {
|
||||
}: PdfTfootProps<TData>) => {
|
||||
return (
|
||||
<View style={[styles.tableRow, styles.summaryRow]}>
|
||||
{columns.map((column, index) => {
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
const cellData = cells.find((c) => c.key === column.key);
|
||||
|
||||
// Get footer content from column definition
|
||||
let footerContent: ReactNode;
|
||||
if (typeof column.footer === 'function') {
|
||||
footerContent = column.footer(data);
|
||||
} else {
|
||||
footerContent = column.footer;
|
||||
}
|
||||
|
||||
// Use label for first column (usually 'no' column)
|
||||
const displayContent = column.key === 'no' ? label : footerContent;
|
||||
|
||||
// Determine alignment
|
||||
const align = column.footerAlign || column.align || 'left';
|
||||
const color = column.footerColor || 'black';
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [
|
||||
styles.tableCellNo,
|
||||
{ flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 },
|
||||
{
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
color,
|
||||
},
|
||||
]
|
||||
: cellData?.align === 'right'
|
||||
: align === 'right'
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
color,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: cellData?.align === 'center'
|
||||
: align === 'center'
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
color,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: isLastColumn
|
||||
? [styles.tableCellLast, { flex: column.flex }]
|
||||
: [
|
||||
styles.tableCell,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
},
|
||||
];
|
||||
? [styles.tableCellLast, { flex: column.flex || 1, color }]
|
||||
: [styles.tableCell, { flex: column.flex || 1, color }];
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
<Text>{column.key === 'no' ? label : cellData?.value || ''}</Text>
|
||||
{displayContent !== undefined && displayContent !== null ? (
|
||||
typeof displayContent === 'string' ||
|
||||
typeof displayContent === 'number' ? (
|
||||
<Text>{String(displayContent)}</Text>
|
||||
) : (
|
||||
displayContent
|
||||
)
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -48,23 +43,37 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTheadProps {
|
||||
columns: PdfColumn[];
|
||||
interface PdfTheadProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data?: TData[];
|
||||
}
|
||||
|
||||
export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
export const PdfThead = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
}: PdfTheadProps<TData>) => {
|
||||
return (
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
{columns.map((column, index) => {
|
||||
const align = column.align || 'center';
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
|
||||
// Get header content from column definition
|
||||
let headerContent: ReactNode;
|
||||
if (typeof column.header === 'function') {
|
||||
headerContent = column.header(data || []);
|
||||
} else {
|
||||
headerContent = column.header || column.key;
|
||||
}
|
||||
|
||||
// Determine alignment - columns align right by default for numeric data
|
||||
const align = column.align || 'left';
|
||||
|
||||
const cellStyle =
|
||||
align === 'right'
|
||||
? [
|
||||
styles.tableCellHeaderRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
textAlign: 'right' as const,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
: [
|
||||
styles.tableCellHeader,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
textAlign: align as 'left' | 'center' | 'right',
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
<Text>{column.header}</Text>
|
||||
{typeof headerContent === 'string' ? (
|
||||
<Text>{headerContent}</Text>
|
||||
) : (
|
||||
headerContent
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable';
|
||||
export { PdfThead } from './PdfThead';
|
||||
export { PdfTbody } from './PdfTbody';
|
||||
export { PdfTfoot } from './PdfTfoot';
|
||||
export type { PdfColumn } from './PdfThead';
|
||||
export type { PdfTbodyCell } from './PdfTbody';
|
||||
export type { PdfTfootCell } from './PdfTfoot';
|
||||
export type { PdfColumn } from './types';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* PdfColumn - Mirip dengan ColumnDef di TanStack Table
|
||||
* Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi
|
||||
*/
|
||||
export interface PdfColumn<TData = Record<string, unknown>> {
|
||||
key: string;
|
||||
flex?: number;
|
||||
|
||||
// Header configuration (thead)
|
||||
header?: string | ((data: TData[]) => ReactNode);
|
||||
|
||||
// Body configuration (tbody)
|
||||
align?: 'left' | 'center' | 'right';
|
||||
cell?: (props: { row: TData; index: number }) => ReactNode | string | number;
|
||||
|
||||
// Footer configuration (tfoot)
|
||||
footer?: string | number | ((data: TData[]) => ReactNode | string | number);
|
||||
footerAlign?: 'left' | 'center' | 'right';
|
||||
footerColor?: string;
|
||||
}
|
||||
|
||||
export type { PdfColumn as default };
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Color } from '@/types/theme';
|
||||
import { Text, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
||||
|
||||
type TypographyVariant = Color | 'default';
|
||||
|
||||
type PdfTypographyProps = {
|
||||
children: React.ReactNode;
|
||||
size?: TypographySize;
|
||||
variant?: TypographyVariant;
|
||||
color?: string;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
h1: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
h4: {
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 3,
|
||||
},
|
||||
p: {
|
||||
fontSize: 10,
|
||||
marginBottom: 4,
|
||||
},
|
||||
small: {
|
||||
fontSize: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
label: {
|
||||
fontSize: 9,
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const variantColors: Record<TypographyVariant, string> = {
|
||||
default: '#333333',
|
||||
primary: '#1f74bf',
|
||||
secondary: '#6B7280',
|
||||
accent: '#8B5CF6',
|
||||
neutral: '#6B7280',
|
||||
info: '#3B82F6',
|
||||
success: '#065F46',
|
||||
warning: '#92400E',
|
||||
error: '#DC2626',
|
||||
none: '#333333',
|
||||
};
|
||||
|
||||
export const PdfTypography = ({
|
||||
children,
|
||||
size = 'p',
|
||||
variant = 'default',
|
||||
color,
|
||||
style,
|
||||
}: PdfTypographyProps) => {
|
||||
const sizeStyle = styles[size];
|
||||
const textColor = color || variantColors[variant];
|
||||
|
||||
return (
|
||||
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
export type StatusColor = {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
};
|
||||
|
||||
// Due status colors (for debt supplier reports)
|
||||
export const dueStatusColors: Record<string, StatusColor> = {
|
||||
'SUDAH JATUH TEMPO': {
|
||||
bg: '#FEE2E2',
|
||||
text: '#991B1B',
|
||||
border: '#F87171',
|
||||
}, // error/red
|
||||
'BELUM JATUH TEMPO': {
|
||||
bg: '#D1FAE5',
|
||||
text: '#065F46',
|
||||
border: '#34D399',
|
||||
}, // success/green
|
||||
'MENDEKATI JATUH TEMPO': {
|
||||
bg: '#FEF3C7',
|
||||
text: '#92400E',
|
||||
border: '#FBBF24',
|
||||
}, // warning/yellow
|
||||
};
|
||||
|
||||
// Payment status colors (for customer payment & debt supplier reports)
|
||||
export const paymentStatusColors: Record<string, StatusColor> = {
|
||||
'BELUM LUNAS': {
|
||||
bg: '#FEF3C7',
|
||||
text: '#92400E',
|
||||
border: '#FBBF24',
|
||||
}, // warning/yellow
|
||||
LUNAS: {
|
||||
bg: '#DBEAFE',
|
||||
text: '#1E40AF',
|
||||
border: '#60A5FA',
|
||||
}, // primary/blue
|
||||
'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
||||
PEMBAYARAN: {
|
||||
bg: '#D1FAE5',
|
||||
text: '#065F46',
|
||||
border: '#34D399',
|
||||
}, // success/green
|
||||
};
|
||||
|
||||
// Fallback color for unknown statuses
|
||||
export const fallbackStatusColor: StatusColor = {
|
||||
bg: '#F3F4F6',
|
||||
text: '#374151',
|
||||
border: '#D1D5DB',
|
||||
}; // neutral
|
||||
|
||||
export const getPDFBadgeStyle = (
|
||||
statusText: string,
|
||||
type: 'due' | 'payment' = 'payment'
|
||||
): StatusColor => {
|
||||
const normalizedStatus = statusText.toUpperCase().trim();
|
||||
|
||||
const colors =
|
||||
type === 'due'
|
||||
? dueStatusColors[normalizedStatus]
|
||||
: paymentStatusColors[normalizedStatus];
|
||||
|
||||
return colors || fallbackStatusColor;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const DataStateSkeleton = ({
|
||||
icon,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ChangeEvent } from 'react';
|
||||
import {
|
||||
PatternFormat,
|
||||
NumberFormatBase,
|
||||
NumberFormatBaseProps,
|
||||
OnValueChange,
|
||||
} from 'react-number-format';
|
||||
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||
|
||||
@@ -246,8 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||
'bg-gray-50 border-base-content/10': isDisabled,
|
||||
'bg-base-100 border-base-content/10': !isDisabled,
|
||||
'bg-base-200 border-base-content/10': isDisabled,
|
||||
'border-error': isError,
|
||||
},
|
||||
className?.inputPrefix
|
||||
@@ -278,28 +278,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn('w-full flex-1', className?.select)}
|
||||
classNames={{
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
cn('w-full border bg-white transition-shadow', 'rounded-lg!', {
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-red-500! ring-2 ring-red-200': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||
cn('w-full border transition-shadow', 'rounded-lg!', {
|
||||
'bg-base-100!': !isDisabled && !readOnly,
|
||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-error!': isError,
|
||||
'ring-2 ring-error/20': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment && !isError,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
'rounded-l-none!': inputPrefix && !startAdornment,
|
||||
'rounded-r-none!': inputSuffix && !startAdornment,
|
||||
}),
|
||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||
placeholder: () =>
|
||||
cn({
|
||||
'text-gray-400 text-sm leading-tight': !isError,
|
||||
'text-red-300!': isError,
|
||||
cn('text-gray-400 text-sm leading-tight', {
|
||||
'text-error!': isError,
|
||||
}),
|
||||
singleValue: () =>
|
||||
cn({
|
||||
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
||||
'text-error!': isError,
|
||||
cn('m-0! text-gray-900 text-sm leading-tight', {
|
||||
'text-error!': isError && !readOnly,
|
||||
'text-gray-900!': readOnly,
|
||||
}),
|
||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||
@@ -370,8 +370,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||
'bg-gray-50 border-base-content/10': isDisabled,
|
||||
'bg-base-100 border-base-content/10': !isDisabled,
|
||||
'bg-base-200 border-base-content/10': isDisabled,
|
||||
'border-error': isError,
|
||||
},
|
||||
className?.inputSuffix
|
||||
@@ -403,31 +403,26 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn('w-full', className?.select)}
|
||||
classNames={{
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
cn(
|
||||
'w-full border bg-white transition-shadow',
|
||||
// Gunakan rounded-lg untuk semua kasus
|
||||
'rounded-lg!',
|
||||
{
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-red-500! ring-2 ring-red-200': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
}
|
||||
),
|
||||
cn('w-full border transition-shadow rounded-lg!', {
|
||||
'bg-base-100!': !isDisabled && !readOnly,
|
||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-error!': isError,
|
||||
'ring-2 ring-error/20': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment && !isError,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
}),
|
||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||
placeholder: () =>
|
||||
cn({
|
||||
'text-gray-400 text-sm leading-tight': !isError,
|
||||
'text-red-300!': isError,
|
||||
cn('text-gray-400 text-sm leading-tight', {
|
||||
'text-error!': isError,
|
||||
}),
|
||||
singleValue: () =>
|
||||
cn({
|
||||
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
||||
'text-error!': isError,
|
||||
cn('m-0! text-gray-900 text-sm leading-tight', {
|
||||
'text-error!': isError && !readOnly,
|
||||
'text-gray-900!': readOnly,
|
||||
}),
|
||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||
|
||||
@@ -104,8 +104,8 @@ const TextInput = ({
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !disabled,
|
||||
'bg-gray-50 border-base-content/10': disabled,
|
||||
'bg-base-100 border-base-content/10': !disabled,
|
||||
'bg-base-200 border-base-content/10': disabled,
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
@@ -118,7 +118,7 @@ const TextInput = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
@@ -126,7 +126,8 @@ const TextInput = ({
|
||||
'rounded-r-none!': inputSuffix,
|
||||
'input-disabled': disabled,
|
||||
'cursor-not-allowed': disabled,
|
||||
'bg-gray-50': disabled,
|
||||
'bg-base-100': !disabled,
|
||||
'bg-base-200': disabled,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
@@ -167,8 +168,8 @@ const TextInput = ({
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !disabled,
|
||||
'bg-gray-50 border-base-content/10': disabled,
|
||||
'bg-base-100 border-base-content/10': !disabled,
|
||||
'bg-base-200 border-base-content/10': disabled,
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
@@ -182,10 +183,12 @@ const TextInput = ({
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
'bg-base-100': !disabled,
|
||||
'bg-base-200': disabled,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
@@ -201,7 +204,14 @@ const TextInput = ({
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
className={cn('grow', className?.input)}
|
||||
className={cn(
|
||||
'grow bg-transparent outline-none',
|
||||
{
|
||||
'cursor-not-allowed': disabled,
|
||||
'text-gray-500': disabled,
|
||||
},
|
||||
className?.input
|
||||
)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||
closeOnBackdrop={closeOnBackdrop}
|
||||
primaryButton={{
|
||||
...primaryButton,
|
||||
onClick: (e) => {
|
||||
onClick: () => {
|
||||
if (primaryButton && primaryButton?.onClick) {
|
||||
primaryButton?.onClick?.(notes);
|
||||
} else {
|
||||
|
||||
+29
-31
@@ -5,28 +5,23 @@ import { useMemo, useState } from 'react';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||
import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable';
|
||||
import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab';
|
||||
import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab';
|
||||
|
||||
import {
|
||||
ClosingGeneralInformation,
|
||||
BaseClosingSales,
|
||||
ClosingHppExpedition,
|
||||
} from '@/types/api/closing';
|
||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||
import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab';
|
||||
import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab';
|
||||
import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab';
|
||||
import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab';
|
||||
import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab';
|
||||
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { useClosingTabStore } from '@/stores/closing/closing-tab.store';
|
||||
interface ClosingDetailProps {
|
||||
id: number;
|
||||
initialValue?: ClosingGeneralInformation;
|
||||
salesData?: BaseClosingSales;
|
||||
hppExpeditionData?: ClosingHppExpedition;
|
||||
projectData?: ProjectFlock;
|
||||
kandangData?: ProjectFlockKandang;
|
||||
}
|
||||
@@ -34,25 +29,24 @@ interface ClosingDetailProps {
|
||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
id,
|
||||
initialValue,
|
||||
salesData,
|
||||
hppExpeditionData,
|
||||
projectData,
|
||||
kandangData,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
|
||||
const tabActions = useClosingTabStore((state) => state.tabActions);
|
||||
|
||||
const closingDetailTabs = useMemo(() => {
|
||||
const validTabs = [
|
||||
{
|
||||
id: 'sapronak',
|
||||
label: 'Sapronak',
|
||||
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
||||
content: <SapronakClosingTab projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'perhitunganSapronak',
|
||||
label: 'Perhitungan Sapronak',
|
||||
content: (
|
||||
<ClosingSapronakCalculationTabContent
|
||||
<SapronakCalculationClosingTab
|
||||
closingGeneralInformation={initialValue}
|
||||
projectFlockId={id}
|
||||
/>
|
||||
@@ -61,13 +55,13 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'penjualan',
|
||||
label: 'Penjualan',
|
||||
content: <SalesReportTable initialValues={salesData} />,
|
||||
content: <SalesClosingTab projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'overhead',
|
||||
label: 'Overhead',
|
||||
content: (
|
||||
<ClosingOverheadTabContent
|
||||
<OverheadClosingTab
|
||||
projectFlockId={id}
|
||||
generalInformation={initialValue}
|
||||
kandangData={kandangData}
|
||||
@@ -77,26 +71,26 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'hppEkspedisi',
|
||||
label: 'HPP Ekspedisi',
|
||||
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
||||
content: <HppExpeditionClosingTab projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'dataProduksi',
|
||||
label: 'Data Produksi',
|
||||
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||
content: <ProductionDataClosingTab projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'keuangan',
|
||||
label: 'Keuangan',
|
||||
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
||||
content: <FinanceClosingTab projectFlockId={id} />,
|
||||
},
|
||||
];
|
||||
|
||||
return validTabs;
|
||||
}, [initialValue]);
|
||||
}, [initialValue, kandangData, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href={
|
||||
@@ -126,13 +120,17 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
activeTabId={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
activeTabId={activeTabId}
|
||||
onTabChange={setActiveTabId}
|
||||
tabs={closingDetailTabs}
|
||||
variant='lifted'
|
||||
variant='boxed'
|
||||
className={{
|
||||
wrapper: 'w-full mt-4',
|
||||
tabHeaderWrapper:
|
||||
'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10',
|
||||
tab: 'w-fit',
|
||||
content: 'p-0 m-0',
|
||||
}}
|
||||
sideContent={tabActions[activeTabId] || null}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
@@ -1,17 +0,0 @@
|
||||
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
|
||||
|
||||
const ClosingFinanceTabContent = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<ClosingFinanceTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingFinanceTabContent;
|
||||
@@ -1,399 +0,0 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const ClosingFinanceTable = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: finance, isLoading } = useSWR(
|
||||
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||
() =>
|
||||
ClosingApi.getFinance(
|
||||
projectFlockId,
|
||||
kandangId ? Number(kandangId) : undefined
|
||||
)
|
||||
);
|
||||
|
||||
const hppTableData: HppItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const customItems = {
|
||||
label: 'HPP dan Pengeluaran',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const purchases = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'purchase'
|
||||
);
|
||||
const totalBudgeting = {
|
||||
label: 'HPP dan Bahan Baku',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const overheads = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'overhead'
|
||||
);
|
||||
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const incomes = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'income'
|
||||
);
|
||||
const purchases = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'purchase'
|
||||
);
|
||||
const overheads = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'overhead'
|
||||
);
|
||||
const grossProfit = {
|
||||
label: 'LABA RUGI BRUTO',
|
||||
code: 'custom_row',
|
||||
type: 'gross_profit',
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
const subtotal = {
|
||||
label: 'Subtotal',
|
||||
code: 'custom_row',
|
||||
type: 'subtotal',
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<>
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-6'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>Laba Rugi Brutto</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.gross_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>Laba Rugi Netto</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='HPP Purchases'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<HppItem>
|
||||
data={hppTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'No.',
|
||||
enableSorting: false,
|
||||
accessorFn: (item, index) => {
|
||||
if (item.code === 'custom_row') return '-';
|
||||
const dataRowsBefore = hppTableData
|
||||
.slice(0, index)
|
||||
.filter((row) => row.code !== 'custom_row').length;
|
||||
return dataRowsBefore + 1;
|
||||
},
|
||||
footer: (props) => {
|
||||
return 'HPP';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||
},
|
||||
{
|
||||
header: 'Budgeting',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'budgeting_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'budgeting_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
||||
0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'budgeting_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realization',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'realization_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'realization_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'realization_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization?.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
></td>
|
||||
<td
|
||||
colSpan={7}
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Profit/Loss'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<ProfitLossItem>
|
||||
data={profitLossTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => item.label,
|
||||
cell: (item) => (
|
||||
<div className=''>
|
||||
{formatTitleCase(item.row.original.label || '-')}
|
||||
</div>
|
||||
),
|
||||
footer: () => (
|
||||
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.amount || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||
>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold ps-6 uppercase'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.amount ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
className={{
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingFinanceTable;
|
||||
@@ -10,18 +10,18 @@ const ClosingKandangList = ({
|
||||
projectData?: ProjectFlock;
|
||||
}) => {
|
||||
return (
|
||||
<div className='w-full my-4 @container'>
|
||||
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
||||
<div className='flex flex-col @sm:flex-row gap-4'>
|
||||
<div className='w-full'>
|
||||
<div className='overflow-x-auto'>
|
||||
<h1 className='font-bold my-4'>Kandang</h1>
|
||||
<div className='flex flex-wrap gap-2 mb-4'>
|
||||
<h1 className='font-bold mb-3'>Kandang</h1>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{projectData?.kandangs?.map((kandang) => (
|
||||
<Button
|
||||
key={kandang.id}
|
||||
variant='outline'
|
||||
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
||||
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||
className='min-w-32'
|
||||
>
|
||||
{kandang.name}
|
||||
</Button>
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
interface ClosingProductionDataTabContentProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingProductionDataTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingProductionDataTabContentProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: productionData, isLoading } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full flex justify-center py-8'>
|
||||
<span className='loading loading-spinner loading-lg' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!productionData || !isResponseSuccess(productionData)) {
|
||||
return (
|
||||
<div className='w-full text-center py-8 text-gray-500'>
|
||||
Gagal memuat data produksi.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { purchase, sales, performance } = productionData.data;
|
||||
|
||||
// Helper for consistent row styling
|
||||
const DataRow = ({
|
||||
label,
|
||||
value,
|
||||
unit = '',
|
||||
valueClassName = 'font-bold text-gray-800',
|
||||
unitClassName = 'text-gray-500 w-12 text-right',
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
valueClassName?: string;
|
||||
unitClassName?: string;
|
||||
}) => (
|
||||
<div className='flex justify-between items-center py-1'>
|
||||
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
||||
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
||||
<span className={valueClassName}>{value}</span>
|
||||
{unit && <span className={unitClassName}>{unit}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full rounded-xl p-8 shadow-sm'>
|
||||
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
|
||||
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
||||
{/* Left Column */}
|
||||
<div className='space-y-10'>
|
||||
{/* Purchase Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Pembelian
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Populasi Awal'
|
||||
value={formatNumber(purchase.initial_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Claim Culling'
|
||||
value={formatNumber(purchase.claim_culling)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Populasi Akhir'
|
||||
value={formatNumber(purchase.final_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Masuk'
|
||||
value={formatNumber(purchase.feed_in)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai'
|
||||
value={formatNumber(purchase.feed_used)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sales Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Penjualan
|
||||
</h3>
|
||||
<div className='space-y-4'>
|
||||
{/* Chicken Sales */}
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Penjualan (Ekor)'
|
||||
value={formatNumber(sales.chicken.sales_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Penjualan (Kg)'
|
||||
value={formatNumber(sales.chicken.sales_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Bobot Rata-Rata'
|
||||
value={formatNumber(sales.chicken.avg_weight)}
|
||||
unit='Kg/Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Rata-Rata'
|
||||
value={formatNumber(sales.chicken.avg_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Egg Sales (if available) */}
|
||||
{sales.egg && (
|
||||
<>
|
||||
<div className='h-px bg-gray-100 my-2' />
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Telur (Butir)'
|
||||
value={formatNumber(sales.egg.egg_pieces)}
|
||||
unit='Butir'
|
||||
/>
|
||||
<DataRow
|
||||
label='Telur (Kg)'
|
||||
value={formatNumber(sales.egg.egg_mass)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Berat Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.avg_egg_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.avg_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Divider Line (Absolute centered) */}
|
||||
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
||||
|
||||
{/* Right Column */}
|
||||
<div className='space-y-10 flex flex-col h-full'>
|
||||
{/* Performance Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Performance
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Deplesi'
|
||||
value={formatNumber(performance.depletion)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Umur'
|
||||
value={formatNumber(performance.age_day)}
|
||||
unit='Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Std'
|
||||
value={formatNumber(performance.mor_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Act'
|
||||
value={formatNumber(performance.mor_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF Mortalitas'
|
||||
value={formatNumber(performance.mor_diff)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
{/* <DataRow
|
||||
label='AWG Std'
|
||||
value={formatNumber(performance.awg_std)}
|
||||
unit='Gr/Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='AWG Act'
|
||||
value={formatNumber(performance.awg_act)}
|
||||
unit='Gr/Hari'
|
||||
/> */}
|
||||
<DataRow
|
||||
label='Feed Intake Std'
|
||||
value={formatNumber(performance.feed_intake_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Feed Intake Act'
|
||||
value={formatNumber(performance.feed_intake)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Std'
|
||||
value={formatNumber(performance.fcr_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Act'
|
||||
value={formatNumber(performance.fcr_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF FCR'
|
||||
value={formatNumber(performance.fcr_diff)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
|
||||
{/* Laying Specific Fields */}
|
||||
{performance.hen_day_act !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Hen Day Std'
|
||||
value={formatNumber(performance.hen_day_std!)}
|
||||
unit='%'
|
||||
/>
|
||||
<DataRow
|
||||
label='Hen Day Act'
|
||||
value={formatNumber(performance.hen_day_act)}
|
||||
unit='%'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.egg_mass !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Egg Mass Std'
|
||||
value={formatNumber(performance.egg_mass_std!)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Egg Mass Act'
|
||||
value={formatNumber(performance.egg_mass)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.egg_weight !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Egg Weight Std'
|
||||
value={formatNumber(performance.egg_weight_std!)}
|
||||
unit='Gr'
|
||||
/>
|
||||
<DataRow
|
||||
label='Egg Weight Act'
|
||||
value={formatNumber(performance.egg_weight)}
|
||||
unit='Gr'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.hen_housed_act !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Hen Housed Std'
|
||||
value={formatNumber(performance.hen_housed_std!)}
|
||||
unit='%'
|
||||
/>
|
||||
<DataRow
|
||||
label='Hen Housed Act'
|
||||
value={formatNumber(performance.hen_housed_act)}
|
||||
unit='%'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingProductionDataTabContent;
|
||||
@@ -1,268 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
RowSapronakCalculation,
|
||||
TotalSapronakCalculation,
|
||||
} from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
interface ClosingSapronakCalculationTableProps {
|
||||
projectFlockId: number;
|
||||
closingGeneralInformation?: ClosingGeneralInformation;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTable = ({
|
||||
projectFlockId,
|
||||
closingGeneralInformation,
|
||||
}: ClosingSapronakCalculationTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
||||
`/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (
|
||||
total?: TotalSapronakCalculation
|
||||
): ColumnDef<RowSapronakCalculation>[] => [
|
||||
{
|
||||
header: 'Tanggal',
|
||||
accessorKey: 'date',
|
||||
cell: (props) =>
|
||||
props.row.original.date
|
||||
? formatDate(props.row.original.date, 'DD MMM YYYY')
|
||||
: '-',
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'reference_number',
|
||||
cell: (props) => (props.row.original.reference_number as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Masuk',
|
||||
accessorKey: 'qty_in',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_in
|
||||
? formatNumber(props.row.original.qty_in as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Keluar',
|
||||
accessorKey: 'qty_out',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_out
|
||||
? formatNumber(props.row.original.qty_out as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Pakai',
|
||||
accessorKey: 'qty_used',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_used
|
||||
? formatNumber(props.row.original.qty_used as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Uraian',
|
||||
accessorKey: 'description',
|
||||
cell: (props) => (props.row.original.description as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Kategori Produk',
|
||||
accessorKey: 'product_category',
|
||||
cell: (props) => (props.row.original.product_category as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Harga Beli/Qty (Rp)',
|
||||
accessorKey: 'unit_price',
|
||||
cell: (props) =>
|
||||
props.row.original.unit_price
|
||||
? formatCurrency(props.row.original.unit_price as number)
|
||||
: '-',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.avg_unit_price
|
||||
? formatCurrency(total?.avg_unit_price)
|
||||
: '-'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Total Harga (Rp)',
|
||||
accessorKey: 'total_amount',
|
||||
cell: (props) =>
|
||||
props.row.original.total_amount
|
||||
? formatCurrency(props.row.original.total_amount as number)
|
||||
: '-',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Keterangan',
|
||||
accessorKey: 'notes',
|
||||
cell: (props) => (props.row.original.notes as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Memoize columns untuk setiap kategori
|
||||
const docColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.doc?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const ovkColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.ovk?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const pakanColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.pakan?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{/* Table DOC jika kategori Project Flock Growing */}
|
||||
<Card
|
||||
title={
|
||||
closingGeneralInformation?.project_type == 'GROWING'
|
||||
? 'DOC'
|
||||
: 'Pullet'
|
||||
}
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.doc?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={docColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.doc?.rows.length > 0
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='OVK'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.ovk?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={ovkColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.ovk?.rows.length > 0
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='Pakan'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.pakan?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={pakanColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.pakan?.rows.length > 0
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTable;
|
||||
@@ -1,36 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
|
||||
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
|
||||
|
||||
interface ClosingSapronakTableProps {
|
||||
projectFlockId?: number;
|
||||
}
|
||||
|
||||
const ClosingSapronakTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingSapronakTableProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<>
|
||||
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<ClosingIncomingSapronaksSummaryTable
|
||||
projectFlockId={projectFlockId}
|
||||
/>
|
||||
|
||||
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<ClosingOutgoingSapronaksSummaryTable
|
||||
projectFlockId={projectFlockId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakTabContent;
|
||||
@@ -1,68 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useState, useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { Closing } from '@/types/api/closing';
|
||||
|
||||
const PROJECT_STATUS_OPTIONS = [
|
||||
{
|
||||
value: 1,
|
||||
label: 'Pengajuan',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'Aktif',
|
||||
},
|
||||
];
|
||||
import { Color } from '@/types/theme';
|
||||
import {
|
||||
ClosingFilterSchema,
|
||||
ClosingFilterType,
|
||||
} from '@/components/pages/closing/filter/ClosingFilter';
|
||||
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
popoverPosition = 'bottom',
|
||||
detailClickHandler,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Closing, unknown>;
|
||||
popoverPosition: 'bottom' | 'top';
|
||||
detailClickHandler: (id: number) => void;
|
||||
}) => {
|
||||
const popoverId = `closing#${props.row.original.id}`;
|
||||
const popoverAnchorName = `--anchor-closing#${props.row.original.id}`;
|
||||
|
||||
const closePopover = () => {
|
||||
document.getElementById(popoverId)?.hidePopover();
|
||||
};
|
||||
|
||||
const detailClickHandlerWrapper = () => {
|
||||
detailClickHandler(props.row.original.id);
|
||||
closePopover();
|
||||
};
|
||||
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
||||
<RequirePermission permissions='lti.closing.detail'>
|
||||
<Button
|
||||
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</RowOptionsMenuWrapper>
|
||||
<div className='relative'>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||
>
|
||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||
<RequirePermission permissions='lti.closing.detail'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={detailClickHandlerWrapper}
|
||||
className='p-3 justify-start text-sm font-semibold w-full'
|
||||
>
|
||||
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||
View Details
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClosingsTable = () => {
|
||||
// ===== ROUTER =====
|
||||
const router = useRouter();
|
||||
|
||||
// ===== STATUS BADGE COLOR HELPER =====
|
||||
const getProjectStatusBadgeColor = (status: string): Color => {
|
||||
const normalizedValue = status.toLowerCase();
|
||||
|
||||
if (normalizedValue === 'aktif') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (normalizedValue === 'pengajuan') {
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
return 'neutral';
|
||||
};
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -72,36 +120,67 @@ const ClosingsTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
nameSort: '',
|
||||
transactionDate: '',
|
||||
realizationDate: '',
|
||||
locationId: '',
|
||||
projectStatus: '',
|
||||
userId: '',
|
||||
// nameSort: '',
|
||||
// transactionDate: '',
|
||||
// realizationDate: '',
|
||||
location_id: '',
|
||||
project_status: '',
|
||||
// userId: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
nameSort: 'sort_name',
|
||||
transactionDate: 'transaction_date',
|
||||
realizationDate: 'realization_date',
|
||||
locationId: 'location_id',
|
||||
projectStatus: 'project_status',
|
||||
userId: 'user_id',
|
||||
// nameSort: 'sort_name',
|
||||
// transactionDate: 'transaction_date',
|
||||
// realizationDate: 'realization_date',
|
||||
// locationId: 'location_id',
|
||||
// projectStatus: 'project_status',
|
||||
// userId: 'user_id',
|
||||
search: 'search',
|
||||
location_id: 'location_id',
|
||||
project_status: 'project_status',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<ClosingFilterType>({
|
||||
initialValues: {
|
||||
location_id: null,
|
||||
project_status: null,
|
||||
},
|
||||
validationSchema: ClosingFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('location_id', values.location_id || '');
|
||||
updateFilter('project_status', values.project_status || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('location_id', '');
|
||||
updateFilter('project_status', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: closings, isLoading: isLoadingClosings } = useSWR(
|
||||
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
||||
ClosingApi.getAllFetcher
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [],
|
||||
[closings]
|
||||
);
|
||||
|
||||
// ===== PAGINATION & STATE =====
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
// ===== TABLE COLUMNS =====
|
||||
const closingsColumns: ColumnDef<Closing>[] = [
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
@@ -133,6 +212,19 @@ const ClosingsTable = () => {
|
||||
{
|
||||
accessorKey: 'project_status',
|
||||
header: 'Status',
|
||||
cell: (props) => {
|
||||
const status = props.row.original.project_status;
|
||||
const badgeColor = getProjectStatusBadgeColor(status);
|
||||
return (
|
||||
<StatusBadge
|
||||
color={badgeColor}
|
||||
text={status}
|
||||
className={{
|
||||
badge: 'whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
@@ -142,27 +234,24 @@ const ClosingsTable = () => {
|
||||
const currentRowRelativeIndex =
|
||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const detailClickHandler = (id: number) => {
|
||||
router.push(`/closing/detail/?closingId=${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageSize > 3 && (
|
||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||
<RowOptionsMenu type='dropdown' props={props} />
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
|
||||
{currentPageSize <= 3 && (
|
||||
<RowCollapseOptions>
|
||||
<RowOptionsMenu type='collapse' props={props} />
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
</>
|
||||
<RowOptionsMenu
|
||||
props={props}
|
||||
detailClickHandler={detailClickHandler}
|
||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ===== LOCATION OPTIONS =====
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
@@ -170,115 +259,246 @@ const ClosingsTable = () => {
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
// ===== PROJECT STATUS OPTIONS =====
|
||||
const projectStatusOptions = useMemo(
|
||||
() => [
|
||||
{ value: '1', label: 'Pengajuan' },
|
||||
{ value: '2', label: 'Aktif' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedLocation(val as OptionType);
|
||||
updateFilter(
|
||||
'locationId',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
// ===== FILTER HELPERS =====
|
||||
const locationIdValue = useMemo(() => {
|
||||
if (!formik.values.location_id) return null;
|
||||
return (
|
||||
locationOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.location_id
|
||||
) || null
|
||||
);
|
||||
};
|
||||
}, [formik.values.location_id, locationOptions]);
|
||||
|
||||
const [selectedProjectStatus, setSelectedProjectStatus] =
|
||||
useState<OptionType | null>(null);
|
||||
|
||||
const projectStatusChangeHandler = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
setSelectedProjectStatus(val as OptionType);
|
||||
updateFilter(
|
||||
'projectStatus',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
const projectStatusValue = useMemo(() => {
|
||||
if (!formik.values.project_status) return null;
|
||||
return (
|
||||
projectStatusOptions.find(
|
||||
(opt) => opt.value === formik.values.project_status
|
||||
) || null
|
||||
);
|
||||
};
|
||||
}, [formik.values.project_status, projectStatusOptions]);
|
||||
|
||||
// ===== ACTIVE FILTERS COUNT =====
|
||||
const activeFiltersCount = useMemo(() => {
|
||||
let count = 0;
|
||||
|
||||
if (tableFilterState.location_id) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if (tableFilterState.project_status) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}, [tableFilterState.location_id, tableFilterState.project_status]);
|
||||
|
||||
const hasFilters = activeFiltersCount > 0;
|
||||
|
||||
// ===== SEARCH CHANGE HANDLER =====
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
|
||||
if (!isNameSorted) {
|
||||
updateFilter('nameSort', '');
|
||||
// updateFilter('nameSort', '');
|
||||
} else {
|
||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
}, [sorting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
|
||||
<div className='w-full'>
|
||||
<div className='flex flex-col mb-4'>
|
||||
<div className='relative w-full p-3 pt-0 px-0 flex flex-row justify-between gap-3 flex-wrap after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10'>
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Closing'
|
||||
value={tableFilterState.search}
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
startAdornment={
|
||||
<Icon
|
||||
icon='heroicons:magnifying-glass'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6',
|
||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||
input:
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Status Project'
|
||||
placeholder='Pilih Status'
|
||||
options={PROJECT_STATUS_OPTIONS}
|
||||
value={selectedProjectStatus}
|
||||
onChange={projectStatusChangeHandler}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={handleFilterModalOpen}
|
||||
className={cn(
|
||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
||||
{
|
||||
'border-primary-gradient text-primary': hasFilters,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
Filter
|
||||
{hasFilters && (
|
||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||
{activeFiltersCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<Closing>
|
||||
data={isResponseSuccess(closings) ? closings?.data : []}
|
||||
columns={closingsColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
|
||||
totalItems={
|
||||
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoadingClosings}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(closings) && closings?.data?.length === 0,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{isLoadingClosings ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<ClosingTableSkeleton
|
||||
columns={closingsColumns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:chart-bar'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title='Data Closing Belum Tersedia'
|
||||
subtitle='Tidak ada data closing untuk saat ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<Closing>
|
||||
data={isResponseSuccess(closings) ? closings?.data : []}
|
||||
columns={closingsColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
|
||||
totalItems={
|
||||
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoadingClosings}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn('mt-3', {
|
||||
'w-full mb-0':
|
||||
isResponseSuccess(closings) && closings?.data?.length === 0,
|
||||
}),
|
||||
headerColumnClassName: 'text-nowrap',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
value={locationIdValue}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
formik.setFieldValue(
|
||||
'location_id',
|
||||
val?.value ? String(val.value) : null
|
||||
);
|
||||
}
|
||||
}}
|
||||
onInputChange={setLocationInputValue}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInputRadio
|
||||
label='Status Project'
|
||||
placeholder='Pilih Status'
|
||||
options={projectStatusOptions}
|
||||
value={projectStatusValue}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
formik.setFieldValue('project_status', val?.value || null);
|
||||
}
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='reset'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type ClosingFilterType = {
|
||||
location_id: string | null;
|
||||
project_status: string | null;
|
||||
};
|
||||
|
||||
export const ClosingFilterSchema = yup.object({
|
||||
location_id: yup.string().nullable(),
|
||||
project_status: yup.string().nullable(),
|
||||
});
|
||||
|
||||
export type ClosingFilterValues = yup.InferType<typeof ClosingFilterSchema>;
|
||||
@@ -1,109 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import { formatCurrency } from '@/lib/helper';
|
||||
import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing';
|
||||
|
||||
interface HppExpeditionReportTableProps {
|
||||
type?: 'detail';
|
||||
initialValues?: BaseHppExpedition;
|
||||
}
|
||||
|
||||
const HppExpeditionReportTable = ({
|
||||
initialValues,
|
||||
}: HppExpeditionReportTableProps) => {
|
||||
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
|
||||
return initialValues?.expedition_costs || [];
|
||||
}, [initialValues]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
const totalHpp = initialValues?.total_hpp_amount || 0;
|
||||
|
||||
return {
|
||||
totalHpp,
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'id',
|
||||
accessorKey: 'id',
|
||||
header: 'No',
|
||||
cell: (props) => {
|
||||
return <div>{props.row.index + 1}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
Total HPP Ekspedisi
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'expedition_vendor_name',
|
||||
accessorKey: 'expedition_vendor_name',
|
||||
header: 'Nama Ekspedisi',
|
||||
cell: (props) => props.getValue() || '-',
|
||||
},
|
||||
{
|
||||
id: 'hpp_amount',
|
||||
accessorKey: 'hpp_amount',
|
||||
header: 'HPP Ekspedisi',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(totals.totalHpp)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[totals]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<div className='p-4'>
|
||||
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full bg-base-100',
|
||||
body: 'p-0',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
data={costOfRevenueExpeditionData}
|
||||
columns={costOfRevenueExpeditionColumns}
|
||||
renderFooter={costOfRevenueExpeditionData.length > 0}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppExpeditionReportTable;
|
||||
@@ -0,0 +1,36 @@
|
||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const ClosingTabSkeleton = <T extends object>({
|
||||
columns,
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
columns: ColumnDef<T, unknown>[];
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className='relative size-full'>
|
||||
<Table
|
||||
data={[]}
|
||||
columns={columns}
|
||||
isLoading={true}
|
||||
className={{
|
||||
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
|
||||
headerColumnClassName: 'whitespace-nowrap',
|
||||
containerClassName: 'mb-0 overflow-hidden',
|
||||
tableWrapperClassName: 'overflow-hidden',
|
||||
}}
|
||||
/>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingTabSkeleton;
|
||||
@@ -0,0 +1,37 @@
|
||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { Closing } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const ClosingTableSkeleton = ({
|
||||
columns,
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
columns: ColumnDef<Closing>[];
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className='relative size-full'>
|
||||
<Table
|
||||
data={[]}
|
||||
columns={columns}
|
||||
isLoading={true}
|
||||
className={{
|
||||
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
|
||||
headerColumnClassName: 'whitespace-nowrap',
|
||||
containerClassName: 'mb-0 overflow-hidden',
|
||||
tableWrapperClassName: 'overflow-hidden',
|
||||
}}
|
||||
/>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingTableSkeleton;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import Card from '@/components/Card';
|
||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||
|
||||
const FinanceClosingSkeleton = ({
|
||||
title = 'Data Keuangan Belum Tersedia',
|
||||
subtitle = 'Tidak ada data keuangan untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-8',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-center p-8'>
|
||||
<DataStateSkeleton
|
||||
icon={
|
||||
<Icon
|
||||
icon={iconName}
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
description={subtitle}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinanceClosingSkeleton;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||
import { BaseExpeditionCost } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const HppExpeditionClosingSkeleton = ({
|
||||
columns,
|
||||
title = 'Data HPP Ekspedisi Belum Tersedia',
|
||||
subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
columns: ColumnDef<BaseExpeditionCost>[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ClosingTabSkeleton<BaseExpeditionCost>
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||
}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppExpeditionClosingSkeleton;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||
import { Overhead } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const OverheadClosingSkeleton = ({
|
||||
columns,
|
||||
title = 'Data Overhead Belum Tersedia',
|
||||
subtitle = 'Tidak ada data overhead untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
columns: ColumnDef<Overhead>[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ClosingTabSkeleton<Overhead>
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||
}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverheadClosingSkeleton;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||
|
||||
const ProductionDataClosingSkeleton = ({
|
||||
title = 'Data Produksi Belum Tersedia',
|
||||
subtitle = 'Tidak ada data produksi untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className='w-full rounded-xl p-8 shadow-sm'>
|
||||
<div className='flex items-center justify-center p-12'>
|
||||
<DataStateSkeleton
|
||||
icon={
|
||||
<Icon
|
||||
icon={iconName}
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
description={subtitle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionDataClosingSkeleton;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||
import { BaseSales } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const SalesClosingSkeleton = ({
|
||||
columns,
|
||||
title = 'Data Penjualan Belum Tersedia',
|
||||
subtitle = 'Tidak ada data penjualan untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
columns: ColumnDef<BaseSales>[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ClosingTabSkeleton<BaseSales>
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||
}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesClosingSkeleton;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||
import { RowSapronakCalculation } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const SapronakCalculationClosingSkeleton = ({
|
||||
columns,
|
||||
title = 'Data Perhitungan Sapronak Belum Tersedia',
|
||||
subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.',
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
columns: ColumnDef<RowSapronakCalculation>[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ClosingTabSkeleton<RowSapronakCalculation>
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||
}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SapronakCalculationClosingSkeleton;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
const SapronakClosingSkeleton = <T extends object>({
|
||||
columns,
|
||||
type = 'incoming',
|
||||
title,
|
||||
subtitle,
|
||||
iconName = 'heroicons:chart-bar',
|
||||
}: {
|
||||
columns: ColumnDef<T, unknown>[];
|
||||
type?: 'incoming' | 'outgoing';
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
iconName?: string;
|
||||
}) => {
|
||||
const defaultTitle =
|
||||
type === 'incoming'
|
||||
? 'Data Sapronak Masuk Belum Tersedia'
|
||||
: 'Data Sapronak Keluar Belum Tersedia';
|
||||
|
||||
const defaultSubtitle =
|
||||
type === 'incoming'
|
||||
? 'Tidak ada data sapronak masuk untuk periode ini.'
|
||||
: 'Tidak ada data sapronak keluar untuk periode ini.';
|
||||
|
||||
return (
|
||||
<ClosingTabSkeleton<T>
|
||||
columns={columns}
|
||||
icon={
|
||||
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||
}
|
||||
title={title || defaultTitle}
|
||||
subtitle={subtitle || defaultSubtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SapronakClosingSkeleton;
|
||||
@@ -0,0 +1,13 @@
|
||||
import FinanceClosingTable from '@/components/pages/closing/table/FinanceClosingTable';
|
||||
|
||||
const FinanceClosingTab = ({ projectFlockId }: { projectFlockId: number }) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<FinanceClosingTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinanceClosingTab;
|
||||
@@ -0,0 +1,19 @@
|
||||
import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable';
|
||||
|
||||
interface HppExpeditionClosingTabProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const HppExpeditionClosingTab = ({
|
||||
projectFlockId,
|
||||
}: HppExpeditionClosingTabProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<HppExpeditionClosingTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppExpeditionClosingTab;
|
||||
+6
-6
@@ -1,22 +1,22 @@
|
||||
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
|
||||
import OverheadClosingTable from '@/components/pages/closing/table/OverheadClosingTable';
|
||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
|
||||
interface ClosingOverheadTabContentProps {
|
||||
interface OverheadClosingTabProps {
|
||||
projectFlockId: number;
|
||||
generalInformation?: ClosingGeneralInformation;
|
||||
kandangData?: ProjectFlockKandang;
|
||||
}
|
||||
|
||||
const ClosingOverheadTabContent = ({
|
||||
const OverheadClosingTab = ({
|
||||
projectFlockId,
|
||||
generalInformation,
|
||||
kandangData,
|
||||
}: ClosingOverheadTabContentProps) => {
|
||||
}: OverheadClosingTabProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<ClosingOverheadTable
|
||||
<OverheadClosingTable
|
||||
projectFlockId={projectFlockId}
|
||||
generalInformation={generalInformation}
|
||||
kandangData={kandangData}
|
||||
@@ -26,4 +26,4 @@ const ClosingOverheadTabContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTabContent;
|
||||
export default OverheadClosingTab;
|
||||
@@ -0,0 +1,321 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import ProductionDataClosingSkeleton from '@/components/pages/closing/skeleton/ProductionDataClosingSkeleton';
|
||||
import Card from '@/components/Card';
|
||||
|
||||
interface ProductionDataClosingTabProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ProductionDataClosingTab = ({
|
||||
projectFlockId,
|
||||
}: ProductionDataClosingTabProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: productionData, isLoading } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <ProductionDataClosingSkeleton />;
|
||||
}
|
||||
|
||||
if (!productionData || !isResponseSuccess(productionData)) {
|
||||
return (
|
||||
<ProductionDataClosingSkeleton
|
||||
iconName='heroicons:exclamation-circle'
|
||||
title='Gagal Memuat Data Produksi'
|
||||
subtitle='Terjadi kesalahan saat memuat data produksi. Silakan coba lagi.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { purchase, sales, performance } = productionData.data;
|
||||
|
||||
// Helper for consistent row styling
|
||||
const DataRow = ({
|
||||
label,
|
||||
value,
|
||||
unit = '',
|
||||
valueClassName = 'font-bold text-gray-800',
|
||||
unitClassName = 'text-gray-500 w-12 text-right',
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
valueClassName?: string;
|
||||
unitClassName?: string;
|
||||
}) => (
|
||||
<div className='flex justify-between items-center py-1'>
|
||||
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
||||
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
||||
<span className={valueClassName}>{value}</span>
|
||||
{unit && <span className={unitClassName}>{unit}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full pt-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Data Produksi'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
||||
{/* Left Column */}
|
||||
<div className='space-y-10'>
|
||||
{/* Purchase Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Pembelian
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Populasi Awal'
|
||||
value={formatNumber(purchase.initial_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Claim Culling'
|
||||
value={formatNumber(purchase.claim_culling)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Populasi Akhir'
|
||||
value={formatNumber(purchase.final_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Masuk'
|
||||
value={formatNumber(purchase.feed_in)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai'
|
||||
value={formatNumber(purchase.feed_used)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sales Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Penjualan
|
||||
</h3>
|
||||
<div className='space-y-4'>
|
||||
{/* Chicken Sales */}
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Penjualan (Ekor)'
|
||||
value={formatNumber(sales.chicken.sales_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Penjualan (Kg)'
|
||||
value={formatNumber(sales.chicken.sales_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Bobot Rata-Rata'
|
||||
value={formatNumber(sales.chicken.avg_weight)}
|
||||
unit='Kg/Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Rata-Rata'
|
||||
value={formatNumber(sales.chicken.avg_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Egg Sales (if available) */}
|
||||
{sales.egg && (
|
||||
<>
|
||||
<div className='h-px bg-gray-100 my-2' />
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Telur (Butir)'
|
||||
value={formatNumber(sales.egg.egg_pieces)}
|
||||
unit='Butir'
|
||||
/>
|
||||
<DataRow
|
||||
label='Telur (Kg)'
|
||||
value={formatNumber(sales.egg.egg_mass)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Berat Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.avg_egg_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.avg_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Divider Line (Absolute centered) */}
|
||||
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
||||
|
||||
{/* Right Column */}
|
||||
<div className='space-y-10 flex flex-col h-full'>
|
||||
{/* Performance Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Performance
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Deplesi'
|
||||
value={formatNumber(performance.depletion)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Umur'
|
||||
value={formatNumber(performance.age_day)}
|
||||
unit='Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Std'
|
||||
value={formatNumber(performance.mor_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Act'
|
||||
value={formatNumber(performance.mor_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF Mortalitas'
|
||||
value={formatNumber(performance.mor_diff)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
{/* <DataRow
|
||||
label='AWG Std'
|
||||
value={formatNumber(performance.awg_std)}
|
||||
unit='Gr/Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='AWG Act'
|
||||
value={formatNumber(performance.awg_act)}
|
||||
unit='Gr/Hari'
|
||||
/> */}
|
||||
<DataRow
|
||||
label='Feed Intake Std'
|
||||
value={formatNumber(performance.feed_intake_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Feed Intake Act'
|
||||
value={formatNumber(performance.feed_intake)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Std'
|
||||
value={formatNumber(performance.fcr_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Act'
|
||||
value={formatNumber(performance.fcr_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF FCR'
|
||||
value={formatNumber(performance.fcr_diff)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
|
||||
{/* Laying Specific Fields */}
|
||||
{performance.hen_day_act !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Hen Day Std'
|
||||
value={formatNumber(performance.hen_day_std!)}
|
||||
unit='%'
|
||||
/>
|
||||
<DataRow
|
||||
label='Hen Day Act'
|
||||
value={formatNumber(performance.hen_day_act)}
|
||||
unit='%'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.egg_mass !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Egg Mass Std'
|
||||
value={formatNumber(performance.egg_mass_std!)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Egg Mass Act'
|
||||
value={formatNumber(performance.egg_mass)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.egg_weight !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Egg Weight Std'
|
||||
value={formatNumber(performance.egg_weight_std!)}
|
||||
unit='Gr'
|
||||
/>
|
||||
<DataRow
|
||||
label='Egg Weight Act'
|
||||
value={formatNumber(performance.egg_weight)}
|
||||
unit='Gr'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{performance.hen_housed_act !== undefined && (
|
||||
<>
|
||||
<DataRow
|
||||
label='Hen Housed Std'
|
||||
value={formatNumber(performance.hen_housed_std!)}
|
||||
unit='%'
|
||||
/>
|
||||
<DataRow
|
||||
label='Hen Housed Act'
|
||||
value={formatNumber(performance.hen_housed_act)}
|
||||
unit='%'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionDataClosingTab;
|
||||
@@ -0,0 +1,15 @@
|
||||
import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable';
|
||||
|
||||
interface SalesClosingTabProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && <SalesClosingTable projectFlockId={projectFlockId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesClosingTab;
|
||||
+6
-6
@@ -1,22 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
|
||||
import SapronakCalculationClosingTable from '@/components/pages/closing/table/SapronakCalculationClosingTable';
|
||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||
|
||||
interface ClosingSapronakCalculationTabContentProps {
|
||||
interface SapronakCalculationClosingTabProps {
|
||||
projectFlockId?: number;
|
||||
closingGeneralInformation?: ClosingGeneralInformation;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTabContent = ({
|
||||
const SapronakCalculationClosingTab = ({
|
||||
projectFlockId,
|
||||
closingGeneralInformation,
|
||||
}: ClosingSapronakCalculationTabContentProps) => {
|
||||
}: SapronakCalculationClosingTabProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<>
|
||||
<ClosingSapronakCalculationTable
|
||||
<SapronakCalculationClosingTable
|
||||
closingGeneralInformation={closingGeneralInformation}
|
||||
projectFlockId={projectFlockId}
|
||||
/>
|
||||
@@ -26,4 +26,4 @@ const ClosingSapronakCalculationTabContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTabContent;
|
||||
export default SapronakCalculationClosingTab;
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable';
|
||||
import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable';
|
||||
import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable';
|
||||
import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable';
|
||||
|
||||
interface SapronakClosingTabProps {
|
||||
projectFlockId?: number;
|
||||
}
|
||||
|
||||
const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<>
|
||||
<IncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<IncomingSapronaksSummaryTable projectFlockId={projectFlockId} />
|
||||
|
||||
<OutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<OutgoingSapronaksSummaryTable projectFlockId={projectFlockId} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SapronakClosingTab;
|
||||
@@ -0,0 +1,507 @@
|
||||
import Alert from '@/components/Alert';
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton';
|
||||
|
||||
const FinanceClosingTable = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: finance, isLoading } = useSWR(
|
||||
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||
() =>
|
||||
ClosingApi.getFinance(
|
||||
projectFlockId,
|
||||
kandangId ? Number(kandangId) : undefined
|
||||
)
|
||||
);
|
||||
|
||||
const hppTableData: HppItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const customItems = {
|
||||
label: 'HPP dan Pengeluaran',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const purchases = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'purchase'
|
||||
);
|
||||
const totalBudgeting = {
|
||||
label: 'HPP dan Bahan Baku',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const overheads = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'overhead'
|
||||
);
|
||||
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const incomes = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'income'
|
||||
);
|
||||
const purchases = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'purchase'
|
||||
);
|
||||
const overheads = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'overhead'
|
||||
);
|
||||
const grossProfit = {
|
||||
label: 'LABA RUGI BRUTO',
|
||||
code: 'custom_row',
|
||||
type: 'gross_profit',
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
const subtotal = {
|
||||
label: 'Subtotal',
|
||||
code: 'custom_row',
|
||||
type: 'subtotal',
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 pt-3'>
|
||||
{isLoading ? (
|
||||
<FinanceClosingSkeleton />
|
||||
) : !isResponseSuccess(finance) ? (
|
||||
<FinanceClosingSkeleton
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Keuangan Tidak Ditemukan'
|
||||
subtitle='Tidak ada data keuangan untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<section className='grid grid-cols-1 md:grid-cols-2 gap-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-xl border border-base-content/10',
|
||||
body: 'p-0',
|
||||
wrapperContent:
|
||||
'h-full flex flex-col items-between justify-between',
|
||||
}}
|
||||
variant='bordered'
|
||||
>
|
||||
<div className='flex flex-row items-center gap-4 px-4 py-4'>
|
||||
<Alert
|
||||
variant='soft'
|
||||
color='success'
|
||||
className='rounded-lg p-3 bg-success/12 flex items-center justify-center'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:chart-bar-square'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Alert>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||
Laba Rugi Brutto
|
||||
</h3>
|
||||
<p className='text-xl font-semibold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.gross_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-xl border border-base-content/10',
|
||||
body: 'p-0',
|
||||
wrapperContent:
|
||||
'h-full flex flex-col items-between justify-between',
|
||||
}}
|
||||
variant='bordered'
|
||||
>
|
||||
<div className='flex flex-row items-center gap-4 px-4 py-4'>
|
||||
<Alert
|
||||
variant='soft'
|
||||
color='info'
|
||||
className='rounded-lg p-3 bg-info/12 flex items-center justify-center'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:currency-dollar'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Alert>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||
Laba Rugi Netto
|
||||
</h3>
|
||||
<p className='text-xl font-semibold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
<Card
|
||||
title='HPP Purchases'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
<div className='p-0'>
|
||||
<Table<HppItem>
|
||||
data={hppTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'No.',
|
||||
enableSorting: false,
|
||||
accessorFn: (item, index) => {
|
||||
if (item.code === 'custom_row') return '-';
|
||||
const dataRowsBefore = hppTableData
|
||||
.slice(0, index)
|
||||
.filter((row) => row.code !== 'custom_row').length;
|
||||
return dataRowsBefore + 1;
|
||||
},
|
||||
footer: () => {
|
||||
return 'HPP';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||
},
|
||||
{
|
||||
header: 'Budgeting',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'budgeting_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'budgeting_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting
|
||||
?.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'budgeting_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realization',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'realization_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id ===
|
||||
'realization_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'realization_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'realization_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp.summary?.realization?.amount ||
|
||||
0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
></td>
|
||||
<td
|
||||
colSpan={7}
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Profit/Loss'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
<div className='p-0'>
|
||||
<Table<ProfitLossItem>
|
||||
data={profitLossTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => item.label,
|
||||
cell: (item) => (
|
||||
<div className=''>
|
||||
{formatTitleCase(item.row.original.label || '-')}
|
||||
</div>
|
||||
),
|
||||
footer: () => (
|
||||
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.amount || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold ps-6 uppercase'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.amount ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinanceClosingTable;
|
||||
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import { formatCurrency } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseExpeditionCost } from '@/types/api/closing';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton';
|
||||
|
||||
interface HppExpeditionClosingTableProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const HppExpeditionClosingTable = ({
|
||||
projectFlockId,
|
||||
}: HppExpeditionClosingTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: hppExpedition, isLoading } = useSWR(
|
||||
kandangId
|
||||
? `/closing/hpp-expedition/${projectFlockId}/${kandangId}`
|
||||
: `/closing/hpp-expedition/${projectFlockId}`,
|
||||
() =>
|
||||
kandangId
|
||||
? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId))
|
||||
: ClosingApi.getHppEkspedisi(projectFlockId)
|
||||
);
|
||||
|
||||
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
|
||||
if (isResponseSuccess(hppExpedition)) {
|
||||
return hppExpedition.data.expedition_costs || [];
|
||||
}
|
||||
return [];
|
||||
}, [hppExpedition]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (isResponseSuccess(hppExpedition)) {
|
||||
return {
|
||||
totalHpp: hppExpedition.data.total_hpp_amount || 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
totalHpp: 0,
|
||||
};
|
||||
}, [hppExpedition]);
|
||||
|
||||
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'id',
|
||||
accessorKey: 'id',
|
||||
header: 'No',
|
||||
cell: (props) => {
|
||||
return <div>{props.row.index + 1}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
Total HPP Ekspedisi
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'expedition_vendor_name',
|
||||
accessorKey: 'expedition_vendor_name',
|
||||
header: 'Nama Ekspedisi',
|
||||
cell: (props) => props.getValue() || '-',
|
||||
},
|
||||
{
|
||||
id: 'hpp_amount',
|
||||
accessorKey: 'hpp_amount',
|
||||
header: 'HPP Ekspedisi',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(totals.totalHpp)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[totals]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full pt-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='HPP Ekspedisi'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
{isLoading ? (
|
||||
<HppExpeditionClosingSkeleton
|
||||
columns={costOfRevenueExpeditionColumns}
|
||||
/>
|
||||
) : costOfRevenueExpeditionData.length === 0 ? (
|
||||
<HppExpeditionClosingSkeleton
|
||||
columns={costOfRevenueExpeditionColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
data={costOfRevenueExpeditionData}
|
||||
columns={costOfRevenueExpeditionColumns}
|
||||
isLoading={isLoading}
|
||||
renderFooter={costOfRevenueExpeditionData.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppExpeditionClosingTable;
|
||||
+81
-38
@@ -1,5 +1,5 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
@@ -14,18 +14,19 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton';
|
||||
|
||||
interface ClosingOverheadTableProps {
|
||||
interface OverheadClosingTableProps {
|
||||
projectFlockId: number;
|
||||
generalInformation?: ClosingGeneralInformation;
|
||||
kandangData?: ProjectFlockKandang;
|
||||
}
|
||||
|
||||
const ClosingOverheadTable = ({
|
||||
const OverheadClosingTable = ({
|
||||
projectFlockId,
|
||||
generalInformation,
|
||||
kandangData,
|
||||
}: ClosingOverheadTableProps) => {
|
||||
}: OverheadClosingTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
@@ -37,7 +38,7 @@ const ClosingOverheadTable = ({
|
||||
}
|
||||
);
|
||||
|
||||
const { data: overheadKandang, isLoading: isLoadingOverheadKandang } = useSWR(
|
||||
const { data: overheadKandang } = useSWR(
|
||||
kandangId
|
||||
? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead`
|
||||
: undefined,
|
||||
@@ -208,42 +209,84 @@ const ClosingOverheadTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full pt-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Pengeluaran Overhead'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<Overhead>
|
||||
data={
|
||||
kandangId
|
||||
? isResponseSuccess(overheadKandang)
|
||||
? (overheadKandang.data?.overheads ?? [])
|
||||
: []
|
||||
: isResponseSuccess(overhead)
|
||||
? (overhead.data?.overheads ?? [])
|
||||
: []
|
||||
}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
headerColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
isLoading={isLoadingOverhead}
|
||||
renderFooter={
|
||||
isResponseSuccess(overhead)
|
||||
? overhead.data?.overheads.length > 0
|
||||
: false
|
||||
}
|
||||
/>
|
||||
{kandangId && (
|
||||
{isLoadingOverhead ? (
|
||||
<OverheadClosingSkeleton columns={columns} />
|
||||
) : !isResponseSuccess(overhead) ? (
|
||||
<OverheadClosingSkeleton
|
||||
columns={columns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Overhead Tidak Ditemukan'
|
||||
subtitle='Tidak ada data overhead untuk periode ini.'
|
||||
/>
|
||||
) : kandangId && !isResponseSuccess(overheadKandang) ? (
|
||||
<OverheadClosingSkeleton
|
||||
columns={columns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Overhead Tidak Ditemukan'
|
||||
subtitle='Tidak ada data overhead untuk periode ini.'
|
||||
/>
|
||||
) : (!kandangId && overhead.data?.overheads.length === 0) ||
|
||||
(kandangId &&
|
||||
isResponseSuccess(overheadKandang) &&
|
||||
overheadKandang.data?.overheads.length === 0) ? (
|
||||
<OverheadClosingSkeleton
|
||||
columns={columns}
|
||||
iconName='heroicons:chart-bar'
|
||||
/>
|
||||
) : (
|
||||
<Table<Overhead>
|
||||
data={
|
||||
kandangId
|
||||
? isResponseSuccess(overheadKandang)
|
||||
? (overheadKandang.data?.overheads ?? [])
|
||||
: []
|
||||
: isResponseSuccess(overhead)
|
||||
? (overhead.data?.overheads ?? [])
|
||||
: []
|
||||
}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName: cn(
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
isLoading={isLoadingOverhead}
|
||||
renderFooter={
|
||||
isResponseSuccess(overhead)
|
||||
? overhead.data?.overheads.length > 0
|
||||
: false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
@@ -298,8 +341,8 @@ const ClosingOverheadTable = ({
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTable;
|
||||
export default OverheadClosingTable;
|
||||
+79
-46
@@ -5,28 +5,47 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
||||
import {
|
||||
BaseClosingSales,
|
||||
BaseSales,
|
||||
ClosingSalesSummary,
|
||||
} from '@/types/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseSales, ClosingSalesSummary } from '@/types/api/closing';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton';
|
||||
|
||||
interface SalesReportTableProps {
|
||||
type?: 'detail';
|
||||
initialValues?: BaseClosingSales;
|
||||
interface SalesClosingTableProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
|
||||
const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: sales, isLoading } = useSWR(
|
||||
kandangId
|
||||
? `/closing/sales/${projectFlockId}/${kandangId}`
|
||||
: `/closing/sales/${projectFlockId}`,
|
||||
() =>
|
||||
kandangId
|
||||
? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId))
|
||||
: ClosingApi.getPenjualan(projectFlockId)
|
||||
);
|
||||
|
||||
const salesData: BaseSales[] = useMemo(() => {
|
||||
return initialValues?.sales || [];
|
||||
}, [initialValues]);
|
||||
if (isResponseSuccess(sales)) {
|
||||
return sales.data.sales || [];
|
||||
}
|
||||
return [];
|
||||
}, [sales]);
|
||||
|
||||
const summary: ClosingSalesSummary | undefined = useMemo(() => {
|
||||
return initialValues?.summary;
|
||||
}, [initialValues]);
|
||||
if (isResponseSuccess(sales)) {
|
||||
return sales.data.summary;
|
||||
}
|
||||
return undefined;
|
||||
}, [sales]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (salesData.length === 0) {
|
||||
@@ -293,41 +312,55 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<div className='p-4'>
|
||||
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
|
||||
<Card
|
||||
<div className='w-full pt-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Penjualan'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SalesClosingSkeleton columns={salesColumns} />
|
||||
) : salesData.length === 0 ? (
|
||||
<SalesClosingSkeleton
|
||||
columns={salesColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
data={salesData}
|
||||
columns={salesColumns}
|
||||
isLoading={isLoading}
|
||||
renderFooter={salesData.length > 0}
|
||||
className={{
|
||||
wrapper: 'w-full bg-base-100',
|
||||
body: 'p-0',
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
data={salesData}
|
||||
columns={salesColumns}
|
||||
renderFooter={salesData.length > 0}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesReportTable;
|
||||
export default SalesClosingTable;
|
||||
@@ -0,0 +1,359 @@
|
||||
'use client';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
RowSapronakCalculation,
|
||||
TotalSapronakCalculation,
|
||||
} from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton';
|
||||
|
||||
interface SapronakCalculationClosingTableProps {
|
||||
projectFlockId: number;
|
||||
closingGeneralInformation?: ClosingGeneralInformation;
|
||||
}
|
||||
|
||||
const SapronakCalculationClosingTable = ({
|
||||
projectFlockId,
|
||||
closingGeneralInformation,
|
||||
}: SapronakCalculationClosingTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
||||
`/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (
|
||||
total?: TotalSapronakCalculation
|
||||
): ColumnDef<RowSapronakCalculation>[] => [
|
||||
{
|
||||
header: 'Tanggal',
|
||||
accessorKey: 'date',
|
||||
cell: (props) =>
|
||||
props.row.original.date
|
||||
? formatDate(props.row.original.date, 'DD MMM YYYY')
|
||||
: '-',
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'reference_number',
|
||||
cell: (props) => (props.row.original.reference_number as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Masuk',
|
||||
accessorKey: 'qty_in',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_in
|
||||
? formatNumber(props.row.original.qty_in as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Keluar',
|
||||
accessorKey: 'qty_out',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_out
|
||||
? formatNumber(props.row.original.qty_out as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Pakai',
|
||||
accessorKey: 'qty_used',
|
||||
cell: (props) =>
|
||||
props.row.original.qty_used
|
||||
? formatNumber(props.row.original.qty_used as number)
|
||||
: '0',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Uraian',
|
||||
accessorKey: 'description',
|
||||
cell: (props) => (props.row.original.description as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Kategori Produk',
|
||||
accessorKey: 'product_category',
|
||||
cell: (props) => (props.row.original.product_category as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Harga Beli/Qty (Rp)',
|
||||
accessorKey: 'unit_price',
|
||||
cell: (props) =>
|
||||
props.row.original.unit_price
|
||||
? formatCurrency(props.row.original.unit_price as number)
|
||||
: '-',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.avg_unit_price
|
||||
? formatCurrency(total?.avg_unit_price)
|
||||
: '-'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Total Harga (Rp)',
|
||||
accessorKey: 'total_amount',
|
||||
cell: (props) =>
|
||||
props.row.original.total_amount
|
||||
? formatCurrency(props.row.original.total_amount as number)
|
||||
: '-',
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Keterangan',
|
||||
accessorKey: 'notes',
|
||||
cell: (props) => (props.row.original.notes as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Memoize columns untuk setiap kategori
|
||||
const docColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.doc?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const ovkColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.ovk?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const pakanColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.pakan?.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 pt-3'>
|
||||
{/* Table DOC jika kategori Project Flock Growing */}
|
||||
<Card
|
||||
title={
|
||||
closingGeneralInformation?.project_type == 'GROWING'
|
||||
? 'DOC'
|
||||
: 'Pullet'
|
||||
}
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
>
|
||||
{isLoading ? (
|
||||
<SapronakCalculationClosingSkeleton columns={docColumns} />
|
||||
) : isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.doc?.rows?.length === 0 ? (
|
||||
<SapronakCalculationClosingSkeleton
|
||||
columns={docColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Perhitungan Sapronak Tidak Ditemukan'
|
||||
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.doc?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={docColumns}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.doc?.rows?.length > 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='OVK'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SapronakCalculationClosingSkeleton columns={ovkColumns} />
|
||||
) : isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.ovk?.rows?.length === 0 ? (
|
||||
<SapronakCalculationClosingSkeleton
|
||||
columns={ovkColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Perhitungan Sapronak Tidak Ditemukan'
|
||||
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.ovk?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={ovkColumns}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.ovk?.rows?.length > 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='Pakan'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SapronakCalculationClosingSkeleton columns={pakanColumns} />
|
||||
) : isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.pakan?.rows?.length === 0 ? (
|
||||
<SapronakCalculationClosingSkeleton
|
||||
columns={pakanColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Perhitungan Sapronak Tidak Ditemukan'
|
||||
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.pakan?.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={pakanColumns}
|
||||
className={{
|
||||
containerClassName: 'w-full mb-0!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
sapronakCalculation.data?.pakan?.rows?.length > 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SapronakCalculationClosingTable;
|
||||
+86
-54
@@ -1,20 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
import { cn, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||
|
||||
interface ClosingIncomingSapronaksSummaryTableProps {
|
||||
projectFlockId: number;
|
||||
@@ -55,20 +55,60 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
||||
}
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
||||
[
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'category',
|
||||
header: 'Kategori',
|
||||
cell: (props) => {
|
||||
const categories = props.row.original.category
|
||||
.split(' ')
|
||||
.filter((cat) => cat.trim());
|
||||
const maxBadges = 4;
|
||||
const visibleCategories = categories.slice(0, maxBadges);
|
||||
const remainingCount = categories.length - maxBadges;
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
||||
{visibleCategories.map((category, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
title={category}
|
||||
>
|
||||
{category.length > 12
|
||||
? `${category.slice(0, 12)}...`
|
||||
: category}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
||||
),
|
||||
}}
|
||||
title={categories.join(' ')}
|
||||
>
|
||||
+{remainingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_qty',
|
||||
@@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
||||
},
|
||||
];
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
@@ -93,44 +129,35 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(incomingSapronakSummaries)
|
||||
? incomingSapronakSummaries.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [incomingSapronakSummaries, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Ringkasan Sapronak Masuk</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
<div className='w-full'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Ringkasan Sapronak Masuk'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
{isLoadingIncomingSapronakSummaries ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='incoming'
|
||||
columns={incomingSapronaksColumns}
|
||||
/>
|
||||
) : isResponseSuccess(incomingSapronakSummaries) &&
|
||||
incomingSapronakSummaries.data.length === 0 ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='incoming'
|
||||
columns={incomingSapronaksColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Ringkasan Sapronak Masuk Tidak Ditemukan'
|
||||
subtitle='Tidak ada ringkasan sapronak masuk untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<ClosingIncomingSapronakSummary>
|
||||
data={
|
||||
isResponseSuccess(incomingSapronakSummaries)
|
||||
@@ -158,16 +185,21 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(incomingSapronakSummaries) &&
|
||||
incomingSapronakSummaries?.data?.length === 0,
|
||||
}),
|
||||
containerClassName: 'w-full mb-5!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
|
||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+107
-58
@@ -9,13 +9,14 @@ import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||
|
||||
interface ClosingIncomingSapronaksTableProps {
|
||||
projectFlockId: number;
|
||||
@@ -51,14 +52,12 @@ const ClosingIncomingSapronaksTable = ({
|
||||
ClosingApi.getAllIncomingSapronakFetcher
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
@@ -81,6 +80,48 @@ const ClosingIncomingSapronaksTable = ({
|
||||
{
|
||||
accessorKey: 'product_category',
|
||||
header: 'Kategori Produk',
|
||||
cell: (props) => {
|
||||
const categories = props.row.original.product_category
|
||||
.split(' ')
|
||||
.filter((cat) => cat.trim());
|
||||
const maxBadges = 4;
|
||||
const visibleCategories = categories.slice(0, maxBadges);
|
||||
const remainingCount = categories.length - maxBadges;
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
||||
{visibleCategories.map((category, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
title={category}
|
||||
>
|
||||
{category.length > 12
|
||||
? `${category.slice(0, 12)}...`
|
||||
: category}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
||||
),
|
||||
}}
|
||||
title={categories.join(' ')}
|
||||
>
|
||||
+{remainingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'source_warehouse',
|
||||
@@ -117,56 +158,59 @@ const ClosingIncomingSapronaksTable = ({
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(incomingSapronaks)
|
||||
? incomingSapronaks.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [incomingSapronaks, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Sapronak Masuk</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
<div className='w-full pt-3'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Sapronak Masuk'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<div className='flex flex-col gap-2 my-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Sapronak Masuk'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
<Icon
|
||||
icon='heroicons:magnifying-glass'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||
input:
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Sapronak Masuk'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoadingIncomingSapronaks ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='incoming'
|
||||
columns={incomingSapronaksColumns}
|
||||
/>
|
||||
) : isResponseSuccess(incomingSapronaks) &&
|
||||
incomingSapronaks.data.length === 0 ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='incoming'
|
||||
columns={incomingSapronaksColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Sapronak Masuk Tidak Ditemukan'
|
||||
subtitle='Tidak ada data sapronak masuk untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<ClosingIncomingSapronak>
|
||||
data={
|
||||
isResponseSuccess(incomingSapronaks)
|
||||
@@ -194,16 +238,21 @@ const ClosingIncomingSapronaksTable = ({
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(incomingSapronaks) &&
|
||||
incomingSapronaks?.data?.length === 0,
|
||||
}),
|
||||
containerClassName: 'w-full mb-5!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
|
||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+86
-54
@@ -1,20 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
import { cn, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||
|
||||
interface ClosingOutgoingSapronaksSummaryTableProps {
|
||||
projectFlockId: number;
|
||||
@@ -55,20 +55,60 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
||||
}
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
||||
[
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'category',
|
||||
header: 'Kategori',
|
||||
cell: (props) => {
|
||||
const categories = props.row.original.category
|
||||
.split(' ')
|
||||
.filter((cat) => cat.trim());
|
||||
const maxBadges = 4;
|
||||
const visibleCategories = categories.slice(0, maxBadges);
|
||||
const remainingCount = categories.length - maxBadges;
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
||||
{visibleCategories.map((category, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
title={category}
|
||||
>
|
||||
{category.length > 12
|
||||
? `${category.slice(0, 12)}...`
|
||||
: category}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
||||
),
|
||||
}}
|
||||
title={categories.join(' ')}
|
||||
>
|
||||
+{remainingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_qty',
|
||||
@@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
||||
},
|
||||
];
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
@@ -93,44 +129,35 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(outgoingSapronakSummaries)
|
||||
? outgoingSapronakSummaries.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [outgoingSapronakSummaries, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Ringkasan Sapronak Keluar</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
<div className='w-full'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg border-none',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Ringkasan Sapronak Keluar'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
{isLoadingOutgoingSapronakSummaries ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='outgoing'
|
||||
columns={outgoingSapronaksColumns}
|
||||
/>
|
||||
) : isResponseSuccess(outgoingSapronakSummaries) &&
|
||||
outgoingSapronakSummaries.data.length === 0 ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='outgoing'
|
||||
columns={outgoingSapronaksColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Ringkasan Sapronak Keluar Tidak Ditemukan'
|
||||
subtitle='Tidak ada ringkasan sapronak keluar untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<ClosingOutgoingSapronakSummary>
|
||||
data={
|
||||
isResponseSuccess(outgoingSapronakSummaries)
|
||||
@@ -158,16 +185,21 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(outgoingSapronakSummaries) &&
|
||||
outgoingSapronakSummaries?.data?.length === 0,
|
||||
}),
|
||||
containerClassName: 'w-full mb-5!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
|
||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+110
-59
@@ -9,13 +9,16 @@ import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||
|
||||
interface ClosingOutgoingSapronaksTableProps {
|
||||
projectFlockId: number;
|
||||
@@ -51,14 +54,12 @@ const ClosingOutgoingSapronaksTable = ({
|
||||
ClosingApi.getAllOutgoingSapronakFetcher
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
@@ -81,6 +82,48 @@ const ClosingOutgoingSapronaksTable = ({
|
||||
{
|
||||
accessorKey: 'product_category',
|
||||
header: 'Kategori Produk',
|
||||
cell: (props) => {
|
||||
const categories = props.row.original.product_category
|
||||
.split(' ')
|
||||
.filter((cat) => cat.trim());
|
||||
const maxBadges = 4;
|
||||
const visibleCategories = categories.slice(0, maxBadges);
|
||||
const remainingCount = categories.length - maxBadges;
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
||||
{visibleCategories.map((category, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
title={category}
|
||||
>
|
||||
{category.length > 12
|
||||
? `${category.slice(0, 12)}...`
|
||||
: category}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
className={{
|
||||
badge: cn(
|
||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
||||
),
|
||||
}}
|
||||
title={categories.join(' ')}
|
||||
>
|
||||
+{remainingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'source_warehouse',
|
||||
@@ -117,56 +160,59 @@ const ClosingOutgoingSapronaksTable = ({
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(outgoingSapronaks)
|
||||
? outgoingSapronaks.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [outgoingSapronaks, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Sapronak Keluar</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
<div className='w-full'>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
title='Sapronak Keluar'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<div className='flex flex-col gap-2 my-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Sapronak Keluar'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
<Icon
|
||||
icon='heroicons:magnifying-glass'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||
input:
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Sapronak Keluar'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoadingOutgoingSapronaks ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='outgoing'
|
||||
columns={outgoingSapronaksColumns}
|
||||
/>
|
||||
) : isResponseSuccess(outgoingSapronaks) &&
|
||||
outgoingSapronaks.data.length === 0 ? (
|
||||
<SapronakClosingSkeleton
|
||||
type='outgoing'
|
||||
columns={outgoingSapronaksColumns}
|
||||
iconName='heroicons:chart-bar'
|
||||
title='Data Sapronak Keluar Tidak Ditemukan'
|
||||
subtitle='Tidak ada data sapronak keluar untuk periode ini.'
|
||||
/>
|
||||
) : (
|
||||
<Table<ClosingOutgoingSapronak>
|
||||
data={
|
||||
isResponseSuccess(outgoingSapronaks)
|
||||
@@ -194,16 +240,21 @@ const ClosingOutgoingSapronaksTable = ({
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(outgoingSapronaks) &&
|
||||
outgoingSapronaks?.data?.length === 0,
|
||||
}),
|
||||
containerClassName: 'w-full mb-5!',
|
||||
tableWrapperClassName:
|
||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
|
||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -256,7 +256,7 @@ export const generateDashboardPDF = async ({
|
||||
pdf.save(fileName);
|
||||
|
||||
toast.success('PDF exported successfully!', { id: 'export-pdf' });
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to export PDF. Please try again.', {
|
||||
id: 'export-pdf',
|
||||
});
|
||||
|
||||
@@ -276,6 +276,13 @@ const ExpensesTable = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'reference_number',
|
||||
header: 'Nomor Referensi',
|
||||
cell: (props) => {
|
||||
return props.row.original.reference_number ?? '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_date',
|
||||
header: 'Tanggal Pengajuan',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ const FormFinanceAddInitialBalance = ({
|
||||
// ===== Formik =====
|
||||
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
||||
// Type assertion to handle potential initial_balance_type field
|
||||
const extendedInitialValues = initialValues as Finance & {
|
||||
initial_balance_type?: string;
|
||||
};
|
||||
// const extendedInitialValues = initialValues as Finance & {
|
||||
// initial_balance_type?: string;
|
||||
// };
|
||||
|
||||
return {
|
||||
party_type_option:
|
||||
@@ -122,8 +122,6 @@ const FormFinanceAddInitialBalance = ({
|
||||
options: bankOptions,
|
||||
rawData: bankRawData,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
setInputValue: setBankInputValue,
|
||||
loadMore: loadMoreBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
// ===== Helper Functions =====
|
||||
|
||||
@@ -28,10 +28,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Alert from '@/components/Alert';
|
||||
import { Icon } from '@iconify/react';
|
||||
import {
|
||||
FINANCE_INJECTION_STATUS,
|
||||
FINANCE_INJECTION_TYPE_OPTIONS,
|
||||
} from '@/config/constant';
|
||||
import { FINANCE_INJECTION_TYPE_OPTIONS } from '@/config/constant';
|
||||
|
||||
interface FormFinanceInjectionProps {
|
||||
type?: 'add' | 'edit';
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Icon } from '@iconify/react';
|
||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
|
||||
const InventoryAdjustmentTable = () => {
|
||||
const {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
InventoryAdjustmentFormSchema,
|
||||
InventoryAdjustmentFormValues,
|
||||
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
ProductApi,
|
||||
ProductCategoryApi,
|
||||
|
||||
@@ -33,7 +33,6 @@ import { toast } from 'react-hot-toast';
|
||||
import { MovementApi } from '@/services/api/inventory';
|
||||
import FileInput from '@/components/input/FileInput';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import Badge from '@/components/Badge';
|
||||
import Card from '@/components/Card';
|
||||
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
||||
|
||||
@@ -15,12 +15,7 @@ import { InventoryProductApi } from '@/services/api/inventory';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||
import { Icon } from '@iconify/react';
|
||||
import {
|
||||
CellContext,
|
||||
ColumnDef,
|
||||
Row,
|
||||
SortingState,
|
||||
} from '@tanstack/react-table';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
||||
@@ -35,6 +35,13 @@ const StockLogTable = ({
|
||||
header: 'Gudang',
|
||||
accessorKey: 'warehouse_name',
|
||||
},
|
||||
{
|
||||
header: 'Stock Akhir',
|
||||
accessorKey: 'stock',
|
||||
cell: (props) => {
|
||||
return formatNumber(props.row.original.stock);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Peningkatan',
|
||||
accessorKey: 'increase',
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table from '@/components/Table';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
InventoryProduct,
|
||||
ProductWarehouseStock,
|
||||
} from '@/types/api/inventory/product';
|
||||
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||
|
||||
const StockProductWarehouseTable = ({
|
||||
productWarehouseStock,
|
||||
|
||||
@@ -48,17 +48,17 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
|
||||
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
|
||||
|
||||
const DeliveryOrderFormModal = ({
|
||||
initialValues,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
}) => {
|
||||
const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add_delivery' ||
|
||||
modalAction === 'edit_delivery' ||
|
||||
@@ -72,19 +72,14 @@ const DeliveryOrderFormModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const { data: marketing, isLoading: isLoadingMarketing } = useSWR(
|
||||
const { data: marketing } = useSWR(
|
||||
isModalActionForForm && marketingId
|
||||
? `detail-marketing-${marketingId}`
|
||||
: undefined,
|
||||
() => MarketingApi.getSingle(Number(marketingId))
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
rawDataApprovals,
|
||||
isLoading: isLoadingApproval,
|
||||
refresh: refreshApproval,
|
||||
} = useApprovalSteps({
|
||||
const { rawDataApprovals, refresh: refreshApproval } = useApprovalSteps({
|
||||
latestApproval: isResponseSuccess(marketing)
|
||||
? marketing?.data.latest_approval
|
||||
: undefined,
|
||||
@@ -107,6 +102,7 @@ const DeliveryOrderFormModal = ({
|
||||
const successModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -279,29 +275,10 @@ const DeliveryOrderFormModal = ({
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const memoSalesOrder = formik.values.sales_order;
|
||||
|
||||
// ================== HANDLER ==================
|
||||
const nextButtonHandler = () => {
|
||||
setStep(step + 1);
|
||||
};
|
||||
const prevButtonHandler = () => {
|
||||
setStep(step - 1);
|
||||
};
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleChangeSalesPerson = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('sales_person_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('sales_person', val as OptionType);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const rejectMarketingHandler = async (notes: string) => {
|
||||
if (!marketingId) {
|
||||
toast.error(`Tidak ada data yang valid untuk di reject.`);
|
||||
@@ -329,6 +306,33 @@ const DeliveryOrderFormModal = ({
|
||||
refreshApproval();
|
||||
};
|
||||
|
||||
const approveMarketingHandler = async (notes: string) => {
|
||||
if (!marketingId) {
|
||||
toast.error(`Tidak ada data yang valid untuk di approve.`);
|
||||
approveModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const approveMarketingRes = await SalesOrderApi.singleApproval(
|
||||
Number(marketingId),
|
||||
'APPROVED',
|
||||
notes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(approveMarketingRes)) {
|
||||
approveModal.closeModal();
|
||||
toast.success(approveMarketingRes?.message as string);
|
||||
closeModalHandler();
|
||||
router.push('/marketing');
|
||||
}
|
||||
if (isResponseError(approveMarketingRes)) {
|
||||
approveModal.closeModal();
|
||||
toast.error(approveMarketingRes?.message as string);
|
||||
}
|
||||
refreshMarketing();
|
||||
refreshApproval();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
@@ -376,7 +380,77 @@ const DeliveryOrderFormModal = ({
|
||||
},
|
||||
[prevButtonHandler]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
|
||||
const isApprovalStep3Approved = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
marketing.data.latest_approval?.step_number === 3 &&
|
||||
marketing.data.latest_approval?.action === 'APPROVED'
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
const handleUpdateDOWithAPI = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
if (!marketingId) {
|
||||
toast.error('Marketing ID tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const updatedDeliveryValues = deliveryOrderValues.map((product) =>
|
||||
product.id === id ? { ...product, ...values } : product
|
||||
);
|
||||
|
||||
const payload = {
|
||||
marketing_id: Number(marketingId),
|
||||
delivery_products: updatedDeliveryValues
|
||||
.map((product) => {
|
||||
if (Boolean(product.delivery_date)) {
|
||||
return {
|
||||
marketing_product_id: product.marketing_product_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
delivery_date: formatDate(
|
||||
product.delivery_date as string,
|
||||
'yyyy-MM-DD'
|
||||
),
|
||||
vehicle_number: product.vehicle_number,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => Boolean(item)),
|
||||
} as UpdateDeliveryOrderPayload;
|
||||
|
||||
const updateDeliveryRes = await DeliveryOrderApi.update(
|
||||
Number(marketingId),
|
||||
payload
|
||||
);
|
||||
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
closeModalHandler();
|
||||
}
|
||||
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
setFormErrorMessage(updateDeliveryRes?.message as string);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[
|
||||
marketingId,
|
||||
deliveryOrderValues,
|
||||
formik.values.sales_order,
|
||||
prevButtonHandler,
|
||||
refreshMarketing,
|
||||
]
|
||||
);
|
||||
|
||||
const handleUpdateDOLocal = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.map((product) =>
|
||||
@@ -405,32 +479,22 @@ const DeliveryOrderFormModal = ({
|
||||
}, []);
|
||||
|
||||
// ================== MEMOIZED ==================
|
||||
const isNextButtonDisabled = useMemo(() => {
|
||||
if (step === 1) {
|
||||
return Boolean(
|
||||
!formik.values.customer_id ||
|
||||
!formik.values.sales_person_id ||
|
||||
!formik.values.so_date ||
|
||||
!formik.values.notes
|
||||
);
|
||||
}
|
||||
// const isNextButtonDisabled = useMemo(() => {
|
||||
// if (step === 1) {
|
||||
// return Boolean(
|
||||
// !formik.values.customer_id ||
|
||||
// !formik.values.sales_person_id ||
|
||||
// !formik.values.so_date ||
|
||||
// !formik.values.notes
|
||||
// );
|
||||
// }
|
||||
|
||||
return true;
|
||||
}, [step, formik.values]);
|
||||
// return true;
|
||||
// }, [step, formik.values]);
|
||||
const deliveryRejected = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
((marketing.data.latest_approval.step_number === 3 &&
|
||||
marketing.data.latest_approval.action === 'REJECTED') ||
|
||||
(marketing.data.latest_approval.step_number === 2 &&
|
||||
marketing.data.latest_approval.action === 'REJECTED'))
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
const isPending = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
marketing.data.latest_approval.step_number === 1
|
||||
marketing.data.latest_approval.action === 'REJECTED'
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
@@ -441,6 +505,7 @@ const DeliveryOrderFormModal = ({
|
||||
modalAction === 'edit_delivery' ||
|
||||
modalAction === 'detail'
|
||||
) {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
}
|
||||
}, [modalAction]);
|
||||
@@ -468,7 +533,26 @@ const DeliveryOrderFormModal = ({
|
||||
);
|
||||
|
||||
formik.setValues(filledInitialValues);
|
||||
setStep(1);
|
||||
|
||||
if (modalAction === 'add_delivery') {
|
||||
// add delivery
|
||||
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
|
||||
if (firstDeliveryItem) {
|
||||
setSelectedDeliveryProduct(firstDeliveryItem);
|
||||
}
|
||||
setStep(2); // Langsung ke form delivery
|
||||
} else if (modalAction === 'edit_delivery') {
|
||||
// edit delivery
|
||||
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
|
||||
if (firstDeliveryItem) {
|
||||
setSelectedDeliveryProduct(firstDeliveryItem);
|
||||
setStep(2); // Langsung ke form edit
|
||||
} else {
|
||||
setStep(1); // Jika belum ada data, tampilkan detail view
|
||||
}
|
||||
} else {
|
||||
setStep(1); // Detail view
|
||||
}
|
||||
}
|
||||
|
||||
if (isResponseError(marketing)) {
|
||||
@@ -479,7 +563,7 @@ const DeliveryOrderFormModal = ({
|
||||
};
|
||||
|
||||
getFilledInitialValues();
|
||||
}, [marketingId, marketing]);
|
||||
}, [marketingId, marketing, modalAction]);
|
||||
|
||||
// Reset error message when step changes
|
||||
useEffect(() => {
|
||||
@@ -562,9 +646,11 @@ const DeliveryOrderFormModal = ({
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Sales Order</td>
|
||||
<td className='text-sm px-4 py-3'>No. Order</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{marketing.data.so_number}
|
||||
{marketing.data.do_number
|
||||
? marketing.data.do_number
|
||||
: marketing.data.so_number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -667,13 +753,7 @@ const DeliveryOrderFormModal = ({
|
||||
<div className='px-4'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={marketing.data}
|
||||
formType={
|
||||
deliveryRejected
|
||||
? 'rejected'
|
||||
: isPending
|
||||
? 'pending'
|
||||
: modalAction
|
||||
}
|
||||
formType={deliveryRejected ? 'rejected' : modalAction}
|
||||
data={deliveryOrderValues}
|
||||
onEdit={handleEditDO}
|
||||
onDelete={handleDeleteDO}
|
||||
@@ -688,7 +768,12 @@ const DeliveryOrderFormModal = ({
|
||||
exisitingValues={deliveryOrderValues}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
onUpdateForm={
|
||||
isApprovalStep3Approved
|
||||
? handleUpdateDOWithAPI
|
||||
: handleUpdateDOLocal
|
||||
}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -715,31 +800,40 @@ const DeliveryOrderFormModal = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 && (
|
||||
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected || isPending}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected || isPending}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 &&
|
||||
marketing?.data?.latest_approval?.step_number !== 3 && (
|
||||
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
// Jika masih di step 1 approval, gunakan single approval API
|
||||
if (
|
||||
marketing?.data?.latest_approval?.step_number === 1
|
||||
) {
|
||||
approveModal.openModal();
|
||||
} else {
|
||||
// Jika sudah di step 2/3, gunakan form submit (delivery products)
|
||||
formRef.current?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -749,25 +843,29 @@ const DeliveryOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add_delivery' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add_delivery' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
className: 'rounded-lg',
|
||||
onClick: (e) => {
|
||||
onClick: () => {
|
||||
closeModalHandler();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={isResponseSuccess(marketing) ? marketing.data : undefined}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={
|
||||
isResponseSuccess(marketing) ? marketing.data : undefined
|
||||
}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModalWithNotes
|
||||
@@ -799,6 +897,21 @@ const DeliveryOrderFormModal = ({
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModalWithNotes
|
||||
ref={approveModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin approve data penjualan?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: approveModal.closeModal,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: approveMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { RefObject } from 'react';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Modal from '@/components/Modal';
|
||||
@@ -9,10 +9,12 @@ import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||
|
||||
interface MarketingFilterModal {
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
@@ -31,25 +33,59 @@ const MarketingFilterModal = ({
|
||||
|
||||
// ===== OPTIONS =====
|
||||
const {
|
||||
options: productsOptions,
|
||||
rawData: productsRawData,
|
||||
isLoadingOptions: isLoadingProductsOptions,
|
||||
setInputValue: setProductsInputValue,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
|
||||
const productsOptions = useMemo(() => {
|
||||
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
|
||||
|
||||
const productsMap = new Map<number, { value: number; label: string }>();
|
||||
|
||||
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
|
||||
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
|
||||
const product = so.product_warehouse?.product;
|
||||
if (product?.id && product?.name) {
|
||||
productsMap.set(product.id, {
|
||||
value: product.id,
|
||||
label: product.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(productsMap.values());
|
||||
}, [productsRawData]);
|
||||
|
||||
const {
|
||||
options: customersOptions,
|
||||
isLoadingOptions: isLoadingCustomersOptions,
|
||||
setInputValue: setCustomersInputValue,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({
|
||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||
label: item.step_name,
|
||||
}));
|
||||
|
||||
const uniqueCustomersOptions = useMemo(() => {
|
||||
const seen = new Set();
|
||||
return customersOptions.filter((customer) => {
|
||||
if (seen.has(customer.value)) return false;
|
||||
seen.add(customer.value);
|
||||
return true;
|
||||
});
|
||||
}, [customersOptions]);
|
||||
|
||||
const statusOptions = [
|
||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||
label: item.step_name,
|
||||
})),
|
||||
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||
];
|
||||
|
||||
const formik = useFormik<{
|
||||
product_ids: OptionType[];
|
||||
@@ -151,7 +187,7 @@ const MarketingFilterModal = ({
|
||||
label='Customer'
|
||||
isClearable
|
||||
placeholder='Pilih customer'
|
||||
options={customersOptions}
|
||||
options={uniqueCustomersOptions}
|
||||
isLoading={isLoadingCustomersOptions}
|
||||
value={formik.values.customer_id}
|
||||
onChange={customerChangeHandler}
|
||||
|
||||
@@ -25,8 +25,6 @@ import { useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
@@ -109,7 +107,9 @@ const RowsOptionsMenu = ({
|
||||
className='p-3 justify-start text-sm font-semibold w-full'
|
||||
>
|
||||
<Icon icon='heroicons:truck' width={20} height={20} />
|
||||
Deliver Item
|
||||
{props.row.original.latest_approval.step_number == 2
|
||||
? 'Deliver Item'
|
||||
: 'Edit Delivery'}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</>
|
||||
@@ -134,7 +134,7 @@ const RowsOptionsMenu = ({
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-0.25 before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5'
|
||||
className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-px before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5'
|
||||
>
|
||||
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||
Delete Item
|
||||
@@ -147,14 +147,11 @@ const RowsOptionsMenu = ({
|
||||
};
|
||||
|
||||
const MarketingTable = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const { permissionCheck } = useAuth();
|
||||
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
@@ -217,6 +214,32 @@ const MarketingTable = () => {
|
||||
updateFilter('customer_id', '');
|
||||
};
|
||||
|
||||
// ===== ACTIVE FILTERS COUNT =====
|
||||
const activeFiltersCount = useMemo(() => {
|
||||
let count = 0;
|
||||
|
||||
// Product filter
|
||||
if (tableFilterState.product_ids) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (tableFilterState.status) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Customer filter
|
||||
if (tableFilterState.customer_id) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}, [
|
||||
tableFilterState.product_ids,
|
||||
tableFilterState.status,
|
||||
tableFilterState.customer_id,
|
||||
]);
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApproveAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
@@ -379,8 +402,13 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_number',
|
||||
accessorKey: 'so_do_number',
|
||||
header: 'No. Order',
|
||||
cell: (props) => {
|
||||
return props.row.original.do_number
|
||||
? props.row.original.do_number
|
||||
: props.row.original.so_number;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
@@ -408,7 +436,7 @@ const MarketingTable = () => {
|
||||
: approval?.step_number == 2
|
||||
? 'info'
|
||||
: approval?.step_number == 3
|
||||
? 'warning'
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
: 'neutral'
|
||||
}
|
||||
@@ -523,7 +551,7 @@ const MarketingTable = () => {
|
||||
</RequirePermission>
|
||||
{idsToProcess.length > 0 && (
|
||||
<>
|
||||
<div className='divider divider-horizontal w-0.25 p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-0.25 after:bg-base-content/10 after:w-0.25'></div>
|
||||
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px'></div>
|
||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||
<Button
|
||||
color='error'
|
||||
@@ -560,16 +588,28 @@ const MarketingTable = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-row gap-3'>
|
||||
<ButtonFilter
|
||||
values={(() => {
|
||||
const { page, pageSize, ...rest } = tableFilterState;
|
||||
return rest;
|
||||
})()}
|
||||
<Button
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
filterModal.openModal();
|
||||
}}
|
||||
className='rounded-lg px-3 py-2.5'
|
||||
/>
|
||||
className={cn(
|
||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
||||
{
|
||||
'border-primary-gradient text-primary':
|
||||
activeFiltersCount > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
Filter
|
||||
{activeFiltersCount > 0 && (
|
||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||
{activeFiltersCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
|
||||
@@ -63,6 +63,10 @@ const SalesOrderFormModal = ({
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add' ||
|
||||
modalAction === 'edit' ||
|
||||
@@ -77,7 +81,7 @@ const SalesOrderFormModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const { data: marketing, isLoading: isLoadingMarketing } = useSWR(
|
||||
const { data: marketing } = useSWR(
|
||||
isModalActionForForm && marketingId
|
||||
? `detail-marketing-${marketingId}`
|
||||
: undefined,
|
||||
@@ -194,6 +198,13 @@ const SalesOrderFormModal = ({
|
||||
: 'KG' // termasuk "QTY" dan "KG"
|
||||
: undefined;
|
||||
|
||||
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
||||
let marketingTypeValue =
|
||||
product.marketing_type?.value?.toUpperCase() || '';
|
||||
if (marketingTypeValue === 'AYAM,AYAM_PULLET') {
|
||||
marketingTypeValue = product.week ? 'AYAM_PULLET' : 'AYAM';
|
||||
}
|
||||
|
||||
return {
|
||||
vehicle_number: product.vehicle_number as string,
|
||||
kandang_id: product.kandang_id as number,
|
||||
@@ -203,12 +214,11 @@ const SalesOrderFormModal = ({
|
||||
qty: parseFloat(String(product.qty || 0)),
|
||||
avg_weight: parseFloat(String(product.avg_weight || 0)),
|
||||
total_price: parseFloat(String(product.total_price || 0)),
|
||||
marketing_type:
|
||||
product.marketing_type?.value?.toUpperCase() || '',
|
||||
marketing_type: marketingTypeValue,
|
||||
convertion_unit: normalizedConvertionUnit,
|
||||
weight_per_convertion:
|
||||
product.weight_per_convertion ?? undefined,
|
||||
week: product.week?.value ?? undefined,
|
||||
week: product.week ?? undefined,
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
@@ -390,7 +400,7 @@ const SalesOrderFormModal = ({
|
||||
}
|
||||
|
||||
formik.setFieldValue('sales_order', updatedProducts);
|
||||
console.log(formik.values);
|
||||
setSelectedMarketingProduct(null);
|
||||
nextButtonHandler();
|
||||
},
|
||||
[memoSalesOrder, nextButtonHandler]
|
||||
@@ -412,7 +422,17 @@ const SalesOrderFormModal = ({
|
||||
// ================== EFFECT ==================
|
||||
useEffect(() => {
|
||||
if (modalAction === 'add' || modalAction === 'edit') {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
|
||||
if (modalAction === 'add') {
|
||||
formik.resetForm();
|
||||
setStep(1);
|
||||
setSelectedMarketingProduct(null);
|
||||
setSelectedDeliveryProduct(null);
|
||||
setFormErrorMessage('');
|
||||
setFormErrorList([]);
|
||||
}
|
||||
}
|
||||
}, [modalAction]);
|
||||
|
||||
@@ -724,24 +744,26 @@ const SalesOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
className: 'rounded-lg',
|
||||
onClick: (e) => {
|
||||
onClick: () => {
|
||||
closeModalHandler();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -128,12 +128,7 @@ export const SalesProductToFieldValues = (
|
||||
label: formatTitleCase(product.convertion_unit),
|
||||
}
|
||||
: null,
|
||||
week: product.week
|
||||
? {
|
||||
value: product.week,
|
||||
label: `Week ${product.week}`,
|
||||
}
|
||||
: null,
|
||||
week: product.week ?? null,
|
||||
total_peti: product.total_peti,
|
||||
weight_per_convertion: product.weight_per_convertion,
|
||||
uom: product.product_warehouse.product.uom.name,
|
||||
|
||||
+7
-21
@@ -30,13 +30,7 @@ type DeliveryOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
|
||||
@@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+52
-23
@@ -34,8 +34,8 @@ const DeliveryOrderProductForm = ({
|
||||
salesOrders,
|
||||
initialValues,
|
||||
exisitingValues,
|
||||
onSubmitForm,
|
||||
onUpdateForm,
|
||||
isLoading,
|
||||
}: {
|
||||
formState: 'add' | 'edit';
|
||||
salesOrders: BaseSalesOrder[];
|
||||
@@ -46,6 +46,7 @@ const DeliveryOrderProductForm = ({
|
||||
id: number,
|
||||
value: DeliveryOrderProductFormValues
|
||||
) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const [formikErrorMessage, setFormErrorMessage] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
@@ -94,12 +95,12 @@ const DeliveryOrderProductForm = ({
|
||||
);
|
||||
|
||||
// Options Week dari minggu 1 - 22
|
||||
const optionsWeek = useMemo(() => {
|
||||
return Array.from({ length: 22 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: `Week ${i + 1}`,
|
||||
}));
|
||||
}, []);
|
||||
// const optionsWeek = useMemo(() => {
|
||||
// return Array.from({ length: 22 }, (_, i) => ({
|
||||
// value: i + 1,
|
||||
// label: `Week ${i + 1}`,
|
||||
// }));
|
||||
// }, []);
|
||||
|
||||
const options = exisitingValues
|
||||
?.map((item) => {
|
||||
@@ -178,6 +179,25 @@ const DeliveryOrderProductForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const hasWeekField = useMemo(() => {
|
||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||
if (marketingType === 'ayam_pullet') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (formik.values.marketing_product?.product_warehouse_data) {
|
||||
return Boolean(
|
||||
formik.values.marketing_product?.product_warehouse_data?.week !==
|
||||
undefined &&
|
||||
formik.values.marketing_product?.product_warehouse_data?.week !==
|
||||
null &&
|
||||
formik.values.marketing_product?.product_warehouse_data?.week > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
@@ -362,20 +382,24 @@ const DeliveryOrderProductForm = ({
|
||||
avg_weight: '',
|
||||
total_weight: '',
|
||||
vehicle_number: '',
|
||||
week: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const soFieldValues = SalesProductToFieldValues(so);
|
||||
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: selected.value as number,
|
||||
marketing_product: SalesProductToFieldValues(so),
|
||||
marketing_product: soFieldValues,
|
||||
qty: so.qty,
|
||||
unit_price: so.unit_price,
|
||||
total_price: so.total_price,
|
||||
avg_weight: so.avg_weight,
|
||||
total_weight: so.total_weight,
|
||||
vehicle_number: so.vehicle_number,
|
||||
week: soFieldValues.week ?? null,
|
||||
});
|
||||
}}
|
||||
startAdornment={
|
||||
@@ -509,21 +533,26 @@ const DeliveryOrderProductForm = ({
|
||||
)}
|
||||
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
{(formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' ||
|
||||
hasWeekField) && (
|
||||
<NumberInput
|
||||
required={
|
||||
formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet'
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
label='Minggu'
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
placeholder='Pilih Week'
|
||||
onBlur={() => handleBlurField('week')}
|
||||
isError={formik.touched.week && Boolean(formik.errors.week)}
|
||||
errorMessage={formik.errors.week as string}
|
||||
placeholder='Masukan Minggu'
|
||||
decimalScale={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -792,8 +821,8 @@ const DeliveryOrderProductForm = ({
|
||||
<div className='absolute sm:w-full bottom-0 right-0 p-4'>
|
||||
<Button
|
||||
type='submit'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
isLoading={formik.isSubmitting || isLoading}
|
||||
disabled={formik.isSubmitting || isLoading}
|
||||
className='w-full p-3 rounded-lg text-base-100 text-sm font-semibold'
|
||||
>
|
||||
Submit
|
||||
|
||||
+7
-21
@@ -37,13 +37,7 @@ type SalesOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
@@ -102,26 +96,18 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+47
-19
@@ -117,6 +117,19 @@ const SalesOrderProductForm = ({
|
||||
isInitialValid: false,
|
||||
});
|
||||
|
||||
const hasWeekField = useMemo(() => {
|
||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||
if (marketingType === 'ayam_pullet') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
selectedProductWarehouse?.week !== undefined &&
|
||||
selectedProductWarehouse?.week !== null &&
|
||||
selectedProductWarehouse?.week > 0
|
||||
);
|
||||
}, [selectedProductWarehouse, formik.values.marketing_type]);
|
||||
|
||||
// ===== Options =====
|
||||
const {
|
||||
options: kandangSourceOptions,
|
||||
@@ -126,12 +139,12 @@ const SalesOrderProductForm = ({
|
||||
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
||||
|
||||
// Options Week dari minggu 1 - 22
|
||||
const optionsWeek = useMemo(() => {
|
||||
return Array.from({ length: 22 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: `Week ${i + 1}`,
|
||||
}));
|
||||
}, []);
|
||||
// const optionsWeek = useMemo(() => {
|
||||
// return Array.from({ length: 22 }, (_, i) => ({
|
||||
// value: i + 1,
|
||||
// label: `Week ${i + 1}`,
|
||||
// }));
|
||||
// }, []);
|
||||
|
||||
const {
|
||||
options: warehouseSourceOptions,
|
||||
@@ -180,10 +193,20 @@ const SalesOrderProductForm = ({
|
||||
setSelectedProductWarehouse(productWarehouse || null);
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||
if (
|
||||
productWarehouse?.week !== undefined &&
|
||||
productWarehouse?.week !== null &&
|
||||
productWarehouse?.week > 0
|
||||
) {
|
||||
formik.setFieldValue('week', productWarehouse.week);
|
||||
} else {
|
||||
formik.setFieldValue('week', null);
|
||||
}
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', '');
|
||||
formik.setFieldValue('uom', '');
|
||||
formik.setFieldValue('week', null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -465,21 +488,26 @@ const SalesOrderProductForm = ({
|
||||
)}
|
||||
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
{(formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' ||
|
||||
hasWeekField) && (
|
||||
<NumberInput
|
||||
required={
|
||||
formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet'
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
label='Minggu'
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
placeholder='Pilih Week'
|
||||
onBlur={() => handleBlurField('week')}
|
||||
isError={formik.touched.week && Boolean(formik.errors.week)}
|
||||
errorMessage={formik.errors.week as string}
|
||||
placeholder='Masukan Minggu'
|
||||
decimalScale={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRef } from 'react';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
@@ -18,6 +19,7 @@ type DeliveryOrderProductTableProps = {
|
||||
| 'detail'
|
||||
| 'rejected'
|
||||
| 'pending'
|
||||
| 'success'
|
||||
| string
|
||||
| null;
|
||||
marketing?: Marketing;
|
||||
@@ -31,7 +33,6 @@ const DeliveryOrderProductTable = ({
|
||||
formType,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAddProductClick,
|
||||
marketing,
|
||||
}: DeliveryOrderProductTableProps) => {
|
||||
const onEditRef = useRef(onEdit);
|
||||
@@ -39,161 +40,195 @@ const DeliveryOrderProductTable = ({
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
|
||||
const approvalStepNumber = marketing?.latest_approval?.step_number;
|
||||
|
||||
const renderTableContent = (item: DeliveryOrderProductFormValues) => {
|
||||
const doItem = marketing?.delivery_order?.find(
|
||||
(doItem) => doItem.do_number === item.do_number
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' &&
|
||||
(formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail') && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{doItem?.warehouse?.name ||
|
||||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Qty</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.qty
|
||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
{Number(item.avg_weight ?? 0) > 0 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(Number(item.avg_weight))} Kg
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Number(item.total_weight ?? 0) > 0 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(Number(item.total_weight))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
<tr className='border-b border-t border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
{approvalStepNumber !== 1 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.delivery_date ? (
|
||||
formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||
) : formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail' ? (
|
||||
<span
|
||||
className='text-error hover:text-error/70 cursor-pointer hover:underline underline-offset-4'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
>
|
||||
Belum diisi
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-error'>Belum diisi</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.do_number && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>{item.do_number}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
{doItem && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Dokumen Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
<DeliveryOrderExport data={marketing} deliveryOrder={doItem} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||
{data.map((item) => {
|
||||
const doItem = marketing?.delivery_order?.find(
|
||||
(doItem) => doItem.do_number === item.do_number
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
key={`table-${item.id}`}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
{data.map((item) => (
|
||||
<div key={`table-${item.id}`}>
|
||||
{formType === 'success' ? (
|
||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
key={`table-${item.id}`}
|
||||
title={
|
||||
item.marketing_product?.product_warehouse?.label || 'Produk'
|
||||
}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full mt-2'>
|
||||
<div>Value</div>
|
||||
{(formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail') && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:pencil'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:trash'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.delivery_date ? (
|
||||
formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||
) : (
|
||||
<span className='text-error'>Belum diisi</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{item.do_number && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>{item.do_number}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.vehicle_number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Qty</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.qty
|
||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.avg_weight as string)
|
||||
) + ' Kg'
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(parseFloat(item.total_weight as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
{doItem && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
Dokumen Pengiriman
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
<DeliveryOrderExport
|
||||
data={marketing}
|
||||
deliveryOrder={doItem}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { useRef } from 'react';
|
||||
|
||||
type SalesOrderProductTableProps = {
|
||||
data: SalesOrderProductFormValues[];
|
||||
@@ -32,268 +27,169 @@ const SalesOrderProductTable = ({
|
||||
const onEditRef = useRef(onEdit);
|
||||
onEditRef.current = onEdit;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({
|
||||
table,
|
||||
}: {
|
||||
table: TanStack.Table<SalesOrderProductFormValues>;
|
||||
}) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
const renderTableContent = (item: SalesOrderProductFormValues) => (
|
||||
<>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||
</Button>
|
||||
{data.length > 1 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
row.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) =>
|
||||
formatNumber(
|
||||
parseFloat(row.original.qty as string),
|
||||
undefined,
|
||||
0,
|
||||
5
|
||||
) +
|
||||
' ' +
|
||||
(row.original.uom ?? ''),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='warning'
|
||||
className='p-1'
|
||||
onClick={() => onEditRef.current(props.row.original.id as number)}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||
<td className='text-sm px-4 py-3'>{item.marketing_type?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>{item.product_warehouse?.label}</td>
|
||||
</tr>
|
||||
{item.marketing_type?.value.toLowerCase() === 'telur' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.convertion_unit?.label}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() === 'ayam_pullet' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>Week {item.week}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_peti} {item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() !== 'trading' && (
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_weight
|
||||
? formatNumber(parseFloat(item.total_weight as string)) +
|
||||
' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(parseFloat(item.avg_weight as string)) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.value === 'telur'
|
||||
? 'Total Butir Telur'
|
||||
: 'Qty'}
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||
{data.map((item) => (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
key={`table-${item.id}`}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full mt-2'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:pencil'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:trash'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
{item.marketing_type?.value.toLowerCase() === 'telur' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.week?.label}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_peti} {item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() !== 'trading' && (
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.total_weight as string)
|
||||
) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.avg_weight as string)
|
||||
) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.value === 'telur'
|
||||
? 'Total Butir Telur'
|
||||
: 'Qty'}
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
<div key={`table-${item.id}`}>
|
||||
{formType === 'success' ? (
|
||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
title={item.product_warehouse?.label || 'Produk'}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{formType != 'add_deliver' &&
|
||||
|
||||
@@ -49,7 +49,7 @@ const DeliveryOrderExport = ({
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 150);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Button from '@/components/Button';
|
||||
import { Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { Document, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||
@@ -43,7 +43,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 150);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
@@ -162,7 +162,7 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
|
||||
</View>
|
||||
</View>
|
||||
{data?.sales_order?.map((item, index) => {
|
||||
const isLastItem = index === (data?.sales_order?.length || 0) - 1;
|
||||
// const isLastItem = index === (data?.sales_order?.length || 0) - 1;
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import {
|
||||
CreateCustomerPayload,
|
||||
@@ -27,7 +27,6 @@ import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import useSWR from 'swr';
|
||||
import { UserApi } from '@/services/api/user';
|
||||
import { TYPE_OPTIONS } from '@/config/constant';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Fcr, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.fcr.detail'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const FcrsTable = () => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: { search: '', nameSort: '' },
|
||||
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' },
|
||||
});
|
||||
|
||||
const {
|
||||
data: fcrs,
|
||||
isLoading,
|
||||
mutate: refreshFcrs,
|
||||
} = useSWR(
|
||||
`${FcrApi.basePath}${getTableFilterQueryString()}`,
|
||||
FcrApi.getAllFetcher
|
||||
);
|
||||
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [selectedFcr, setSelectedFcr] = useState<Fcr | undefined>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const fcrsColumns: ColumnDef<Fcr>[] = [
|
||||
{
|
||||
header: '#',
|
||||
cell: (props) =>
|
||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||
props.row.index +
|
||||
1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Nama',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props) => {
|
||||
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||
const currentRowRelativeIndex =
|
||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedFcr(props.row.original);
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageSize > 2 && (
|
||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||
<RowOptionsMenu
|
||||
type='dropdown'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
|
||||
{currentPageSize <= 2 && (
|
||||
<RowCollapseOptions>
|
||||
<RowOptionsMenu
|
||||
type='collapse'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const deleteResponse = await FcrApi.delete(selectedFcr?.id as number);
|
||||
|
||||
if (isResponseError(deleteResponse)) {
|
||||
toast.error(deleteResponse.message);
|
||||
setIsDeleteLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshFcrs();
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete FCR!');
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
|
||||
setPageSize(newVal.value as number);
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
|
||||
if (!isNameSorted) {
|
||||
updateFilter('nameSort', '');
|
||||
} else {
|
||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.fcr.create'>
|
||||
<Button
|
||||
href='/master-data/fcr/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari FCR'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end'>
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
value={{
|
||||
label: String(tableFilterState.pageSize),
|
||||
value: tableFilterState.pageSize,
|
||||
}}
|
||||
onChange={pageSizeChangeHandler}
|
||||
className={{ wrapper: 'max-w-28' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<Fcr>
|
||||
data={isResponseSuccess(fcrs) ? fcrs?.data : []}
|
||||
columns={fcrsColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
page={isResponseSuccess(fcrs) ? fcrs?.meta?.page : 0}
|
||||
totalItems={isResponseSuccess(fcrs) ? fcrs?.meta?.total_results : 0}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoading}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20': isResponseSuccess(fcrs) && fcrs?.data?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data FCR ini (${selectedFcr?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrsTable;
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const FcrStandardSchema: Yup.ObjectSchema<{
|
||||
weight: number | string;
|
||||
fcr_number: number | string;
|
||||
mortality: number | string;
|
||||
}> = Yup.object({
|
||||
weight: Yup.number().nullable().required('Bobot wajib diisi!'),
|
||||
fcr_number: Yup.number()
|
||||
.nullable()
|
||||
.typeError('FCR harus angka!')
|
||||
.required('FCR harus diisi!'),
|
||||
mortality: Yup.number().nullable().required('Mortalitas wajib diisi!'),
|
||||
});
|
||||
|
||||
export const FcrFormSchema = Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
fcrStandards: Yup.array()
|
||||
.of(FcrStandardSchema)
|
||||
.min(1, 'Minimal 1 FCR Standard diisi1')
|
||||
.required('FCR wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdateFcrFormSchema = FcrFormSchema;
|
||||
|
||||
export type FcrFormValues = Yup.InferType<typeof FcrFormSchema>;
|
||||
@@ -1,401 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFormik } from 'formik';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
FcrFormSchema,
|
||||
FcrFormValues,
|
||||
UpdateFcrFormSchema,
|
||||
} from '@/components/pages/master-data/fcr/form/FcrForm.schema';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import {
|
||||
CreateFcrPayload,
|
||||
Fcr,
|
||||
FcrWithStandards,
|
||||
UpdateFcrPayload,
|
||||
} from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
|
||||
interface FcrFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
initialValues?: FcrWithStandards;
|
||||
}
|
||||
|
||||
const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [fcrFormErrorMessage, setFcrFormErrorMessage] = useState('');
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const createFcrHandler = useCallback(
|
||||
async (payload: CreateFcrPayload) => {
|
||||
const createFcrRes = await FcrApi.create(payload);
|
||||
|
||||
if (isResponseError(createFcrRes)) {
|
||||
setFcrFormErrorMessage(createFcrRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(createFcrRes?.message as string);
|
||||
router.push('/master-data/fcr');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const updateFcrHandler = useCallback(
|
||||
async (fcrId: number, payload: UpdateFcrPayload) => {
|
||||
const updateFcrRes = await FcrApi.update(fcrId, payload);
|
||||
|
||||
if (updateFcrRes?.status === 'error') {
|
||||
setFcrFormErrorMessage(updateFcrRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(updateFcrRes?.message as string);
|
||||
router.refresh();
|
||||
router.push('/master-data/fcr');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const formikInitialValues = useMemo<FcrFormValues>(() => {
|
||||
return {
|
||||
name: initialValues?.name ?? '',
|
||||
fcrStandards: initialValues?.fcr_standards
|
||||
? initialValues?.fcr_standards
|
||||
: [
|
||||
{
|
||||
weight: '',
|
||||
fcr_number: '',
|
||||
mortality: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<FcrFormValues>({
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: type === 'edit' ? UpdateFcrFormSchema : FcrFormSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFcrFormErrorMessage('');
|
||||
|
||||
const fcrPayload: CreateFcrPayload = {
|
||||
name: values.name,
|
||||
fcr_standards: values.fcrStandards as CreateFcrPayload['fcr_standards'],
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'add':
|
||||
await createFcrHandler(fcrPayload);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
await updateFcrHandler(initialValues?.id as number, fcrPayload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
const addFcrStandard = () =>
|
||||
formik.setFieldValue('fcrStandards', [
|
||||
...formik.values.fcrStandards,
|
||||
{
|
||||
weight: '',
|
||||
fcr_number: '',
|
||||
mortality: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const removeFcrStandard = (i: number) =>
|
||||
formik.setFieldValue(
|
||||
'fcrStandards',
|
||||
formik.values.fcrStandards.filter((_, idx) => idx !== i)
|
||||
);
|
||||
|
||||
const deleteFcrClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await FcrApi.delete(initialValues?.id as number);
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete FCR!');
|
||||
setIsDeleteLoading(false);
|
||||
router.push('/master-data/fcr');
|
||||
};
|
||||
|
||||
const isRepeaterInputError = (
|
||||
column: keyof CreateFcrPayload['fcr_standards'][0],
|
||||
idx: number
|
||||
) => {
|
||||
return (
|
||||
formik.touched.fcrStandards?.[idx]?.[column] &&
|
||||
Boolean(
|
||||
formik.errors.fcrStandards?.[idx] instanceof Object &&
|
||||
formik.errors.fcrStandards?.[idx]?.[column]
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formikInitialValues);
|
||||
}, [formikSetValues, formikInitialValues]);
|
||||
|
||||
// ===== Formik Error List =====
|
||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-5xl'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/master-data/fcr'
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
{type === 'add' && 'Tambah FCR'}
|
||||
{type === 'edit' && 'Edit FCR'}
|
||||
{type === 'detail' && 'Detail FCR'}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={handleFormSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<TextInput
|
||||
required
|
||||
label='Nama'
|
||||
name='name'
|
||||
placeholder='Masukkan nama FCR'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.name && Boolean(formik.errors.name)}
|
||||
errorMessage={formik.errors.name}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bobot</th>
|
||||
<th>FCR</th>
|
||||
<th>Mortalitas</th>
|
||||
{type !== 'detail' && <th>Aksi</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{formik.values.fcrStandards.map((fcrStandard, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].weight`}
|
||||
placeholder='Masukkan bobot'
|
||||
value={fcrStandard.weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('weight', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].fcr_number`}
|
||||
placeholder='Masukkan FCR'
|
||||
value={fcrStandard.fcr_number}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('fcr_number', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].mortality`}
|
||||
placeholder='Masukkan mortalitas'
|
||||
value={fcrStandard.mortality}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('mortality', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{type !== 'detail' && (
|
||||
<td>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeFcrStandard(idx)}
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type !== 'detail' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addFcrStandard}
|
||||
className='w-fit mx-auto'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah FCR
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteFcrClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type !== 'detail' && (
|
||||
<div
|
||||
className={cn('flex flex-row justify-end gap-2', {
|
||||
'w-full': type === 'add',
|
||||
})}
|
||||
>
|
||||
<Button type='reset' color='warning' className='px-4'>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
color='primary'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
className='px-4'
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fcrFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{fcrFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{type !== 'add' && (
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data FCR ini (${initialValues?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrForm;
|
||||
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFormik } from 'formik';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
KandangFormValues,
|
||||
UpdateKandangFormSchema,
|
||||
} from '@/components/pages/master-data/kandang/form/KandangForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import {
|
||||
Kandang,
|
||||
CreateKandangPayload,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user